diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bcb2719
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+repos
+.claude
diff --git a/README.md b/README.md
index e69de29..a6bab35 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,48 @@
+# at
+
+- https://github.com/bluesky-social/atproto
+- https://github.com/bluesky-social/atproto/discussions/2026
+
+|word|name|example|
+|---|---|---|
+|at|uri|at://example.com|
+|@|user|@example.com|
+|[at]proto|repo|`git@github.com:bluesky-social/atproto`|
+|[at]mosphere|system|pds, bsky(appview), ozone, bgs, plc|
+|[a]uthenticated [t]ransfer|protocol|[did](https://www.w3.org/TR/did-core/)|
+
+- https://atproto.com/ja/guides/glossary
+
+## account
+
+- [ai@syu.is](https://syu.is/profile/did:plc:6qyecktefllvenje24fcxnie)
+- [ai@bsky.app](https://bsky.app/profile/did:plc:6qyecktefllvenje24fcxnie)
+- https://plc.syu.is/did:plc:6qyecktefllvenje24fcxnie
+- https://plc.directory/did:plc:6qyecktefllvenje24fcxnie
+
+```sh
+$ curl -sL syu.is/xrpc/_health
+
+# latest
+# https://github.com/bluesky-social/atproto/blob/main/packages/pds/package.json
+$ curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/packages/pds/package.json |jq -r .version
+```
+
+```sh
+$ handle=ai.syui.ai
+$ curl -sL "syu.is/xrpc/com.atproto.repo.describeRepo?repo=${handle}" |jq -r .did
+did:plc:6qyecktefllvenje24fcxnie
+
+$ curl -sL "syu.is/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=app.bsky.feed.post&reverse=true&limit=1"
+{"records":[{"uri":"at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.post/3l6s2riuouk2j","cid":"bafyreibjohl7va4upkibw5twaxdd4jg3l6rmfatu4dpjjfd5xkb2ijtlx4","value":{"text":"hello","$type":"app.bsky.feed.post","langs":["ja"],"createdAt":"2024-10-18T13:21:39.809Z"}}],"cursor":"3l6s2riuouk2j"}
+```
+
+## feed
+
+> at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/cmd
+
+- https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd
+- https://feed.syu.is/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd
+- https://desc.syu.is/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/cmd
+
+
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..22fda73
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,139 @@
+services:
+
+ database:
+ image: postgres:16-alpine
+ restart: always
+ env_file:
+ - ./envs/postgres
+ volumes:
+ - ./configs/postgres/init/:/docker-entrypoint-initdb.d/
+ - ./data/postgres/:/var/lib/postgresql/data/
+ healthcheck:
+ test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
+ interval: 5s
+ retries: 20
+
+ redis:
+ image: redis:alpine
+ restart: always
+ volumes:
+ - ./data/redis/:/data/
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping", "|", "grep", "PONG"]
+ interval: 1s
+ timeout: 5s
+ retries: 5
+
+ plc:
+ ports:
+ - 2582:3000
+ build:
+ context: ./repos/did-method-plc/
+ dockerfile: packages/server/Dockerfile
+ restart: always
+ env_file:
+ - ./envs/plc
+ depends_on:
+ database:
+ condition: service_healthy
+
+ pds:
+ ports:
+ - 2583:3000
+ build:
+ context: ./repos/atproto/
+ dockerfile: services/pds/Dockerfile
+ restart: always
+ env_file:
+ - ./envs/pds
+ volumes:
+ - ./data/pds/:/data/
+ command: node --enable-source-maps index.js
+ depends_on:
+ database:
+ condition: service_healthy
+
+ bsky:
+ ports:
+ - 2584:2584
+ build:
+ context: ./repos/atproto/
+ dockerfile: services/bsky/Dockerfile
+ restart: always
+ env_file:
+ - ./envs/bsky
+ user: root
+ volumes:
+ - ./data/bsky/:/data/
+ command: node --enable-source-maps api.js
+ depends_on:
+ database:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+
+ bgs:
+ ports:
+ - 2470:2470
+ build:
+ context: ./repos/indigo/
+ dockerfile: cmd/bigsky/Dockerfile
+ restart: always
+ env_file:
+ - ./envs/bgs
+ volumes:
+ - ./data/bgs/:/data/
+ depends_on:
+ database:
+ condition: service_healthy
+
+ social-app:
+ ports:
+ - 8100:8100
+ build:
+ context: ./repos/social-app/
+ dockerfile: Dockerfile
+ restart: always
+ env_file:
+ - ./envs/social-app
+ command: "/usr/bin/bskyweb serve"
+
+ jetstream:
+ build:
+ context: ./repos/jetstream/
+ dockerfile: Dockerfile
+ ports:
+ - 6008:6008
+ volumes:
+ - ./data/jetstream:/data
+ restart: always
+ env_file:
+ - ./envs/jetstream
+
+ ozone-web:
+ build:
+ context: ./repos/ozone/
+ ports:
+ - 2586:3000
+ restart: always
+ volumes:
+ - ./data/ozone/:/data/
+ env_file:
+ - ./envs/ozone
+ depends_on:
+ database:
+ condition: service_healthy
+
+ ozone:
+ build:
+ context: ./repos/atproto/
+ dockerfile: services/ozone/Dockerfile
+ ports:
+ - 2585:3000
+ restart: always
+ command: node --enable-source-maps api.js
+ volumes:
+ - ./data/ozone/:/data/
+ env_file:
+ - ./envs/ozone
+
diff --git a/configs/postgres/init/init.sql b/configs/postgres/init/init.sql
new file mode 100644
index 0000000..f86b7c9
--- /dev/null
+++ b/configs/postgres/init/init.sql
@@ -0,0 +1,35 @@
+-- PLC
+CREATE DATABASE plc;
+GRANT ALL PRIVILEGES ON DATABASE plc TO postgres;
+
+-- BGS
+CREATE DATABASE bgs;
+CREATE DATABASE carstore;
+GRANT ALL PRIVILEGES ON DATABASE bgs TO postgres;
+GRANT ALL PRIVILEGES ON DATABASE carstore TO postgres;
+
+-- bsky(AppView)
+--CREATE DATABASE appview;
+--GRANT ALL PRIVILEGES ON DATABASE appview TO postgres;
+CREATE DATABASE bsky;
+GRANT ALL PRIVILEGES ON DATABASE bsky TO postgres;
+
+-- ozone(Moderation)
+--CREATE DATABASE moderation;
+--GRANT ALL PRIVILEGES ON DATABASE moderation TO postgres;
+CREATE DATABASE ozone;
+GRANT ALL PRIVILEGES ON DATABASE ozone TO postgres;
+
+-- search(palomar)
+CREATE DATABASE search;
+GRANT ALL PRIVILEGES ON DATABASE search TO postgres;
+--CREATE DATABASE palomar;
+--GRANT ALL PRIVILEGES ON DATABASE palomar TO postgres;
+
+-- PDS
+CREATE DATABASE pds;
+GRANT ALL PRIVILEGES ON DATABASE pds TO postgres;
+
+-- BSYNC
+CREATE DATABASE bsync;
+GRANT ALL PRIVILEGES ON DATABASE bsync TO postgres;
diff --git a/envs/bgs b/envs/bgs
new file mode 100644
index 0000000..a5097f3
--- /dev/null
+++ b/envs/bgs
@@ -0,0 +1,6 @@
+DATABASE_URL=postgres://postgres:postgres@database/bgs
+CARSTORE_DATABASE_URL=postgres://postgres:postgres@database/carstore
+DATA_DIR=/data
+ATP_PLC_HOST=https://plc.${host}
+BGS_NEW_PDS_PER_DAY_LIMIT=1000
+BGS_ADMIN_KEY=
diff --git a/envs/bsky b/envs/bsky
new file mode 100644
index 0000000..5bebd98
--- /dev/null
+++ b/envs/bsky
@@ -0,0 +1,19 @@
+BSKY_PORT=2584
+BSKY_BLOB_CACHE_LOC=/data/
+BSKY_BSYNC_HTTP_VERSION=1.1
+BSKY_BSYNC_PORT=3002
+BSKY_BSYNC_URL=http://bsky:3002
+BSKY_COURIER_URL=http://fake-courier.example.invalid/
+BSKY_DATAPLANE_HTTP_VERSION=1.1
+BSKY_DATAPLANE_PORT=3001
+BSKY_DATAPLANE_URLS=http://bsky:3001
+BSKY_DB_POSTGRES_URL=postgres://postgres:postgres@database/bsky
+BSKY_DID_PLC_URL=https://plc.${host}
+BSKY_PUBLIC_URL=https://bsky.${host}
+BSKY_REPO_PROVIDER=wss://bgs.${host}
+BSKY_SERVER_DID=did:web:bsky.${host}
+MOD_SERVICE_DID=did:web:ozone.${host}
+
+#BSKY_IMG_URI_ENDPOINT=https://bsky.${host}/img
+BSKY_ADMIN_PASSWORDS
+BSKY_SERVICE_SIGNING_KEY
diff --git a/envs/jetstream b/envs/jetstream
new file mode 100644
index 0000000..324bbd7
--- /dev/null
+++ b/envs/jetstream
@@ -0,0 +1,5 @@
+JETSTREAM_WS_URL=wss://bgs.${host}/xrpc/com.atproto.sync.subscribeRepos
+JETSTREAM_DATA_DIR=/data
+JETSTREAM_LISTEN_ADDR=:6008
+JETSTREAM_METRICS_LISTEN_ADDR=:6009
+JETSTREAM_LIVENESS_TTL=96h
diff --git a/envs/ozone b/envs/ozone
new file mode 100644
index 0000000..72e89eb
--- /dev/null
+++ b/envs/ozone
@@ -0,0 +1,29 @@
+OZONE_SERVER_DID=did:web:ozone.${host}
+OZONE_PUBLIC_URL=https://ozone.${host}
+OZONE_ADMIN_HANDLE=${user}.${host}
+OZONE_MODERATOR_DIDS=${did}
+OZONE_ADMIN_DIDS=${did}
+OZONE_DB_POSTGRES_URL=postgres://postgres:postgres@database/ozone
+OZONE_DID_PLC_URL=https://plc.${host}
+NEXT_PUBLIC_PLC_DIRECTORY_URL=https://plc.${host}
+NEXT_PUBLIC_OZONE_SERVICE_DID=did:web:ozone.${host}
+NEXT_PUBLIC_SOCIAL_APP_DOMAIN=mod.${host}
+NEXT_PUBLIC_SOCIAL_APP_URL=https://mod.${host}
+OZONE_APPVIEW_DID=did:web:bsky.${host}
+OZONE_APPVIEW_URL=https://bsky.${host}
+OZONE_APPVIEW_PUSH_EVENTS=false
+OZONE_PDS_DID=did:web:${host}
+OZONE_PDS_URL=https://${host}
+OZONE_DEV_MODE=true
+OZONE_DB_MIGRATE=1
+
+OZONE_ADMIN_PASSWORD
+OZONE_SIGNING_KEY_HEX
+OZONE_BLOB_DIVERT_ADMIN_PASSWORD
+OZONE_VERIFIER_URL
+OZONE_VERIFIER_DID
+OZONE_VERIFIER_PASSWORD
+OZONE_VERIFIER_ISSUERS_TO_INDEX
+OZONE_VERIFIER_JETSTREAM_URL
+
+OZONE_APPVIEW_PUSH_EVENTS=true
diff --git a/envs/pds b/envs/pds
new file mode 100644
index 0000000..b2f0241
--- /dev/null
+++ b/envs/pds
@@ -0,0 +1,23 @@
+PDS_HOSTNAME=${host}
+PDS_DB_POSTGRES_URL=postgres://postgres:postgres@database/pds
+PDS_DATA_DIRECTORY=/data
+PDS_BLOBSTORE_DISK_LOCATION=/data/img/static
+#PDS_BLOBSTORE_DISK_TMP_LOCATION=/data/img/tmp
+PDS_BSKY_APP_VIEW_DID=did:web:bsky.${host}
+PDS_BSKY_APP_VIEW_URL=https://bsky.${host}
+PDS_CRAWLERS=https://bgs.${host}
+PDS_SEQUENCER_ENABLED=true
+PDS_SEQUENCER_DB_LOCATION=/data/sequencer.sqlite
+PDS_DEV_MODE=true
+PDS_DID_PLC_URL=https://plc.${host}
+PDS_ENABLE_DID_DOC_WITH_SESSION=true
+PDS_INVITE_INTERVAL=604800000
+PDS_SERVICE_DID=did:web:${host}
+PDS_EMAIL_FROM_ADDRESS=no-reply@${host}
+
+PDS_INVITE_REQUIRED=true
+PDS_EMAIL_SMTP_URL=smtps://${user}:${app_password}@smtp.gmail.com
+PDS_ADMIN_PASSWORD
+PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX
+PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX
+PDS_JWT_SECRET
diff --git a/envs/plc b/envs/plc
new file mode 100644
index 0000000..f0ccb52
--- /dev/null
+++ b/envs/plc
@@ -0,0 +1,4 @@
+DATABASE_URL=postgres://postgres:postgres@database/plc
+DB_CREDS_JSON='{"username":"postgres","password":"postgres","host":"database","port":"5432","database":"plc"}'
+ENABLE_MIGRATIONS=true
+DB_MIGRATE_CREDS_JSON='{"username":"postgres","password":"postgres","host":"database","port":"5432","database":"plc"}'
diff --git a/envs/postgres b/envs/postgres
new file mode 100644
index 0000000..4184165
--- /dev/null
+++ b/envs/postgres
@@ -0,0 +1,4 @@
+#POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=postgres
+POSTGRES_DB=healthcheck
diff --git a/envs/social-app b/envs/social-app
new file mode 100644
index 0000000..a1bc28f
--- /dev/null
+++ b/envs/social-app
@@ -0,0 +1,3 @@
+ATP_APPVIEW_HOST=https://public.api.bsky.app
+EXPO_PUBLIC_BLUESKY_PROXY_DID=did:web:api.bsky.app
+EXPO_PUBLIC_ENV=production
diff --git a/icons/Logotype.tsx b/icons/Logotype.tsx
new file mode 100644
index 0000000..b087a56
--- /dev/null
+++ b/icons/Logotype.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import Svg, {Path, SvgProps, PathProps} from 'react-native-svg'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+
+const ratio = 17 / 64
+
+export function Logotype({
+ fill,
+ ...rest
+}: {fill?: PathProps['fill']} & SvgProps) {
+ const pal = usePalette('default')
+ // @ts-ignore it's fiiiiine
+ const size = parseInt(rest.width || 32)
+
+ return (
+
+ )
+}
diff --git a/icons/title.svg b/icons/title.svg
new file mode 100644
index 0000000..73a0b2f
--- /dev/null
+++ b/icons/title.svg
@@ -0,0 +1,32 @@
+
diff --git a/install.zsh b/install.zsh
new file mode 100755
index 0000000..54a91f1
--- /dev/null
+++ b/install.zsh
@@ -0,0 +1,328 @@
+#!/bin/zsh
+
+# ./install.zsh $HOST
+repos_v='{}'
+function at-repos-env() {
+ host=$1
+ if [ -z "$1" ];then
+ host=syu.is
+ fi
+ did=did:plc:6qyecktefllvenje24fcxnie
+ icon=https://git.syui.ai/ai/at/raw/branch/main/icons/Logotype.tsx
+ repos=(
+ https://github.com/did-method-plc/did-method-plc
+ https://github.com/bluesky-social/indigo
+ https://github.com/bluesky-social/atproto
+ https://github.com/bluesky-social/social-app
+ https://github.com/bluesky-social/feed-generator
+ https://github.com/bluesky-social/ozone
+ https://github.com/bluesky-social/jetstream
+ )
+ services=( bsky plc pds jetstream bgs ozone social-app )
+ d=${0:a:h}
+ dh=${0:a:h:h}
+ name=${host%%.*}
+ domain=${host##*.}
+ dport=5000
+}
+
+function at-repos-json() {
+ f=~/.config/atproto/token.json
+ j="{ \"did\": \"did:plc:6qyecktefllvenje24fcxnie\", \"didDoc\": { \"service\": [ { \"serviceEndpoint\": \"https://syu.is\" } ] }, \"handle\": \"ai.syu.is\", \"accessJwt\": \"xxx\" }"
+ if [ ! -f "$f" ];then
+ mkdir -p ~/.config/atproto
+ echo $j >> $f
+ fi
+ echo $f
+}
+
+function at-repos-token() {
+ at-repos-json
+ if [ -z "$host" ] && [ -f $f ];then
+ host=`cat $f|jq -r ".didDoc.service.[].serviceEndpoint"`
+ handle=`cat $f|jq -r ".handle"`
+ did=`cat $f|jq -r ".did"`
+ token=`cat $f|jq -r ".token"`
+ host=${host##*/}
+ fi
+ name=${host%%.*}
+ domain=${host##*.}
+}
+
+function at-repos-clone() {
+ if [ ! -d $d/repos ];then
+ mkdir -p $d/repos
+ fi
+ cd $d/repos
+ for ((i=1; i<=${#repos}; i++)); do
+ repo=${repos[$i]}
+ echo $repo
+ if [ ! -d $d/repos/${repo##*/} ];then
+ git clone $repo
+
+ fi
+ done
+ if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then
+ cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/
+ fi
+}
+
+function at-repos-pull() {
+ cd $d/repos
+ for ((i=1; i<=${#repos}; i++)); do
+ repo=${repos[$i]}
+ echo $repo
+ if [ -d $d/repos/${repo##*/} ];then
+ cd $d/repos/${repo##*/}
+ git stash
+ if ! git pull;then
+ rm -rf $d/repos/${repo##*/}
+ at-repos-clone
+ fi
+ fi
+ rv=$(echo "$repos_v" | jq -r ".[\"${repo##*/}\"]")
+ if [ "$rv" != "null" ];then
+ cd $d/repos/${repo##*/}
+ git reset --hard $rv
+ cd ..
+ fi
+ done
+ cd $d
+}
+
+function at-repos-social-app-icon() {
+ curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/main/src/view/icons/Logotype.tsx -o $d/repos/social-app/src/view/icons/Logotype.tsx
+ if [ -d $d/icons ];then
+ mkdir -p $d/icons
+ fi
+ cp -rf $d/repos/social-app/src/view/icons/Logotype.tsx $d/icons/
+}
+
+function at-repos-social-app-icon-origin() {
+ curl -sL $icon -o $d/icons/Logotype.tsx
+}
+
+function at-repos-social-app-avatar-write() {
+ did_admin=did:plc:6qyecktefllvenje24fcxnie
+ dt=$d/repos/social-app/src
+ cd $dt
+ f=$dt/lib/constants.ts
+ sed -i "s#export const BSKY_SERVICE = 'https://bsky.social'#export const BSKY_SERVICE = 'https://${host}'#g" $f
+ sed -i "s#export const BSKY_SERVICE_DID = 'did:web:bsky.social'#export const BSKY_SERVICE_DID = 'did:web:${host}'#g" $f
+ sed -i "s#export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'#export const PUBLIC_BSKY_SERVICE = 'https://bsky.${host}'#g" $f
+ sed -i "s#export const PUBLIC_APPVIEW = 'https://api.bsky.app'#export const PUBLIC_APPVIEW = 'https://bsky.${host}'#g" $f
+ sed -i "s#export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'#export const PUBLIC_APPVIEW_DID = 'did:web:bsky.${host}'#g" $f
+
+ # Disable external services (CORS fix)
+ f=$dt/state/geolocation/const.ts
+ curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/state/geolocation/const.ts -o $f
+ cat > $f << 'GEOEOF'
+import {type GeolocationStatus} from '#/state/geolocation/types'
+import {BAPP_CONFIG_DEV_URL, IS_DEV} from '#/env'
+import {type Device} from '#/storage'
+
+export const IPCC_URL = `https://bsky.app/ipcc`
+// Disabled for self-hosted environment to avoid CORS errors
+export const BAPP_CONFIG_URL_PROD = null
+export const BAPP_CONFIG_URL = null
+export const GEOLOCATION_CONFIG_URL = BAPP_CONFIG_URL
+
+export const DEFAULT_GEOLOCATION_CONFIG: Device['geolocation'] = {
+ countryCode: undefined,
+ regionCode: undefined,
+ ageRestrictedGeos: [],
+ ageBlockedGeos: [],
+}
+
+export const DEFAULT_GEOLOCATION_STATUS: GeolocationStatus = {
+ countryCode: undefined,
+ regionCode: undefined,
+ isAgeRestrictedGeo: false,
+ isAgeBlockedGeo: false,
+}
+GEOEOF
+
+ # Add null check to geolocation config.ts to prevent fetch(null) errors
+ f=$dt/state/geolocation/config.ts
+ curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/state/geolocation/config.ts -o $f
+ # Add null check at the beginning of getGeolocationConfig function (after line with 'url: string,')
+ sed -i "s/): Promise {/): Promise {\n if (!url) return undefined/" $f
+
+ # Disable Statsig (CORS fix)
+ f=$dt/lib/statsig/statsig.tsx
+ sed -i "s#api: 'https://events.bsky.app/v2'#api: '' // Disabled for self-hosted#g" $f
+ # Disable SDK initialization to prevent statsigapi.net connections
+ sed -i "s#const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'#const SDK_KEY = '' // Disabled for self-hosted#g" $f
+
+ f=$dt/view/icons/Logotype.tsx
+ o=$d/icons/Logotype.tsx
+ cp -rf $o $f
+
+ f=$dt/view/com/util/UserAvatar.tsx
+ curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/view/com/util/UserAvatar.tsx -o $f
+ sed -i "s#/img/avatar/plain/#https://cdn.web.syu.is/img/avatar/plain/#g" $f
+ sed -i "s#/img/avatar_thumbnail/plain/#https://bsky.${host}/img/avatar/plain/#g" $f
+ sed -i "s#source={{uri: avatar}}#source={{ uri: hackModifyThumbnailPath(avatar, 1 > 0), }}#g" $f
+ curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/lib/strings/url-helpers.ts -o $dt/lib/strings/url-helpers.ts
+ sed -i "s#https://go.web.syu.is/redirect?u=\${encodeURIComponent(url)}#\${url}#g" $dt/lib/strings/url-helpers.ts
+ grep -R $did_admin .|cut -d : -f 1|sort -u|xargs sed -i "s/${did_admin}/${did}/g"
+}
+
+function at-repos-atproto-service-bsky-api-patch() {
+ # https://github.com/itaru2622/bluesky-selfhost-env/blob/master/patching/105-atproto-services-for-docker.diff
+ f=$d/repos/atproto/services/bsky/api.js
+ curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/bsky/api.js -o $f
+ d_=$d/repos/atproto
+ p_=$d/patching/4367-atproto-services-bsky-api.diff
+ echo "applying patch: under ${f} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_}
+ popd
+}
+
+function at-repos-atproto-service-pds-index-patch() {
+ f=$d/repos/atproto/services/pds/index.js
+ curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/pds/index.js -o $f
+ d_=$d/repos/atproto
+ p_=$d/patching/4367-atproto-services-pds-index.diff
+ echo "applying patch: under ${f} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_}
+ popd
+}
+
+function at-repos-social-app-agent-patch() {
+ f=$d/repos/social-app/src/state/session/agent.ts
+ p_=$d/patching/8980-social-app-disable-proxy.diff
+ d_=$d/repos/social-app
+ echo "applying patch: under ${f} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_}
+ popd
+}
+
+function at-repos-social-app-disable-external-services-patch() {
+ f=$d/repos/social-app/src/state/geolocation/const.ts
+ p_=$d/patching/8980-social-app-disable-external-services.diff
+ d_=$d/repos/social-app
+ echo "applying patch: under ${f} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_}
+ popd
+}
+
+function at-repos-atproto-service-ozone-api-patch() {
+ f=$d/repos/atproto/services/ozone/api.js
+ d_=$d/repos/atproto
+ p_=$d/patching/130-atproto-ozone-enable-daemon-v2.patch
+ echo "applying patch: under ${f} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_}
+ popd
+}
+
+function at-repos-ozone-patch() {
+ #DOMAIN=syu.is
+ cd $d/repos
+ d_=$d/repos/ozone
+ rm -rf ${d_}
+ p_=$d/patching/120-ozone-runtimeEnvVars.diff
+ git clone https://github.com/bluesky-social/ozone
+ cd ${d_}
+ pushd ${d_}
+ echo "applying patch: under ${d_} for ${p_}"
+ patch -p1 < ${p_}
+ popd
+
+ p_=$d/patching/122-ozone-enable-daemon.diff
+ echo "applying patch: under ${d_} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_}
+ popd
+
+ p_=$d/patching/121-ozone-constants-fix.patch
+ echo "applying patch: under ${d_} for ${p_}"
+ pushd ${d_}
+ patch -p1 < ${p_} || true
+ popd
+}
+
+function at-repos-build-docker-atproto() {
+ cd $d
+ docker image prune -a
+ if [ -z "$1" ];then
+ for ((i=1; i<=${#services}; i++)); do
+ service=${services[$i]}
+ docker compose build --no-cache $service
+ done
+ else
+ docker compose build --no-cache $1
+ fi
+}
+
+function at-repos-push-reset() {
+ docker restart registry
+ docker stop registry
+ docker rm registry
+ docker volume rm registry-data 2>/dev/null || true
+ docker run -d -p ${dport}:${dport} --name registry \
+ --restart=always \
+ -v registry-data:/var/lib/registry \
+ registry:2
+ sleep 3
+ docker run -d -p ${dport}:${dport} --name registry --restart=always registry:2
+}
+
+function at-repos-push-docker() {
+ if [ -z "$1" ];then
+ for ((i=1; i<=${#services}; i++)); do
+ service=${services[$i]}
+ docker tag at-${service}:latest localhost:${dport}/${service}:latest
+ docker push localhost:${dport}/${service}:latest
+ if [ "$service" = "ozone" ];then
+ docker tag at-${service}:latest localhost:${dport}/${service}-web:latest
+ docker push localhost:${dport}/${service}-web:latest
+ fi
+ done
+ else
+ docker tag at-${1}:latest localhost:${dport}/${1}:latest
+ docker push localhost:${dport}/${1}:latest
+ fi
+}
+
+function at-repos-pull-docker() {
+ cd $d
+ docker image prune -a
+ docker compose up -d --pull always
+}
+
+at-repos-env
+case "`cat /etc/hostname`" in
+ at)
+ at-repos-pull-docker
+ exit
+ ;;
+ *)
+ at-repos-push-reset
+ at-repos-clone
+ at-repos-pull
+ at-repos-social-app-icon
+ at-repos-social-app-icon-origin
+ at-repos-social-app-avatar-write
+ at-repos-social-app-agent-patch
+ at-repos-social-app-disable-external-services-patch
+ at-repos-atproto-service-bsky-api-patch
+ at-repos-atproto-service-pds-index-patch
+ at-repos-atproto-service-ozone-api-patch
+ at-repos-ozone-patch
+ if [ -n "$1" ];then
+ at-repos-build-docker-atproto $1
+ at-repos-push-docker $1
+ exit
+ fi
+ at-repos-build-docker-atproto
+ at-repos-push-docker
+ cd $d; docker compose down
+ ;;
+esac
+
diff --git a/ios/.keep b/ios/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/ios/AppInfo.tsx b/ios/AppInfo.tsx
new file mode 100644
index 0000000..6242b12
--- /dev/null
+++ b/ios/AppInfo.tsx
@@ -0,0 +1,134 @@
+import React from 'react'
+import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
+
+interface AppInfoProps {
+ onLinkPress?: (url: string) => void
+}
+
+export default function AppInfo({onLinkPress}: AppInfoProps) {
+ const handleLinkPress = (url: string) => {
+ if (onLinkPress) {
+ onLinkPress(url)
+ } else {
+ Linking.openURL(url)
+ }
+ }
+
+ return (
+
+
+ About This App
+
+ This is a customized AT Protocol social networking client. It allows you to
+ connect to any Personal Data Server (PDS) and participate in the decentralized
+ social network.
+
+
+
+
+ Key Features
+
+ • Connect to any AT Protocol PDS
+ • Post text, images, and videos
+ • Follow users and view timelines
+ • Customize feeds and moderation settings
+ • Direct messaging support
+
+
+
+
+ Open Source
+
+ This application is based on the Bluesky social-app, licensed under the MIT
+ License. The original source code is available at:
+
+
+ handleLinkPress('https://github.com/bluesky-social/social-app')
+ }>
+ github.com/bluesky-social/social-app
+
+
+
+
+ AT Protocol
+
+ This app uses the AT Protocol (Authenticated Transfer Protocol), an open and
+ decentralized standard for social applications.
+
+ handleLinkPress('https://atproto.com')}>
+ atproto.com
+
+
+
+
+ License
+
+ Copyright 2023–2025 Bluesky Social PBC
+
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software.
+
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
+
+
+
+
+ Contact
+ handleLinkPress('https://syu.is')}>
+ https://syu.is
+
+
+
+
+ Version 1.0.0
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ section: {
+ marginBottom: 24,
+ },
+ sectionTitle: {
+ fontSize: 20,
+ fontWeight: '600',
+ color: '#1d1d1f',
+ marginBottom: 12,
+ },
+ paragraph: {
+ fontSize: 15,
+ lineHeight: 22,
+ color: '#3a3a3c',
+ marginBottom: 8,
+ },
+ list: {
+ marginLeft: 8,
+ marginTop: 8,
+ },
+ listItem: {
+ fontSize: 15,
+ lineHeight: 24,
+ color: '#3a3a3c',
+ },
+ link: {
+ fontSize: 15,
+ color: '#007aff',
+ textDecorationLine: 'underline',
+ marginTop: 8,
+ },
+ versionText: {
+ fontSize: 13,
+ color: '#8e8e93',
+ fontStyle: 'italic',
+ },
+})
diff --git a/ios/LicenseNotice.tsx b/ios/LicenseNotice.tsx
new file mode 100644
index 0000000..496d367
--- /dev/null
+++ b/ios/LicenseNotice.tsx
@@ -0,0 +1,95 @@
+import React from 'react'
+import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
+
+export default function LicenseNotice() {
+ return (
+
+ Open Source Licenses
+
+
+ Bluesky Social App
+ MIT License
+ Copyright 2023–2025 Bluesky Social PBC
+
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+
+ Linking.openURL('https://github.com/bluesky-social/social-app')
+ }>
+ View Source Code
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 20,
+ color: '#1d1d1f',
+ },
+ section: {
+ marginBottom: 24,
+ padding: 16,
+ backgroundColor: '#f5f5f7',
+ borderRadius: 8,
+ },
+ projectName: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: '#1d1d1f',
+ },
+ license: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: '#007aff',
+ marginBottom: 4,
+ },
+ copyright: {
+ fontSize: 13,
+ color: '#3a3a3c',
+ marginBottom: 12,
+ },
+ licenseText: {
+ fontSize: 12,
+ lineHeight: 18,
+ color: '#3a3a3c',
+ marginBottom: 12,
+ },
+ link: {
+ fontSize: 14,
+ color: '#007aff',
+ textDecorationLine: 'underline',
+ marginTop: 8,
+ },
+})
diff --git a/ios/PrivacyContent.tsx b/ios/PrivacyContent.tsx
new file mode 100644
index 0000000..d780104
--- /dev/null
+++ b/ios/PrivacyContent.tsx
@@ -0,0 +1,163 @@
+import React from 'react'
+import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
+
+interface PrivacyContentProps {
+ onLinkPress?: (url: string) => void
+}
+
+export default function PrivacyContent({onLinkPress}: PrivacyContentProps) {
+ const handleLinkPress = (url: string) => {
+ if (onLinkPress) {
+ onLinkPress(url)
+ } else {
+ Linking.openURL(url)
+ }
+ }
+
+ return (
+
+
+ Introduction
+
+ This Privacy Policy explains how this AT Protocol client application
+ (hereinafter referred to as "the App") handles personal information.
+ Please read this policy carefully before using the App.
+
+
+
+
+ Information We Collect
+
+ The App may collect and use the following information:
+
+
+ 1. Information Collected Automatically
+
+ • Device information (model, OS version)
+ • App usage data (sessions, features used)
+ • Crash logs and performance data
+
+
+ 2. Information Provided by Users
+
+
+ • DID (Decentralized Identifier) and handle for authentication
+
+ • Posts, media, and social interactions
+ • Profile information (avatar, display name, bio)
+
+
+
+
+ Important: Your data is stored on your chosen PDS (Personal Data Server).
+ This app does not store your content on our servers.
+
+
+
+
+
+ How We Use Your Information
+
+
+ • To provide AT Protocol social networking features
+
+ • To improve app performance and user experience
+ • To diagnose and fix technical issues
+
+
+
+
+ Data Sharing
+
+ The App interacts with your chosen PDS and AppView services. Your posts and
+ profile information are shared according to the AT Protocol specification and
+ your privacy settings.
+
+
+
+
+ Your Rights
+
+ You have the right to access, modify, or delete your data through your PDS.
+ You can also switch to a different PDS at any time while maintaining your
+ identity.
+
+
+
+
+ Contact
+
+ For questions about this Privacy Policy, please contact:
+
+ handleLinkPress('https://syu.is')}>
+ https://syu.is
+
+
+
+
+ Last Updated: December 3, 2025
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ section: {
+ marginBottom: 24,
+ },
+ sectionTitle: {
+ fontSize: 20,
+ fontWeight: '600',
+ color: '#1d1d1f',
+ marginBottom: 12,
+ },
+ subTitle: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#1d1d1f',
+ marginTop: 12,
+ marginBottom: 8,
+ },
+ paragraph: {
+ fontSize: 15,
+ lineHeight: 22,
+ color: '#3a3a3c',
+ marginBottom: 8,
+ },
+ list: {
+ marginLeft: 8,
+ marginTop: 8,
+ },
+ listItem: {
+ fontSize: 15,
+ lineHeight: 24,
+ color: '#3a3a3c',
+ },
+ highlight: {
+ backgroundColor: '#fff3cd',
+ borderLeftWidth: 4,
+ borderLeftColor: '#ffc107',
+ padding: 12,
+ marginTop: 12,
+ borderRadius: 4,
+ },
+ highlightText: {
+ fontSize: 14,
+ lineHeight: 20,
+ color: '#856404',
+ },
+ link: {
+ fontSize: 15,
+ color: '#007aff',
+ textDecorationLine: 'underline',
+ marginTop: 8,
+ },
+ lastUpdated: {
+ fontSize: 13,
+ color: '#8e8e93',
+ fontStyle: 'italic',
+ },
+})
diff --git a/ios/PrivacyPolicy.screen.tsx b/ios/PrivacyPolicy.screen.tsx
new file mode 100644
index 0000000..df5d5c8
--- /dev/null
+++ b/ios/PrivacyPolicy.screen.tsx
@@ -0,0 +1,42 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect} from '@react-navigation/native'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {
+ type CommonNavigatorParams,
+ type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {s} from '#/lib/styles'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {ScrollView} from '#/view/com/util/Views'
+import * as Layout from '#/components/Layout'
+import {ViewHeader} from '../com/util/ViewHeader'
+import PrivacyContent from '#/components/custom/PrivacyContent'
+
+type Props = NativeStackScreenProps
+export const PrivacyPolicyScreen = (_props: Props) => {
+ const pal = usePalette('default')
+ const {_} = useLingui()
+ const setMinimalShellMode = useSetMinimalShellMode()
+
+ useFocusEffect(
+ React.useCallback(() => {
+ setMinimalShellMode(false)
+ }, [setMinimalShellMode]),
+ )
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/ios/Support.screen.tsx b/ios/Support.screen.tsx
new file mode 100644
index 0000000..3fddf92
--- /dev/null
+++ b/ios/Support.screen.tsx
@@ -0,0 +1,38 @@
+import React from 'react'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect} from '@react-navigation/native'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {
+ type CommonNavigatorParams,
+ type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {s} from '#/lib/styles'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {ScrollView} from '#/view/com/util/Views'
+import * as Layout from '#/components/Layout'
+import AppInfo from '#/components/custom/AppInfo'
+
+type Props = NativeStackScreenProps
+export const SupportScreen = (_props: Props) => {
+ const pal = usePalette('default')
+ const setMinimalShellMode = useSetMinimalShellMode()
+ const {_} = useLingui()
+
+ useFocusEffect(
+ React.useCallback(() => {
+ setMinimalShellMode(false)
+ }, [setMinimalShellMode]),
+ )
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/ios/app.config.patch.js b/ios/app.config.patch.js
new file mode 100644
index 0000000..136a4d9
--- /dev/null
+++ b/ios/app.config.patch.js
@@ -0,0 +1,9 @@
+// Aiat app configuration overrides
+module.exports = {
+ name: 'Aiat',
+ slug: 'aiat',
+ scheme: 'aiat',
+ owner: 'syui', // Your Expo account
+ bundleIdentifier: 'ai.syui.at',
+ // Icon will be set separately
+}
diff --git a/ios/bin/build.zsh b/ios/bin/build.zsh
new file mode 100755
index 0000000..d1fe8ea
--- /dev/null
+++ b/ios/bin/build.zsh
@@ -0,0 +1,44 @@
+#!/bin/zsh
+set -e
+
+d=~/ai/at/repos/social-app
+APP_NAME=Aiat
+PKG=aiat
+TEAM_NAME=
+TEAM_ID=
+CERT="Apple Distribution: ${TEAM_NAME} (${TEAM_ID})"
+MAIL=user@example.com
+KEY_CHAIN=EXAMPLE
+
+cd $d
+# npx expo prebuild --clean
+# cd ios && pod install && cd ..
+
+## アーカイブ
+xcodebuild -workspace ios/${PKG}.xcworkspace \
+ -scheme ${PKG} \
+ -configuration Release \
+ -archivePath build/${APP_NAME}.xcarchive \
+ -allowProvisioningUpdates \
+ archive
+
+cd build
+
+# IPA作成
+rm -rf Payload ${APP_NAME}.ipa
+mkdir -p Payload
+cp -R ${APP_NAME}.xcarchive/Products/Applications/${PKG}.app Payload/
+cp ../store.mobileprovision Payload/${PKG}.app/embedded.mobileprovision
+
+# entitlements抽出
+security cms -D -i Payload/${PKG}.app/embedded.mobileprovision > /tmp/profile.plist
+/usr/libexec/PlistBuddy -x -c "Print :Entitlements" /tmp/profile.plist > /tmp/entitlements.plist
+
+codesign -f -s "$CERT" Payload/${PKG}.app/Frameworks/*.framework 2>/dev/null || true
+codesign -f -s "$CERT" --entitlements /tmp/entitlements.plist Payload/${PKG}.app
+
+zip -r ${APP_NAME}.ipa Payload
+
+xcrun altool --upload-app -f ${APP_NAME}.ipa -t ios -u "${MAIL}" -p "@keychain:${KEY_CHAIN}"
+
+echo "Upload complete"
diff --git a/ios/bin/install.zsh b/ios/bin/install.zsh
new file mode 100644
index 0000000..2e372c7
--- /dev/null
+++ b/ios/bin/install.zsh
@@ -0,0 +1,86 @@
+#!/bin/zsh
+
+if [ "$1" = "social-app-custom" ];then
+ at-social-app-custom-pages
+ at-social-app-custom-screens
+ at-social-app-aiat-config
+ at-social-app-aiat-logo
+ at-origin-social-app
+ exit
+fi
+
+function at-social-app-custom-pages() {
+ d_=$d/repos/social-app
+ custom=$d/social-app-custom
+
+ echo "copying custom components to social-app"
+
+ # Create components directory if not exists
+ mkdir -p ${d_}/src/components/custom
+
+ # Copy custom components
+ cp ${custom}/PrivacyContent.tsx ${d_}/src/components/custom/
+ cp ${custom}/AppInfo.tsx ${d_}/src/components/custom/
+
+ echo "custom components copied successfully"
+}
+
+function at-social-app-aiat-config() {
+ d_=$d/repos/social-app
+ custom=$d/social-app-custom
+
+ echo "applying Aiat configuration"
+
+ # Update app.config.js
+ cd ${d_}
+
+ # Backup original
+ cp app.config.js app.config.js.orig
+
+ # Apply changes using sed
+ sed -i "s/name: 'Bluesky'/name: 'Aiat'/g" app.config.js
+ sed -i "s/slug: 'bluesky'/slug: 'aiat'/g" app.config.js
+ sed -i "s/scheme: 'bluesky'/scheme: 'aiat'/g" app.config.js
+ sed -i "s/owner: 'blueskysocial'/owner: 'syui'/g" app.config.js
+ sed -i "s/bundleIdentifier: 'xyz.blueskyweb.app'/bundleIdentifier: 'ai.syui.at'/g" app.config.js
+
+ # Update package.json name
+ sed -i 's/"name": "bsky.app"/"name": "aiat"/g' package.json
+
+ echo "Aiat configuration applied"
+}
+
+function at-social-app-aiat-logo() {
+ d_=$d/repos/social-app
+ custom=$d/social-app-custom
+
+ echo "applying Aiat logo"
+
+ # Create logo directory if not exists
+ mkdir -p ${custom}/assets
+
+ # Copy logo if exists in custom folder
+ if [ -f ${custom}/assets/icon.png ]; then
+ cp ${custom}/assets/icon.png ${d_}/assets/app-icons/ios_icon_default_next.png
+ echo "Aiat logo applied"
+ else
+ echo "Warning: Logo file not found at ${custom}/assets/icon.png"
+ echo "Please add your logo file there"
+ fi
+}
+
+function at-social-app-custom-screens() {
+ d_=$d/repos/social-app
+ custom=$d/social-app-custom
+
+ echo "applying custom screens"
+
+ # Copy custom screen files
+ cp ${custom}/PrivacyPolicy.screen.tsx ${d_}/src/view/screens/PrivacyPolicy.tsx
+ cp ${custom}/Support.screen.tsx ${d_}/src/view/screens/Support.tsx
+ cp ${custom}/LicenseNotice.tsx ${d_}/src/components/custom/
+
+ echo "custom screens applied"
+}
+
+
diff --git a/patching/.keep b/patching/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/patching/120-ozone-runtimeEnvVars.diff b/patching/120-ozone-runtimeEnvVars.diff
new file mode 100644
index 0000000..4f369a8
--- /dev/null
+++ b/patching/120-ozone-runtimeEnvVars.diff
@@ -0,0 +1,119 @@
+diff --git a/app/layout.tsx b/app/layout.tsx
+index bfc3470..9350629 100644
+--- a/app/layout.tsx
++++ b/app/layout.tsx
+@@ -5,6 +5,7 @@ import 'yet-another-react-lightbox/styles.css'
+ import 'yet-another-react-lightbox/plugins/thumbnails.css'
+ import 'yet-another-react-lightbox/plugins/captions.css'
+ import { ToastContainer } from 'react-toastify'
++import { PublicEnvScript } from 'next-runtime-env';
+
+ import { Shell } from '@/shell/Shell'
+ import { CommandPaletteRoot } from '@/shell/CommandPalette/Root'
+@@ -36,6 +37,7 @@ export default function RootLayout({
+ isDarkModeEnabled() ? 'dark' : ''
+ }`}
+ >
++
+ Ozone
+
+
++
++
+
+ blueSkyUrlMatcher.test(url)
+
+diff --git a/package.json b/package.json
+index 8919841..750dce9 100644
+--- a/package.json
++++ b/package.json
+@@ -37,6 +37,7 @@
+ "kbar": "^0.1.0-beta.45",
+ "lande": "^1.0.10",
+ "next": "15.2.4",
++ "next-runtime-env": "^3.2.1",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-dropzone": "^14.3.5",
diff --git a/patching/121-ozone-constants-fix.patch b/patching/121-ozone-constants-fix.patch
new file mode 100644
index 0000000..42ba11d
--- /dev/null
+++ b/patching/121-ozone-constants-fix.patch
@@ -0,0 +1,99 @@
+--- a/lib/constants.ts
++++ b/lib/constants.ts
+@@ -1,29 +1,32 @@
++import { env } from 'next-runtime-env';
++
+ export const OAUTH_SCOPE = 'atproto transition:generic transition:chat.bsky'
+
+ export const OZONE_SERVICE_DID =
+- process.env.NEXT_PUBLIC_OZONE_SERVICE_DID || undefined
++ env('NEXT_PUBLIC_OZONE_SERVICE_DID') || undefined
+
+ export const OZONE_PUBLIC_URL =
+- process.env.NEXT_PUBLIC_OZONE_PUBLIC_URL || undefined
++ env('NEXT_PUBLIC_OZONE_PUBLIC_URL') || undefined
+
+ export const PLC_DIRECTORY_URL =
+- process.env.NEXT_PUBLIC_PLC_DIRECTORY_URL ||
++ env('NEXT_PUBLIC_PLC_DIRECTORY_URL') ||
+ (process.env.NODE_ENV === 'development'
+ ? 'http://localhost:2582'
+ : 'https://plc.directory')
+
+-export const QUEUE_CONFIG = process.env.NEXT_PUBLIC_QUEUE_CONFIG || '{}'
++export const QUEUE_CONFIG = env('NEXT_PUBLIC_QUEUE_CONFIG') || '{}'
+
+-export const QUEUE_SEED = process.env.NEXT_PUBLIC_QUEUE_SEED || ''
++export const QUEUE_SEED = env('NEXT_PUBLIC_QUEUE_SEED') || ''
+
++export const SOCIAL_APP_DOMAIN = env('NEXT_PUBLIC_SOCIAL_APP_DOMAIN') || 'bsky.app'
+ export const SOCIAL_APP_URL =
+- process.env.NEXT_PUBLIC_SOCIAL_APP_URL ||
++ env('NEXT_PUBLIC_SOCIAL_APP_URL') ||
+ (process.env.NODE_ENV === 'development'
+ ? 'http://localhost:2584'
+- : 'https://bsky.app')
++ : `https://${SOCIAL_APP_DOMAIN}`)
+
+ export const HANDLE_RESOLVER_URL =
+- process.env.NEXT_PUBLIC_HANDLE_RESOLVER_URL ||
++ env('NEXT_PUBLIC_HANDLE_RESOLVER_URL') ||
+ (process.env.NODE_ENV === 'development'
+ ? 'http://localhost:2584'
+ : 'https://api.bsky.app')
+@@ -36,25 +39,25 @@
+
+ export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process.env
+ .NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS
+- ? parseInt(process.env.NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS)
++ ? parseInt(env('NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'))
+ : 7
+
+ export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process.env
+ .NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS
+- ? parseInt(process.env.NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS)
++ ? parseInt(env('NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'))
+ : 30
+
+ export const DOMAINS_ALLOWING_EMAIL_COMMUNICATION = (
+- process.env.NEXT_PUBLIC_DOMAINS_ALLOWING_EMAIL_COMMUNICATION || ''
++ env('NEXT_PUBLIC_DOMAINS_ALLOWING_EMAIL_COMMUNICATION') || ''
+ ).split(',')
+
+ export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process.env
+ .NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD
+- ? parseInt(process.env.NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD)
++ ? parseInt(env('NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD'))
+ : Infinity
+
+ export const FALLBACK_VIDEO_URL = (
+- process.env.NEXT_PUBLIC_FALLBACK_VIDEO_URL || ''
++ env('NEXT_PUBLIC_FALLBACK_VIDEO_URL') || ''
+ ).split(':')
+
+ // strike to account suspension duration mapping (in hours)
+@@ -87,18 +90,18 @@
+
+ export const STRIKE_TO_SUSPENSION_DURATION_IN_HOURS =
+ parseStrikeSuspensionConfig(
+- process.env.NEXT_PUBLIC_STRIKE_SUSPENSION_CONFIG || '',
++ env('NEXT_PUBLIC_STRIKE_SUSPENSION_CONFIG') || '',
+ )
+
+ export const AUTOMATED_ACTION_EMAIL_IDS = {
+ warningWithTakedown:
+- process.env.NEXT_PUBLIC_WARNING_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID,
++ env('NEXT_PUBLIC_WARNING_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID'),
+ suspensionWithTakedown:
+- process.env.NEXT_PUBLIC_SUSPENSION_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID,
++ env('NEXT_PUBLIC_SUSPENSION_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID'),
+ suspensionWithoutTakedown:
+- process.env.NEXT_PUBLIC_SUSPENSION_WITHOUT_TAKEDOWN_EMAIL_TEMPLATE_ID,
++ env('NEXT_PUBLIC_SUSPENSION_WITHOUT_TAKEDOWN_EMAIL_TEMPLATE_ID'),
+ permanentTakedown:
+- process.env.NEXT_PUBLIC_PERMANENT_TAKEDOWN_EMAIL_TEMPLATE_ID,
++ env('NEXT_PUBLIC_PERMANENT_TAKEDOWN_EMAIL_TEMPLATE_ID'),
+ takedownWithoutStrike:
+- process.env.NEXT_PUBLIC_TAKEDOWN_WITHOUT_STRIKE_EMAIL_TEMPLATE_ID,
++ env('NEXT_PUBLIC_TAKEDOWN_WITHOUT_STRIKE_EMAIL_TEMPLATE_ID'),
+ }
diff --git a/patching/122-ozone-enable-daemon.diff b/patching/122-ozone-enable-daemon.diff
new file mode 100644
index 0000000..3b783df
--- /dev/null
+++ b/patching/122-ozone-enable-daemon.diff
@@ -0,0 +1,82 @@
+diff --git a/service/index.js b/service/index.js
+index 943c281..7721cd9 100644
+--- a/service/index.js
++++ b/service/index.js
+@@ -1,5 +1,7 @@
+ const next = require('next')
+-const {
++const ozone = require('@atproto/ozone')
++/*
++{
+ readEnv,
+ httpLogger,
+ envToCfg,
+@@ -7,6 +9,7 @@ const {
+ OzoneService,
+ Database,
+ } = require('@atproto/ozone')
++*/
+ const pkg = require('@atproto/ozone/package.json')
+
+ async function main() {
+@@ -16,37 +19,48 @@ async function main() {
+ const frontendHandler = frontend.getRequestHandler()
+ await frontend.prepare()
+ // backend
+- const env = readEnv()
++ const env = ozone.readEnv()
+ env.version ??= pkg.version
+- const config = envToCfg(env)
+- const secrets = envToSecrets(env)
++ const config = ozone.envToCfg(env)
++ const secrets = ozone.envToSecrets(env)
+ const migrate = process.env.OZONE_DB_MIGRATE === '1'
+ if (migrate) {
+- const db = new Database({
++ const db = new ozone.Database({
+ url: config.db.postgresUrl,
+ schema: config.db.postgresSchema,
+ })
+ await db.migrateToLatestOrThrow()
+ await db.close()
+ }
+- const ozone = await OzoneService.create(config, secrets)
++ const server = await ozone.OzoneService.create(config, secrets)
+ // setup handlers
+- ozone.app.get('/.well-known/ozone-metadata.json', (_req, res) => {
++ server.app.get('/.well-known/ozone-metadata.json', (_req, res) => {
+ return res.json({
+- did: ozone.ctx.cfg.service.did,
+- url: ozone.ctx.cfg.service.publicUrl,
+- publicKey: ozone.ctx.signingKey.did(),
++ did: server.ctx.cfg.service.did,
++ url: server.ctx.cfg.service.publicUrl,
++ publicKey: server.ctx.signingKey.did(),
+ })
+ })
+ // Note: We must use `use()` here. This should be the last middleware.
+- ozone.app.use((req, res) => {
++ server.app.use((req, res) => {
+ void frontendHandler(req, res, undefined)
+ })
+ // run
+- const httpServer = await ozone.start()
++ const httpServer = await server.start()
++ // starts: involve ops from atproto/packages/dev-env/src/ozone.ts >>>
++ ozone.httpLogger.info('starts ozone daemon')
++ const daemon = await ozone.OzoneDaemon.create(config, secrets)
++ await daemon.start()
++ //if (process.env.OZONE_ENABLE_EVENT_REVERSER != 'true') // atproto/services/ozone/daemon.js doesn't stop eventReverser
++ //{
++ // ozone.httpLogger.info('disable ozone daemon eventReverser')
++ // await daemon.ctx.eventReverser.destroy()
++ //}
++ // ends: involve ops from atproto/packages/dev-env/src/ozone.ts <<<
++
+ /** @type {import('net').AddressInfo} */
+ const addr = httpServer.address()
+- httpLogger.info(`Ozone is running at http://localhost:${addr.port}`)
++ ozone.httpLogger.info(`Ozone is running at http://localhost:${addr.port}`)
+ }
+
+ main().catch(console.error)
diff --git a/patching/130-atproto-ozone-enable-daemon-v2.patch b/patching/130-atproto-ozone-enable-daemon-v2.patch
new file mode 100644
index 0000000..792c655
--- /dev/null
+++ b/patching/130-atproto-ozone-enable-daemon-v2.patch
@@ -0,0 +1,28 @@
+--- a/services/ozone/api.js
++++ b/services/ozone/api.js
+@@ -23,6 +23,7 @@
+ Database,
+ OzoneService,
+ envToCfg,
++ OzoneDaemon,
+ envToSecrets,
+ httpLogger,
+ readEnv,
+@@ -79,10 +80,17 @@
+
+ httpLogger.info('ozone is running')
+
++ // Start OzoneDaemon for label events
++ httpLogger.info('starting ozone daemon')
++ const daemon = await OzoneDaemon.create(cfg, secrets)
++ await daemon.start()
++ httpLogger.info('ozone daemon is running')
++
+ // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)
+ process.on('SIGTERM', async () => {
+ httpLogger.info('ozone is stopping')
+
++ await daemon.destroy()
+ await ozone.destroy()
+
+ httpLogger.info('ozone is stopped')
diff --git a/patching/130-atproto-ozone-enable-daemon.patch b/patching/130-atproto-ozone-enable-daemon.patch
new file mode 100644
index 0000000..7a10d3d
--- /dev/null
+++ b/patching/130-atproto-ozone-enable-daemon.patch
@@ -0,0 +1,33 @@
+--- a/services/ozone/api.js
++++ b/services/ozone/api.js
+@@ -20,6 +20,7 @@ const {
+ MultiImageInvalidator,
+ } = require('@atproto/aws')
+ const {
+ Database,
+ OzoneService,
++ OzoneDaemon,
+ envToCfg,
+ envToSecrets,
+ httpLogger,
+@@ -76,10 +77,17 @@ const main = async () => {
+ const ozone = await OzoneService.create(cfg, secrets, { imgInvalidator })
+
+ await ozone.start()
+
+ httpLogger.info('ozone is running')
+
++ // Start OzoneDaemon for label events
++ httpLogger.info('starting ozone daemon')
++ const daemon = await OzoneDaemon.create(cfg, secrets)
++ await daemon.start()
++ httpLogger.info('ozone daemon is running')
++
+ // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)
+ process.on('SIGTERM', async () => {
+ httpLogger.info('ozone is stopping')
+
++ await daemon.destroy()
+ await ozone.destroy()
+
+ httpLogger.info('ozone is stopped')
diff --git a/patching/4367-atproto-services-bsky-api.diff b/patching/4367-atproto-services-bsky-api.diff
new file mode 100644
index 0000000..f887230
--- /dev/null
+++ b/patching/4367-atproto-services-bsky-api.diff
@@ -0,0 +1,160 @@
+--- a/services/bsky/api.js 2025-12-03 11:04:54
++++ b/services/bsky/api.js 2025-12-03 11:00:02
+@@ -1,62 +1,105 @@
+ /* eslint-env node */
+ /* eslint-disable import/order */
+-
++// https://github.com/bluesky-social/atproto/blob/main/services/bsky/api.js
+ 'use strict'
+
+-const dd = require('dd-trace')
++//const dd = require('dd-trace')
++//
++//dd.tracer
++// .init()
++// .use('http2', {
++// client: true, // calls into dataplane
++// server: false,
++// })
++// .use('express', {
++// hooks: {
++// request: (span, req) => {
++// maintainXrpcResource(span, req)
++// },
++// },
++// })
+
+-dd.tracer
+- .init()
+- .use('http2', {
+- client: true, // calls into dataplane
+- server: false,
+- })
+- .use('express', {
+- hooks: {
+- request: (span, req) => {
+- maintainXrpcResource(span, req)
+- },
+- },
+- })
+-
+ // modify tracer in order to track calls to dataplane as a service with proper resource names
+ const DATAPLANE_PREFIX = '/bsky.Service/'
+-const origStartSpan = dd.tracer._tracer.startSpan
+-dd.tracer._tracer.startSpan = function (name, options) {
+- if (
+- name !== 'http.request' ||
+- options?.tags?.component !== 'http2' ||
+- !options?.tags?.['http.url']
+- ) {
+- return origStartSpan.call(this, name, options)
+- }
+- const uri = new URL(options.tags['http.url'])
+- if (!uri.pathname.startsWith(DATAPLANE_PREFIX)) {
+- return origStartSpan.call(this, name, options)
+- }
+- options.tags['service.name'] = 'dataplane-bsky'
+- options.tags['resource.name'] = uri.pathname.slice(DATAPLANE_PREFIX.length)
+- return origStartSpan.call(this, name, options)
+-}
++//const origStartSpan = dd.tracer._tracer.startSpan
++//dd.tracer._tracer.startSpan = function (name, options) {
++// if (
++// name !== 'http.request' ||
++// options?.tags?.component !== 'http2' ||
++// !options?.tags?.['http.url']
++// ) {
++// return origStartSpan.call(this, name, options)
++// }
++// const uri = new URL(options.tags['http.url'])
++// if (!uri.pathname.startsWith(DATAPLANE_PREFIX)) {
++// return origStartSpan.call(this, name, options)
++// }
++// options.tags['service.name'] = 'dataplane-bsky'
++// options.tags['resource.name'] = uri.pathname.slice(DATAPLANE_PREFIX.length)
++// return origStartSpan.call(this, name, options)
++//}
+
+ // Tracer code above must come before anything else
+ const assert = require('node:assert')
+ const cluster = require('node:cluster')
+ const path = require('node:path')
+
+-const { BskyAppView, ServerConfig } = require('@atproto/bsky')
+-const { Secp256k1Keypair } = require('@atproto/crypto')
++const bsky = require('/app/packages/bsky') // import all bsky features
++const { Secp256k1Keypair } = require('/app/packages/crypto')
+
+ const main = async () => {
+ const env = getEnv()
+- const config = ServerConfig.readEnv()
++ const config = bsky.ServerConfig.readEnv()
+ assert(env.serviceSigningKey, 'must set BSKY_SERVICE_SIGNING_KEY')
+ const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey)
+- const bsky = BskyAppView.create({ config, signingKey })
+- await bsky.start()
++
++// starts: involve logics in packages/dev-env/src/bsky.ts >>>>>>>>>>>>>
++// Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..."
++ const migrationDb = new bsky.Database({
++ url: env.dbPostgresUrl,
++ schema: env.dbPostgresSchema,
++ })
++ if (env.migration) {
++ await migrationDb.migrateToOrThrow(env.migration)
++ } else {
++ await migrationDb.migrateToLatestOrThrow()
++ }
++ await migrationDb.close()
++
++ const db = new bsky.Database({
++ url: env.dbPostgresUrl,
++ schema: env.dbPostgresSchema,
++ poolSize: 10,
++ })
++
++ const dataplane = await bsky.DataPlaneServer.create(
++ db,
++ env.dataplanePort,
++ config.didPlcUrl
++ )
++
++ const bsync = await bsky.MockBsync.create(db, env.bsyncPort)
++
++// ends: involve logics in packages/dev-env/src/bsky.ts <<<<<<<<<<<<<
++
++ const server = bsky.BskyAppView.create({ config, signingKey })
++// starts: involve logics in packages/dev-env/src/bsky.ts >>>>>>>>>>>>>
++ const sub = new bsky.RepoSubscription({
++ service: env.repoProvider,
++ db,
++ idResolver: dataplane.idResolver,
++ background: new bsky.BackgroundQueue(db),
++ })
++// ends: involve logics in packages/dev-env/src/bsky.ts <<<<<<<<<<<<<
++ await server.start()
++ sub.start() // involve logics in packages/dev-env/src/bsky.ts
+ // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)
+ const shutdown = async () => {
+- await bsky.destroy()
++ await server.destroy()
++ await bsync.destroy()
++ await dataplane.destroy()
++ await sub.destroy()
++ await db.close()
+ }
+ process.on('SIGTERM', shutdown)
+ process.on('disconnect', shutdown) // when clustering
+@@ -64,6 +107,12 @@
+
+ const getEnv = () => ({
+ serviceSigningKey: process.env.BSKY_SERVICE_SIGNING_KEY || undefined,
++ dbPostgresUrl: process.env.BSKY_DB_POSTGRES_URL || undefined,
++ dbPostgresSchema: process.env.BSKY_DB_POSTGRES_SCHEMA || undefined,
++ dataplanePort : maybeParseInt(process.env.BSKY_DATAPLANE_PORT) || undefined,
++ bsyncPort : maybeParseInt(process.env.BSKY_BSYNC_PORT) || undefined,
++ migration: process.env.ENABLE_MIGRATIONS === 'true' || undefined,
++ repoProvider: process.env.BSKY_REPO_PROVIDER || undefined
+ })
+
+ const maybeParseInt = (str) => {
diff --git a/patching/4367-atproto-services-pds-index.diff b/patching/4367-atproto-services-pds-index.diff
new file mode 100644
index 0000000..a8165f8
--- /dev/null
+++ b/patching/4367-atproto-services-pds-index.diff
@@ -0,0 +1,20 @@
+--- a/services/pds/index.js 2025-12-03 11:04:54
++++ b/services/pds/index.js 2025-12-02 22:11:39
+@@ -1,5 +1,5 @@
+ /* eslint-env node */
+-
++// https://github.com/bluesky-social/atproto/blob/main/services/pds/index.js
+ 'use strict'
+
+ const {
+@@ -8,8 +8,8 @@
+ envToSecrets,
+ httpLogger,
+ readEnv,
+-} = require('@atproto/pds')
+-const pkg = require('@atproto/pds/package.json')
++} = require('/app/packages/pds')
++const pkg = require('/app/packages/pds/package.json')
+
+ const main = async () => {
+ const env = readEnv()
diff --git a/patching/8980-social-app-disable-external-services.diff b/patching/8980-social-app-disable-external-services.diff
new file mode 100644
index 0000000..74ffc22
--- /dev/null
+++ b/patching/8980-social-app-disable-external-services.diff
@@ -0,0 +1,17 @@
+--- a/src/state/geolocation/const.ts
++++ b/src/state/geolocation/const.ts
+@@ -3,9 +3,10 @@ import {BAPP_CONFIG_DEV_URL, IS_DEV} from '#/env'
+ import {type Device} from '#/storage'
+
+ export const IPCC_URL = `https://bsky.app/ipcc`
+-export const BAPP_CONFIG_URL_PROD = `https://ip.bsky.app/config`
+-export const BAPP_CONFIG_URL = IS_DEV
+- ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_URL_PROD)
+- : BAPP_CONFIG_URL_PROD
++// Disabled for self-hosted environment to avoid CORS errors
++// export const BAPP_CONFIG_URL_PROD = `https://ip.bsky.app/config`
++// export const BAPP_CONFIG_URL = IS_DEV
++// ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_URL_PROD)
++// : BAPP_CONFIG_URL_PROD
++export const BAPP_CONFIG_URL = null
+ export const GEOLOCATION_CONFIG_URL = BAPP_CONFIG_URL
diff --git a/patching/8980-social-app-disable-proxy.diff b/patching/8980-social-app-disable-proxy.diff
new file mode 100644
index 0000000..4bc454f
--- /dev/null
+++ b/patching/8980-social-app-disable-proxy.diff
@@ -0,0 +1,44 @@
+diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts
+index 36d19299b..ba095436a 100644
+--- a/src/state/session/agent.ts
++++ b/src/state/session/agent.ts
+@@ -39,7 +39,8 @@ export function createPublicAgent() {
+ configureModerationForGuest() // Side effect but only relevant for tests
+
+ const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE})
+- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
++ // Disable proxy for self-hosted environments
++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
+ return agent
+ }
+
+@@ -77,7 +78,8 @@ export async function createAgentAndResume(
+ }
+ }
+
+- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
++ // Disable proxy for self-hosted environments
++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
+
+ return agent.prepare(gates, moderation, onSessionChange)
+ }
+@@ -112,7 +114,8 @@ export async function createAgentAndLogin(
+ const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
+ const moderation = configureModerationForAccount(agent, account)
+
+- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
++ // Disable proxy for self-hosted environments
++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
+
+ return agent.prepare(gates, moderation, onSessionChange)
+ }
+@@ -201,7 +204,8 @@ export async function createAgentAndCreateAccount(
+ logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`})
+ }
+
+- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
++ // Disable proxy for self-hosted environments
++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
+
+ return agent.prepare(gates, moderation, onSessionChange)
+ }