Live video on the AT Protocol

oproxy: move to server-side long-lived oauth (#157)

* oauth: basic server oauth init

* oauth: successfully making posts and whatnot!

* docs: add oauth docs

* lol hax

* checkpoint

* oauth: PAR part works great

* model: oauth_session --> oauth_session_upstream

* oauth: remove weird xrpc endpoint

* oauth: getting to token creation!

* oauth: we're issuing tokens!

* oauth: we're generating a jwt woo

* build: move to streamplace indigo

* build: update Makefile to work with subproject atproto

* atproto: rewrite repo sync (no more partials)

* oauth: token issuance is happening!

* oauth: just one table plz

* atproto: working profile retrieval!

* oauth: refactor to wrapped oauth client

* oauth: working DPoP!

* oauth: roll back makedb changes

* oauth: implement refresh tokens

* oauth: add downstream authorization code

* oauth: implement revocation

* oproxy: big broken move of lots of files

* checkpoint

* oproxy: implemented PAR

* oproxy: implemented authorize

* oproxy: implement return

* oproxy: implement token issuance

* oproxy: implement token revocation

* oproxy: implement middleware

* oproxy: fixes

* oproxy: login works again!

* oproxy: cleanup

* oproxy: move to forked atproto library

* docs: we love this image, sorry i deleted it

* oproxy: somewhat-working DPoP nonce enforcement!

* oproxy: nonce working in corner cases

* oproxy: roll nonce on token requests

* spxrpc: add wildcard atproto function

* oproxy: implement upstream refresh

* login: ship basic login screen

* @streamplace/atproto-oauth-client-react-native: moving in

* oproxy: add PublicHost and whatnot

* oproxy: customization of redirect urls

* oproxy: restructure

* oproxy: display handle rather than did

* oproxy: working error handling

* oproxy: handle resolution, error handling

* app: lie about did:web too

* build: don't commit .tsbuildinfo

* build: unfork indigo

* docs: make directory if we run first

* Apply suggestions from code review

* prettier

authored by

Eli Mallon and committed by
GitHub
2c0ee687 f7a5155b

+3165 -337
+1
.gitignore
··· 18 18 *.heap 19 19 /api 20 20 oom 21 + *.tsbuildinfo
+18 -15
Makefile
··· 112 112 && sed -i.bak 's/AppBskyGraphBlock\.Main/AppBskyGraphBlock\.Record/' $$(find ./js/app/lexicons/types/place/stream -type f) \ 113 113 && sed -i.bak 's/PlaceStreamChatProfile\.Main/PlaceStreamChatProfile\.Record/' $$(find ./js/app/lexicons/types/place/stream -type f) \ 114 114 && sed -i.bak "s/import\ \*\ as\ AppBskyFeedDefs\ from\ '.\/defs'/import \{ AppBskyFeedDefs } from '@atproto\/api'/" $$(find ./js/app/lexicons/types -type f) \ 115 + && sed -i.bak "s/import\ \*\ as\ AppBskyActorDefs\ from\ '.\/defs'/import \{ AppBskyActorDefs } from '@atproto\/api'/" $$(find ./js/app/lexicons -type f) \ 115 116 && find . | grep bak$$ | xargs rm 116 117 117 118 .PHONY: md-lexicons 118 119 md-lexicons: 119 120 yarn exec lexmd \ 120 121 lexicons/place/stream \ 121 - js/docs/src/content/docs/lex-reference 122 + js/docs/src/content/docs/lex-reference \ 123 + && $(MAKE) fix 122 124 123 125 .PHONY: lexgen 124 126 lexgen: ··· 127 129 128 130 .PHONY: lexgen-types 129 131 lexgen-types: 130 - go run github.com/bluesky-social/indigo/cmd/lexgen --package streamplace \ 131 - --types-import place.stream:stream.place/streamplace/pkg/streamplace \ 132 - -outdir ./pkg/streamplace \ 133 - --prefix place.stream \ 132 + go run github.com/bluesky-social/indigo/cmd/lexgen \ 133 + -outdir ./pkg/spxrpc \ 134 134 --build-file util/lexgen-types.json \ 135 + --external-lexicons subprojects/atproto/lexicons \ 135 136 lexicons/place/stream \ 136 137 ./subprojects/atproto/lexicons 137 138 138 - .PHONY: ci-lexicons 139 - ci-lexicons: 140 - $(MAKE) lexicons \ 141 - && if ! git diff --exit-code >/dev/null; then echo "lexicons are out of date, run 'make lexicons' to fix"; exit 1; fi 142 - 143 139 .PHONY: lexgen-server 144 140 lexgen-server: 145 - mkdir -p ./pkg/spxrpc 146 - go run github.com/bluesky-social/indigo/cmd/lexgen --package spxrpc \ 141 + mkdir -p ./pkg/spxrpc \ 142 + && go run github.com/bluesky-social/indigo/cmd/lexgen \ 147 143 --gen-server \ 148 144 --types-import place.stream:stream.place/streamplace/pkg/streamplace \ 149 145 --types-import app.bsky:github.com/bluesky-social/indigo/api/bsky \ ··· 151 147 --types-import chat.bsky:github.com/bluesky-social/indigo/api/chat \ 152 148 --types-import tools.ozone:github.com/bluesky-social/indigo/api/ozone \ 153 149 -outdir ./pkg/spxrpc \ 154 - --prefix place.stream \ 155 - --build-file util/lexgen-server.json \ 150 + --build-file util/lexgen-types.json \ 151 + --external-lexicons subprojects/atproto/lexicons \ 152 + --package spxrpc \ 156 153 lexicons/place/stream \ 157 - lexicons/app/bsky 154 + lexicons/app/bsky \ 155 + lexicons/com/atproto 156 + 157 + .PHONY: ci-lexicons 158 + ci-lexicons: 159 + $(MAKE) lexicons \ 160 + && if ! git diff --exit-code >/dev/null; then echo "lexicons are out of date, run 'make lexicons' to fix"; exit 1; fi 158 161 159 162 .PHONY: test 160 163 test:
+13 -6
go.mod
··· 8 8 9 9 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4 10 10 11 + replace github.com/AxisCommunications/go-dpop => github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 12 + 13 + replace github.com/haileyok/atproto-oauth-golang => github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab 14 + 11 15 require ( 12 16 firebase.google.com/go/v4 v4.14.1 13 17 git.stream.place/streamplace/c2pa-go v0.7.0 14 18 github.com/99designs/gqlgen v0.17.64 19 + github.com/AxisCommunications/go-dpop v1.1.2 15 20 github.com/NYTimes/gziphandler v1.1.1 16 21 github.com/ThalesGroup/crypto11 v0.0.0-00010101000000-000000000000 17 22 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d 18 - github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 23 + github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb 19 24 github.com/decred/dcrd/dcrec/secp256k1 v1.0.4 20 25 github.com/dunglas/httpsfv v1.0.2 21 26 github.com/ethereum/go-ethereum v1.14.7 22 27 github.com/go-git/go-git/v5 v5.12.0 23 28 github.com/go-gst/go-glib v1.4.0 24 29 github.com/go-gst/go-gst v1.4.0 30 + github.com/golang-jwt/jwt/v5 v5.2.1 25 31 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 26 32 github.com/golang/glog v1.2.4 27 33 github.com/google/uuid v1.6.0 28 34 github.com/gorilla/websocket v1.5.3 35 + github.com/haileyok/atproto-oauth-golang v0.0.2 29 36 github.com/ipfs/go-cid v0.4.1 30 - github.com/ipfs/go-datastore v0.6.0 31 - github.com/ipfs/go-ipfs-blockstore v1.3.1 32 37 github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c 33 38 github.com/julienschmidt/httprouter v1.3.0 34 39 github.com/labstack/echo/v4 v4.13.3 ··· 52 57 go.opentelemetry.io/otel v1.35.0 53 58 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 54 59 go.opentelemetry.io/otel/sdk v1.35.0 60 + go.opentelemetry.io/otel/trace v1.35.0 55 61 go.uber.org/goleak v1.3.0 56 62 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 57 63 golang.org/x/image v0.22.0 58 64 golang.org/x/net v0.35.0 59 65 golang.org/x/sync v0.11.0 60 66 golang.org/x/term v0.29.0 67 + golang.org/x/time v0.8.0 61 68 golang.org/x/tools v0.25.0 62 69 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 63 70 google.golang.org/api v0.189.0 ··· 130 137 github.com/go-logr/stdr v1.2.2 // indirect 131 138 github.com/go-sql-driver/mysql v1.8.1 // indirect 132 139 github.com/goccy/go-json v0.10.2 // indirect 133 - github.com/gocql/gocql v0.0.0-00010101000000-000000000000 // indirect 140 + github.com/gocql/gocql v1.7.0 // indirect 134 141 github.com/gogo/protobuf v1.3.2 // indirect 135 142 github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 136 143 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect ··· 153 160 github.com/ipfs/bbloom v0.0.4 // indirect 154 161 github.com/ipfs/go-block-format v0.2.0 // indirect 155 162 github.com/ipfs/go-blockservice v0.5.2 // indirect 163 + github.com/ipfs/go-datastore v0.6.0 // indirect 164 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 156 165 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 157 166 github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 158 167 github.com/ipfs/go-ipfs-util v0.0.3 // indirect ··· 252 261 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 253 262 go.opentelemetry.io/otel/metric v1.35.0 // indirect 254 263 go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 255 - go.opentelemetry.io/otel/trace v1.35.0 // indirect 256 264 go.opentelemetry.io/proto/otlp v1.5.0 // indirect 257 265 go.uber.org/atomic v1.11.0 // indirect 258 266 go.uber.org/multierr v1.11.0 // indirect ··· 261 269 golang.org/x/mod v0.21.0 // indirect 262 270 golang.org/x/oauth2 v0.26.0 // indirect 263 271 golang.org/x/text v0.22.0 // indirect 264 - golang.org/x/time v0.8.0 // indirect 265 272 google.golang.org/appengine/v2 v2.0.2 // indirect 266 273 google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect 267 274 google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
+10
go.sum
··· 74 74 github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 75 75 github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 76 76 github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 77 + github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb h1:qfkNGUq//RzFBRFNBVwRKcFwyXS+1jQ5VnLW9Jfh6Vc= 78 + github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 77 79 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 78 80 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 79 81 github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= ··· 208 210 github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 209 211 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= 210 212 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 213 + github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 214 + github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 211 215 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 212 216 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 213 217 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= ··· 387 391 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 388 392 github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:2n/HCxBM7oa5PNCPKIhV26EtJkaPXFfcVojPAT3ujTU= 389 393 github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:B9OPZOhZ3FIi6bu54lAgCMzXLh11Z7ilr3rOr/ClP+E= 394 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 395 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 390 396 github.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds= 391 397 github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 392 398 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= ··· 612 618 github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= 613 619 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 614 620 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 621 + github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab h1:cXikoKjZxnUiRQ+IY4+3XU4eJL1g0JJ5TzRA2keUNBg= 622 + github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 623 + github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc= 624 + github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0= 615 625 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 616 626 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 617 627 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+1 -1
js/app/.env.development
··· 1 1 EXPO_PUBLIC_STREAMPLACE_URL=http://127.0.0.1:38080 2 - EXPO_PUBLIC_WEB_TRY_LOCAL=false 2 + EXPO_PUBLIC_WEB_TRY_LOCAL=true 3 3 EXPO_USE_METRO_WORKSPACE_ROOT=1
+70 -93
js/app/components/login/login.tsx
··· 1 + import { AtpBaseClient } from "lexicons"; 1 2 import NameColorPicker from "components/name-color-picker/name-color-picker"; 2 3 import { 3 4 login, 4 5 logout, 6 + selectIsReady, 5 7 selectLogin, 6 8 selectPDS, 7 9 selectUserProfile, 8 10 setPDS, 9 11 } from "features/bluesky/blueskySlice"; 10 - import { useState } from "react"; 11 - import { Keyboard } from "react-native"; 12 + import { useEffect, useState } from "react"; 13 + import { Keyboard, KeyboardAvoidingView } from "react-native"; 12 14 import { useAppDispatch, useAppSelector } from "store/hooks"; 13 - import { Button, Form, H3, Input, Sheet, Spinner, Text, View } from "tamagui"; 15 + import { 16 + Button, 17 + Form, 18 + H3, 19 + H5, 20 + Input, 21 + Sheet, 22 + Spinner, 23 + Text, 24 + View, 25 + } from "tamagui"; 26 + import useStreamplaceNode from "hooks/useStreamplaceNode"; 27 + import Loading from "components/loading/loading"; 28 + import { useToastController } from "@tamagui/toast"; 14 29 15 30 export default function Login() { 16 31 const dispatch = useAppDispatch(); ··· 18 33 const pds = useAppSelector(selectPDS); 19 34 const loginState = useAppSelector(selectLogin); 20 35 const [open, setOpen] = useState(false); 36 + const [handle, setHandle] = useState(""); 37 + const isReady = useAppSelector(selectIsReady); 38 + const toast = useToastController(); 21 39 const onOpenChange = (open: boolean) => { 22 40 setOpen(open); 23 41 Keyboard.dismiss(); 24 42 }; 43 + 44 + useEffect(() => { 45 + if (loginState?.error) { 46 + toast.show("Login error", { 47 + message: loginState.error, 48 + }); 49 + } 50 + }, [loginState?.error]); 51 + 52 + if (!isReady) { 53 + return ( 54 + <View f={1} jc="center" ai="stretch" gap="$3"> 55 + <Loading /> 56 + </View> 57 + ); 58 + } 25 59 26 60 if (userProfile) { 27 61 return ( ··· 51 85 } 52 86 53 87 return ( 54 - <View f={1} jc="center" ai="center" backgroundColor="$gray1" padding="$4"> 55 - <ChangePDS open={open} onOpenChange={onOpenChange} /> 56 - {/* <Text>{error}</Text> */} 57 - <Button 88 + <KeyboardAvoidingView style={{ flex: 1 }} behavior="padding"> 89 + <View 90 + f={1} 91 + jc="center" 92 + ai="center" 93 + backgroundColor="$gray1" 94 + padding="$4" 58 95 width="100%" 59 - onPress={async () => { 60 - await dispatch(login(`https://${pds.url}`)); 61 - }} 62 - margin="$4" 63 - backgroundColor="$accentColor" 64 - disabled={loginState.loading} 65 - > 66 - <Text> 67 - {loginState.loading ? <Spinner /> : `Log in with ${pds.url}`} 68 - </Text> 69 - </Button> 70 - <Button width="100%" onPress={() => onOpenChange(true)} margin="$4"> 71 - Change PDS 72 - </Button> 73 - </View> 74 - ); 75 - } 76 - 77 - export function ChangePDS({ 78 - open, 79 - onOpenChange, 80 - }: { 81 - open: boolean; 82 - onOpenChange: (open: boolean) => void; 83 - }) { 84 - const pds = useAppSelector(selectPDS); 85 - const dispatch = useAppDispatch(); 86 - const [newURL, setNewURL] = useState(""); 87 - return ( 88 - <Sheet 89 - forceRemoveScrollEnabled={open} 90 - modal={true} 91 - open={open} 92 - onOpenChange={onOpenChange} 93 - dismissOnSnapToBottom 94 - zIndex={100_000} 95 - animation="medium" 96 - > 97 - <Sheet.Overlay 98 - animation="lazy" 99 - enterStyle={{ opacity: 0 }} 100 - exitStyle={{ opacity: 0 }} 101 - /> 102 - 103 - <Sheet.Handle /> 104 - <Sheet.Frame 105 - padding="$4" 106 - justifyContent="center" 107 - alignItems="center" 108 - gap="$5" 109 - backgroundColor="$accentBackground" 96 + maxWidth={800} 97 + marginHorizontal="auto" 110 98 > 111 99 <Form 112 - justifyContent="center" 113 - alignItems="stretch" 114 100 width="100%" 115 - gap="$5" 116 - f={1} 117 - display="flex" 101 + maxWidth={800} 102 + jc="center" 103 + ai="center" 118 104 onSubmit={async () => { 119 - await dispatch(setPDS(newURL)); 120 - onOpenChange(false); 105 + await dispatch(login(handle)); 121 106 }} 122 107 > 123 - {/* <Button 124 - size="$6" 125 - circular 126 - icon={ChevronDown} 127 - onPress={() => onOpenChange(false)} 128 - /> */} 129 - <H3 width="100%" textAlign="left"> 130 - Custom PDS URL: 131 - </H3> 108 + <H3>Log in with ATProto | Bluesky</H3> 109 + <H5 alignSelf="flex-start">Handle:</H5> 132 110 <Input 133 111 width="100%" 134 - placeholder="example.com" 135 - textContentType="URL" 112 + placeholder="example.bsky.social" 113 + value={handle} 114 + onChangeText={(text) => setHandle(text)} 136 115 keyboardType="url" 137 - value={newURL} 138 - onChangeText={(text) => setNewURL(text)} 116 + autoCapitalize="none" 117 + autoComplete="off" 118 + autoCorrect={false} 139 119 /> 140 - <Form.Trigger asChild disabled={pds.loading}> 141 - <Button width="100%" size="$6" backgroundColor="$accentColor"> 142 - {pds.loading ? <Spinner /> : <Text>Save</Text>} 120 + <Form.Trigger asChild> 121 + <Button 122 + width="100%" 123 + margin="$4" 124 + backgroundColor="$accentColor" 125 + disabled={loginState.loading} 126 + > 127 + <Text> 128 + {loginState.loading ? <Spinner /> : `Log in with ATProto`} 129 + </Text> 143 130 </Button> 144 131 </Form.Trigger> 145 - <Button 146 - width="100%" 147 - size="$6" 148 - onPress={async () => { 149 - await dispatch(setPDS("bsky.social")); 150 - onOpenChange(false); 151 - }} 152 - > 153 - <Text>Use default (bsky.social)</Text> 154 - </Button> 155 132 </Form> 156 - </Sheet.Frame> 157 - </Sheet> 133 + </View> 134 + </KeyboardAvoidingView> 158 135 ); 159 136 }
+2 -1
js/app/features/bluesky/blueskyProvider.tsx
··· 6 6 getProfile, 7 7 loadOAuthClient, 8 8 oauthCallback, 9 + oauthError, 9 10 selectOAuthSession, 10 11 selectUserProfile, 11 12 } from "./blueskySlice"; ··· 31 32 setLastLink(url); 32 33 if (url.includes("?")) { 33 34 const params = new URLSearchParams(url.split("?")[1]); 34 - if (params.has("code") && params.has("state") && params.has("iss")) { 35 + if (params.has("error") || params.has("code")) { 35 36 dispatch(oauthCallback(url)); 36 37 } 37 38 }
+37 -6
js/app/features/bluesky/blueskySlice.tsx
··· 144 144 }, 145 145 ), 146 146 147 + oauthError: create.reducer( 148 + ( 149 + state, 150 + { payload }: { payload: { error: string; description: string } }, 151 + ) => { 152 + return { 153 + ...state, 154 + login: { 155 + loading: false, 156 + error: payload.description || payload.error, 157 + }, 158 + status: "loggedOut", 159 + }; 160 + }, 161 + ), 162 + 147 163 login: create.asyncThunk( 148 - async (pds: string, thunkAPI) => { 164 + async (handle: string, thunkAPI) => { 149 165 let { bluesky } = thunkAPI.getState() as { 150 166 bluesky: BlueskyState; 151 167 }; ··· 156 172 if (!bluesky.client) { 157 173 throw new Error("No client"); 158 174 } 159 - const u = await bluesky.client.authorize(pds); 175 + const u = await bluesky.client.authorize(handle, {}); 160 176 thunkAPI.dispatch(openLoginLink(u.toString())); 161 177 // cheeky 500ms delay so you don't see the text flash back 162 - await new Promise((resolve) => setTimeout(resolve, 500)); 178 + await new Promise((resolve) => setTimeout(resolve, 5000)); 163 179 }, 164 180 { 165 181 pending: (state) => { ··· 182 198 }; 183 199 }, 184 200 rejected: (state, action) => { 201 + console.error("login rejected", action.error); 185 202 return { 186 203 ...state, 187 204 login: { ··· 215 232 ...state, 216 233 oauthSession: null, 217 234 pdsAgent: null, 235 + status: "loggedOut", 218 236 }; 219 237 }, 220 238 rejected: (state) => { ··· 253 271 }, 254 272 rejected: (state, action) => { 255 273 clearQueryParams(); 274 + return { 275 + ...state, 276 + status: "loggedOut", 277 + }; 256 278 // state.status = "failed"; 257 279 }, 258 280 }, ··· 266 288 } 267 289 const params = new URLSearchParams(url.split("?")[1]); 268 290 if (!(params.has("code") && params.has("state") && params.has("iss"))) { 291 + if (params.has("error")) { 292 + thunkAPI.dispatch( 293 + oauthError({ 294 + error: params.get("error") ?? "", 295 + description: params.get("error_description") ?? "", 296 + }), 297 + ); 298 + } 269 299 throw new Error("Missing params, got: " + url); 270 300 } 271 301 const { bluesky } = thunkAPI.getState() as { ··· 659 689 }; 660 690 }, 661 691 rejected: (state, action) => { 662 - console.error("getProfile rejected", action.error); 692 + console.error("createStreamKeyRecord rejected", action.error); 663 693 // state.status = "failed"; 664 694 }, 665 695 }, ··· 775 805 }; 776 806 }, 777 807 rejected: (state, action) => { 778 - console.error("getProfile rejected", action.error); 808 + console.error("createLivestreamRecord rejected", action.error); 779 809 return { 780 810 ...state, 781 811 newLivestream: { ··· 912 942 }; 913 943 }, 914 944 rejected: (state, action) => { 915 - console.error("getProfile rejected", action.error); 945 + console.error("createChatProfileRecord rejected", action.error); 916 946 return { 917 947 ...state, 918 948 chatProfile: { ··· 1048 1078 golivePost, 1049 1079 oauthCallback, 1050 1080 setPDS, 1081 + oauthError, 1051 1082 createStreamKeyRecord, 1052 1083 clearStreamKeyRecord, 1053 1084 createLivestreamRecord,
+1 -1
js/app/features/bluesky/blueskyTypes.tsx
··· 1 - import { OAuthSession } from "@aquareum/atproto-oauth-client-react-native"; 1 + import { OAuthSession } from "@streamplace/atproto-oauth-client-react-native"; 2 2 import { Agent } from "@atproto/api"; 3 3 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 4 4 import { StreamKey } from "features/base/baseSlice";
+39 -3
js/app/features/bluesky/oauthClient.tsx
··· 2 2 ClientMetadata, 3 3 clientMetadataSchema, 4 4 ReactNativeOAuthClient, 5 - } from "@aquareum/atproto-oauth-client-react-native"; 5 + } from "@streamplace/atproto-oauth-client-react-native"; 6 6 import Constants from "expo-constants"; 7 7 import { Platform } from "react-native"; 8 + import { isWeb } from "tamagui"; 8 9 9 10 export type StreamplaceOAuthClient = Omit< 10 11 ReactNativeOAuthClient, ··· 58 59 dpop_bound_access_tokens: true, 59 60 }; 60 61 } else { 62 + const redirectURI = isWeb 63 + ? `${streamplaceUrl}/login` 64 + : `${streamplaceUrl}/api/app-return`; 61 65 const res = await fetch( 62 - `${streamplaceUrl}/api/atproto-oauth/${Platform.OS}`, 66 + `${streamplaceUrl}/oauth/downstream/client-metadata.json?redirect_uri=${encodeURIComponent(redirectURI)}`, 63 67 ); 64 68 meta = await res.json(); 65 69 } 66 70 clientMetadataSchema.parse(meta); 67 71 return new ReactNativeOAuthClient({ 68 - handleResolver: "https://bsky.social", // backend instances should use a DNS based resolver 72 + fetch: async (input, init) => { 73 + // Normalize input to a Request object 74 + let request: Request; 75 + if (typeof input === "string" || input instanceof URL) { 76 + request = new Request(input, init); 77 + } else { 78 + request = input; 79 + } 80 + 81 + // Lie to the oauth client and use our upstream server instead 82 + if ( 83 + request.url.includes("plc.directory") || 84 + request.url.endsWith("did.json") 85 + ) { 86 + const res = await fetch(request, init); 87 + if (!res.ok) { 88 + return res; 89 + } 90 + const data = await res.json(); 91 + const service = data.service.find((s: any) => s.id === "#atproto_pds"); 92 + if (!service) { 93 + return res; 94 + } 95 + service.serviceEndpoint = streamplaceUrl; 96 + return new Response(JSON.stringify(data), { 97 + status: res.status, 98 + headers: res.headers, 99 + }); 100 + } 101 + 102 + return fetch(request, init); 103 + }, 104 + handleResolver: streamplaceUrl, 69 105 responseMode: "query", // or "fragment" (frontend only) or "form_post" (backend only) 70 106 71 107 // These must be the same metadata as the one exposed on the
+1 -1
js/app/package.json
··· 25 25 "preset": "jest-expo" 26 26 }, 27 27 "dependencies": { 28 - "@aquareum/atproto-oauth-client-react-native": "^0.0.1", 29 28 "@atproto-labs/pipe": "^0.1.0", 30 29 "@atproto/crypto": "^0.4.2", 31 30 "@atproto/jwk-jose": "^0.1.2", ··· 40 39 "@react-navigation/native": "^6.1.18", 41 40 "@react-navigation/native-stack": "^6.11.0", 42 41 "@reduxjs/toolkit": "^2.3.0", 42 + "@streamplace/atproto-oauth-client-react-native": "workspace:*", 43 43 "@tamagui/config": "^1.123.17", 44 44 "@tamagui/lucide-icons": "^1.123.17", 45 45 "@tamagui/toast": "^1.123.17",
+3
js/atproto-oauth-client-react-native/.gitignore
··· 1 + node_modules 2 + dist 3 + tsconfig.build.tsbuildinfo
+89
js/atproto-oauth-client-react-native/README.md
··· 1 + # atproto OAuth Client for React Native 2 + 3 + This package implements an atproto OAuth client usable on the React Native 4 + platform. It uses [react-native-quick-crypto] for cryptographic operations and 5 + [expo-sqlite] for persistence. Its usage is very similar to the atproto OAuth 6 + client for the browser, so refer to that [README] and [example] for general 7 + usage. Some differences are noted below. 8 + 9 + ## expo-sqlite 10 + 11 + This library uses [expo-sqlite] to store the OAuth state and session data in a 12 + SQLite database. The schema is automatically created when the client is 13 + instantiated. 14 + 15 + Because this database is storing sensitive cryptographic keys, it is highly 16 + reccomended to use the optional SQLCipher extension. This can be accomplished in 17 + your app.json file: 18 + 19 + ```json 20 + { 21 + "expo": { 22 + "plugins": [ 23 + [ 24 + "expo-sqlite", 25 + { 26 + "useSQLCipher": true 27 + } 28 + ] 29 + ] 30 + } 31 + } 32 + ``` 33 + 34 + ## Login and session restore flow 35 + 36 + The basic login flow will involve popping up a web browser and allowing users to 37 + authenticate with their selected PDS. This can be accomplished with the 38 + `expo-web-browser` library: 39 + 40 + ```tsx 41 + import { openAuthSessionAsync } from "expo-web-browser"; 42 + 43 + // inside your login onPress, perhaps: 44 + const loginUrl = await oauthClient.authorize(pds); 45 + const res = await openAuthSessionAsync(loginUrl); 46 + if (res.type === "success") { 47 + const params = new URLSearchParams(url.split("?")[1]); 48 + const { session, state } = await oauthClient.callback(params); 49 + console.log(`logged in as ${session.sub}`); 50 + } 51 + ``` 52 + 53 + ## Development on localhost 54 + 55 + The atproto OAuth specification has a special case for development on localhost, 56 + but it is required to use a redirectUrl that returns to `127.0.0.1` or `[::1]`. 57 + This prevents the localhost OAuth flow from returning you directly to your app. 58 + As a workaround, you can host a static HTML server on 127.0.0.1 that recieves 59 + the incoming OAuth callback and then redirects to your app. (If you have a web 60 + version of your React Native app, you can just use that.) Such a redirect page 61 + might look something like this: 62 + 63 + ```tsx 64 + import { useEffect } from "react"; 65 + import { View, Text } from "react-native"; 66 + 67 + export default function AppReturnScreen({ route }) { 68 + useEffect(() => { 69 + document.location.href = `com.example.app:/app-return${document.location.search}`; 70 + }, []); 71 + return ( 72 + <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}> 73 + <Text>Redirecting you back to the app...</Text> 74 + </View> 75 + ); 76 + } 77 + ``` 78 + 79 + This flow will work on the iOS simulator and on Android devices provided you've 80 + forwarded the port with `adb reverse`. For testing on iOS hardware, you'll 81 + instead need to set up TLS. 82 + 83 + [react-native-quick-crypto]: 84 + https://github.com/margelo/react-native-quick-crypto 85 + [expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/ 86 + [README]: 87 + https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser 88 + [example]: 89 + https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
+56
js/atproto-oauth-client-react-native/package.json
··· 1 + { 2 + "name": "@streamplace/atproto-oauth-client-react-native", 3 + "version": "0.0.2", 4 + "license": "MIT", 5 + "description": "ATProto OAuth client for React Native", 6 + "keywords": [ 7 + "atproto", 8 + "oauth", 9 + "client", 10 + "node" 11 + ], 12 + "homepage": "https://atproto.com", 13 + "repository": { 14 + "type": "git", 15 + "url": "https://github.com/bluesky-social/atproto", 16 + "directory": "packages/oauth/oauth-client-react-native" 17 + }, 18 + "type": "commonjs", 19 + "main": "dist/index.js", 20 + "types": "dist/index.d.ts", 21 + "exports": { 22 + ".": { 23 + "types": "./dist/index.d.ts", 24 + "default": "./dist/index.js" 25 + } 26 + }, 27 + "files": [ 28 + "dist" 29 + ], 30 + "dependencies": { 31 + "@atproto-labs/did-resolver": "0.1.5", 32 + "@atproto-labs/handle-resolver-node": "0.1.7", 33 + "@atproto-labs/simple-store": "0.1.1", 34 + "@atproto-labs/simple-store-memory": "0.1.1", 35 + "@atproto/did": "0.1.3", 36 + "@atproto/jwk": "0.1.1", 37 + "@atproto/jwk-jose": "0.1.2", 38 + "@atproto/jwk-webcrypto": "0.1.2", 39 + "@atproto/oauth-client": "0.3.2", 40 + "@atproto/oauth-client-browser": "0.3.2", 41 + "@atproto/oauth-types": "0.2.1", 42 + "abortcontroller-polyfill": "^1.7.6", 43 + "event-target-shim": "^6.0.2", 44 + "expo-sqlite": "^15.0.3", 45 + "jose": "^5.2.0", 46 + "react-native-quick-crypto": "^0.7.7" 47 + }, 48 + "devDependencies": { 49 + "@types/node": "^22.10.1", 50 + "typescript": "^5.6.3" 51 + }, 52 + "scripts": { 53 + "build": "tsc --build tsconfig.build.json", 54 + "postinstall": "yarn run build" 55 + } 56 + }
+4
js/atproto-oauth-client-react-native/src/index.ts
··· 1 + import "./polyfills"; 2 + 3 + export * from "@atproto/oauth-client"; 4 + export * from "./oauth-client-react-native";
+188
js/atproto-oauth-client-react-native/src/oauth-client-react-native.native.ts
··· 1 + import { SimpleStore } from "@atproto-labs/simple-store"; 2 + import { jwkValidator } from "@atproto/jwk"; 3 + import { JoseKey } from "@atproto/jwk-jose"; 4 + import { 5 + InternalStateData, 6 + OAuthClient, 7 + OAuthClientFetchMetadataOptions, 8 + OAuthClientOptions, 9 + OAuthSession, 10 + Session, 11 + SessionStore, 12 + StateStore, 13 + } from "@atproto/oauth-client"; 14 + import { JWK } from "jose"; 15 + import QuickCrypto from "react-native-quick-crypto"; 16 + import { 17 + CryptoKey, 18 + SubtleAlgorithm, 19 + } from "react-native-quick-crypto/lib/typescript/src/keys"; 20 + import { JoseKeyStore, SQLiteKVStore } from "./sqlite-keystore"; 21 + 22 + export type ReactNativeOAuthClientOptions = Omit< 23 + OAuthClientOptions, 24 + // Provided by this lib 25 + | "runtimeImplementation" 26 + // Provided by this lib but can be overridden 27 + | "sessionStore" 28 + | "stateStore" 29 + > & { 30 + sessionStore?: SessionStore; 31 + stateStore?: StateStore; 32 + didStore?: SimpleStore<string, string>; 33 + }; 34 + 35 + export type ReactNativeOAuthClientFromMetadataOptions = 36 + OAuthClientFetchMetadataOptions & 37 + Omit<ReactNativeOAuthClientOptions, "clientMetadata">; 38 + 39 + export class ReactNativeOAuthClient extends OAuthClient { 40 + didStore: SimpleStore<string, string>; 41 + 42 + static async fromClientId( 43 + options: ReactNativeOAuthClientFromMetadataOptions, 44 + ) { 45 + const clientMetadata = await OAuthClient.fetchMetadata(options); 46 + return new ReactNativeOAuthClient({ ...options, clientMetadata }); 47 + } 48 + 49 + constructor({ 50 + fetch, 51 + responseMode = "query", 52 + 53 + ...options 54 + }: ReactNativeOAuthClientOptions) { 55 + if (!options.stateStore) { 56 + options.stateStore = new JoseKeyStore<InternalStateData>( 57 + new SQLiteKVStore("state"), 58 + ); 59 + } 60 + if (!options.sessionStore) { 61 + options.sessionStore = new JoseKeyStore<Session>( 62 + new SQLiteKVStore("session"), 63 + ); 64 + } 65 + if (!options.didStore) { 66 + options.didStore = new SQLiteKVStore("did"); 67 + } 68 + super({ 69 + ...options, 70 + 71 + sessionStore: options.sessionStore, 72 + stateStore: options.stateStore, 73 + fetch, 74 + responseMode, 75 + runtimeImplementation: { 76 + createKey: async (algs): Promise<JoseKey> => { 77 + console.log("GOT HEREEEE!"); 78 + const errors: unknown[] = []; 79 + for (const alg of algs) { 80 + try { 81 + let subtle = QuickCrypto?.webcrypto?.subtle; 82 + const subalg = toSubtleAlgorithm(alg); 83 + const keyPair = (await subtle.generateKey(subalg, true, [ 84 + "sign", 85 + "verify", 86 + ])) as CryptoKeyPair; 87 + 88 + const ex = (await subtle.exportKey( 89 + "jwk", 90 + keyPair.privateKey as unknown as CryptoKey, 91 + )) as JWK; 92 + ex.alg = alg; 93 + // these have trailing periods sometimes for some reason 94 + for (const k of ["x", "y", "d"]) { 95 + if (ex[k].endsWith(".")) { 96 + ex[k] = ex[k].slice(0, -1); 97 + } 98 + } 99 + 100 + // RNQC doesn't give us a kid, so let's do a quick hash of the key 101 + const kid = QuickCrypto.createHash("sha256") 102 + .update(JSON.stringify(ex)) 103 + .digest("hex"); 104 + const use = "sig"; 105 + 106 + return new JoseKey(jwkValidator.parse({ ...ex, kid, use })); 107 + } catch (err) { 108 + errors.push(err); 109 + } 110 + } 111 + throw new Error("None of the algorithms worked"); 112 + }, 113 + getRandomValues: (length) => 114 + new Uint8Array(QuickCrypto.randomBytes(length)), 115 + digest: (bytes, algorithm) => 116 + QuickCrypto.createHash(algorithm.name) 117 + .update(bytes as unknown as ArrayBuffer) 118 + .digest(), 119 + }, 120 + clientMetadata: options.clientMetadata, 121 + }); 122 + this.didStore = options.didStore; 123 + } 124 + 125 + async init(refresh?: boolean) { 126 + const sub = await this.didStore.get(`(sub)`); 127 + if (sub) { 128 + try { 129 + const session = await this.restore(sub, refresh); 130 + return { session }; 131 + } catch (err) { 132 + this.didStore.del(`(sub)`); 133 + throw err; 134 + } 135 + } 136 + } 137 + 138 + async callback(params: URLSearchParams): Promise<{ 139 + session: OAuthSession; 140 + state: string | null; 141 + }> { 142 + const { session, state } = await super.callback(params); 143 + await this.didStore.set(`(sub)`, session.sub); 144 + return { session, state }; 145 + } 146 + } 147 + 148 + export function toSubtleAlgorithm( 149 + alg: string, 150 + crv?: string, 151 + options?: { modulusLength?: number }, 152 + ): SubtleAlgorithm { 153 + switch (alg) { 154 + case "PS256": 155 + case "PS384": 156 + case "PS512": 157 + return { 158 + name: "RSA-PSS", 159 + hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`, 160 + modulusLength: options?.modulusLength ?? 2048, 161 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 162 + }; 163 + case "RS256": 164 + case "RS384": 165 + case "RS512": 166 + return { 167 + name: "RSASSA-PKCS1-v1_5", 168 + hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`, 169 + modulusLength: options?.modulusLength ?? 2048, 170 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 171 + }; 172 + case "ES256": 173 + case "ES384": 174 + return { 175 + name: "ECDSA", 176 + namedCurve: `P-${alg.slice(-3) as "256" | "384"}`, 177 + }; 178 + case "ES512": 179 + return { 180 + name: "ECDSA", 181 + namedCurve: "P-521", 182 + }; 183 + default: 184 + // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 185 + 186 + throw new TypeError(`Unsupported alg "${alg}"`); 187 + } 188 + }
+4
js/atproto-oauth-client-react-native/src/oauth-client-react-native.ts
··· 1 + // browser fallback 2 + // export * from "@atproto/oauth-client-browser"; 3 + import { BrowserOAuthClient } from "@atproto/oauth-client-browser"; 4 + export { BrowserOAuthClient as ReactNativeOAuthClient };
+36
js/atproto-oauth-client-react-native/src/polyfills.native.ts
··· 1 + import { Event, EventTarget } from "event-target-shim"; 2 + import { install as installRNQC } from "react-native-quick-crypto"; 3 + 4 + // Polyfill for the `throwIfAborted` method of the AbortController 5 + // used in @atproto/oauth-client 6 + import "abortcontroller-polyfill/dist/polyfill-patch-fetch"; 7 + 8 + // Polyfill for jose. It tries to detect whether it's been passed a CryptoKey 9 + // instance, and isn't willing to accept RNQC's equivalent. So, this ensures that 10 + // `key instanceof CryptoKey` will always be true. 11 + // @ts-ignore 12 + global.CryptoKey = Object; 13 + 14 + // This is needed to populate the `crypto` global for jose's export here 15 + // https://github.com/panva/jose/blob/1e8b430b08a18a18883a69e7991832c9c602ca1a/src/runtime/browser/webcrypto.ts#L1 16 + installRNQC(); 17 + 18 + // These two are needed for @atproto/oauth-client's `CustomEventTarget` to work. 19 + // @ts-ignore 20 + global.EventTarget = EventTarget; 21 + // @ts-ignore 22 + global.Event = Event; 23 + 24 + // And finally, this happens on React Native with every possible input: 25 + // URL.canParse("http://example.com") => false 26 + // I do not know why. Used in @atproto/oauth and @atproto/common-web 27 + if (!URL.canParse("http://example.com")) { 28 + URL.canParse = (url: string | URL, base?: string) => { 29 + try { 30 + new URL(url, base); 31 + return true; 32 + } catch (e) { 33 + return false; 34 + } 35 + }; 36 + }
js/atproto-oauth-client-react-native/src/polyfills.ts

This is a binary file and will not be displayed.

+69
js/atproto-oauth-client-react-native/src/sqlite-keystore.ts
··· 1 + import { SimpleStore } from "@atproto-labs/simple-store"; 2 + import { jwkValidator, Key } from "@atproto/jwk"; 3 + import { JoseKey } from "@atproto/jwk-jose"; 4 + import Storage from "expo-sqlite/kv-store"; 5 + 6 + interface HasDPoPKey { 7 + dpopKey: Key | undefined; 8 + } 9 + 10 + const NAMESPACE = `@@atproto/oauth-client-react-native`; 11 + 12 + /** 13 + * An expo-sqlite store that handles serializing and deserializing 14 + * our Jose DPoP keys. Wraps SQLiteKVStore or whatever other SimpleStore 15 + * that a user might provide. 16 + */ 17 + export class JoseKeyStore<T extends HasDPoPKey> { 18 + private store: SimpleStore<string, string>; 19 + constructor(store: SimpleStore<string, string>) { 20 + this.store = store; 21 + } 22 + 23 + async get(key: string): Promise<T | undefined> { 24 + const itemStr = await this.store.get(key); 25 + if (!itemStr) return undefined; 26 + const item = JSON.parse(itemStr) as T; 27 + if (item.dpopKey) { 28 + item.dpopKey = new JoseKey(jwkValidator.parse(item.dpopKey)); 29 + } 30 + return item; 31 + } 32 + 33 + async set(key: string, value: T): Promise<void> { 34 + if (value.dpopKey) { 35 + value = { 36 + ...value, 37 + dpopKey: (value.dpopKey as JoseKey).privateJwk, 38 + }; 39 + } 40 + return await this.store.set(key, JSON.stringify(value)); 41 + } 42 + 43 + async del(key: string): Promise<void> { 44 + return await this.store.del(key); 45 + } 46 + } 47 + 48 + /** 49 + * Simple wrapper around expo-sqlite's KVStore. Default implementation 50 + * unless a user brings their own KV store. 51 + */ 52 + export class SQLiteKVStore implements SimpleStore<string, string> { 53 + private namespace: string; 54 + constructor(namespace: string) { 55 + this.namespace = `${NAMESPACE}:${namespace}`; 56 + } 57 + 58 + async get(key: string): Promise<string | undefined> { 59 + return (await Storage.getItem(`${this.namespace}:${key}`)) ?? undefined; 60 + } 61 + 62 + async set(key: string, value: string): Promise<void> { 63 + return await Storage.setItem(`${this.namespace}:${key}`, value); 64 + } 65 + 66 + async del(key: string): Promise<void> { 67 + return await Storage.removeItem(`${this.namespace}:${key}`); 68 + } 69 + }
+8
js/atproto-oauth-client-react-native/tsconfig.build.json
··· 1 + { 2 + "extends": "../app/tsconfig.base.json", 3 + "compilerOptions": { 4 + "rootDir": "./src", 5 + "outDir": "./dist" 6 + }, 7 + "include": ["./src"] 8 + }
-1
js/atproto-oauth-client-react-native/tsconfig.build.tsbuildinfo
··· 1 - {"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/tslib/tslib.d.ts","../../node_modules/@types/react/global.d.ts","../../node_modules/csstype/index.d.ts","../../node_modules/@types/prop-types/index.d.ts","../../node_modules/@types/react/index.d.ts","../../node_modules/@types/react/jsx-runtime.d.ts","./src/polyfills.ts","../../node_modules/@atproto/did/node_modules/zod/lib/helpers/typealiases.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/helpers/util.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/zoderror.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/locales/en.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/errors.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/helpers/parseutil.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/helpers/enumutil.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/helpers/errorutil.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/helpers/partialutil.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/types.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/external.d.ts","../../node_modules/@atproto/did/node_modules/zod/lib/index.d.ts","../../node_modules/@atproto/did/node_modules/zod/index.d.ts","../../node_modules/@atproto/did/dist/did.d.ts","../../node_modules/@atproto/did/dist/atproto.d.ts","../../node_modules/@atproto/did/dist/did-document.d.ts","../../node_modules/@atproto/did/dist/did-error.d.ts","../../node_modules/@atproto/did/dist/methods/plc.d.ts","../../node_modules/@atproto/did/dist/methods/web.d.ts","../../node_modules/@atproto/did/dist/methods.d.ts","../../node_modules/@atproto/did/dist/index.d.ts","../../node_modules/@atproto-labs/simple-store/dist/simple-store.d.ts","../../node_modules/@atproto-labs/simple-store/dist/cached-getter.d.ts","../../node_modules/@atproto-labs/simple-store/dist/index.d.ts","../../node_modules/@atproto-labs/simple-store-memory/dist/index.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/did-method.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/did-resolver.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/did-cache.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/did-cache-memory.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/did-resolver-base.d.ts","../../node_modules/@atproto-labs/fetch/dist/fetch-error.d.ts","../../node_modules/@atproto-labs/fetch/dist/util.d.ts","../../node_modules/@atproto-labs/fetch/dist/fetch.d.ts","../../node_modules/@atproto-labs/fetch/dist/fetch-request.d.ts","../../node_modules/@atproto-labs/pipe/dist/transformer.d.ts","../../node_modules/@atproto-labs/pipe/dist/pipe.d.ts","../../node_modules/@atproto-labs/pipe/dist/index.d.ts","../../node_modules/@atproto-labs/fetch/node_modules/zod/index.d.ts","../../node_modules/@atproto-labs/fetch/dist/fetch-response.d.ts","../../node_modules/@atproto-labs/fetch/dist/fetch-wrap.d.ts","../../node_modules/@atproto-labs/fetch/dist/index.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/methods/plc.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/methods/web.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/util.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/did-resolver-common.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/methods.d.ts","../../node_modules/@atproto-labs/did-resolver/dist/index.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/types.d.ts","../../node_modules/@atproto-labs/handle-resolver/node_modules/zod/index.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/app-view-handle-resolver.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/internal-resolvers/dns-handle-resolver.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/internal-resolvers/well-known-handler-resolver.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/atproto-handle-resolver.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/atproto-doh-handle-resolver.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/cached-handle-resolver.d.ts","../../node_modules/@atproto-labs/handle-resolver/dist/index.d.ts","../../node_modules/@atproto/oauth-types/dist/constants.d.ts","../../node_modules/@atproto/oauth-types/node_modules/zod/index.d.ts","../../node_modules/@atproto/oauth-types/dist/uri.d.ts","../../node_modules/@atproto/oauth-types/dist/util.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-redirect-uri.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-scope.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-client-id-loopback.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-client-metadata.d.ts","../../node_modules/@atproto/oauth-types/dist/atproto-loopback-client-metadata.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-access-token.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-code-grant-token-request.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-details.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-request-jar.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-request-par.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-request-parameters.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-request-query.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-request-uri.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-authorization-server-metadata.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-client-credentials-grant-token-request.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-client-credentials.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-client-id-discoverable.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-client-id.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-endpoint-auth-method.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-endpoint-name.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-grant-type.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-token-type.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-introspection-response.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-issuer-identifier.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-par-response.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-password-grant-token-request.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-protected-resource-metadata.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-refresh-token-grant-token-request.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-refresh-token.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-request-uri.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-response-mode.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-response-type.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-token-identification.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-token-request.d.ts","../../node_modules/@atproto/oauth-types/dist/oauth-token-response.d.ts","../../node_modules/@atproto/oauth-types/dist/oidc-claims-parameter.d.ts","../../node_modules/@atproto/oauth-types/dist/oidc-claims-properties.d.ts","../../node_modules/@atproto/oauth-types/dist/oidc-entity-type.d.ts","../../node_modules/@atproto/oauth-types/dist/index.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-authorization-server-metadata-resolver.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-callback-error.d.ts","../../node_modules/@atproto-labs/identity-resolver/dist/identity-resolver.d.ts","../../node_modules/@atproto-labs/identity-resolver/dist/index.d.ts","../../node_modules/@atproto/jwk/node_modules/zod/index.d.ts","../../node_modules/@atproto/jwk/dist/jwk.d.ts","../../node_modules/@atproto/jwk/dist/alg.d.ts","../../node_modules/@atproto/jwk/dist/errors.d.ts","../../node_modules/@atproto/jwk/dist/jwks.d.ts","../../node_modules/@atproto/jwk/dist/jwt.d.ts","../../node_modules/@atproto/jwk/dist/jwt-decode.d.ts","../../node_modules/@atproto/jwk/dist/util.d.ts","../../node_modules/@atproto/jwk/dist/jwt-verify.d.ts","../../node_modules/@atproto/jwk/dist/key.d.ts","../../node_modules/@atproto/jwk/dist/keyset.d.ts","../../node_modules/@atproto/jwk/dist/index.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-protected-resource-metadata-resolver.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-resolver.d.ts","../../node_modules/@atproto/oauth-client/node_modules/zod/index.d.ts","../../node_modules/@atproto/oauth-client/dist/util.d.ts","../../node_modules/@atproto/oauth-client/dist/atproto-token-response.d.ts","../../node_modules/@atproto/oauth-client/dist/runtime-implementation.d.ts","../../node_modules/@atproto/oauth-client/dist/runtime.d.ts","../../node_modules/@atproto/oauth-client/dist/types.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-server-agent.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-server-factory.d.ts","../../node_modules/@atproto/oauth-client/dist/errors/token-invalid-error.d.ts","../../node_modules/@atproto/oauth-client/dist/errors/token-refresh-error.d.ts","../../node_modules/@atproto/oauth-client/dist/errors/token-revoked-error.d.ts","../../node_modules/@atproto/oauth-client/dist/session-getter.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-session.d.ts","../../node_modules/@atproto/oauth-client/dist/state-store.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-client.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-resolver-error.d.ts","../../node_modules/@atproto/oauth-client/dist/oauth-response-error.d.ts","../../node_modules/@atproto/oauth-client/dist/index.d.ts","./node_modules/@atproto/oauth-client-browser/dist/disposable-polyfill/index.d.ts","../../node_modules/jose/dist/types/types.d.ts","../../node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","../../node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","../../node_modules/jose/dist/types/jwe/general/decrypt.d.ts","../../node_modules/jose/dist/types/jwe/general/encrypt.d.ts","../../node_modules/jose/dist/types/jws/compact/verify.d.ts","../../node_modules/jose/dist/types/jws/flattened/verify.d.ts","../../node_modules/jose/dist/types/jws/general/verify.d.ts","../../node_modules/jose/dist/types/jwt/verify.d.ts","../../node_modules/jose/dist/types/jwt/decrypt.d.ts","../../node_modules/jose/dist/types/jwt/produce.d.ts","../../node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","../../node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","../../node_modules/jose/dist/types/jws/compact/sign.d.ts","../../node_modules/jose/dist/types/jws/flattened/sign.d.ts","../../node_modules/jose/dist/types/jws/general/sign.d.ts","../../node_modules/jose/dist/types/jwt/sign.d.ts","../../node_modules/jose/dist/types/jwt/encrypt.d.ts","../../node_modules/jose/dist/types/jwk/thumbprint.d.ts","../../node_modules/jose/dist/types/jwk/embedded.d.ts","../../node_modules/jose/dist/types/jwks/local.d.ts","../../node_modules/jose/dist/types/jwks/remote.d.ts","../../node_modules/jose/dist/types/jwt/unsecured.d.ts","../../node_modules/jose/dist/types/key/export.d.ts","../../node_modules/jose/dist/types/key/import.d.ts","../../node_modules/jose/dist/types/util/decode_protected_header.d.ts","../../node_modules/jose/dist/types/util/decode_jwt.d.ts","../../node_modules/jose/dist/types/util/errors.d.ts","../../node_modules/jose/dist/types/key/generate_key_pair.d.ts","../../node_modules/jose/dist/types/key/generate_secret.d.ts","../../node_modules/jose/dist/types/util/base64url.d.ts","../../node_modules/jose/dist/types/util/runtime.d.ts","../../node_modules/jose/dist/types/index.d.ts","../../node_modules/@atproto/jwk-jose/dist/jose-key.d.ts","../../node_modules/@atproto/jwk-jose/dist/index.d.ts","../../node_modules/@atproto/jwk-webcrypto/dist/webcrypto-key.d.ts","../../node_modules/@atproto/jwk-webcrypto/dist/index.d.ts","./node_modules/@atproto/oauth-client-browser/dist/util.d.ts","./node_modules/@atproto/oauth-client-browser/dist/browser-oauth-client.d.ts","./node_modules/@atproto/oauth-client-browser/dist/errors.d.ts","./node_modules/@atproto/oauth-client-browser/dist/index.d.ts","./src/oauth-client-react-native.ts","./src/index.ts","../../node_modules/@craftzdog/react-native-buffer/index.d.ts","../../node_modules/safe-buffer/index.d.ts","../../node_modules/react-native-quick-crypto/node_modules/buffer/index.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/aes.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/nativequickcrypto/aes.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/nativequickcrypto/sig.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/nativequickcrypto/keygen.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/cipher.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/nativequickcrypto/cipher.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/rsa.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/nativequickcrypto/rsa.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/nativequickcrypto/webcrypto.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/keys.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/hashnames.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/utils.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/random.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/sig.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/hmac.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/hash.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/subtle.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/keygen.d.ts","../../node_modules/react-native-quick-crypto/lib/typescript/src/index.d.ts","../../node_modules/expo-sqlite/build/storage.d.ts","../../node_modules/expo-sqlite/kv-store.d.ts","./src/sqlite-keystore.ts","./src/oauth-client-react-native.native.ts","../../node_modules/event-target-shim/index.d.ts","./src/polyfills.native.ts","../../node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/@types/node/compatibility/index.d.ts","../../node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/buffer/index.d.ts","../../node_modules/@types/node/node_modules/undici-types/header.d.ts","../../node_modules/@types/node/node_modules/undici-types/readable.d.ts","../../node_modules/@types/node/node_modules/undici-types/file.d.ts","../../node_modules/@types/node/node_modules/undici-types/fetch.d.ts","../../node_modules/@types/node/node_modules/undici-types/formdata.d.ts","../../node_modules/@types/node/node_modules/undici-types/connector.d.ts","../../node_modules/@types/node/node_modules/undici-types/client.d.ts","../../node_modules/@types/node/node_modules/undici-types/errors.d.ts","../../node_modules/@types/node/node_modules/undici-types/dispatcher.d.ts","../../node_modules/@types/node/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/@types/node/node_modules/undici-types/global-origin.d.ts","../../node_modules/@types/node/node_modules/undici-types/pool-stats.d.ts","../../node_modules/@types/node/node_modules/undici-types/pool.d.ts","../../node_modules/@types/node/node_modules/undici-types/handlers.d.ts","../../node_modules/@types/node/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/@types/node/node_modules/undici-types/agent.d.ts","../../node_modules/@types/node/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/@types/node/node_modules/undici-types/mock-agent.d.ts","../../node_modules/@types/node/node_modules/undici-types/mock-client.d.ts","../../node_modules/@types/node/node_modules/undici-types/mock-pool.d.ts","../../node_modules/@types/node/node_modules/undici-types/mock-errors.d.ts","../../node_modules/@types/node/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/@types/node/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/@types/node/node_modules/undici-types/retry-handler.d.ts","../../node_modules/@types/node/node_modules/undici-types/retry-agent.d.ts","../../node_modules/@types/node/node_modules/undici-types/api.d.ts","../../node_modules/@types/node/node_modules/undici-types/interceptors.d.ts","../../node_modules/@types/node/node_modules/undici-types/util.d.ts","../../node_modules/@types/node/node_modules/undici-types/cookies.d.ts","../../node_modules/@types/node/node_modules/undici-types/patch.d.ts","../../node_modules/@types/node/node_modules/undici-types/websocket.d.ts","../../node_modules/@types/node/node_modules/undici-types/eventsource.d.ts","../../node_modules/@types/node/node_modules/undici-types/filereader.d.ts","../../node_modules/@types/node/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/@types/node/node_modules/undici-types/content-type.d.ts","../../node_modules/@types/node/node_modules/undici-types/cache.d.ts","../../node_modules/@types/node/node_modules/undici-types/index.d.ts","../../node_modules/@types/node/globals.d.ts","../../node_modules/@types/node/assert.d.ts","../../node_modules/@types/node/assert/strict.d.ts","../../node_modules/@types/node/async_hooks.d.ts","../../node_modules/@types/node/buffer.d.ts","../../node_modules/@types/node/child_process.d.ts","../../node_modules/@types/node/cluster.d.ts","../../node_modules/@types/node/console.d.ts","../../node_modules/@types/node/constants.d.ts","../../node_modules/@types/node/crypto.d.ts","../../node_modules/@types/node/dgram.d.ts","../../node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/@types/node/dns.d.ts","../../node_modules/@types/node/dns/promises.d.ts","../../node_modules/@types/node/domain.d.ts","../../node_modules/@types/node/dom-events.d.ts","../../node_modules/@types/node/events.d.ts","../../node_modules/@types/node/fs.d.ts","../../node_modules/@types/node/fs/promises.d.ts","../../node_modules/@types/node/http.d.ts","../../node_modules/@types/node/http2.d.ts","../../node_modules/@types/node/https.d.ts","../../node_modules/@types/node/inspector.d.ts","../../node_modules/@types/node/module.d.ts","../../node_modules/@types/node/net.d.ts","../../node_modules/@types/node/os.d.ts","../../node_modules/@types/node/path.d.ts","../../node_modules/@types/node/perf_hooks.d.ts","../../node_modules/@types/node/process.d.ts","../../node_modules/@types/node/punycode.d.ts","../../node_modules/@types/node/querystring.d.ts","../../node_modules/@types/node/readline.d.ts","../../node_modules/@types/node/readline/promises.d.ts","../../node_modules/@types/node/repl.d.ts","../../node_modules/@types/node/sea.d.ts","../../node_modules/@types/node/sqlite.d.ts","../../node_modules/@types/node/stream.d.ts","../../node_modules/@types/node/stream/promises.d.ts","../../node_modules/@types/node/stream/consumers.d.ts","../../node_modules/@types/node/stream/web.d.ts","../../node_modules/@types/node/string_decoder.d.ts","../../node_modules/@types/node/test.d.ts","../../node_modules/@types/node/timers.d.ts","../../node_modules/@types/node/timers/promises.d.ts","../../node_modules/@types/node/tls.d.ts","../../node_modules/@types/node/trace_events.d.ts","../../node_modules/@types/node/tty.d.ts","../../node_modules/@types/node/url.d.ts","../../node_modules/@types/node/util.d.ts","../../node_modules/@types/node/v8.d.ts","../../node_modules/@types/node/vm.d.ts","../../node_modules/@types/node/wasi.d.ts","../../node_modules/@types/node/worker_threads.d.ts","../../node_modules/@types/node/zlib.d.ts","../../node_modules/@types/node/index.d.ts"],"fileIdsList":[[185,221,260,299,342],[299,342],[221,259,260,261,262,299,342],[80,85,221,264,299,342],[80,85,110,201,221,255,257,278,287,290,299,342],[80,85,263,299,342],[80,85,287,292,299,342],[80,85,110,201,257,289,299,342],[107,111,114,299,342],[107,110,112,113,299,342],[107,299,342],[107,112,113,299,342],[116,128,129,130,299,342],[107,112,299,342],[107,112,113,114,115,130,131,132,299,342],[128,129,299,342],[107,112,127,299,342],[117,119,299,342],[99,117,118,123,299,342],[119,299,342],[118,299,342],[117,118,119,120,125,126,299,342],[98,299,342],[99,134,299,342],[134,139,299,342],[134,137,138,299,342],[110,134,299,342],[134,136,139,140,141,299,342],[134,299,342],[133,142,299,342],[188,299,342],[121,122,299,342],[121,299,342],[110,299,342],[108,299,342],[108,109,299,342],[99,100,299,342],[99,299,342],[100,101,102,103,106,299,342],[104,105,299,342],[100,299,342],[89,90,299,342],[87,88,89,91,92,96,299,342],[88,89,299,342],[97,299,342],[89,299,342],[87,88,89,92,93,94,95,299,342],[87,88,98,299,342],[256,299,342],[201,255,299,342],[258,299,342],[201,257,299,342],[191,299,342],[191,192,193,194,195,196,197,198,199,200,299,342],[195,299,342],[195,197,299,342],[191,195,198,299,342],[194,195,197,198,199,299,342],[99,205,299,342],[107,127,133,142,185,186,187,202,207,209,210,211,212,213,214,215,216,217,218,219,220,299,342],[110,127,185,299,342],[127,133,142,185,186,189,201,202,203,205,207,208,209,210,211,215,216,217,299,342],[185,186,189,202,299,342],[127,299,342],[107,110,127,185,201,203,206,208,209,299,342],[127,185,186,201,203,208,209,210,299,342],[107,127,185,206,210,215,299,342],[201,205,299,342],[201,207,299,342],[107,110,201,208,210,211,212,213,214,299,342],[110,201,299,342],[99,185,205,299,342],[149,150,299,342],[143,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,299,342],[99,147,148,299,342],[154,168,299,342],[299,339,342],[299,341,342],[342],[299,342,347,377],[299,342,343,348,354,355,362,374,385],[299,342,343,344,354,362],[294,295,296,299,342],[299,342,345,386],[299,342,346,347,355,363],[299,342,347,374,382],[299,342,348,350,354,362],[299,341,342,349],[299,342,350,351],[299,342,354],[299,342,352,354],[299,341,342,354],[299,342,354,355,356,374,385],[299,342,354,355,356,369,374,377],[299,337,342,390],[299,337,342,350,354,357,362,374,385],[299,342,354,355,357,358,362,374,382,385],[299,342,357,359,374,382,385],[297,298,299,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391],[299,342,354,360],[299,342,361,385],[299,342,350,354,362,374],[299,309,313,342,385],[299,309,342,374,385],[299,304,342],[299,306,309,342,382,385],[299,342,362,382],[299,342,392],[299,304,342,392],[299,306,309,342,362,385],[299,301,302,305,308,342,354,374,385],[299,309,316,342],[299,301,307,342],[299,309,330,331,342],[299,305,309,342,377,385,392],[299,330,342,392],[299,303,304,342,392],[299,309,342],[299,303,304,305,306,307,308,309,310,311,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,331,332,333,334,335,336,342],[299,309,324,342],[299,309,316,317,342],[299,307,309,317,318,342],[299,308,342],[299,301,304,309,342],[299,309,313,317,318,342],[299,313,342],[299,307,309,312,342,385],[299,301,306,309,316,342],[299,342,374],[299,304,309,330,342,390,392],[299,342,363],[299,342,364],[299,341,342,365],[299,339,340,341,342,343,344,345,346,347,348,349,350,351,352,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391],[299,342,367],[299,342,368],[299,342,354,369,370],[299,342,369,371,386,388],[299,342,354,374,375,377],[299,342,376,377],[299,342,374,375],[299,342,377],[299,342,378],[299,339,342,374],[299,342,354,380,381],[299,342,380,381],[299,342,347,362,374,382],[299,342,383],[299,342,362,384],[299,342,357,368,385],[299,342,347,386],[299,342,374,387],[299,342,361,388],[299,342,389],[299,342,347,354,356,365,374,385,388,390],[299,342,374,391],[81,82,83,299,342],[84,299,342],[288,299,342],[223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,299,342],[223,299,342],[223,233,299,342],[278,280,299,342],[266,277,278,280,299,342,347],[266,278,280,299,342],[278,299,342],[266,280,299,342],[266,273,278,280,281,282,283,284,285,286,299,342],[273,277,280,299,342],[269,277,278,299,342],[266,273,278,280,299,342],[277,299,342],[275,277,278,299,342],[270,271,272,274,276,278,280,299,342],[266,267,278,279,299,342,347]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"8bf8b5e44e3c9c36f98e1007e8b7018c0f38d8adc07aecef42f5200114547c70","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"b5ce7a470bc3628408429040c4e3a53a27755022a32fd05e2cb694e7015386c7","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"4245fee526a7d1754529d19227ecbf3be066ff79ebb6a380d78e41648f2f224d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a882ffbb4ed09d9b7734f784aebb1dfe488d63725c40759165c5d9c657ca029","impliedFormat":1},{"version":"36a2e4c9a67439aca5f91bb304611d5ae6e20d420503e96c230cf8fcdc948d94","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","impliedFormat":1},{"version":"247a952efd811d780e5630f8cfd76f495196f5fa74f6f0fee39ac8ba4a3c9800","impliedFormat":1},{"version":"aa17748c522bd586f8712b1a308ea23af59c309b2fd278f6d4f406647c72e659","affectsGlobalScope":true,"impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",{"version":"5487b97cfa28b26b4a9ef0770f872bdbebd4c46124858de00f242c3eed7519f4","impliedFormat":1},{"version":"c2869c4f2f79fd2d03278a68ce7c061a5a8f4aed59efb655e25fe502e3e471d5","impliedFormat":1},{"version":"b8fe42dbf4b0efba2eb4dbfb2b95a3712676717ff8469767dc439e75d0c1a3b6","impliedFormat":1},{"version":"8485b6da53ec35637d072e516631d25dae53984500de70a6989058f24354666f","impliedFormat":1},{"version":"ebe80346928736532e4a822154eb77f57ef3389dbe2b3ba4e571366a15448ef2","impliedFormat":1},{"version":"83306c97a4643d78420f082547ea0d488a0d134c922c8e65fc0b4f08ef66d92b","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"98a9cc18f661d28e6bd31c436e1984f3980f35e0f0aa9cf795c54f8ccb667ffe","impliedFormat":1},{"version":"c76b0c5727302341d0bdfa2cc2cee4b19ff185b554edb6e8543f0661d8487116","impliedFormat":1},{"version":"dccd26a5c85325a011aff40f401e0892bd0688d44132ba79e803c67e68fffea5","impliedFormat":1},{"version":"f5ef066942e4f0bd98200aa6a6694b831e73200c9b3ade77ad0aa2409e8fe1b1","impliedFormat":1},{"version":"b9e99cd94f4166a245f5158f7286c05406e2a4c694619bceb7a4f3519d1d768e","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"a3b8ebbff16895842be69c31658dd2dae0c33c3b1ac650cbccd60900aca1238c","impliedFormat":1},{"version":"9264e6f9617bbb4894738f198790191a6ec58e1fa997ed4c3bdbff93c09ef918","impliedFormat":1},{"version":"124ea88466db219a0ed430fb735a4ecd824bdad9781293fe66e94eeb8f4055f7","impliedFormat":1},{"version":"2e812554c576fa240ffaa71d0ca5259181cfb00cab70c5c1f78eb7d8e3330e44","impliedFormat":1},{"version":"fd225bb43195e90690cd17cbc7fc416b8ac4242671c8d9ea47b1f5100386711c","impliedFormat":1},{"version":"b89c26a54fb97eed00f94fe3f6791a5bcf62adf3d8f9d1e1c5cfcf3517c79e93","impliedFormat":1},{"version":"a09dee614aa1423e888a527e4bf11ada691427416a3af8911b5b5f9ecf21ebc7","impliedFormat":1},{"version":"c63e1447746a359ffcb3ea45786b3af5523cf34d36df470bfaddc21ad747eefa","impliedFormat":1},{"version":"7e4b99ecd027a7cbd707e9874129f0bd76b4bd6a17a94d960502b71dc40a3bab","impliedFormat":1},{"version":"39b8a37ded69edea6a8accdf58b133e8cde30b0b782aced256ecbd16905a5d91","impliedFormat":1},{"version":"52983c713dac09b4ba96153f24b852bbdfa470929965b93a538126e2b50bad1b","impliedFormat":1},{"version":"84d5257c97dc9f8cb1500cec23a011744c7a13f0dd72133705d2af838ee4564f","impliedFormat":1},{"version":"0856233bdf7a3f9f2d8da63236e81b759a409b7e371e94b57619401e80703a0f","impliedFormat":1},{"version":"779007337a0ce855eaef8fae082043a48ea00ccbafc5fd182bae4d94a1ea8d46","impliedFormat":1},{"version":"3f20270c0b9e2dcd1265e22f4060f0bd64d21397aa8dce5c071fec232c0d1dba","impliedFormat":1},{"version":"68abc9d3fdc5cd30079d5aa79aee3fe335e97fd2465a6e0d97a911ed5f126be7","impliedFormat":1},{"version":"bfd78208936ad2643e815006c8e00f5a116731d41d2764fda8a9467f5eb255f0","impliedFormat":1},{"version":"15377b7cd10daa2c5f141cbdb6e6b1b5cf2293c9508eec8096e72e61c34ebd08","impliedFormat":1},{"version":"666eb4a8473b9620466e13f59470a90bbdd907e17b64005ceaf6b03a26cd0eed","impliedFormat":1},{"version":"8f01a3f96136b81ab57882f99b1153162f23601bb4fc4e698e742d46255757f1","impliedFormat":1},{"version":"114ea62a1bbdad2497709eb102870eb27acb298f06c8e3963662305ca2ed970b","impliedFormat":1},{"version":"ac892647a4874d9293cbb86c16a22a199a0c11805d4a0b72b7cfc7c17e73b65c","impliedFormat":1},{"version":"f52d2c00e4bf0da52873694d895aa42949255ce23545282416fadb4d9ebed6bd","impliedFormat":1},{"version":"91312ddbe6a7aa9060cf4137367ce912564388a4eeabe6f903a81f9bf4d78c76","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"8954ec7b7db8a029e40fefb3af01f0e5c700fba8a702f0d9dcce221f7f4cd207","impliedFormat":1},{"version":"33ba37fbce1b78c28e860fb312b6284587b0224640bf8233cca69ebbb78b1629","impliedFormat":1},{"version":"ab4ec5d270e1b668b58c02984e3b108f28521d6c0e3ef5226306d1548e88f19b","impliedFormat":1},{"version":"ac5d2b8e7fd7b8a311841bbd185e6c3fffc8dd8c1e5aa1d8feba29b82143c1f3","impliedFormat":1},{"version":"62ba04b53cd345bed495e5201a7826432cc92a585250df75234cf5af0955c406","impliedFormat":1},{"version":"93cfb20661b6e888148813f15c85f7b0c88fac9f01ac8d42e28f2c9bfef44aa6","impliedFormat":1},{"version":"0b5d14c81c906ba9dd283d8fbb0d3c5cdf98339688fc4eed0b10ba2e07cef7b2","impliedFormat":1},{"version":"a09dee614aa1423e888a527e4bf11ada691427416a3af8911b5b5f9ecf21ebc7","impliedFormat":1},{"version":"9acb19b8be6cf416b6ad97c47d54e4cd1dfe1e4df7de2513668692be67be2213","impliedFormat":1},{"version":"d9ca3f9825c359810f4d5da8030e494b24ab8cd6ddfa7ed74095480aa48fe8e7","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"c149ea06ee58817eec6c0190b559f0a6ef0173bf2bc870ee94c88a7265913a5e","impliedFormat":1},{"version":"d7c447aaf48a7dc6f2c05de54b6f69fef43d470db301a578563b6743a8af6c5b","impliedFormat":1},{"version":"9072df8167c523cb92a0ade4494b84ba744ed3b0779b690d6671a8febb130d6d","impliedFormat":1},{"version":"9146d34a054ad4e3b4c3f53f64d4c106e643f41209bf05b94d95b593bd6c76c0","impliedFormat":1},{"version":"ac26beae2bf3f2c845b7ffad4f55328ed160dd0f463a58372704fa7a6c27f793","impliedFormat":1},{"version":"3dba40834ea8ad5f08ec3b551553ae547030608f1460c92ecefef66acf9971e6","impliedFormat":1},{"version":"02e4fb209668b155c16cb98113e9c83e47bcd5fcf5035b5e2698ebdc95b746a3","impliedFormat":1},{"version":"281cc4fead96675e77eb85d6f5208a514500188b2fbda2935134da18c77ff5c9","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"82c726b64759224520bb3c187cd3494f7731a9244573445d917122b22fa41f8b","impliedFormat":1},{"version":"5a476527f1a31455d2aa76d8187a53a06555293cb797460c193f0653c6a92f81","impliedFormat":1},{"version":"86c8920dfdfd2fcf21905d8104c33616504180585641f317c5631048fcfe4a03","impliedFormat":1},{"version":"4184b667a9e014f1a46865116c8b72e59686d36472504a563b7bc0bfb5da0a70","impliedFormat":1},{"version":"947b215236fa225e762a0ccade1202a510904e0ae1c2dab37c1ab54311005e99","impliedFormat":1},{"version":"a3fd2df2cf87e74ba90866b3c0db0f3cae37a810ff1bf4d651d7e1a5f16691cb","impliedFormat":1},{"version":"b56044abdb4c999f71dbd0c3ddd7070484c323465ce5d3c2dc8f856f929be25d","impliedFormat":1},{"version":"727950dfc1b8ea42553aa73d4070d302db0a72b6cba269e4ff330561f90e1ac1","impliedFormat":1},{"version":"8319050f75e283498e0a4be07509ea3dc5328868cfd075b24f4f00fa4d1e2e12","impliedFormat":1},{"version":"39c4884d89bf2b8ea56944dc1d4f68fd605b9731a542394d3f168149440daf3f","impliedFormat":1},{"version":"bb7e593672a1ba37acfeda32959bb875b0c1b99b42ac7d2d16ecd90112444ea3","impliedFormat":1},{"version":"d2870deb69d0c7b772dfb9b5bc98322d65a2ae0b4466e6661ea57f0a41acdd17","impliedFormat":1},{"version":"29e6ba9418652c0dae9967a701e1675436274f1c2188eb86d75788916eea78d8","impliedFormat":1},{"version":"032f2579c9feb398784fef78e80a9b12acbcde0787ef67280f5d6db1fc05eea1","impliedFormat":1},{"version":"d1d18efda8252075a82a6d3e6c373e144322b1bf1a018ec831aec2a34cfc6384","impliedFormat":1},{"version":"e34554a2fdf20912ecb78d26e6d235561e0d579c7bac05b88b61dc69728ce636","impliedFormat":1},{"version":"e3f4430a9aa0d8cbb580de46fdc9eae49137275a62e5129e916d3aa03f1f7c81","impliedFormat":1},{"version":"8fdf0d718f6a6cc522c6b9d187fb5ccbdbbc4f36d5c5f53cf868ddc8cb619fc0","impliedFormat":1},{"version":"18e13513020f10291752f64a8b14556bdcec2af968e5ddbafa42f0c81c669718","impliedFormat":1},{"version":"d08347843fa76bb14ba0e003803278711515b04c2f0ab0bf0a14baa89acfe3b9","impliedFormat":1},{"version":"14a56bbcee52b698f1907c3d9428b2d9bef8ea611ddd6f5f76af3f601d9c6c6d","impliedFormat":1},{"version":"540e084b06df30a4e27b271bc2163c8f88b3d181c18497173f8ab3c6218107b5","impliedFormat":1},{"version":"75739fdfe4274aa1603b8c3e08ab21d2465ba4fa598912aa447590af2ebe35a7","impliedFormat":1},{"version":"60387bc1f3a8ac59f3cecd4e37ae632852982b9d0a37849b113502f96abad4dd","impliedFormat":1},{"version":"448088258817dcfac1af44820f02268d3a733fba3165a4df27eeadbec2416064","impliedFormat":1},{"version":"f838227553bed5ea4557c9eb3a3782ac2e9395c01918223f087f0b760eb726b8","impliedFormat":1},{"version":"981bda3857b717fb54aa64d28bb60afc509ea4c27aa32f140a30f91b75426abf","impliedFormat":1},{"version":"5dc248f7d6c401a87b4468922af2cedb4efa98f0ac10f7f0547cf13988f99e48","impliedFormat":1},{"version":"31ed79e3763b49014680e3bc871b776311303d73af813638f59d89ad3b0cf50e","impliedFormat":1},{"version":"939bfbbc861cbe104793567d5505609012a0ce84901a9c044e282d180981982d","impliedFormat":1},{"version":"88069fbc0eaf70d82d1439504a0cada34250d761b65de8ff350a778e3fb3063d","impliedFormat":1},{"version":"da592d0fdb1a2897803ccb0f949320dfeb76dad033fec0f8d5d6933fddfa0f4b","impliedFormat":1},{"version":"7be3fe0dd8fd7e3a6296c2a0b9e017b8dec496e461d46d6ba66925d8b0d778cf","impliedFormat":1},{"version":"7b32a09d43a93680b366cc7a7637c884e1dd817e3939f413c5aa0cdad914afeb","impliedFormat":1},{"version":"1a5a65d70494b82429b5bd78ce6cdf73037b10e7342ecd825a660b11ff72d630","impliedFormat":1},{"version":"5a0c4a6099823aba7dafd1d73bd3aa09084807e3696a24400a3ea3ba3755c987","impliedFormat":1},{"version":"d8a711cb6e0725f842cbe33ba8aa2bc7bdda76431d86e471a7ff7aea7163f323","impliedFormat":1},{"version":"f339feba19bec1ecf5861b9bf95290b5f97a33f996e7fc848975eb18b32c8f5f","impliedFormat":1},{"version":"3b0452b59d5f6643cbe359791d6436d984fc96cf26dedc8d00ec97c617458122","impliedFormat":1},{"version":"f95cad3d309ede51f10e1aa0c2c7821a41193fa10de7712c8166663225aec4d0","impliedFormat":1},{"version":"cdbbb6fa13c0f825cb905b44ddb851e062040d1f39819991aeb13ef063470fbb","impliedFormat":1},{"version":"4381966f54c1fa6be438a3da9638ad10f7bc929a40a9f19a860728f0ec2e6f82","impliedFormat":1},{"version":"a8dd5ff55b15c2966589edf335832b73f09e1a4dd6c3d5e75a99384f3ff364f4","impliedFormat":1},{"version":"145ed100d0e72ac22eb687fda563b24a5615b939f93a0418aac7f3b5fbce73b6","impliedFormat":1},{"version":"9bf18fada27d27d8e7651eee691a12e35d14a4395bc3fe322418216ea04682c6","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"a50a2a9a52dd91015ec199535c494a2a368023979446d0fa2e602f04674360a3","impliedFormat":1},{"version":"2c2a2cd1a1a66abf9d5780332a57d12e5e7d6e43c73be623d99f6b3c1e6eb363","impliedFormat":1},{"version":"1667c652b307c2827c64ce3bbb2e635345c55a92c3d4abd7253e56869f5a7e61","impliedFormat":1},{"version":"7d98f81bc8af2f8bc430326b639fcda09cb509308cfeb8f86ce236fdbe9799c3","impliedFormat":1},{"version":"a76b5e27202b6feb8970aef217cba4d2328675b0c0201d242abb51693be83893","impliedFormat":1},{"version":"0f6ccb7b3426efd6222202f72b7c651f62ef6c9f0a77f6cc87db1bc4e4b48805","impliedFormat":1},{"version":"7186f05a8dfdf3169ff00d11ee20d54aa9ba8994271767cda47a1f36d3be89fd","impliedFormat":1},{"version":"7be1d0dbcff8a34b274691fcdf0cbb015f765b3e0cd46751405236510cf5fa41","impliedFormat":1},{"version":"ef90eb477def9a6ff3883e7a87d1fa24d65a76814dd7d86df4fa4ee73d065f62","impliedFormat":1},{"version":"b73e70161031064cf8e2f078d811a4f6a87178ff8ab90a7b95366386bbef20b5","impliedFormat":1},{"version":"abe84361596dadaeec62ab34380721d4b7bfca8b2884e9be2323e22316c36d52","impliedFormat":1},{"version":"72a015948cce8a3e8b79b56de8e8d39edbc69adc13d854d0cda90bb8f5e7f732","impliedFormat":1},{"version":"b5147bbd2e4dbb97a9e2bf0a89e22734f53c68c514b79c7535472b536c497a03","impliedFormat":1},{"version":"5568d7c32e5cf5f35e092649f4e5e168c3114c800b1d7545b7ae5e0415704802","impliedFormat":1},{"version":"01025cde3d0388f2942959e9ef24e8126ea755f55f6a17eb2100a3b44ea71422","impliedFormat":1},{"version":"0aaa84e0df52dc8b5859d24502a184d3279e6ea85163958f08155e4dbb3d4d45","impliedFormat":1},{"version":"45182be842c1c6809296c46f24fd45a1fd6298f7b4b84a414436fd2ad5827d4e","impliedFormat":1},{"version":"e5dfe656913c2450a17c62988011b9d821edb4fea05dbd602675e3aa7c9d73f6","impliedFormat":1},{"version":"5c75b213bf38325e9c299f72affa809deb59a0dc97e4bf3fdc63ccbdc20653e1","impliedFormat":1},{"version":"7fb9b305032813f96ccb246951989ab24d8ef48836fdcfa38dd047c61a7ed30e","impliedFormat":1},{"version":"ae3992dabaca90ca1558568800318eeeb32ec95f88a943642227373e2244d38c","impliedFormat":1},{"version":"d4ae12fd53be33b90aeda73d73f644337b7623fb0e60b69ad32c526dbf785a5a","impliedFormat":1},{"version":"da7476aaf1035c16565fb8402a2704cc8c8e3d7cb39180120bb3711525d0ab8b","impliedFormat":1},{"version":"99030f98187f417f3b9ef1e4110e7c9ca1ab6ed98b6e12e6130d168f943be079","impliedFormat":1},{"version":"d78c03d2fb5ac98e156dac9e246ac9402fd3ae7b8054838e865f308417bc9e8e","impliedFormat":1},{"version":"b73c573cea63c32054fcac34f3b24c9e4d913526a5ee1409d8b0f3c0533d42ad","impliedFormat":1},{"version":"0020080bb907fe6440979fad8c50f3525d1306d3971ec799295dad5bb183e0f9","impliedFormat":1},{"version":"68f96a3f108f74e8edcc2fcdfb3879c7b692be557f179a0fc3c3bcbb100d0282","impliedFormat":1},{"version":"fa8b0784e57a825ac298e89a5c43ac4ec88b8dbde74f9bef87c38f891fc2e162","impliedFormat":1},{"version":"738634e0df0b2c4a4ba1b2969e3de2a7661b1f19d0001eda6fa53e4ca6e0b788","impliedFormat":1},{"version":"cf3fb5afd21918cceb0f7ab76a046c475a4a086264a7ab0b279804224124e578","impliedFormat":1},{"version":"3835f8d92ad0699690cf572ad0da8aa3bfa5cc1c66fbd2609c52a02b9da828dc","impliedFormat":1},{"version":"6db928ecb8b9450aecc2a5ecdead68fa8b7a72130008c77e7524dfa7ef6c7002","impliedFormat":1},{"version":"4083e6d84bfe72b0835b600185c7b7ce321da3d6053f866859185eefc161e7a0","impliedFormat":1},{"version":"b883e245dc30c73b655ffe175712cac82981fc999d6284685f0ed7c1dac8aa6f","impliedFormat":1},{"version":"626e3504b81883fa94578c2a97eff345fadc5eae17a57c39f585655eef5b8272","impliedFormat":1},{"version":"e9a15eeba29ceb0ee109dd5e0282d2877d8165d87251f2ea9741a82685a25c61","impliedFormat":1},{"version":"c6cb06cc021d9149301f3c51762a387f9d7571feed74273b157d934c56857fac","impliedFormat":1},{"version":"cd7c133395a1c72e7c9e546f62292f839819f50a8aa46050f8588b63ef56df88","impliedFormat":1},{"version":"196f5f74208ce4accea017450ed2abc9ce4ab13c29a9ea543db4c2d715a19183","impliedFormat":1},{"version":"4687c961ab2e3107379f139d22932253afb7dd52e75a18890e70d4a376cdf5d9","impliedFormat":1},{"version":"ae8cfe2e3bdef3705fc294d07869a0ab8a52d9b623d1cc0482b6fc2be262b015","impliedFormat":1},{"version":"94c8e9c00244bbf1c868ca526b12b4db1fab144e3f5e18af3591b5b471854157","impliedFormat":1},{"version":"827d576995f67a6205c0f048ae32f6a1cf7bda9a7a76917ab286ef11d7987fd7","impliedFormat":1},{"version":"cb5dc83310a61d2bb351ddcdcaa6ec1cf60cc965d26ce6f156a28b4062e96ab2","impliedFormat":1},{"version":"0091cb2456a823e123fe76faa8b94dea81db421770d9a9c9ade1b111abe0fcd1","impliedFormat":1},{"version":"034d811fd7fb2262ad35b21df0ecab14fdd513e25dbf563572068e3f083957d9","impliedFormat":1},{"version":"298bcc906dd21d62b56731f9233795cd11d88e062329f5df7cdb4e499207cdd4","impliedFormat":1},{"version":"f7e64be58c24f2f0b7116bed8f8c17e6543ddcdc1f46861d5c54217b4a47d731","impliedFormat":1},{"version":"966394e0405e675ca1282edbfa5140df86cb6dc025e0f957985f059fe4b9d5d6","impliedFormat":1},{"version":"b0587deb3f251b7ad289240c54b7c41161bb6488807d1f713e0a14c540cbcaee","impliedFormat":1},{"version":"4254aab77d0092cab52b34c2e0ab235f24f82a5e557f11d5409ae02213386e29","impliedFormat":1},{"version":"19db45929fad543b26b12504ee4e3ff7d9a8bddc1fc3ed39723c2259e3a4590f","impliedFormat":1},{"version":"b21934bebe4cd01c02953ab8d17be4d33d69057afdb5469be3956e84a09a8d99","impliedFormat":1},{"version":"b2b734c414d440c92a17fd409fa8dac89f425031a6fc7843bac765c6c174d1ca","impliedFormat":1},{"version":"239f39e8ad95065f5188a7acd8dbefbbbf94d9e00c460ffdc331e24bc1f63a54","impliedFormat":1},{"version":"d44f78893cb79e00e16a028e3023a65c1f2968352378e8e323f8c8f88b8da495","impliedFormat":1},{"version":"32afc9daae92391cb4efeb0d2dac779dc0fb17c69be0eb171fd5ed7f7908eeb4","impliedFormat":1},{"version":"b835c6e093ad9cda87d376c248735f7e4081f64d304b7c54a688f1276875cbf0","impliedFormat":1},{"version":"a9eabe1d0b20e967a18758a77884fbd61b897d72a57ddd9bf7ea6ef1a3f4514b","impliedFormat":1},{"version":"64c5059e7d7a80fe99d7dad639f3ba765f8d5b42c5b265275d7cd68f8426be75","impliedFormat":1},{"version":"05dc1970dc02c54db14d23ff7a30af00efbd7735313aa8af45c4fd4f5c3d3a33","impliedFormat":1},{"version":"a0caf07fe750954ad4cf079c5cf036be2191a758c2700424085ffde6af60d185","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"eab89b3aa37e9e48b2679f4abe685d56ac371daa8fbe68526c6b0c914eb28474","impliedFormat":1},{"version":"debc18aa3dba1f28f19b3cc632a0c538288b09962301d901b074903b3c09ec62","impliedFormat":1},{"version":"64456bf67e3e27ee199ebf28b90105f293c479dcdbb1720c041f9f54b2447f67","impliedFormat":1},{"version":"c983d19453192b5db84646c85f1bc4a5cbc6d856bf91df190e93c49c302ebe36","impliedFormat":1},{"version":"6b75f3cb3254ed2ebde6f7c9487711bd498406d86310033f5b705655016d3086","impliedFormat":1},{"version":"8155d7700604f77d1273a0de29d912c8cdfc531c03b18582764c6c7038ff1c42","impliedFormat":1},{"version":"03258b5d794eb03434318b909c0c6a8b7cd031fc33207799b0134e3c3da81532","impliedFormat":1},{"version":"25fa594d7e17d731fd20195af7569bf71e087ec5b724c7cbd406777f37b601b1","impliedFormat":1},{"version":"f357d723890ac3267efde52d144eff7a52311eddef5e5fc8fcea5650bef0d785","impliedFormat":1},"5d6d4f8ee5a85e90a7b04737b1fd2bc72b2a041d216152013d5c00a186ab681b","11194ddb8eba1fbdf82deefd6decabc03419c36c4996998407ed225e90317411",{"version":"16e9731ed48d605f85e6cc674d0c02c40a55af94712a7efce7b7a94f1f73f2a7","impliedFormat":1},{"version":"5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","impliedFormat":1},{"version":"4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","impliedFormat":1},{"version":"5657303e23d101f6a111507b6d4b1dc410d290781227109c0d8fb23a13fec2c7","impliedFormat":1},{"version":"75658069e9e161c850e43235d5d4723278875fc9f0dbe2c33a3bfe766b2144f0","impliedFormat":1},{"version":"62d686b226dbd48728c3f4fd9da3e00f557bd46ae6309e7f1cb580e80719f9db","impliedFormat":1},{"version":"6473f6211926331f9282d681de4394ef79ba31ada3f7f796709bd22c669c92de","impliedFormat":1},{"version":"6bd2840cc43df36daff0e9d4fe54a69dc628865f34ef8b82995aad2df246ba51","impliedFormat":1},{"version":"3efd242947bcee3c1fa5c1dd4a60657c63fbee41ae2d15d9124b7167f76be07e","impliedFormat":1},{"version":"5a916b0b34823d78155aee81cb1f07bc6d7ef18b0724dea07d43fae4d388e69c","impliedFormat":1},{"version":"67c7c2751c0613f3fb6e2ff30c406bc29314997ae27f93d7d0dfa01f5e2e76c4","impliedFormat":1},{"version":"3a6763b96331c6c403f1865972c448d3d8be8cfa310ff391c6b0cc5b97f617a4","impliedFormat":1},{"version":"5695684cb098d4e453174fee4eaf0f754e0cf561ddbf242ef3ed94a6ecaf7443","impliedFormat":1},{"version":"c51775b4135a2e3118c017c074146f26975e160ca366b1f115a35dad52b69a60","impliedFormat":1},{"version":"9dc2ba999c784da232c0aa23af0d54ab73d36413531f977fdd3e597b0ef4ed95","impliedFormat":1},{"version":"eefeb19fd479b86b1bf375a4f16f65249f98a49625911e901b98cd454d3fbdfa","impliedFormat":1},{"version":"751ef481734eba0c88fb72ca3e1aa4ed35d13bb2a1912893d25e08af9ed50f02","impliedFormat":1},{"version":"913c60104f1e7b295c60466c2fcef99e38604aed64c9e479a418841085baa1aa","impliedFormat":1},{"version":"2d7b8eea2ef2b1fbde4f9ac077058ef8b7f56fcdf378243acb9864afd5cf6531","impliedFormat":1},{"version":"2e270467f9bb7d8d056a739bbefaf327d8f5a085e09b2d957cc33af1d63dc7f9","impliedFormat":1},{"version":"1962026ce36685070aa45b6beca78e97f0d5beb315b535ccf0753c93efdfe06b","impliedFormat":1},{"version":"e6ec4654e0ba6ab3c5fa64175101ca6c9701998af53291f303d8698b776030cd","impliedFormat":1},{"version":"1cdf77975bc0c45040d4d6f98a515359a2e9a97a0c6c347ad8231a49ceb3e9a2","impliedFormat":1},{"version":"79079ca43a2015d655f4fb15d207f375e5bfcc488b7a5a37987f380a50dd00c2","impliedFormat":1},"ab0476d8eca69ab2fc9c8795df596a6c67d433a5c20c2929264376775619696b",{"version":"78318debb1be4ce1f1c00a74c1c3ffa0a844c1989379b23b5e523d46b0e12ee1","signature":"f9f52ed38e10831bfd9c0870e9b26323e0e87f1417403a3a2310da82ff2c90d7"},{"version":"63633f5796c4cf53210ce75f02e5d6e81b88012f5c8832af32c35d0a8b75cdde","impliedFormat":1},"7641d4860b2272873c6def15838b5b5a89e6b4866584d33905688d7f1817a0ab",{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"3b724a66c071d616203133f8d099a0cb881b0b43fd42e8621e611243c5f30cd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a38efe83ff77c34e0f418a806a01ca3910c02ee7d64212a59d59bca6c2c38fa1","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"3fe4022ba1e738034e38ad9afacbf0f1f16b458ed516326f5bf9e4a31e9be1dc","impliedFormat":1},{"version":"a957197054b074bcdf5555d26286e8461680c7c878040d0f4e2d5509a7524944","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"e9b97d69510658d2f4199b7d384326b7c4053b9e6645f5c19e1c2a54ede427fc","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"f478f6f5902dc144c0d6d7bdc919c5177cac4d17a8ca8653c2daf6d7dc94317f","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"b200675fd112ffef97c166d0341fb33f6e29e9f27660adde7868e95c5bc98beb","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a0a1dda070290b92da5a50113b73ecc4dd6bcbffad66e3c86503d483eafbadcf","impliedFormat":1},{"version":"59dcad36c4549175a25998f6a8b33c1df8e18df9c12ebad1dfb25af13fd4b1ce","impliedFormat":1},{"version":"9ba5b6a30cb7961b68ad4fb18dca148db151c2c23b8d0a260fc18b83399d19d3","impliedFormat":1},{"version":"3f3edb8e44e3b9df3b7ca3219ab539710b6a7f4fe16bd884d441af207e03cd57","impliedFormat":1},{"version":"528b62e4272e3ddfb50e8eed9e359dedea0a4d171c3eb8f337f4892aac37b24b","impliedFormat":1},{"version":"d71535813e39c23baa113bc4a29a0e187b87d1105ccc8c5a6ebaca38d9a9bff2","impliedFormat":1},{"version":"8cf7e92bdb2862c2d28ba4535c43dc599cfbc0025db5ed9973d9b708dcbe3d98","affectsGlobalScope":true,"impliedFormat":1},{"version":"8a410a7fa4baf13dd45c9bba6d71806027dc0e4e5027cdf74f36466ae9b240b7","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"636302a00dfd1f9fe6e8e91e4e9350c6518dcc8d51a474e4fc3a9ba07135100b","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"e1ce1d622f1e561f6cdf246372ead3bbc07ce0342024d0e9c7caf3136f712698","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"27e4532aaaa1665d0dd19023321e4dc12a35a741d6b8e1ca3517fcc2544e0efe","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"8c2ad42d5d1a2e8e6112625767f8794d9537f1247907378543106f7ba6c7df90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"98ffdf93dfdd206516971d28e3e473f417a5cfd41172e46b4ce45008f640588e","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"12e8ce658dd17662d82fb0509d2057afc5e6ee30369a2e9e0957eff725b1f11d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74736930d108365d7bbe740c7154706ccfb1b2a3855a897963ab3e5c07ecbf19","impliedFormat":1},{"version":"858f999b3e4a45a4e74766d43030941466460bf8768361d254234d5870480a53","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"63b05afa6121657f25e99e1519596b0826cda026f09372c9100dfe21417f4bd6","affectsGlobalScope":true,"impliedFormat":1},{"version":"3797dd6f4ea3dc15f356f8cdd3128bfa18122213b38a80d6c1f05d8e13cbdad8","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1}],"root":[86,264,265,290,291,293],"options":{"allowJs":false,"allowSyntheticDefaultImports":true,"downlevelIteration":true,"esModuleInterop":true,"importHelpers":true,"jsx":4,"module":99,"noEmitOnError":false,"noImplicitAny":false,"noImplicitReturns":false,"noUnusedLocals":false,"noUnusedParameters":false,"outDir":"./dist","preserveConstEnums":true,"removeComments":false,"rootDir":"./src","skipLibCheck":true,"sourceMap":false,"strictNullChecks":true,"target":7,"useUnknownInCatchVariables":false},"referencedMap":[[261,1],[222,2],[262,2],[263,3],[260,2],[78,2],[79,2],[13,2],[15,2],[14,2],[2,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[3,2],[24,2],[25,2],[4,2],[26,2],[30,2],[27,2],[28,2],[29,2],[31,2],[32,2],[33,2],[5,2],[34,2],[35,2],[36,2],[37,2],[6,2],[41,2],[38,2],[39,2],[40,2],[42,2],[7,2],[43,2],[48,2],[49,2],[44,2],[45,2],[46,2],[47,2],[8,2],[53,2],[50,2],[51,2],[52,2],[54,2],[9,2],[55,2],[56,2],[57,2],[59,2],[58,2],[60,2],[61,2],[10,2],[62,2],[63,2],[64,2],[11,2],[65,2],[66,2],[67,2],[68,2],[69,2],[1,2],[70,2],[71,2],[12,2],[75,2],[73,2],[77,2],[72,2],[76,2],[74,2],[265,4],[291,5],[264,6],[293,7],[86,2],[290,8],[115,9],[114,10],[112,11],[116,12],[131,13],[113,14],[133,15],[132,16],[128,17],[129,17],[130,2],[117,2],[120,18],[125,19],[126,20],[119,21],[127,22],[118,2],[124,23],[136,24],[140,25],[139,26],[141,27],[142,28],[137,29],[138,29],[134,11],[135,23],[188,30],[189,31],[123,32],[122,33],[121,2],[111,34],[109,35],[110,36],[108,2],[101,37],[102,37],[103,2],[100,38],[107,39],[106,40],[104,41],[105,41],[99,23],[91,42],[97,43],[93,2],[94,2],[92,44],[95,23],[87,2],[88,2],[98,45],[90,46],[96,47],[89,48],[257,49],[256,50],[259,51],[258,52],[192,53],[193,2],[201,54],[191,38],[194,38],[196,55],[198,56],[195,38],[199,57],[200,58],[197,38],[190,23],[206,59],[212,2],[213,2],[214,2],[221,60],[186,61],[187,2],[218,62],[202,61],[219,2],[203,63],[220,64],[210,65],[211,66],[216,67],[207,68],[208,69],[215,70],[217,71],[209,72],[205,2],[204,23],[151,73],[143,2],[185,74],[152,38],[153,38],[154,38],[155,38],[156,38],[157,38],[158,38],[159,38],[160,38],[161,38],[162,38],[163,38],[149,75],[164,38],[150,38],[165,38],[166,2],[167,38],[169,76],[170,38],[171,38],[172,38],[173,38],[147,38],[174,38],[175,38],[176,38],[177,38],[178,38],[148,38],[179,38],[180,38],[181,38],[168,38],[182,38],[183,38],[184,38],[145,38],[146,2],[144,23],[266,2],[339,77],[340,77],[341,78],[299,79],[342,80],[343,81],[344,82],[294,2],[297,83],[295,2],[296,2],[345,84],[346,85],[347,86],[348,87],[349,88],[350,89],[351,89],[353,90],[352,91],[354,92],[355,93],[356,94],[338,95],[298,2],[357,96],[358,97],[359,98],[392,99],[360,100],[361,101],[362,102],[316,103],[326,104],[315,103],[336,105],[307,106],[306,107],[335,108],[329,109],[334,110],[309,111],[323,112],[308,113],[332,114],[304,115],[303,108],[333,116],[305,117],[310,118],[311,2],[314,118],[301,2],[337,119],[327,120],[318,121],[319,122],[321,123],[317,124],[320,125],[330,108],[312,126],[313,127],[322,128],[302,129],[325,120],[324,118],[328,2],[331,130],[363,131],[364,132],[365,133],[366,134],[367,135],[368,136],[369,137],[370,137],[371,138],[372,2],[373,2],[374,139],[376,140],[375,141],[377,142],[378,143],[379,144],[380,145],[381,146],[382,147],[383,148],[384,149],[385,150],[386,151],[387,152],[388,153],[389,154],[390,155],[391,156],[83,2],[81,2],[84,157],[85,158],[300,2],[82,2],[292,2],[288,2],[289,159],[255,160],[224,161],[234,161],[225,161],[235,161],[226,161],[227,161],[242,161],[241,161],[243,161],[244,161],[236,161],[228,161],[237,161],[229,161],[238,161],[230,161],[232,161],[240,162],[233,161],[239,162],[245,162],[231,161],[246,161],[251,161],[252,161],[247,161],[223,2],[253,2],[249,161],[248,161],[250,161],[254,161],[269,163],[273,164],[284,165],[279,166],[283,167],[287,168],[286,166],[278,169],[270,170],[274,171],[272,172],[276,173],[271,172],[277,174],[281,167],[275,163],[282,163],[285,163],[280,175],[268,2],[267,2],[80,2]],"version":"5.8.3"}
+4
js/atproto-oauth-client-react-native/tsconfig.json
··· 1 + { 2 + "include": [], 3 + "references": [{ "path": "./tsconfig.build.json" }] 4 + }
+1 -1
js/docs/package.json
··· 5 5 "scripts": { 6 6 "dev": "astro dev --host 0.0.0.0 --port 38082", 7 7 "start": "astro dev --host 0.0.0.0 --port 38082", 8 - "build": "astro build && rm -rf ../app/dist/docs && cp -r dist ../app/dist/docs", 8 + "build": "astro build && rm -rf ../app/dist/docs && mkdir -p ../app/dist && cp -r dist ../app/dist/docs", 9 9 "preview": "astro preview", 10 10 "astro": "astro" 11 11 },
+43
js/docs/src/content/docs/lex-reference/account/place-stream-account-defs.md
··· 1 + --- 2 + title: place.stream.account.defs 3 + description: Reference for the place.stream.account.defs lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="loginresponse"></a> 11 + 12 + ### `loginResponse` 13 + 14 + **Type:** `object` 15 + 16 + **Properties:** 17 + 18 + | Name | Type | Req'd | Description | Constraints | 19 + | ------------- | -------- | ----- | ----------- | ------------- | 20 + | `redirectUrl` | `string` | ✅ | | Format: `uri` | 21 + 22 + --- 23 + 24 + ## Lexicon Source 25 + 26 + ```json 27 + { 28 + "lexicon": 1, 29 + "id": "place.stream.account.defs", 30 + "defs": { 31 + "loginResponse": { 32 + "type": "object", 33 + "required": ["redirectUrl"], 34 + "properties": { 35 + "redirectUrl": { 36 + "type": "string", 37 + "format": "uri" 38 + } 39 + } 40 + } 41 + } 42 + } 43 + ```
+74
js/docs/src/content/docs/lex-reference/account/place-stream-account-login.md
··· 1 + --- 2 + title: place.stream.account.login 3 + description: Reference for the place.stream.account.login lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="main"></a> 11 + 12 + ### `main` 13 + 14 + **Type:** `procedure` 15 + 16 + Get a redirect URL for the login flow. 17 + 18 + **Parameters:** _(None defined)_ 19 + 20 + **Input:** 21 + 22 + - **Encoding:** `application/json` 23 + - **Schema:** 24 + 25 + **Schema Type:** `object` 26 + 27 + | Name | Type | Req'd | Description | Constraints | 28 + | ------------- | -------- | ----- | ------------------------------------------ | ----------- | 29 + | `handleOrDID` | `string` | ✅ | The handle or DID of the account to login. | | 30 + 31 + **Output:** 32 + 33 + - **Encoding:** `application/json` 34 + - **Schema:** 35 + 36 + **Schema Type:** 37 + [`place.stream.account.defs#loginResponse`](/lex-reference/place-stream-account-defs#loginresponse) 38 + 39 + --- 40 + 41 + ## Lexicon Source 42 + 43 + ```json 44 + { 45 + "lexicon": 1, 46 + "id": "place.stream.account.login", 47 + "defs": { 48 + "main": { 49 + "type": "procedure", 50 + "description": "Get a redirect URL for the login flow.", 51 + "input": { 52 + "encoding": "application/json", 53 + "schema": { 54 + "type": "object", 55 + "required": ["handleOrDID"], 56 + "properties": { 57 + "handleOrDID": { 58 + "type": "string", 59 + "description": "The handle or DID of the account to login." 60 + } 61 + } 62 + } 63 + }, 64 + "output": { 65 + "encoding": "application/json", 66 + "schema": { 67 + "type": "ref", 68 + "ref": "place.stream.account.defs#loginResponse" 69 + } 70 + } 71 + } 72 + } 73 + } 74 + ```
+28
lexicons/app/bsky/actor/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.getProfile", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "Handle or DID of account to fetch profile of." 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "ref", 23 + "ref": "app.bsky.actor.defs#profileViewDetailed" 24 + } 25 + } 26 + } 27 + } 28 + }
+37
lexicons/com/atproto/identity/resolveHandle.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.identity.resolveHandle", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["handle"], 11 + "properties": { 12 + "handle": { 13 + "type": "string", 14 + "format": "handle", 15 + "description": "The handle to resolve." 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["did"], 24 + "properties": { 25 + "did": { "type": "string", "format": "did" } 26 + } 27 + } 28 + }, 29 + "errors": [ 30 + { 31 + "name": "HandleNotFound", 32 + "description": "The resolution process confirmed that the handle does not resolve to any DID." 33 + } 34 + ] 35 + } 36 + } 37 + }
+1 -1
package.json
··· 9 9 "check:workspaces": "cd js/app && yarn run check", 10 10 "fix": "git ls-files | xargs prettier --write --ignore-unknown", 11 11 "postinstall": "husky && make js-lexicons", 12 - "build": "yarn workspaces foreach --parallel --all run build", 12 + "build": "yarn workspaces foreach -t --parallel --all run build", 13 13 "prepare": "husky", 14 14 "release": "lerna publish --force-publish", 15 15 "precommit": "make precommit",
+19 -27
pkg/api/api.go
··· 12 12 "net/http/httputil" 13 13 "net/url" 14 14 "os" 15 - "slices" 16 15 "strings" 17 16 "sync" 18 17 "time" ··· 37 36 "stream.place/streamplace/pkg/mist/mistconfig" 38 37 "stream.place/streamplace/pkg/model" 39 38 "stream.place/streamplace/pkg/notifications" 39 + "stream.place/streamplace/pkg/oproxy" 40 40 "stream.place/streamplace/pkg/spmetrics" 41 41 "stream.place/streamplace/pkg/spxrpc" 42 42 "stream.place/streamplace/pkg/streamplace" ··· 121 121 // api/playback/iame.li/hls/source/000000000000.ts 122 122 123 123 func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) { 124 + var xrpc http.Handler 124 125 xrpc, err := spxrpc.NewServer(a.CLI, a.Model) 125 126 if err != nil { 126 127 return nil, err 127 128 } 129 + op := oproxy.New(&oproxy.Config{ 130 + Host: a.CLI.PublicHost, 131 + CreateOAuthSession: a.Model.CreateOAuthSession, 132 + UpdateOAuthSession: a.Model.UpdateOAuthSession, 133 + LoadOAuthSession: a.Model.LoadOAuthSession, 134 + Scope: "atproto transition:generic", 135 + UpstreamJWK: a.CLI.JWK, 136 + DownstreamJWK: a.CLI.AccessJWK, 137 + }) 138 + 139 + xrpc = op.OAuthMiddleware(xrpc) 128 140 router := httprouter.New() 141 + router.Handler("GET", "/oauth/*anything", op.Handler()) 142 + router.Handler("POST", "/oauth/*anything", op.Handler()) 143 + router.Handler("GET", "/.well-known/oauth-authorization-server", op.Handler()) 144 + router.Handler("GET", "/.well-known/oauth-protected-resource", op.Handler()) 129 145 apiRouter := httprouter.New() 130 146 apiRouter.HandlerFunc("POST", "/api/notification", a.HandleNotification(ctx)) 131 147 // old clients 132 148 router.HandlerFunc("GET", "/app-updates", a.HandleAppUpdates(ctx)) 149 + 133 150 // new ones 134 151 apiRouter.HandlerFunc("GET", "/api/manifest", a.HandleAppUpdates(ctx)) 135 152 apiRouter.GET("/api/desktop-updates/:platform/:architecture/:version/:buildTime/:file", a.HandleDesktopUpdates(ctx)) ··· 156 173 apiRouter.GET("/api/segment/recent", a.HandleRecentSegments(ctx)) 157 174 apiRouter.GET("/api/segment/recent/:repoDID", a.HandleUserRecentSegments(ctx)) 158 175 apiRouter.GET("/api/bluesky/resolve/:handle", a.HandleBlueskyResolve(ctx)) 159 - for _, platform := range atproto.AllowedPlatforms { 160 - apiRouter.GET(fmt.Sprintf("/api/atproto-oauth/%s", platform), a.HandleATProtoOAuth(ctx, platform)) 161 - } 162 176 apiRouter.GET("/api/live-users", a.HandleLiveUsers(ctx)) 163 177 apiRouter.GET("/api/view-count/:user", a.HandleViewCount(ctx)) 164 178 apiRouter.NotFound = a.HandleAPI404(ctx) ··· 591 605 } 592 606 } 593 607 594 - func (a *StreamplaceAPI) HandleATProtoOAuth(ctx context.Context, platform string) httprouter.Handle { 595 - return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 596 - host, _, err := net.SplitHostPort(req.Host) 597 - if err != nil { 598 - host = req.Host 599 - } 600 - if !slices.Contains(atproto.AllowedPlatforms, platform) { 601 - apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil) 602 - return 603 - } 604 - 605 - meta := atproto.GetMetadata(host, platform, a.CLI.AppBundleID) 606 - bs, err := json.Marshal(meta) 607 - if err != nil { 608 - apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err) 609 - return 610 - } 611 - w.Header().Set("Content-Type", "application/json") 612 - w.Write(bs) 613 - } 614 - } 615 - 616 608 type ChatResponse struct { 617 609 Post *bsky.FeedPost `json:"post"` 618 610 Repo *model.Repo `json:"repo"` ··· 778 770 limiter, exists := a.limiters[ip] 779 771 if !exists { 780 772 // 5 actions per second with a burst of 3 781 - limiter = rate.NewLimiter(rate.Limit(10.0), 8) 773 + limiter = rate.NewLimiter(rate.Limit(20.0), 16) 782 774 a.limiters[ip] = limiter 783 775 } 784 776
+14
pkg/api/api_internal.go
··· 423 423 w.Write(bs) 424 424 }) 425 425 426 + router.GET("/oauth-sessions", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 427 + sessions, err := a.Model.ListOAuthSessions() 428 + if err != nil { 429 + errors.WriteHTTPInternalServerError(w, "unable to get oauth sessions", err) 430 + return 431 + } 432 + bs, err := json.Marshal(sessions) 433 + if err != nil { 434 + errors.WriteHTTPInternalServerError(w, "unable to marshal oauth sessions", err) 435 + return 436 + } 437 + w.Write(bs) 438 + }) 439 + 426 440 router.POST("/notification-blast", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 427 441 var payload notificationpkg.NotificationBlast 428 442 if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
+1 -1
pkg/api/app-return.html
··· 37 37 // authorized twice? I don't know why. I spent two hours trying to figure out why 38 38 // without luck. You're welcome to try more if you want! In the meantime, using 39 39 // an annoying button makes it work every time. 40 - // document.location.href = `${appBundleId}:/${window.location.search}`; 40 + document.location.href = `${appBundleId}:/${window.location.search}`; 41 41 document.querySelector("button").addEventListener("click", () => { 42 42 document.location.href = `${appBundleId}:/${window.location.search}`; 43 43 });
+19
pkg/api/playback.go
··· 226 226 } 227 227 addrBytes = decoded[:32] 228 228 didBytes = decoded[32:] 229 + priv, err = atcrypto.ParsePrivateBytesK256(addrBytes) 230 + if err != nil { 231 + errors.WriteHTTPUnauthorized(w, "invalid authorization key (not valid atcrypto)", err) 232 + return 233 + } 229 234 } 230 235 231 236 key, _ := secp256k1.PrivKeyFromBytes(addrBytes) ··· 234 239 return 235 240 } 236 241 var signer crypto.Signer = key.ToECDSA() 242 + pub, err := priv.PublicKey() 243 + if err != nil { 244 + apierrors.WriteHTTPUnauthorized(w, "invalid authorization key (could not parse as atcrypto)", err) 245 + return 246 + } 237 247 238 248 did := string(didBytes) 239 249 ··· 246 256 err = a.CLI.StreamIsAllowed(repo.DID) 247 257 if err != nil { 248 258 apierrors.WriteHTTPUnauthorized(w, "user is not allowed to stream", err) 259 + return 260 + } 261 + signingKey, err := a.Model.GetSigningKey(ctx, pub.DIDKey(), repo.DID) 262 + if err != nil { 263 + apierrors.WriteHTTPUnauthorized(w, "signing key not found", err) 264 + return 265 + } 266 + if signingKey == nil { 267 + apierrors.WriteHTTPUnauthorized(w, "signing key not found", nil) 249 268 return 250 269 } 251 270 } else {
+16 -69
pkg/atproto/atproto.go
··· 13 13 "github.com/bluesky-social/indigo/atproto/identity" 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 "github.com/bluesky-social/indigo/repo" 16 - "github.com/bluesky-social/indigo/util" 17 16 "github.com/bluesky-social/indigo/xrpc" 18 17 "github.com/ipfs/go-cid" 19 - "github.com/ipfs/go-datastore" 20 - blockstore "github.com/ipfs/go-ipfs-blockstore" 21 18 "go.opentelemetry.io/otel" 22 19 "stream.place/streamplace/pkg/aqhttp" 23 20 "stream.place/streamplace/pkg/constants" ··· 69 66 } 70 67 71 68 func (atsync *ATProtoSynchronizer) SyncBlueskyRepo(ctx context.Context, handle string, mod model.Model) (*model.Repo, error) { 72 - ctx = log.WithLogValues(ctx, "func", "SyncBlueskyRepo") 69 + ctx = log.WithLogValues(ctx, "func", "SyncBlueskyRepo", "handle", handle) 73 70 // Get handle-specific lock and ensure synchronized access 74 71 75 72 ident, err := ResolveIdent(ctx, handle) ··· 96 93 DID: ident.DID.String(), 97 94 PDS: ident.PDSEndpoint(), 98 95 Version: "", 99 - RootCID: "", 100 96 Handle: ident.Handle.String(), 101 97 } 102 98 err = mod.UpdateRepo(&newRepo) ··· 130 126 131 127 log.Log(ctx, "got diff", "bytes", len(repoBytes)) 132 128 133 - bs := blockstore.NewBlockstore(datastore.NewMapDatastore()) 134 - root, err := repo.IngestRepo(ctx, bs, bytes.NewReader(repoBytes)) 135 - if err != nil { 136 - return nil, fmt.Errorf("failed to ingest repo for %s: %w", ident.DID.String(), err) 137 - } 138 - log.Log(ctx, "ingested repo", "root", root) 139 - if oldRepo != nil && oldRepo.RootCID != "" { 140 - oldRoot, err := cid.Decode(oldRepo.RootCID) 141 - if err != nil { 142 - return nil, fmt.Errorf("failed to decode old root CID for %s: %w", ident.DID.String(), err) 143 - } 144 - if oldRoot.Equals(root) { 145 - log.Debug(ctx, "no changes to repo", "root", root) 146 - return oldRepo, nil 147 - } 148 - } 149 - 150 129 r, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(repoBytes)) 151 130 if err != nil { 152 131 return nil, fmt.Errorf("failed to parse repo CAR data for %s: %w", ident.DID.String(), err) 153 132 } 154 - 155 - mstNodes := map[string]mstNode{} 156 - err = r.ForEach(ctx, "", func(k string, v cid.Cid) error { 157 - nsid, rkey, err := syntax.ParseRepoPath(k) 158 - if err != nil { 159 - log.Warn(ctx, "failed to parse repo path", "k", k, "err", err) 160 - return err 161 - } 162 - hash := v.Hash().HexString() 163 - log.Debug(ctx, "got mst node", "cid", v, "rkey", rkey, "nsid", nsid, "hash", hash) 164 - mstNodes[hash] = mstNode{ 165 - rkey: rkey, 166 - collection: nsid, 167 - } 168 - return nil 169 - }) 170 - if err != nil { 171 - return nil, fmt.Errorf("failed to iterate over repo: %w", err) 172 - } 173 - 174 133 // extract DID from repo commit 175 134 sc := r.SignedCommit() 176 135 signerDID, err := syntax.ParseDID(sc.Did) ··· 181 140 return nil, fmt.Errorf("signer DID %s does not match identity %s", signerDID, ident.DID.String()) 182 141 } 183 142 184 - bs = r.Blockstore() 185 - cst := util.CborStore(bs) 186 - allKeys, err := bs.AllKeysChan(ctx) 187 - if err != nil { 188 - return nil, fmt.Errorf("failed to get all keys: %w", err) 189 - } 190 - for k := range allKeys { 191 - blk, err := bs.Get(ctx, k) 143 + err = r.ForEach(ctx, "", func(k string, v cid.Cid) error { 144 + nsid, rkey, err := syntax.ParseRepoPath(k) 192 145 if err != nil { 193 - return nil, fmt.Errorf("failed to get block for key %s: %w", k, err) 146 + log.Warn(ctx, "failed to parse repo path", "k", k, "err", err) 147 + return fmt.Errorf("could not parse repo path %s: %w", k, err) 194 148 } 195 - rec := map[string]any{} 196 - err = cst.Get(ctx, k, &rec) 149 + _, bs, err := r.GetRecordBytes(ctx, k) 197 150 if err != nil { 198 - return nil, fmt.Errorf("failed to get block for key %s: %w", k, err) 199 - } 200 - typ, ok := rec["$type"] 201 - if !ok { 202 - log.Debug(ctx, "record type not found", "key", k) 203 - continue 151 + log.Warn(ctx, "failed to get record bytes", "k", k, "rkey", rkey, "err", err) 152 + return fmt.Errorf("could not retrieve record bytes for %s (rkey: %s): %w", k, rkey, err) 204 153 } 205 - log.Debug(ctx, "record type", "key", k, "type", typ) 206 - hash := k.Hash().HexString() 207 - node, ok := mstNodes[hash] 208 - if !ok { 209 - log.Warn(ctx, "no mst node found for record", "key", k, "hash", hash) 210 - continue 211 - } 212 - rawData := blk.RawData() 213 - err = atsync.handleCreateUpdate(ctx, signerDID.String(), node.rkey, &rawData, k.String(), node.collection) 154 + log.Debug(ctx, "record type", "key", k, "type", nsid.String()) 155 + err = atsync.handleCreateUpdate(ctx, signerDID.String(), rkey, bs, v.String(), nsid) 214 156 if err != nil { 215 157 log.Warn(ctx, "failed to handle create update", "err", err) 158 + // invalid CBOR and stuff should get ignored, so 159 + // return fmt.Errorf("failed to process record update for %s (type: %s): %w", k, nsid.String(), err) 216 160 } 161 + return nil 162 + }) 163 + if err != nil { 164 + return nil, fmt.Errorf("failed to iterate over repo: %w", err) 217 165 } 218 166 219 167 newRepo := model.Repo{ 220 168 DID: ident.DID.String(), 221 169 PDS: ident.PDSEndpoint(), 222 170 Version: sc.Rev, 223 - RootCID: root.String(), 224 171 Handle: ident.Handle.String(), 225 172 } 226 173 err = mod.UpdateRepo(&newRepo)
-78
pkg/atproto/client_metadata.go
··· 1 - package atproto 2 - 3 - import ( 4 - "fmt" 5 - ) 6 - 7 - var AllowedPlatforms = []string{"ios", "android", "web"} 8 - 9 - type OAuthClientMetadata struct { 10 - RedirectURIs []string `json:"redirect_uris"` 11 - ResponseTypes []string `json:"response_types,omitempty"` 12 - GrantTypes []string `json:"grant_types,omitempty"` 13 - Scope string `json:"scope,omitempty"` 14 - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` 15 - TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"` 16 - UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"` 17 - UserinfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"` 18 - JwksURI string `json:"jwks_uri,omitempty"` 19 - ApplicationType string `json:"application_type,omitempty"` // "web" or "native" 20 - SubjectType string `json:"subject_type,omitempty"` // "public" or "pairwise" 21 - RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"` 22 - IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"` 23 - AuthorizationSignedResponseAlg string `json:"authorization_signed_response_alg,omitempty"` 24 - AuthorizationEncryptedResponseEnc string `json:"authorization_encrypted_response_enc,omitempty"` 25 - AuthorizationEncryptedResponseAlg string `json:"authorization_encrypted_response_alg,omitempty"` 26 - ClientID string `json:"client_id,omitempty"` 27 - ClientName string `json:"client_name,omitempty"` 28 - ClientURI string `json:"client_uri,omitempty"` 29 - PolicyURI string `json:"policy_uri,omitempty"` 30 - TosURI string `json:"tos_uri,omitempty"` 31 - LogoURI string `json:"logo_uri,omitempty"` 32 - DefaultMaxAge int `json:"default_max_age,omitempty"` 33 - RequireAuthTime *bool `json:"require_auth_time,omitempty"` 34 - Contacts []string `json:"contacts,omitempty"` 35 - TLSClientCertificateBoundAccessTokens *bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` 36 - DPoPBoundAccessTokens *bool `json:"dpop_bound_access_tokens,omitempty"` 37 - AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"` 38 - // Jwks *JWKSet `json:"jwks,omitempty"` // You'll need to define JWKSet type 39 - } 40 - 41 - func boolPtr(b bool) *bool { 42 - return &b 43 - } 44 - 45 - func GetMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata { 46 - meta := &OAuthClientMetadata{ 47 - ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/%s", host, platform), 48 - ClientURI: fmt.Sprintf("https://%s", host), 49 - // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)}, 50 - Scope: "atproto transition:generic", 51 - TokenEndpointAuthMethod: "none", 52 - ClientName: "Streamplace", 53 - ResponseTypes: []string{"code"}, 54 - GrantTypes: []string{"authorization_code", "refresh_token"}, 55 - DPoPBoundAccessTokens: boolPtr(true), 56 - } 57 - if platform == "web" { 58 - meta.RedirectURIs = []string{fmt.Sprintf("https://%s/login", host)} 59 - meta.ApplicationType = "web" 60 - } else { 61 - meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)} 62 - meta.ApplicationType = "native" 63 - } 64 - return meta 65 - } 66 - 67 - // clientMetadata: { 68 - // client_id: "http://localhost?scope=atproto%20transition:generic", 69 - // redirect_uris: ["http://127.0.0.1:38081"], 70 - // scope: "atproto transition:generic", 71 - // token_endpoint_auth_method: "none", 72 - // // jwks_uri: "https://my-app.example/jwks.json", 73 - // client_name: "Loopback client", 74 - // response_types: ["code"], 75 - // grant_types: ["authorization_code", "refresh_token"], 76 - // application_type: "native", 77 - // dpop_bound_access_tokens: true, 78 - // },
+45
pkg/atproto/jwks.go
··· 1 + package atproto 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "os" 7 + 8 + oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers" 9 + "github.com/lestrrat-go/jwx/v2/jwk" 10 + "stream.place/streamplace/pkg/log" 11 + ) 12 + 13 + func EnsureJWK(ctx context.Context, fPath string) (jwk.Key, error) { 14 + var key jwk.Key 15 + _, err := os.Stat(fPath) 16 + if err == nil { 17 + b, err := os.ReadFile(fPath) 18 + if err != nil { 19 + return nil, err 20 + } 21 + key, err = jwk.ParseKey(b) 22 + if err != nil { 23 + return nil, err 24 + } 25 + } else if os.IsNotExist(err) { 26 + key, err = oauth_helpers.GenerateKey(nil) 27 + if err != nil { 28 + return nil, err 29 + } 30 + 31 + b, err := json.Marshal(key) 32 + if err != nil { 33 + return nil, err 34 + } 35 + 36 + if err := os.WriteFile(fPath, b, 0600); err != nil { 37 + return nil, err 38 + } 39 + log.Log(ctx, "generated JWK", "path", fPath) 40 + } else { 41 + return nil, err 42 + } 43 + 44 + return key, nil 45 + }
+8 -4
pkg/atproto/sync.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 6 7 "reflect" 7 8 "time" ··· 19 20 ) 20 21 21 22 func (atsync *ATProtoSynchronizer) handleCreateUpdate(ctx context.Context, userDID string, rkey syntax.RecordKey, recCBOR *[]byte, cid string, collection syntax.NSID) error { 22 - ctx = log.WithLogValues(ctx, "func", "handleCreateUpdate") 23 + ctx = log.WithLogValues(ctx, "func", "handleCreateUpdate", "userDID", userDID, "rkey", rkey.String(), "cid", cid, "collection", collection.String()) 23 24 now := time.Now() 24 25 r, err := atsync.Model.GetRepo(userDID) 25 26 if err != nil { ··· 32 33 } 33 34 d, err := data.UnmarshalCBOR(*recCBOR) 34 35 if err != nil { 35 - return fmt.Errorf("failed to parse record CBOR: %w", err) 36 + return fmt.Errorf("failed to unmarhsal record CBOR: %w", err) 36 37 } 37 38 cb, err := lexutil.CborDecodeValue(*recCBOR) 38 - if err != nil { 39 - return fmt.Errorf("failed to parse record CBOR: %w", err) 39 + if errors.Is(err, lexutil.ErrUnrecognizedType) { 40 + log.Debug(ctx, "unrecognized record type", "key", rkey.String(), "type", err) 41 + return nil 42 + } else if err != nil { 43 + return fmt.Errorf("failed to decode record CBOR: %w", err) 40 44 } 41 45 switch rec := cb.(type) { 42 46 case *bsky.GraphFollow:
+16
pkg/cmd/streamplace.go
··· 145 145 fs.StringVar(&cli.RelayHost, "relay-host", "wss://bsky.network", "websocket url for relay firehose") 146 146 fs.Bool("insecure", false, "DEPRECATED, does nothing.") 147 147 fs.StringVar(&cli.Color, "color", "", "'true' to enable colorized logging, 'false' to disable") 148 + fs.StringVar(&cli.PublicHost, "public-host", "", "public host for this streamplace node (excluding https:// e.g. stream.place)") 148 149 fs.BoolVar(&cli.Thumbnail, "thumbnail", true, "enable thumbnail generation") 149 150 fs.BoolVar(&cli.SmearAudio, "smear-audio", false, "enable audio smearing to create 'perfect' segment timestamps") 150 151 fs.BoolVar(&cli.ExternalSigning, "external-signing", false, "enable external signing via exec (prevents potential memory leak)") ··· 288 289 return err 289 290 } 290 291 } 292 + 293 + jwkPath := cli.DataFilePath([]string{"jwk.json"}) 294 + jwk, err := atproto.EnsureJWK(ctx, jwkPath) 295 + if err != nil { 296 + return err 297 + } 298 + cli.JWK = jwk 299 + 300 + accessJWKPath := cli.DataFilePath([]string{"access-jwk.json"}) 301 + accessJWK, err := atproto.EnsureJWK(ctx, accessJWKPath) 302 + if err != nil { 303 + return err 304 + } 305 + cli.AccessJWK = accessJWK 306 + 291 307 b := bus.NewBus() 292 308 atsync := &atproto.ATProtoSynchronizer{ 293 309 CLI: &cli,
+3 -2
pkg/config/config.go
··· 91 91 SmearAudio bool 92 92 ExternalSigning bool 93 93 TracingEndpoint string 94 + PublicHost string 94 95 JWK jwk.Key 95 - 96 - dataDirFlags []*string 96 + AccessJWK jwk.Key 97 + dataDirFlags []*string 97 98 } 98 99 99 100 var STREAMPLACE_SCHEME_PREFIX = "streamplace://"
+10 -1
pkg/model/model.go
··· 13 13 slogGorm "github.com/orandin/slog-gorm" 14 14 "gorm.io/driver/sqlite" 15 15 "gorm.io/gorm" 16 + "stream.place/streamplace/pkg/config" 16 17 "stream.place/streamplace/pkg/log" 18 + "stream.place/streamplace/pkg/oproxy" 17 19 "stream.place/streamplace/pkg/streamplace" 18 20 ) 19 21 20 22 type DBModel struct { 21 - DB *gorm.DB 23 + DB *gorm.DB 24 + CLI *config.CLI 22 25 } 23 26 24 27 type Model interface { ··· 79 82 80 83 CreateChatProfile(ctx context.Context, profile *ChatProfile) error 81 84 GetChatProfile(ctx context.Context, repoDID string) (*ChatProfile, error) 85 + 86 + CreateOAuthSession(id string, session *oproxy.OAuthSession) error 87 + LoadOAuthSession(id string) (*oproxy.OAuthSession, error) 88 + UpdateOAuthSession(id string, session *oproxy.OAuthSession) error 89 + ListOAuthSessions() ([]oproxy.OAuthSession, error) 82 90 } 83 91 84 92 func MakeDB(dbURL string) (Model, error) { ··· 135 143 Block{}, 136 144 ChatMessage{}, 137 145 ChatProfile{}, 146 + oproxy.OAuthSession{}, 138 147 } { 139 148 err = db.AutoMigrate(model) 140 149 if err != nil {
+42
pkg/model/oauth_session.go
··· 1 + package model 2 + 3 + import ( 4 + "errors" 5 + 6 + "gorm.io/gorm" 7 + "stream.place/streamplace/pkg/oproxy" 8 + ) 9 + 10 + func (m *DBModel) CreateOAuthSession(id string, session *oproxy.OAuthSession) error { 11 + return m.DB.Create(session).Error 12 + } 13 + 14 + func (m *DBModel) LoadOAuthSession(id string) (*oproxy.OAuthSession, error) { 15 + var session oproxy.OAuthSession 16 + if err := m.DB.Where("downstream_dpop_jkt = ?", id).First(&session).Error; err != nil { 17 + if errors.Is(err, gorm.ErrRecordNotFound) { 18 + return nil, nil 19 + } 20 + return nil, err 21 + } 22 + return &session, nil 23 + } 24 + 25 + func (m *DBModel) UpdateOAuthSession(id string, session *oproxy.OAuthSession) error { 26 + res := m.DB.Model(&oproxy.OAuthSession{}).Where("downstream_dpop_jkt = ?", id).Updates(session) 27 + if res.Error != nil { 28 + return res.Error 29 + } 30 + if res.RowsAffected == 0 { 31 + return errors.New("no rows affected") 32 + } 33 + return nil 34 + } 35 + 36 + func (m *DBModel) ListOAuthSessions() ([]oproxy.OAuthSession, error) { 37 + var sessions []oproxy.OAuthSession 38 + if err := m.DB.Find(&sessions).Error; err != nil { 39 + return nil, err 40 + } 41 + return sessions, nil 42 + }
+197
pkg/oproxy/dpop_helpers.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "crypto/ecdsa" 5 + "crypto/ed25519" 6 + "crypto/elliptic" 7 + "crypto/rsa" 8 + "encoding/base64" 9 + "encoding/json" 10 + "math/big" 11 + "strings" 12 + 13 + "github.com/AxisCommunications/go-dpop" 14 + "github.com/golang-jwt/jwt/v5" 15 + ) 16 + 17 + // all of this code borrowed from https://github.com/AxisCommunications/go-dpop 18 + // MIT license 19 + func keyFunc(t *jwt.Token) (interface{}, error) { 20 + // Return the required jwkHeader header. See https://datatracker.ietf.org/doc/html/rfc9449#section-4.2 21 + // Used to validate the signature of the DPoP proof. 22 + jwkHeader := t.Header["jwk"] 23 + if jwkHeader == nil { 24 + return nil, dpop.ErrMissingJWK 25 + } 26 + 27 + jwkMap, ok := jwkHeader.(map[string]interface{}) 28 + if !ok { 29 + return nil, dpop.ErrMissingJWK 30 + } 31 + 32 + return parseJwk(jwkMap) 33 + } 34 + 35 + // Parses a JWK and inherently strips it of optional fields 36 + func parseJwk(jwkMap map[string]interface{}) (interface{}, error) { 37 + // Ensure that JWK kty is present and is a string. 38 + kty, ok := jwkMap["kty"].(string) 39 + if !ok { 40 + return nil, dpop.ErrInvalidProof 41 + } 42 + switch kty { 43 + case "EC": 44 + // Ensure that the required fields are present and are strings. 45 + x, ok := jwkMap["x"].(string) 46 + if !ok { 47 + return nil, dpop.ErrInvalidProof 48 + } 49 + y, ok := jwkMap["y"].(string) 50 + if !ok { 51 + return nil, dpop.ErrInvalidProof 52 + } 53 + crv, ok := jwkMap["crv"].(string) 54 + if !ok { 55 + return nil, dpop.ErrInvalidProof 56 + } 57 + 58 + // Decode the coordinates from Base64. 59 + // 60 + // According to RFC 7518, they are Base64 URL unsigned integers. 61 + // https://tools.ietf.org/html/rfc7518#section-6.3 62 + xCoordinate, err := base64urlTrailingPadding(x) 63 + if err != nil { 64 + return nil, err 65 + } 66 + yCoordinate, err := base64urlTrailingPadding(y) 67 + if err != nil { 68 + return nil, err 69 + } 70 + 71 + // Read the specified curve of the key. 72 + var curve elliptic.Curve 73 + switch crv { 74 + case "P-256": 75 + curve = elliptic.P256() 76 + case "P-384": 77 + curve = elliptic.P384() 78 + case "P-521": 79 + curve = elliptic.P521() 80 + default: 81 + return nil, dpop.ErrUnsupportedCurve 82 + } 83 + 84 + return &ecdsa.PublicKey{ 85 + X: big.NewInt(0).SetBytes(xCoordinate), 86 + Y: big.NewInt(0).SetBytes(yCoordinate), 87 + Curve: curve, 88 + }, nil 89 + case "RSA": 90 + // Ensure that the required fields are present and are strings. 91 + e, ok := jwkMap["e"].(string) 92 + if !ok { 93 + return nil, dpop.ErrInvalidProof 94 + } 95 + n, ok := jwkMap["n"].(string) 96 + if !ok { 97 + return nil, dpop.ErrInvalidProof 98 + } 99 + 100 + // Decode the exponent and modulus from Base64. 101 + // 102 + // According to RFC 7518, they are Base64 URL unsigned integers. 103 + // https://tools.ietf.org/html/rfc7518#section-6.3 104 + exponent, err := base64urlTrailingPadding(e) 105 + if err != nil { 106 + return nil, err 107 + } 108 + modulus, err := base64urlTrailingPadding(n) 109 + if err != nil { 110 + return nil, err 111 + } 112 + return &rsa.PublicKey{ 113 + N: big.NewInt(0).SetBytes(modulus), 114 + E: int(big.NewInt(0).SetBytes(exponent).Uint64()), 115 + }, nil 116 + case "OKP": 117 + // Ensure that the required fields are present and are strings. 118 + x, ok := jwkMap["x"].(string) 119 + if !ok { 120 + return nil, dpop.ErrInvalidProof 121 + } 122 + 123 + publicKey, err := base64urlTrailingPadding(x) 124 + if err != nil { 125 + return nil, err 126 + } 127 + 128 + return ed25519.PublicKey(publicKey), nil 129 + case "OCT": 130 + return nil, dpop.ErrUnsupportedKeyAlgorithm 131 + default: 132 + return nil, dpop.ErrUnsupportedKeyAlgorithm 133 + } 134 + } 135 + 136 + // Borrowed from MicahParks/keyfunc See: https://github.com/MicahParks/keyfunc/blob/master/keyfunc.go#L56 137 + // 138 + // base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant 139 + // JWKS contain padding at the end values for base64url encoded public keys. 140 + // 141 + // Trailing padding is required to be removed from base64url encoded keys. 142 + // RFC 7517 Section 1.1 defines base64url the same as RFC 7515 Section 2: 143 + // https://datatracker.ietf.org/doc/html/rfc7517#section-1.1 144 + // https://datatracker.ietf.org/doc/html/rfc7515#section-2 145 + func base64urlTrailingPadding(s string) ([]byte, error) { 146 + s = strings.TrimRight(s, "=") 147 + return base64.RawURLEncoding.DecodeString(s) 148 + } 149 + 150 + // Strips eventual optional members of a JWK in order to be able to compute the thumbprint of it 151 + // https://datatracker.ietf.org/doc/html/rfc7638#section-3.2 152 + func getThumbprintableJwkJSONbytes(jwk map[string]interface{}) ([]byte, error) { 153 + minimalJwk, err := parseJwk(jwk) 154 + if err != nil { 155 + return nil, err 156 + } 157 + jwkHeaderJSONBytes, err := getKeyStringRepresentation(minimalJwk) 158 + if err != nil { 159 + return nil, err 160 + } 161 + return jwkHeaderJSONBytes, nil 162 + } 163 + 164 + // Returns the string representation of a key in JSON format. 165 + func getKeyStringRepresentation(key interface{}) ([]byte, error) { 166 + var keyParts interface{} 167 + switch key := key.(type) { 168 + case *ecdsa.PublicKey: 169 + // Calculate the size of the byte array representation of an elliptic curve coordinate 170 + // and ensure that the byte array representation of the key is padded correctly. 171 + bits := key.Curve.Params().BitSize 172 + keyCurveBytesSize := bits/8 + bits%8 173 + 174 + keyParts = map[string]interface{}{ 175 + "kty": "EC", 176 + "crv": key.Curve.Params().Name, 177 + "x": base64.RawURLEncoding.EncodeToString(key.X.FillBytes(make([]byte, keyCurveBytesSize))), 178 + "y": base64.RawURLEncoding.EncodeToString(key.Y.FillBytes(make([]byte, keyCurveBytesSize))), 179 + } 180 + case *rsa.PublicKey: 181 + keyParts = map[string]interface{}{ 182 + "kty": "RSA", 183 + "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.E)).Bytes()), 184 + "n": base64.RawURLEncoding.EncodeToString(key.N.Bytes()), 185 + } 186 + case ed25519.PublicKey: 187 + keyParts = map[string]interface{}{ 188 + "kty": "OKP", 189 + "crv": "Ed25519", 190 + "x": base64.RawURLEncoding.EncodeToString(key), 191 + } 192 + default: 193 + return nil, dpop.ErrUnsupportedKeyAlgorithm 194 + } 195 + 196 + return json.Marshal(keyParts) 197 + }
+99
pkg/oproxy/helpers.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "crypto/sha256" 5 + "encoding/base64" 6 + "errors" 7 + "fmt" 8 + "strings" 9 + 10 + "github.com/AxisCommunications/go-dpop" 11 + "github.com/golang-jwt/jwt/v5" 12 + "github.com/google/uuid" 13 + ) 14 + 15 + func boolPtr(b bool) *bool { 16 + return &b 17 + } 18 + 19 + func codeUUID(prefix string) string { 20 + uu, err := uuid.NewV7() 21 + if err != nil { 22 + panic(err) 23 + } 24 + return fmt.Sprintf("%s-%s", prefix, uu.String()) 25 + } 26 + 27 + var urnPrefix = "urn:ietf:params:oauth:request_uri:" 28 + 29 + const UUID_LENGTH = 37 30 + 31 + func makeURN(jkt string) string { 32 + uu, err := uuid.NewV7() 33 + if err != nil { 34 + panic(err) 35 + } 36 + return fmt.Sprintf("%s%s-%s", urnPrefix, uu.String(), jkt) 37 + } 38 + 39 + // urn --> jkt, uu 40 + func parseURN(urn string) (string, string, error) { 41 + if !strings.HasPrefix(urn, urnPrefix) { 42 + return "", "", fmt.Errorf("invalid URN: %s", urn) 43 + } 44 + withoutPrefix := urn[len(urnPrefix):] 45 + uu := withoutPrefix[:UUID_LENGTH] 46 + suffix := withoutPrefix[UUID_LENGTH:] 47 + return suffix, uu, nil 48 + } 49 + 50 + func makeState(jkt string) string { 51 + uu, err := uuid.NewV7() 52 + if err != nil { 53 + panic(err) 54 + } 55 + return fmt.Sprintf("%s-%s", uu.String(), jkt) 56 + } 57 + 58 + func parseState(state string) (string, string, error) { 59 + if len(state) < UUID_LENGTH { 60 + return "", "", fmt.Errorf("invalid state: %s", state) 61 + } 62 + uu := state[:UUID_LENGTH] 63 + suffix := state[UUID_LENGTH:] 64 + return suffix, uu, nil 65 + } 66 + 67 + func makeNonce() string { 68 + uu, err := uuid.NewV7() 69 + if err != nil { 70 + panic(err) 71 + } 72 + return fmt.Sprintf("nonce-%s", uu.String()) 73 + } 74 + 75 + // returns jkt, nonce, error 76 + func getJKT(dpopJWT string) (string, string, error) { 77 + var claims dpop.ProofTokenClaims 78 + token, err := jwt.ParseWithClaims(dpopJWT, &claims, keyFunc) 79 + if err != nil { 80 + return "", "", err 81 + } 82 + jwk, ok := token.Header["jwk"].(map[string]any) 83 + if !ok { 84 + return "", "", fmt.Errorf("missing jwk in DPoP JWT header") 85 + } 86 + jwkJSONbytes, err := getThumbprintableJwkJSONbytes(jwk) 87 + if err != nil { 88 + // keyFunc used with parseWithClaims should ensure that this can not happen but better safe than sorry. 89 + return "", "", errors.Join(dpop.ErrInvalidProof, err) 90 + } 91 + h := sha256.New() 92 + _, err = h.Write(jwkJSONbytes) 93 + if err != nil { 94 + return "", "", errors.Join(dpop.ErrInvalidProof, err) 95 + } 96 + b64URLjwkHash := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 97 + 98 + return b64URLjwkHash, claims.Nonce, nil 99 + }
+155
pkg/oproxy/oauth_0_metadata.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "github.com/haileyok/atproto-oauth-golang/helpers" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (o *OProxy) HandleOAuthAuthorizationServer(c echo.Context) error { 13 + c.Response().Header().Set("Access-Control-Allow-Origin", "*") 14 + c.Response().Header().Set("Content-Type", "application/json") 15 + c.Response().WriteHeader(200) 16 + json.NewEncoder(c.Response().Writer).Encode(generateOAuthServerMetadata(o.host)) 17 + return nil 18 + } 19 + 20 + func (o *OProxy) HandleOAuthProtectedResource(c echo.Context) error { 21 + return c.JSON(200, map[string]interface{}{ 22 + "resource": fmt.Sprintf("https://%s", o.host), 23 + "authorization_servers": []string{ 24 + fmt.Sprintf("https://%s", o.host), 25 + }, 26 + "scopes_supported": []string{}, 27 + "bearer_methods_supported": []string{ 28 + "header", 29 + }, 30 + "resource_documentation": "https://atproto.com", 31 + }) 32 + } 33 + 34 + func (o *OProxy) HandleClientMetadataUpstream(c echo.Context) error { 35 + meta := o.GetUpstreamMetadata() 36 + return c.JSON(200, meta) 37 + } 38 + 39 + func (o *OProxy) HandleJwksUpstream(c echo.Context) error { 40 + pubKey, err := o.upstreamJWK.PublicKey() 41 + if err != nil { 42 + return echo.NewHTTPError(http.StatusInternalServerError, "could not get public key") 43 + } 44 + return c.JSON(200, helpers.CreateJwksResponseObject(pubKey)) 45 + } 46 + 47 + func (o *OProxy) HandleClientMetadataDownstream(c echo.Context) error { 48 + redirectURI := c.QueryParam("redirect_uri") 49 + meta, err := o.GetDownstreamMetadata(redirectURI) 50 + if err != nil { 51 + return err 52 + } 53 + return c.JSON(200, meta) 54 + } 55 + 56 + func (o *OProxy) GetUpstreamMetadata() *OAuthClientMetadata { 57 + // publicKey, err := o.upstreamJWK.PublicKey() 58 + // if err != nil { 59 + // panic(err) 60 + // } 61 + // jwks := jwk.NewSet() 62 + // err = jwks.AddKey(publicKey) 63 + // if err != nil { 64 + // panic(err) 65 + // } 66 + // ro := helpers.CreateJwksResponseObject(publicKey) 67 + meta := &OAuthClientMetadata{ 68 + ClientID: fmt.Sprintf("https://%s/oauth/upstream/client-metadata.json", o.host), 69 + JwksURI: fmt.Sprintf("https://%s/oauth/upstream/jwks.json", o.host), 70 + ClientURI: fmt.Sprintf("https://%s", o.host), 71 + // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)}, 72 + Scope: "atproto transition:generic", 73 + TokenEndpointAuthMethod: "private_key_jwt", 74 + ClientName: "Streamplace", 75 + ResponseTypes: []string{"code"}, 76 + GrantTypes: []string{"authorization_code", "refresh_token"}, 77 + DPoPBoundAccessTokens: boolPtr(true), 78 + TokenEndpointAuthSigningAlg: "ES256", 79 + RedirectURIs: []string{fmt.Sprintf("https://%s/oauth/return", o.host)}, 80 + // Jwks: ro, 81 + } 82 + return meta 83 + } 84 + 85 + func generateOAuthServerMetadata(host string) map[string]any { 86 + oauthServerMetadata := map[string]any{ 87 + "issuer": fmt.Sprintf("https://%s", host), 88 + "request_parameter_supported": true, 89 + "request_uri_parameter_supported": true, 90 + "require_request_uri_registration": true, 91 + "scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"}, 92 + "subject_types_supported": []string{"public"}, 93 + "response_types_supported": []string{"code"}, 94 + "response_modes_supported": []string{"query", "fragment", "form_post"}, 95 + "grant_types_supported": []string{"authorization_code", "refresh_token"}, 96 + "code_challenge_methods_supported": []string{"S256"}, 97 + "ui_locales_supported": []string{"en-US"}, 98 + "display_values_supported": []string{"page", "popup", "touch"}, 99 + "authorization_response_iss_parameter_supported": true, 100 + "request_object_encryption_alg_values_supported": []string{}, 101 + "request_object_encryption_enc_values_supported": []string{}, 102 + "jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host), 103 + "authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host), 104 + "token_endpoint": fmt.Sprintf("https://%s/oauth/token", host), 105 + "token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"}, 106 + "revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host), 107 + "introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host), 108 + "pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host), 109 + "require_pushed_authorization_requests": true, 110 + "client_id_metadata_document_supported": true, 111 + "request_object_signing_alg_values_supported": []string{ 112 + "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", 113 + "ES256", "ES256K", "ES384", "ES512", "none", 114 + }, 115 + "token_endpoint_auth_signing_alg_values_supported": []string{ 116 + "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", 117 + "ES256", "ES256K", "ES384", "ES512", 118 + }, 119 + "dpop_signing_alg_values_supported": []string{ 120 + "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", 121 + "ES256", "ES256K", "ES384", "ES512", 122 + }, 123 + } 124 + return oauthServerMetadata 125 + } 126 + 127 + func (o *OProxy) GetDownstreamMetadata(redirectURI string) (*OAuthClientMetadata, error) { 128 + meta := &OAuthClientMetadata{ 129 + ClientID: fmt.Sprintf("https://%s/oauth/downstream/client-metadata.json", o.host), 130 + ClientURI: fmt.Sprintf("https://%s", o.host), 131 + // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)}, 132 + Scope: "atproto transition:generic", 133 + TokenEndpointAuthMethod: "none", 134 + ClientName: "Streamplace", 135 + ResponseTypes: []string{"code"}, 136 + GrantTypes: []string{"authorization_code", "refresh_token"}, 137 + DPoPBoundAccessTokens: boolPtr(true), 138 + RedirectURIs: []string{fmt.Sprintf("https://%s/login", o.host), fmt.Sprintf("https://%s/api/app-return", o.host)}, 139 + ApplicationType: "web", 140 + } 141 + if redirectURI != "" { 142 + found := false 143 + for _, uri := range meta.RedirectURIs { 144 + if uri == redirectURI { 145 + found = true 146 + break 147 + } 148 + } 149 + if !found { 150 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", redirectURI)) 151 + } 152 + meta.RedirectURIs = []string{redirectURI} 153 + } 154 + return meta, nil 155 + }
+178
pkg/oproxy/oauth_1_par.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "net/http" 9 + "net/url" 10 + "slices" 11 + 12 + "github.com/AxisCommunications/go-dpop" 13 + "github.com/labstack/echo/v4" 14 + "go.opentelemetry.io/otel" 15 + ) 16 + 17 + type PAR struct { 18 + ClientID string `json:"client_id"` 19 + RedirectURI string `json:"redirect_uri"` 20 + CodeChallenge string `json:"code_challenge"` 21 + CodeChallengeMethod string `json:"code_challenge_method"` 22 + State string `json:"state"` 23 + LoginHint string `json:"login_hint"` 24 + ResponseMode string `json:"response_mode"` 25 + ResponseType string `json:"response_type"` 26 + Scope string `json:"scope"` 27 + } 28 + 29 + type PARResponse struct { 30 + RequestURI string `json:"request_uri"` 31 + ExpiresIn int `json:"expires_in"` 32 + } 33 + 34 + var ErrFirstNonce = echo.NewHTTPError(http.StatusBadRequest, "first time seeing this key, come back with a nonce") 35 + 36 + func (o *OProxy) HandleOAuthPAR(c echo.Context) error { 37 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthPAR") 38 + defer span.End() 39 + c.Response().Header().Set("Access-Control-Allow-Origin", "*") 40 + var par PAR 41 + if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil { 42 + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 43 + } 44 + 45 + dpopHeader := c.Request().Header.Get("DPoP") 46 + if dpopHeader == "" { 47 + return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 48 + } 49 + 50 + resp, err := o.NewPAR(ctx, c, &par, dpopHeader) 51 + if errors.Is(err, ErrFirstNonce) { 52 + res := map[string]interface{}{ 53 + "error": "use_dpop_nonce", 54 + "error_description": "Authorization server requires nonce in DPoP proof", 55 + } 56 + return c.JSON(http.StatusBadRequest, res) 57 + } else if err != nil { 58 + return err 59 + } 60 + return c.JSON(http.StatusCreated, resp) 61 + } 62 + 63 + func (o *OProxy) NewPAR(ctx context.Context, c echo.Context, par *PAR, dpopHeader string) (*PARResponse, error) { 64 + jkt, nonce, err := getJKT(dpopHeader) 65 + if err != nil { 66 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get JKT from DPoP header header=%s: %s", dpopHeader, err)) 67 + } 68 + session, err := o.loadOAuthSession(jkt) 69 + if err != nil { 70 + return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to load OAuth session: %s", err)) 71 + } 72 + // special case - if this is the first request, we need to send it back for a new nonce 73 + if session == nil { 74 + _, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{ 75 + Nonce: nonce, // normally this would be bad! but on the first request we're revalidating nonce anyway 76 + TimeWindow: &dpopTimeWindow, 77 + }) 78 + if err != nil { 79 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse DPoP header: %s", err)) 80 + } 81 + newNonce := makeNonce() 82 + err = o.createOAuthSession(jkt, &OAuthSession{ 83 + DownstreamDPoPJKT: jkt, 84 + DownstreamDPoPNonce: newNonce, 85 + }) 86 + if err != nil { 87 + return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth session: %s", err)) 88 + } 89 + // come back later, nerd 90 + c.Response().Header().Set("DPoP-Nonce", newNonce) 91 + return nil, ErrFirstNonce 92 + } 93 + if session.DownstreamDPoPNonce != nonce { 94 + return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid nonce") 95 + } 96 + proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{ 97 + Nonce: session.DownstreamDPoPNonce, 98 + TimeWindow: &dpopTimeWindow, 99 + }) 100 + // Check the error type to determine response 101 + if err != nil { 102 + // if ok := errors.Is(err, dpop.ErrInvalidProof); ok { 103 + // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil) 104 + // return 105 + // } 106 + // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err) 107 + // return 108 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid DPoP proof: %s", err)) 109 + } 110 + if proof.PublicKey() != jkt { 111 + panic("invalid code path: parsed DPoP proof twice and got different keys?!") 112 + } 113 + 114 + clientMetadata, err := o.GetDownstreamMetadata(par.RedirectURI) 115 + if err != nil { 116 + return nil, err 117 + } 118 + if par.ClientID != clientMetadata.ClientID { 119 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid client_id: expected %s, got %s", clientMetadata.ClientID, par.ClientID)) 120 + } 121 + 122 + if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) { 123 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", par.RedirectURI)) 124 + } 125 + 126 + if par.CodeChallengeMethod != "S256" { 127 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid code challenge method: expected S256, got %s", par.CodeChallengeMethod)) 128 + } 129 + 130 + if par.ResponseMode != "query" { 131 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response mode: expected query, got %s", par.ResponseMode)) 132 + } 133 + 134 + if par.ResponseType != "code" { 135 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response type: expected code, got %s", par.ResponseType)) 136 + } 137 + 138 + if par.Scope != o.scope { 139 + return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope") 140 + } 141 + 142 + if par.LoginHint == "" { 143 + return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS") 144 + } 145 + 146 + if par.State == "" { 147 + return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required") 148 + } 149 + 150 + if par.Scope != o.scope { 151 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope)) 152 + } 153 + 154 + urn := makeURN(jkt) 155 + 156 + newNonce := makeNonce() 157 + 158 + err = o.updateOAuthSession(jkt, &OAuthSession{ 159 + DownstreamDPoPJKT: jkt, 160 + DownstreamDPoPNonce: newNonce, 161 + DownstreamPARRequestURI: urn, 162 + DownstreamCodeChallenge: par.CodeChallenge, 163 + DownstreamState: par.State, 164 + DownstreamRedirectURI: par.RedirectURI, 165 + Handle: par.LoginHint, 166 + }) 167 + if err != nil { 168 + return nil, fmt.Errorf("could not create oauth session: %w", err) 169 + } 170 + c.Response().Header().Set("DPoP-Nonce", newNonce) 171 + 172 + resp := &PARResponse{ 173 + RequestURI: urn, 174 + ExpiresIn: int(dpopTimeWindow.Seconds()), 175 + } 176 + 177 + return resp, nil 178 + }
+165
pkg/oproxy/oauth_2_authorize.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + "net/url" 9 + "time" 10 + 11 + oauth "github.com/haileyok/atproto-oauth-golang" 12 + "github.com/haileyok/atproto-oauth-golang/helpers" 13 + "github.com/labstack/echo/v4" 14 + "go.opentelemetry.io/otel" 15 + ) 16 + 17 + func (o *OProxy) HandleOAuthAuthorize(c echo.Context) error { 18 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthAuthorize") 19 + defer span.End() 20 + c.Response().Header().Set("Access-Control-Allow-Origin", "*") 21 + requestURI := c.QueryParam("request_uri") 22 + if requestURI == "" { 23 + return echo.NewHTTPError(http.StatusBadRequest, "request_uri is required") 24 + } 25 + clientID := c.QueryParam("client_id") 26 + if clientID == "" { 27 + return echo.NewHTTPError(http.StatusBadRequest, "client_id is required") 28 + } 29 + redirectURL, err := o.Authorize(ctx, requestURI, clientID) 30 + if err != nil { 31 + // we're a redirect; if we fail we need to send the user back 32 + jkt, _, err := parseURN(requestURI) 33 + if err != nil { 34 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err)) 35 + } 36 + 37 + session, err := o.loadOAuthSession(jkt) 38 + if err != nil { 39 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err)) 40 + } 41 + 42 + u, err := url.Parse(session.DownstreamRedirectURI) 43 + if err != nil { 44 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err)) 45 + } 46 + q := u.Query() 47 + q.Set("error", "authorize_failed") 48 + q.Set("error_description", err.Error()) 49 + u.RawQuery = q.Encode() 50 + return c.Redirect(http.StatusTemporaryRedirect, u.String()) 51 + } 52 + return c.Redirect(http.StatusTemporaryRedirect, redirectURL) 53 + } 54 + 55 + // downstream --> upstream transition; attempt to send user to the upstream auth server 56 + func (o *OProxy) Authorize(ctx context.Context, requestURI, clientID string) (string, *echo.HTTPError) { 57 + downstreamMeta, err := o.GetDownstreamMetadata("") 58 + if err != nil { 59 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get downstream metadata: %s", err)) 60 + } 61 + if downstreamMeta.ClientID != clientID { 62 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("client ID mismatch: %s != %s", downstreamMeta.ClientID, clientID)) 63 + } 64 + 65 + jkt, _, err := parseURN(requestURI) 66 + if err != nil { 67 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err)) 68 + } 69 + 70 + session, err := o.loadOAuthSession(jkt) 71 + if err != nil { 72 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err)) 73 + } 74 + 75 + if session == nil { 76 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no session found for jkt=%s", jkt)) 77 + } 78 + 79 + if session.Status() != OAuthSessionStatePARCreated { 80 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in par-created state: %s", session.Status())) 81 + } 82 + 83 + if session.DownstreamPARRequestURI != requestURI { 84 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("request URI mismatch: %s != %s", session.DownstreamPARRequestURI, requestURI)) 85 + } 86 + 87 + now := time.Now() 88 + session.DownstreamPARUsedAt = &now 89 + err = o.updateOAuthSession(jkt, session) 90 + if err != nil { 91 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err)) 92 + } 93 + 94 + upstreamMeta := o.GetUpstreamMetadata() 95 + oclient, err := oauth.NewClient(oauth.ClientArgs{ 96 + ClientJwk: o.upstreamJWK, 97 + ClientId: upstreamMeta.ClientID, 98 + RedirectUri: upstreamMeta.RedirectURIs[0], 99 + }) 100 + if err != nil { 101 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth client: %s", err)) 102 + } 103 + 104 + did, err := ResolveHandle(ctx, session.Handle) 105 + if err != nil { 106 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve handle '%s': %s", session.DID, err)) 107 + } 108 + 109 + service, err := ResolveService(ctx, did) 110 + if err != nil { 111 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve service for DID '%s': %s", did, err)) 112 + } 113 + 114 + authserver, err := oclient.ResolvePdsAuthServer(ctx, service) 115 + if err != nil { 116 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve PDS auth server for service '%s': %s", service, err)) 117 + } 118 + 119 + authmeta, err := oclient.FetchAuthServerMetadata(ctx, authserver) 120 + if err != nil { 121 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to fetch auth server metadata from '%s': %s", authserver, err)) 122 + } 123 + 124 + k, err := helpers.GenerateKey(nil) 125 + if err != nil { 126 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate DPoP key: %s", err)) 127 + } 128 + 129 + state := makeState(jkt) 130 + 131 + opts := oauth.ParAuthRequestOpts{ 132 + State: state, 133 + } 134 + parResp, err := oclient.SendParAuthRequest(ctx, authserver, authmeta, session.Handle, upstreamMeta.Scope, k, opts) 135 + if err != nil { 136 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to send PAR auth request to '%s': %s", authserver, err)) 137 + } 138 + 139 + jwkJSON, err := json.Marshal(k) 140 + if err != nil { 141 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to marshal DPoP key to JSON: %s", err)) 142 + } 143 + 144 + u, err := url.Parse(authmeta.AuthorizationEndpoint) 145 + if err != nil { 146 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse auth server metadata: %s", err)) 147 + } 148 + u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(upstreamMeta.ClientID), parResp.RequestUri) 149 + str := u.String() 150 + 151 + session.DID = did 152 + session.PDSUrl = service 153 + session.UpstreamState = parResp.State 154 + session.UpstreamAuthServerIssuer = authserver 155 + session.UpstreamPKCEVerifier = parResp.PkceVerifier 156 + session.UpstreamDPoPNonce = parResp.DpopAuthserverNonce 157 + session.UpstreamDPoPPrivateJWK = string(jwkJSON) 158 + 159 + err = o.updateOAuthSession(jkt, session) 160 + if err != nil { 161 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err)) 162 + } 163 + 164 + return str, nil 165 + }
+156
pkg/oproxy/oauth_3_return.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "net/url" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/api/atproto" 11 + "github.com/bluesky-social/indigo/xrpc" 12 + oauth "github.com/haileyok/atproto-oauth-golang" 13 + "github.com/labstack/echo/v4" 14 + "github.com/lestrrat-go/jwx/v2/jwk" 15 + "go.opentelemetry.io/otel" 16 + ) 17 + 18 + func (o *OProxy) HandleOAuthReturn(c echo.Context) error { 19 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthReturn") 20 + defer span.End() 21 + code := c.QueryParam("code") 22 + iss := c.QueryParam("iss") 23 + state := c.QueryParam("state") 24 + errorCode := c.QueryParam("error") 25 + errorDescription := c.QueryParam("error_description") 26 + var httpError *echo.HTTPError 27 + var redirectURL string 28 + if errorCode != "" { 29 + httpError = echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("%s (%s)", errorDescription, errorCode)) 30 + } else { 31 + redirectURL, httpError = o.Return(ctx, code, iss, state) 32 + } 33 + if httpError != nil { 34 + // we're a redirect; if we fail we need to send the user back 35 + jkt, _, err := parseState(state) 36 + if err != nil { 37 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err)) 38 + } 39 + 40 + session, err := o.loadOAuthSession(jkt) 41 + if err != nil { 42 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err)) 43 + } 44 + 45 + u, err := url.Parse(session.DownstreamRedirectURI) 46 + if err != nil { 47 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err)) 48 + } 49 + q := u.Query() 50 + q.Set("error", "return_failed") 51 + q.Set("error_description", httpError.Error()) 52 + u.RawQuery = q.Encode() 53 + return c.Redirect(http.StatusTemporaryRedirect, u.String()) 54 + } 55 + return c.Redirect(http.StatusTemporaryRedirect, redirectURL) 56 + } 57 + 58 + func (o *OProxy) Return(ctx context.Context, code string, iss string, state string) (string, *echo.HTTPError) { 59 + upstreamMeta := o.GetUpstreamMetadata() 60 + oclient, err := oauth.NewClient(oauth.ClientArgs{ 61 + ClientJwk: o.upstreamJWK, 62 + ClientId: upstreamMeta.ClientID, 63 + RedirectUri: upstreamMeta.RedirectURIs[0], 64 + }) 65 + 66 + jkt, _, err := parseState(state) 67 + if err != nil { 68 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse state: %s", err)) 69 + } 70 + 71 + session, err := o.loadOAuthSession(jkt) 72 + if err != nil { 73 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get OAuth session: %s", err)) 74 + } 75 + if session == nil { 76 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no OAuth session found for state: %s", state)) 77 + } 78 + 79 + if session.Status() != OAuthSessionStateUpstream { 80 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in upstream state: %s", session.Status())) 81 + } 82 + 83 + if session.UpstreamState != state { 84 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("state mismatch: %s != %s", session.UpstreamState, state)) 85 + } 86 + 87 + if iss != session.UpstreamAuthServerIssuer { 88 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("issuer mismatch: %s != %s", iss, session.UpstreamAuthServerIssuer)) 89 + } 90 + 91 + key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 92 + if err != nil { 93 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to parse DPoP private JWK: %s", err)) 94 + } 95 + 96 + itResp, err := oclient.InitialTokenRequest(ctx, code, iss, session.UpstreamPKCEVerifier, session.UpstreamDPoPNonce, key) 97 + if err != nil { 98 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to request initial token: %s", err)) 99 + } 100 + now := time.Now() 101 + 102 + if itResp.Sub != session.DID { 103 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("sub mismatch: %s != %s", itResp.Sub, session.DID)) 104 + } 105 + 106 + if itResp.Scope != upstreamMeta.Scope { 107 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("scope mismatch: %s != %s", itResp.Scope, upstreamMeta.Scope)) 108 + } 109 + 110 + downstreamCode, err := generateAuthorizationCode() 111 + if err != nil { 112 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate downstream code: %s", err)) 113 + } 114 + 115 + expiry := now.Add(time.Second * time.Duration(itResp.ExpiresIn)).UTC() 116 + session.UpstreamAccessToken = itResp.AccessToken 117 + session.UpstreamAccessTokenExp = &expiry 118 + session.UpstreamRefreshToken = itResp.RefreshToken 119 + session.DownstreamAuthorizationCode = downstreamCode 120 + 121 + authArgs := &oauth.XrpcAuthedRequestArgs{ 122 + Did: session.DID, 123 + AccessToken: session.UpstreamAccessToken, 124 + PdsUrl: session.PDSUrl, 125 + Issuer: session.UpstreamAuthServerIssuer, 126 + DpopPdsNonce: session.UpstreamDPoPNonce, 127 + DpopPrivateJwk: key, 128 + } 129 + 130 + xrpcClient := &oauth.XrpcClient{ 131 + OnDpopPdsNonceChanged: func(did, newNonce string) {}, 132 + } 133 + 134 + // brief check to make sure we can actually do stuff 135 + var out atproto.ServerCheckAccountStatus_Output 136 + if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.server.checkAccountStatus", nil, nil, &out); err != nil { 137 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to check account status: %s", err)) 138 + } 139 + 140 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 141 + if err != nil { 142 + return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err)) 143 + } 144 + 145 + u, err := url.Parse(session.DownstreamRedirectURI) 146 + if err != nil { 147 + return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err)) 148 + } 149 + q := u.Query() 150 + q.Set("iss", fmt.Sprintf("https://%s", o.host)) 151 + q.Set("state", session.DownstreamState) 152 + q.Set("code", session.DownstreamAuthorizationCode) 153 + u.RawQuery = q.Encode() 154 + 155 + return u.String(), nil 156 + }
+221
pkg/oproxy/oauth_4_token.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "crypto/sha256" 6 + "encoding/base64" 7 + "encoding/json" 8 + "fmt" 9 + "net/http" 10 + "net/url" 11 + "time" 12 + 13 + "github.com/AxisCommunications/go-dpop" 14 + "github.com/golang-jwt/jwt/v5" 15 + "github.com/google/uuid" 16 + "github.com/labstack/echo/v4" 17 + "go.opentelemetry.io/otel" 18 + ) 19 + 20 + type TokenRequest struct { 21 + GrantType string `json:"grant_type"` 22 + RedirectURI string `json:"redirect_uri"` 23 + Code string `json:"code"` 24 + CodeVerifier string `json:"code_verifier"` 25 + ClientID string `json:"client_id"` 26 + RefreshToken string `json:"refresh_token"` 27 + } 28 + 29 + type RevokeRequest struct { 30 + Token string `json:"token"` 31 + ClientID string `json:"client_id"` 32 + } 33 + 34 + type TokenResponse struct { 35 + AccessToken string `json:"access_token"` 36 + TokenType string `json:"token_type"` 37 + RefreshToken string `json:"refresh_token"` 38 + Scope string `json:"scope"` 39 + ExpiresIn int `json:"expires_in"` 40 + Sub string `json:"sub"` 41 + } 42 + 43 + var OAuthTokenExpiry = time.Hour * 24 44 + 45 + var dpopTimeWindow = time.Duration(30 * time.Second) 46 + 47 + func (o *OProxy) HandleOAuthToken(c echo.Context) error { 48 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthToken") 49 + defer span.End() 50 + var tokenRequest TokenRequest 51 + if err := json.NewDecoder(c.Request().Body).Decode(&tokenRequest); err != nil { 52 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err)) 53 + } 54 + 55 + dpopHeader := c.Request().Header.Get("DPoP") 56 + if dpopHeader == "" { 57 + return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 58 + } 59 + 60 + res, err := o.Token(ctx, &tokenRequest, dpopHeader) 61 + if err != nil { 62 + return err 63 + } 64 + jkt, _, err := getJKT(dpopHeader) 65 + if err != nil { 66 + return err 67 + } 68 + sess, err := o.loadOAuthSession(jkt) 69 + if err != nil { 70 + return err 71 + } 72 + sess.DownstreamDPoPNonce = makeNonce() 73 + err = o.updateOAuthSession(sess.DownstreamDPoPJKT, sess) 74 + if err != nil { 75 + return err 76 + } 77 + c.Response().Header().Set("DPoP-Nonce", sess.DownstreamDPoPNonce) 78 + 79 + return c.JSON(http.StatusOK, res) 80 + } 81 + 82 + func (o *OProxy) Token(ctx context.Context, tokenRequest *TokenRequest, dpopHeader string) (*TokenResponse, error) { 83 + proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/token"}, dpop.ParseOptions{ 84 + Nonce: "", 85 + TimeWindow: &dpopTimeWindow, 86 + }) 87 + if err != nil { 88 + return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof") 89 + } 90 + 91 + jkt := proof.PublicKey() 92 + session, err := o.loadOAuthSession(jkt) 93 + if err != nil { 94 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not get oauth session: %s", err)) 95 + } 96 + 97 + if tokenRequest.GrantType == "authorization_code" { 98 + return o.AccessToken(ctx, tokenRequest, session) 99 + } else if tokenRequest.GrantType == "refresh_token" { 100 + return o.RefreshToken(ctx, tokenRequest, session) 101 + } 102 + return nil, echo.NewHTTPError(http.StatusBadRequest, "unsupported grant type") 103 + } 104 + 105 + func (o *OProxy) AccessToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) { 106 + if session.Status() != OAuthSessionStateDownstream { 107 + return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in downstream state: %s", session.Status())) 108 + } 109 + 110 + // Hash the code verifier using SHA-256 111 + hasher := sha256.New() 112 + hasher.Write([]byte(tokenRequest.CodeVerifier)) 113 + codeChallenge := hasher.Sum(nil) 114 + 115 + encodedChallenge := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(codeChallenge) 116 + 117 + if session.DownstreamCodeChallenge != encodedChallenge { 118 + return nil, fmt.Errorf("invalid code challenge") 119 + } 120 + 121 + if session.DownstreamAuthorizationCode != tokenRequest.Code { 122 + return nil, fmt.Errorf("invalid authorization code") 123 + } 124 + 125 + accessToken, err := o.generateJWT(session) 126 + if err != nil { 127 + return nil, fmt.Errorf("could not generate access token: %w", err) 128 + } 129 + 130 + refreshToken, err := generateRefreshToken() 131 + if err != nil { 132 + return nil, fmt.Errorf("could not generate refresh token: %w", err) 133 + } 134 + 135 + session.DownstreamAccessToken = accessToken 136 + session.DownstreamRefreshToken = refreshToken 137 + 138 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 139 + if err != nil { 140 + return nil, fmt.Errorf("could not update downstream session: %w", err) 141 + } 142 + 143 + return &TokenResponse{ 144 + AccessToken: accessToken, 145 + TokenType: "DPoP", 146 + RefreshToken: refreshToken, 147 + Scope: "atproto transition:generic", 148 + ExpiresIn: int(OAuthTokenExpiry.Seconds()), 149 + Sub: session.DID, 150 + }, nil 151 + } 152 + 153 + func (o *OProxy) RefreshToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) { 154 + 155 + if session.Status() != OAuthSessionStateReady { 156 + return nil, echo.NewHTTPError(http.StatusBadRequest, "session is not in ready state") 157 + } 158 + 159 + if session.DownstreamRefreshToken != tokenRequest.RefreshToken { 160 + return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid refresh token") 161 + } 162 + 163 + newJWT, err := o.generateJWT(session) 164 + if err != nil { 165 + return nil, fmt.Errorf("could not generate new access token: %w", err) 166 + } 167 + 168 + session.DownstreamAccessToken = newJWT 169 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 170 + if err != nil { 171 + return nil, fmt.Errorf("could not update downstream session: %w", err) 172 + } 173 + 174 + return &TokenResponse{ 175 + AccessToken: newJWT, 176 + TokenType: "DPoP", 177 + RefreshToken: session.DownstreamRefreshToken, 178 + Scope: "atproto transition:generic", 179 + ExpiresIn: int(OAuthTokenExpiry.Seconds()), 180 + Sub: session.DID, 181 + }, nil 182 + } 183 + 184 + func (o *OProxy) generateJWT(session *OAuthSession) (string, error) { 185 + uu, err := uuid.NewV7() 186 + if err != nil { 187 + return "", err 188 + } 189 + downstreamMeta, err := o.GetDownstreamMetadata("") 190 + if err != nil { 191 + return "", err 192 + } 193 + now := time.Now() 194 + token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ 195 + "jti": uu.String(), 196 + "sub": session.DID, 197 + "exp": now.Add(OAuthTokenExpiry).Unix(), 198 + "iat": now.Unix(), 199 + "nbf": now.Unix(), 200 + "cnf": map[string]any{ 201 + "jkt": session.DownstreamDPoPJKT, 202 + }, 203 + "aud": fmt.Sprintf("did:web:%s", o.host), 204 + "scope": downstreamMeta.Scope, 205 + "client_id": downstreamMeta.ClientID, 206 + "iss": fmt.Sprintf("https://%s", o.host), 207 + }) 208 + 209 + var rawKey any 210 + if err := o.downstreamJWK.Raw(&rawKey); err != nil { 211 + return "", err 212 + } 213 + 214 + tokenString, err := token.SignedString(rawKey) 215 + 216 + if err != nil { 217 + return "", err 218 + } 219 + 220 + return tokenString, nil 221 + }
+56
pkg/oproxy/oauth_5_revoke.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + "net/url" 9 + "time" 10 + 11 + "github.com/AxisCommunications/go-dpop" 12 + "github.com/labstack/echo/v4" 13 + "go.opentelemetry.io/otel" 14 + ) 15 + 16 + func (o *OProxy) HandleOAuthRevoke(c echo.Context) error { 17 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthRevoke") 18 + defer span.End() 19 + var revokeRequest RevokeRequest 20 + if err := json.NewDecoder(c.Request().Body).Decode(&revokeRequest); err != nil { 21 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err)) 22 + } 23 + dpopHeader := c.Request().Header.Get("DPoP") 24 + if dpopHeader == "" { 25 + return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 26 + } 27 + err := o.Revoke(ctx, dpopHeader, &revokeRequest) 28 + if err != nil { 29 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not handle oauth revoke: %s", err)) 30 + } 31 + return c.JSON(http.StatusOK, map[string]interface{}{}) 32 + } 33 + 34 + func (o *OProxy) Revoke(ctx context.Context, dpopHeader string, revokeRequest *RevokeRequest) error { 35 + proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/revoke"}, dpop.ParseOptions{ 36 + Nonce: "", 37 + TimeWindow: &dpopTimeWindow, 38 + }) 39 + if err != nil { 40 + return echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof") 41 + } 42 + 43 + session, err := o.loadOAuthSession(proof.PublicKey()) 44 + if err != nil { 45 + return fmt.Errorf("could not get downstream session: %w", err) 46 + } 47 + 48 + now := time.Now() 49 + session.RevokedAt = &now 50 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 51 + if err != nil { 52 + return fmt.Errorf("could not update downstream session: %w", err) 53 + } 54 + 55 + return nil 56 + }
+234
pkg/oproxy/oauth_middleware.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "crypto/ecdsa" 6 + "crypto/sha256" 7 + "encoding/base64" 8 + "encoding/json" 9 + "errors" 10 + "fmt" 11 + "net/http" 12 + "net/url" 13 + "strings" 14 + 15 + "github.com/AxisCommunications/go-dpop" 16 + "github.com/golang-jwt/jwt/v5" 17 + "github.com/labstack/echo/v4" 18 + ) 19 + 20 + var OAuthSessionContextKey = oauthSessionContextKeyType{} 21 + 22 + type oauthSessionContextKeyType struct{} 23 + 24 + var OProxyContextKey = oproxyContextKeyType{} 25 + 26 + type oproxyContextKeyType struct{} 27 + 28 + func GetOAuthSession(ctx context.Context) (*OAuthSession, *XrpcClient) { 29 + o, ok := ctx.Value(OProxyContextKey).(*OProxy) 30 + if !ok { 31 + return nil, nil 32 + } 33 + session, ok := ctx.Value(OAuthSessionContextKey).(*OAuthSession) 34 + if !ok { 35 + return nil, nil 36 + } 37 + client, err := o.GetXrpcClient(session) 38 + if err != nil { 39 + return nil, nil 40 + } 41 + return session, client 42 + } 43 + 44 + func (o *OProxy) OAuthMiddleware(next http.Handler) http.Handler { 45 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 + // todo: see what these were set to before it got to us. 47 + w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh 48 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP") 49 + w.Header().Set("Access-Control-Allow-Methods", "*") 50 + w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce") 51 + 52 + ctx := r.Context() 53 + session, err := o.getOAuthSession(r, w) 54 + if err != nil { 55 + if errors.Is(err, dpop.ErrIncorrectNonce) { 56 + // w.Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce", error_description="Invalid nonce"`) 57 + w.Header().Set("content-type", "application/json") 58 + w.WriteHeader(http.StatusUnauthorized) 59 + bs, _ := json.Marshal(map[string]interface{}{ 60 + "error": "use_dpop_nonce", 61 + "error_description": "Authorization server requires nonce in DPoP proof", 62 + }) 63 + w.Write(bs) 64 + return 65 + } 66 + w.WriteHeader(http.StatusInternalServerError) 67 + w.Write([]byte(err.Error())) 68 + return 69 + } 70 + if session == nil { 71 + next.ServeHTTP(w, r) 72 + return 73 + } 74 + ctx = context.WithValue(ctx, OAuthSessionContextKey, session) 75 + ctx = context.WithValue(ctx, OProxyContextKey, o) 76 + next.ServeHTTP(w, r.WithContext(ctx)) 77 + }) 78 + } 79 + 80 + func getMethod(method string) (dpop.HTTPVerb, error) { 81 + switch method { 82 + case "POST": 83 + return dpop.POST, nil 84 + case "GET": 85 + return dpop.GET, nil 86 + } 87 + return "", fmt.Errorf("invalid method") 88 + } 89 + 90 + func (o *OProxy) getOAuthSession(r *http.Request, w http.ResponseWriter) (*OAuthSession, error) { 91 + 92 + authHeader := r.Header.Get("Authorization") 93 + if authHeader == "" { 94 + return nil, nil 95 + } 96 + if !strings.HasPrefix(authHeader, "DPoP ") { 97 + return nil, fmt.Errorf("invalid authorization header (must start with DPoP)") 98 + } 99 + token := strings.TrimPrefix(authHeader, "DPoP ") 100 + 101 + dpopHeader := r.Header.Get("DPoP") 102 + if dpopHeader == "" { 103 + return nil, fmt.Errorf("missing DPoP header") 104 + } 105 + 106 + dpopMethod, err := getMethod(r.Method) 107 + if err != nil { 108 + return nil, fmt.Errorf("invalid method: %w", err) 109 + } 110 + 111 + u, err := url.Parse(r.URL.String()) 112 + if err != nil { 113 + return nil, fmt.Errorf("invalid url: %w", err) 114 + } 115 + u.Scheme = "https" 116 + u.Host = r.Host 117 + u.RawQuery = "" 118 + u.Fragment = "" 119 + 120 + jkt, nonce, err := getJKT(dpopHeader) 121 + 122 + session, err := o.loadOAuthSession(jkt) 123 + if err != nil { 124 + return nil, fmt.Errorf("could not get oauth session: %w", err) 125 + } 126 + if session == nil { 127 + return nil, fmt.Errorf("oauth session not found") 128 + } 129 + if session.RevokedAt != nil { 130 + return nil, fmt.Errorf("oauth session revoked") 131 + } 132 + if session.DownstreamDPoPNonce != nonce { 133 + w.Header().Set("WWW-Authenticate", `DPoP algs="RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES256K ES384 ES512", error="use_dpop_nonce", error_description="Authorization server requires nonce in DPoP proof"`) 134 + w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce) 135 + return nil, dpop.ErrIncorrectNonce 136 + } 137 + 138 + session.DownstreamDPoPNonce = makeNonce() 139 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 140 + if err != nil { 141 + return nil, fmt.Errorf("could not update downstream session: %w", err) 142 + } 143 + w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce) 144 + 145 + proof, err := dpop.Parse(dpopHeader, dpopMethod, u, dpop.ParseOptions{ 146 + Nonce: nonce, 147 + TimeWindow: &dpopTimeWindow, 148 + }) 149 + // Check the error type to determine response 150 + if err != nil { 151 + if ok := errors.Is(err, dpop.ErrInvalidProof); ok { 152 + // Return 'invalid_dpop_proof' 153 + return nil, fmt.Errorf("invalid DPoP proof: %w", err) 154 + } 155 + return nil, fmt.Errorf("error validating proof proof: %w", err) 156 + } 157 + 158 + // Hash the token with base64 and SHA256 159 + // Get the access token JWT (introspect if needed) 160 + // Parse the access token JWT and verify the signature 161 + // Hash the access token with SHA-256 162 + hasher := sha256.New() 163 + hasher.Write([]byte(token)) 164 + hash := hasher.Sum(nil) 165 + 166 + // Encode the hash in URL-safe base64 format without padding 167 + // accessTokenHash := base64.RawURLEncoding.EncodeToString(hash) 168 + accessTokenHash := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) 169 + pubKey, err := o.downstreamJWK.PublicKey() 170 + if err != nil { 171 + return nil, fmt.Errorf("could not get access jwk public key: %w", err) 172 + } 173 + var pubKeyECDSA ecdsa.PublicKey 174 + err = pubKey.Raw(&pubKeyECDSA) 175 + if err != nil { 176 + return nil, fmt.Errorf("could not get access jwk public key: %w", err) 177 + } 178 + 179 + // Parse the access token JWT 180 + claims := &dpop.BoundAccessTokenClaims{} 181 + accessTokenJWT, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) { 182 + return &pubKeyECDSA, nil 183 + }) 184 + 185 + if err != nil { 186 + return nil, fmt.Errorf("could not parse access token: %w", err) 187 + } 188 + 189 + err = proof.Validate([]byte(accessTokenHash), accessTokenJWT) 190 + // Check the error type to determine response 191 + if err != nil { 192 + return nil, fmt.Errorf("invalid proof: %w", err) 193 + } 194 + 195 + return session, nil 196 + } 197 + 198 + func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 199 + return func(c echo.Context) error { 200 + dpopHeader := c.Request().Header.Get("DPoP") 201 + if dpopHeader == "" { 202 + return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header") 203 + } 204 + 205 + jkt, _, err := getJKT(dpopHeader) 206 + if err != nil { 207 + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 208 + } 209 + 210 + session, err := o.loadOAuthSession(jkt) 211 + if err != nil { 212 + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 213 + } 214 + 215 + c.Set("session", session) 216 + return next(c) 217 + } 218 + } 219 + 220 + func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 221 + return func(c echo.Context) error { 222 + err := next(c) 223 + if err == nil { 224 + return nil 225 + } 226 + httpError, ok := err.(*echo.HTTPError) 227 + if ok { 228 + o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal) 229 + return err 230 + } 231 + o.slog.Error("unhandled error", "error", err) 232 + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 233 + } 234 + }
+156
pkg/oproxy/oauth_session.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "time" 8 + 9 + oauth "github.com/haileyok/atproto-oauth-golang" 10 + "github.com/lestrrat-go/jwx/v2/jwk" 11 + ) 12 + 13 + var refreshWhenRemaining = time.Minute * 59 14 + 15 + // OAuthSession stores authentication data needed during the OAuth flow 16 + type OAuthSession struct { 17 + DID string `json:"did" gorm:"column:repo_did;index"` 18 + Handle string `json:"handle" gorm:"column:handle;index"` // possibly also did if they have no handle 19 + PDSUrl string `json:"pds_url" gorm:"column:pds_url;index"` 20 + 21 + // Upstream fields 22 + UpstreamState string `json:"upstream_state" gorm:"column:upstream_state;index"` 23 + UpstreamAuthServerIssuer string `json:"upstream_auth_server_issuer" gorm:"column:upstream_auth_server_issuer"` 24 + UpstreamPKCEVerifier string `json:"upstream_pkce_verifier" gorm:"column:upstream_pkce_verifier"` 25 + UpstreamDPoPNonce string `json:"upstream_dpop_nonce" gorm:"column:upstream_dpop_nonce"` 26 + UpstreamDPoPPrivateJWK string `json:"upstream_dpop_private_jwk" gorm:"column:upstream_dpop_private_jwk;type:text"` 27 + UpstreamAccessToken string `json:"upstream_access_token" gorm:"column:upstream_access_token"` 28 + UpstreamAccessTokenExp *time.Time `json:"upstream_access_token_exp" gorm:"column:upstream_access_token_exp"` 29 + UpstreamRefreshToken string `json:"upstream_refresh_token" gorm:"column:upstream_refresh_token"` 30 + 31 + // Downstream fields 32 + DownstreamDPoPNonce string `json:"downstream_dpop_nonce" gorm:"column:downstream_dpop_nonce"` 33 + DownstreamDPoPJKT string `json:"downstream_dpop_jkt" gorm:"column:downstream_dpop_jkt;primaryKey"` 34 + DownstreamAccessToken string `json:"downstream_access_token" gorm:"column:downstream_access_token;index"` 35 + DownstreamRefreshToken string `json:"downstream_refresh_token" gorm:"column:downstream_refresh_token;index"` 36 + DownstreamAuthorizationCode string `json:"downstream_authorization_code" gorm:"column:downstream_authorization_code;index"` 37 + DownstreamState string `json:"downstream_state" gorm:"column:downstream_state"` 38 + DownstreamScope string `json:"downstream_scope" gorm:"column:downstream_scope"` 39 + DownstreamCodeChallenge string `json:"downstream_code_challenge" gorm:"column:downstream_code_challenge"` 40 + DownstreamPARRequestURI string `json:"downstream_par_request_uri" gorm:"column:downstream_par_request_uri"` 41 + DownstreamPARUsedAt *time.Time `json:"downstream_par_used_at" gorm:"column:downstream_par_used_at"` 42 + DownstreamRedirectURI string `json:"downstream_redirect_uri" gorm:"column:downstream_redirect_uri"` 43 + 44 + RevokedAt *time.Time `json:"revoked_at" gorm:"column:revoked_at"` 45 + CreatedAt time.Time `json:"created_at"` 46 + UpdatedAt time.Time `json:"updated_at"` 47 + } 48 + 49 + // for gorm. this is prettier than "o_auth_sessions" 50 + func (o *OAuthSession) TableName() string { 51 + return "oauth_sessions" 52 + } 53 + 54 + type OAuthSessionStatus string 55 + 56 + const ( 57 + // We've gotten the first request and sent it back for a new nonce 58 + OAuthSessionStatePARPending OAuthSessionStatus = "par-pending" 59 + // PAR has been created, but not yet used 60 + OAuthSessionStatePARCreated OAuthSessionStatus = "par-created" 61 + // PAR has been used, but maybe upstream will fail for some reason 62 + OAuthSessionStatePARUsed OAuthSessionStatus = "par-used" 63 + // PAR has been used, we're waiting to hear back from upstream 64 + OAuthSessionStateUpstream OAuthSessionStatus = "upstream" 65 + // Upstream came back, we've issued the user a code but it hasn't been used yet 66 + OAuthSessionStateDownstream OAuthSessionStatus = "downstream" 67 + // Code has been used, everything is good 68 + OAuthSessionStateReady OAuthSessionStatus = "ready" 69 + // For any reason we're done. Revoked or expired 70 + OAuthSessionStateRejected OAuthSessionStatus = "rejected" 71 + ) 72 + 73 + func (o *OAuthSession) Status() OAuthSessionStatus { 74 + if o.RevokedAt != nil { 75 + return OAuthSessionStateRejected 76 + } 77 + if o.DownstreamAccessToken != "" { 78 + return OAuthSessionStateReady 79 + } 80 + if o.DownstreamAuthorizationCode != "" { 81 + return OAuthSessionStateDownstream 82 + } 83 + if o.UpstreamDPoPPrivateJWK != "" { 84 + return OAuthSessionStateUpstream 85 + } 86 + if o.DownstreamPARUsedAt != nil { 87 + return OAuthSessionStatePARUsed 88 + } 89 + if o.DownstreamPARRequestURI != "" { 90 + return OAuthSessionStatePARCreated 91 + } 92 + if o.DownstreamDPoPNonce != "" { 93 + return OAuthSessionStatePARPending 94 + } 95 + bs, _ := json.Marshal(o) 96 + fmt.Printf("unknown oauth session status: %s\n", string(bs)) 97 + // todo: this should never happen, log a warning? panic? 98 + return OAuthSessionStateRejected 99 + } 100 + 101 + func (o *OProxy) loadOAuthSession(jkt string) (*OAuthSession, error) { 102 + session, err := o.userLoadOAuthSession(jkt) 103 + if err != nil { 104 + return nil, err 105 + } 106 + if session == nil { 107 + return nil, nil 108 + } 109 + if session.Status() != OAuthSessionStateReady { 110 + return session, nil 111 + } 112 + if session.UpstreamAccessTokenExp.Sub(time.Now()) > refreshWhenRemaining { 113 + return session, nil 114 + } 115 + 116 + upstreamMeta := o.GetUpstreamMetadata() 117 + 118 + oclient, err := oauth.NewClient(oauth.ClientArgs{ 119 + ClientJwk: o.upstreamJWK, 120 + ClientId: upstreamMeta.ClientID, 121 + RedirectUri: upstreamMeta.RedirectURIs[0], 122 + }) 123 + 124 + dpopKey, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 125 + if err != nil { 126 + return nil, fmt.Errorf("failed to parse upstream dpop private key: %w", err) 127 + } 128 + 129 + // refresh upstream before returning 130 + resp, err := oclient.RefreshTokenRequest(context.Background(), session.UpstreamRefreshToken, session.UpstreamAuthServerIssuer, session.UpstreamDPoPNonce, dpopKey) 131 + if err != nil { 132 + // revoke, probably 133 + o.slog.Error("failed to refresh upstream token, revoking downstream session", "error", err) 134 + now := time.Now() 135 + session.RevokedAt = &now 136 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 137 + if err != nil { 138 + o.slog.Error("after upstream token refresh, failed to revoke downstream session", "error", err) 139 + } 140 + return nil, fmt.Errorf("failed to refresh upstream token: %w", err) 141 + } 142 + 143 + exp := time.Now().Add(time.Second * time.Duration(resp.ExpiresIn)).UTC() 144 + session.UpstreamAccessToken = resp.AccessToken 145 + session.UpstreamAccessTokenExp = &exp 146 + session.UpstreamRefreshToken = resp.RefreshToken 147 + 148 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 149 + if err != nil { 150 + return nil, fmt.Errorf("failed to update downstream session after upstream token refresh: %w", err) 151 + } 152 + 153 + o.slog.Debug("refreshed upstream token", "session", session.DownstreamDPoPJKT) 154 + 155 + return session, nil 156 + }
+74
pkg/oproxy/oproxy.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "os" 7 + 8 + "github.com/labstack/echo/v4" 9 + "github.com/lestrrat-go/jwx/v2/jwk" 10 + ) 11 + 12 + type OProxy struct { 13 + createOAuthSession func(id string, session *OAuthSession) error 14 + updateOAuthSession func(id string, session *OAuthSession) error 15 + userLoadOAuthSession func(id string) (*OAuthSession, error) 16 + e *echo.Echo 17 + host string 18 + scope string 19 + upstreamJWK jwk.Key 20 + downstreamJWK jwk.Key 21 + slog *slog.Logger 22 + } 23 + 24 + type Config struct { 25 + CreateOAuthSession func(id string, session *OAuthSession) error 26 + UpdateOAuthSession func(id string, session *OAuthSession) error 27 + LoadOAuthSession func(id string) (*OAuthSession, error) 28 + Host string 29 + Scope string 30 + UpstreamJWK jwk.Key 31 + DownstreamJWK jwk.Key 32 + Slog *slog.Logger 33 + } 34 + 35 + func New(conf *Config) *OProxy { 36 + e := echo.New() 37 + mySlog := conf.Slog 38 + if mySlog == nil { 39 + mySlog = slog.New(slog.NewTextHandler(os.Stderr, nil)) 40 + } 41 + o := &OProxy{ 42 + createOAuthSession: conf.CreateOAuthSession, 43 + updateOAuthSession: conf.UpdateOAuthSession, 44 + userLoadOAuthSession: conf.LoadOAuthSession, 45 + e: e, 46 + host: conf.Host, 47 + scope: conf.Scope, 48 + upstreamJWK: conf.UpstreamJWK, 49 + downstreamJWK: conf.DownstreamJWK, 50 + slog: mySlog, 51 + } 52 + o.e.GET("/.well-known/oauth-authorization-server", o.HandleOAuthAuthorizationServer) 53 + o.e.GET("/.well-known/oauth-protected-resource", o.HandleOAuthProtectedResource) 54 + o.e.POST("/oauth/par", o.HandleOAuthPAR) 55 + o.e.GET("/oauth/authorize", o.HandleOAuthAuthorize) 56 + o.e.GET("/oauth/return", o.HandleOAuthReturn) 57 + o.e.POST("/oauth/token", o.DPoPNonceMiddleware(o.HandleOAuthToken)) 58 + o.e.POST("/oauth/revoke", o.DPoPNonceMiddleware(o.HandleOAuthRevoke)) 59 + o.e.GET("/oauth/upstream/client-metadata.json", o.HandleClientMetadataUpstream) 60 + o.e.GET("/oauth/upstream/jwks.json", o.HandleJwksUpstream) 61 + o.e.GET("/oauth/downstream/client-metadata.json", o.HandleClientMetadataDownstream) 62 + o.e.Use(o.ErrorHandlingMiddleware) 63 + return o 64 + } 65 + 66 + func (o *OProxy) Handler() http.Handler { 67 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 + w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh 69 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP") 70 + w.Header().Set("Access-Control-Allow-Methods", "*") 71 + w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce") 72 + o.e.ServeHTTP(w, r) 73 + }) 74 + }
+124
pkg/oproxy/resolution.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "net" 9 + "net/http" 10 + "strings" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + ) 14 + 15 + // mostly borrowed from github.com/haileyok/atproto-oauth-golang, MIT license 16 + func ResolveHandle(ctx context.Context, handle string) (string, error) { 17 + var did string 18 + 19 + _, err := syntax.ParseHandle(handle) 20 + if err != nil { 21 + return "", err 22 + } 23 + 24 + recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle)) 25 + if err == nil { 26 + for _, rec := range recs { 27 + if strings.HasPrefix(rec, "did=") { 28 + did = strings.Split(rec, "did=")[1] 29 + break 30 + } 31 + } 32 + } 33 + 34 + if did == "" { 35 + req, err := http.NewRequestWithContext( 36 + ctx, 37 + "GET", 38 + fmt.Sprintf("https://%s/.well-known/atproto-did", handle), 39 + nil, 40 + ) 41 + if err != nil { 42 + return "", err 43 + } 44 + 45 + resp, err := http.DefaultClient.Do(req) 46 + if err != nil { 47 + return "", err 48 + } 49 + defer resp.Body.Close() 50 + 51 + if resp.StatusCode != http.StatusOK { 52 + io.Copy(io.Discard, resp.Body) 53 + return "", fmt.Errorf("unable to resolve handle") 54 + } 55 + 56 + b, err := io.ReadAll(resp.Body) 57 + if err != nil { 58 + return "", err 59 + } 60 + 61 + maybeDid := string(b) 62 + 63 + if _, err := syntax.ParseDID(maybeDid); err != nil { 64 + return "", fmt.Errorf("unable to resolve handle") 65 + } 66 + 67 + did = maybeDid 68 + } 69 + 70 + return did, nil 71 + } 72 + 73 + func ResolveService(ctx context.Context, did string) (string, error) { 74 + type Identity struct { 75 + Service []struct { 76 + ID string `json:"id"` 77 + Type string `json:"type"` 78 + ServiceEndpoint string `json:"serviceEndpoint"` 79 + } `json:"service"` 80 + } 81 + 82 + var ustr string 83 + if strings.HasPrefix(did, "did:plc:") { 84 + ustr = fmt.Sprintf("https://plc.directory/%s", did) 85 + } else if strings.HasPrefix(did, "did:web:") { 86 + ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 87 + } else { 88 + return "", fmt.Errorf("did was not a supported did type") 89 + } 90 + 91 + req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 92 + if err != nil { 93 + return "", err 94 + } 95 + 96 + resp, err := http.DefaultClient.Do(req) 97 + if err != nil { 98 + return "", err 99 + } 100 + defer resp.Body.Close() 101 + 102 + if resp.StatusCode != 200 { 103 + io.Copy(io.Discard, resp.Body) 104 + return "", fmt.Errorf("could not find identity in plc registry") 105 + } 106 + 107 + var identity Identity 108 + if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil { 109 + return "", err 110 + } 111 + 112 + var service string 113 + for _, svc := range identity.Service { 114 + if svc.ID == "#atproto_pds" { 115 + service = svc.ServiceEndpoint 116 + } 117 + } 118 + 119 + if service == "" { 120 + return "", fmt.Errorf("could not find atproto_pds service in identity services") 121 + } 122 + 123 + return service, nil 124 + }
+23
pkg/oproxy/token_generation.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/google/uuid" 7 + ) 8 + 9 + func generateRefreshToken() (string, error) { 10 + uu, err := uuid.NewV7() 11 + if err != nil { 12 + return "", err 13 + } 14 + return fmt.Sprintf("refresh-%s", uu.String()), nil 15 + } 16 + 17 + func generateAuthorizationCode() (string, error) { 18 + uu, err := uuid.NewV7() 19 + if err != nil { 20 + return "", err 21 + } 22 + return fmt.Sprintf("code-%s", uu.String()), nil 23 + }
+35
pkg/oproxy/types.go
··· 1 + package oproxy 2 + 3 + import "github.com/haileyok/atproto-oauth-golang/helpers" 4 + 5 + type OAuthClientMetadata struct { 6 + RedirectURIs []string `json:"redirect_uris"` 7 + ResponseTypes []string `json:"response_types,omitempty"` 8 + GrantTypes []string `json:"grant_types,omitempty"` 9 + Scope string `json:"scope,omitempty"` 10 + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` 11 + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"` 12 + UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"` 13 + UserinfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"` 14 + JwksURI string `json:"jwks_uri,omitempty"` 15 + ApplicationType string `json:"application_type,omitempty"` // "web" or "native" 16 + SubjectType string `json:"subject_type,omitempty"` // "public" or "pairwise" 17 + RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"` 18 + IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"` 19 + AuthorizationSignedResponseAlg string `json:"authorization_signed_response_alg,omitempty"` 20 + AuthorizationEncryptedResponseEnc string `json:"authorization_encrypted_response_enc,omitempty"` 21 + AuthorizationEncryptedResponseAlg string `json:"authorization_encrypted_response_alg,omitempty"` 22 + ClientID string `json:"client_id,omitempty"` 23 + ClientName string `json:"client_name,omitempty"` 24 + ClientURI string `json:"client_uri,omitempty"` 25 + PolicyURI string `json:"policy_uri,omitempty"` 26 + TosURI string `json:"tos_uri,omitempty"` 27 + LogoURI string `json:"logo_uri,omitempty"` 28 + DefaultMaxAge int `json:"default_max_age,omitempty"` 29 + RequireAuthTime *bool `json:"require_auth_time,omitempty"` 30 + Contacts []string `json:"contacts,omitempty"` 31 + TLSClientCertificateBoundAccessTokens *bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` 32 + DPoPBoundAccessTokens *bool `json:"dpop_bound_access_tokens,omitempty"` 33 + AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"` 34 + Jwks *helpers.JwksResponseObject `json:"jwks,omitempty"` 35 + }
+62
pkg/oproxy/xrpc_client.go
··· 1 + package oproxy 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + 8 + "github.com/bluesky-social/indigo/xrpc" 9 + oauth "github.com/haileyok/atproto-oauth-golang" 10 + "github.com/labstack/echo/v4" 11 + "github.com/lestrrat-go/jwx/v2/jwk" 12 + ) 13 + 14 + var xrpcClient *oauth.XrpcClient 15 + 16 + type XrpcClient struct { 17 + client *oauth.XrpcClient 18 + authArgs *oauth.XrpcAuthedRequestArgs 19 + } 20 + 21 + func (o *OProxy) GetXrpcClient(session *OAuthSession) (*XrpcClient, error) { 22 + key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 23 + if err != nil { 24 + return nil, fmt.Errorf("failed to parse DPoP private JWK: %w", err) 25 + } 26 + authArgs := &oauth.XrpcAuthedRequestArgs{ 27 + Did: session.DID, 28 + AccessToken: session.UpstreamAccessToken, 29 + PdsUrl: session.PDSUrl, 30 + Issuer: session.UpstreamAuthServerIssuer, 31 + DpopPdsNonce: session.UpstreamDPoPNonce, 32 + DpopPrivateJwk: key, 33 + } 34 + 35 + xrpcClient := &oauth.XrpcClient{ 36 + OnDpopPdsNonceChanged: func(did, newNonce string) { 37 + sess, err := o.loadOAuthSession(session.DownstreamDPoPJKT) 38 + if err != nil { 39 + o.slog.Error("failed to get OAuth session in OnDpopPdsNonceChanged", "error", err) 40 + return 41 + } 42 + sess.UpstreamDPoPNonce = newNonce 43 + err = o.updateOAuthSession(session.DownstreamDPoPJKT, sess) 44 + if err != nil { 45 + o.slog.Error("failed to update OAuth session in OnDpopPdsNonceChanged", "error", err) 46 + } 47 + }, 48 + } 49 + return &XrpcClient{client: xrpcClient, authArgs: authArgs}, nil 50 + } 51 + 52 + func (c *XrpcClient) Do(ctx context.Context, kind xrpc.XRPCRequestType, inpenc, method string, params map[string]any, bodyobj any, out any) error { 53 + err := c.client.Do(ctx, c.authArgs, kind, inpenc, method, params, bodyobj, out) 54 + if err == nil { 55 + return nil 56 + } 57 + xErr, ok := err.(*xrpc.Error) 58 + if !ok { 59 + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 60 + } 61 + return echo.NewHTTPError(xErr.StatusCode, xErr.Error()) 62 + }
+27
pkg/spxrpc/app_bsky_actor.go
··· 1 + package spxrpc 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + 7 + appbskytypes "github.com/bluesky-social/indigo/api/bsky" 8 + "github.com/bluesky-social/indigo/xrpc" 9 + "github.com/labstack/echo/v4" 10 + "stream.place/streamplace/pkg/oproxy" 11 + ) 12 + 13 + func (s *Server) handleAppBskyActorGetProfile(ctx context.Context, actor string) (*appbskytypes.ActorDefs_ProfileViewDetailed, error) { 14 + session, client := oproxy.GetOAuthSession(ctx) 15 + if session == nil { 16 + return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") 17 + } 18 + 19 + // brief check to make sure we can actually do stuff 20 + var out appbskytypes.ActorDefs_ProfileViewDetailed 21 + err := client.Do(ctx, xrpc.Query, "application/json", "app.bsky.actor.getProfile", map[string]any{"actor": actor}, nil, &out) 22 + if err != nil { 23 + return nil, err 24 + } 25 + 26 + return &out, nil 27 + }
+16
pkg/spxrpc/com_atproto_identity.go
··· 1 + package spxrpc 2 + 3 + import ( 4 + "context" 5 + 6 + comatprototypes "github.com/bluesky-social/indigo/api/atproto" 7 + "stream.place/streamplace/pkg/oproxy" 8 + ) 9 + 10 + func (s *Server) handleComAtprotoIdentityResolveHandle(ctx context.Context, handle string) (*comatprototypes.IdentityResolveHandle_Output, error) { 11 + did, err := oproxy.ResolveHandle(ctx, handle) 12 + if err != nil { 13 + return nil, err 14 + } 15 + return &comatprototypes.IdentityResolveHandle_Output{Did: did}, nil 16 + }
+6
pkg/spxrpc/spxrpc.go
··· 29 29 if err != nil { 30 30 return nil, err 31 31 } 32 + err = s.RegisterHandlersComAtproto(e) 33 + if err != nil { 34 + return nil, err 35 + } 36 + e.GET("/xrpc/*", s.HandleWildcard) 37 + e.POST("/xrpc/*", s.HandleWildcard) 32 38 return s, nil 33 39 } 34 40
+31
pkg/spxrpc/stubs.go
··· 3 3 import ( 4 4 "strconv" 5 5 6 + comatprototypes "github.com/bluesky-social/indigo/api/atproto" 6 7 appbskytypes "github.com/bluesky-social/indigo/api/bsky" 7 8 "github.com/labstack/echo/v4" 8 9 "go.opentelemetry.io/otel" ··· 10 11 ) 11 12 12 13 func (s *Server) RegisterHandlersAppBsky(e *echo.Echo) error { 14 + e.GET("/xrpc/app.bsky.actor.getProfile", s.HandleAppBskyActorGetProfile) 13 15 e.GET("/xrpc/app.bsky.feed.getFeedSkeleton", s.HandleAppBskyFeedGetFeedSkeleton) 14 16 return nil 17 + } 18 + 19 + func (s *Server) HandleAppBskyActorGetProfile(c echo.Context) error { 20 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleAppBskyActorGetProfile") 21 + defer span.End() 22 + actor := c.QueryParam("actor") 23 + var out *appbskytypes.ActorDefs_ProfileViewDetailed 24 + var handleErr error 25 + // func (s *Server) handleAppBskyActorGetProfile(ctx context.Context,actor string) (*appbskytypes.ActorDefs_ProfileViewDetailed, error) 26 + out, handleErr = s.handleAppBskyActorGetProfile(ctx, actor) 27 + if handleErr != nil { 28 + return handleErr 29 + } 30 + return c.JSON(200, out) 15 31 } 16 32 17 33 func (s *Server) HandleAppBskyFeedGetFeedSkeleton(c echo.Context) error { ··· 45 61 } 46 62 47 63 func (s *Server) RegisterHandlersComAtproto(e *echo.Echo) error { 64 + e.GET("/xrpc/com.atproto.identity.resolveHandle", s.HandleComAtprotoIdentityResolveHandle) 48 65 return nil 66 + } 67 + 68 + func (s *Server) HandleComAtprotoIdentityResolveHandle(c echo.Context) error { 69 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleComAtprotoIdentityResolveHandle") 70 + defer span.End() 71 + handle := c.QueryParam("handle") 72 + var out *comatprototypes.IdentityResolveHandle_Output 73 + var handleErr error 74 + // func (s *Server) handleComAtprotoIdentityResolveHandle(ctx context.Context,handle string) (*comatprototypes.IdentityResolveHandle_Output, error) 75 + out, handleErr = s.handleComAtprotoIdentityResolveHandle(ctx, handle) 76 + if handleErr != nil { 77 + return handleErr 78 + } 79 + return c.JSON(200, out) 49 80 } 50 81 51 82 func (s *Server) RegisterHandlersPlaceStream(e *echo.Echo) error {
+57
pkg/spxrpc/wildcard.go
··· 1 + package spxrpc 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "strings" 7 + 8 + "github.com/bluesky-social/indigo/xrpc" 9 + "github.com/labstack/echo/v4" 10 + "go.opentelemetry.io/otel" 11 + "stream.place/streamplace/pkg/log" 12 + "stream.place/streamplace/pkg/oproxy" 13 + ) 14 + 15 + func (s *Server) HandleWildcard(c echo.Context) error { 16 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleWildcard") 17 + defer span.End() 18 + 19 + session, client := oproxy.GetOAuthSession(ctx) 20 + if session == nil { 21 + return echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") 22 + } 23 + 24 + var out map[string]any 25 + 26 + // Get the last path segment in the URL 27 + path := c.Request().URL.Path 28 + segments := strings.Split(path, "/") 29 + lastSegment := segments[len(segments)-1] 30 + 31 + var xrpcType xrpc.XRPCRequestType 32 + var err error 33 + if c.Request().Method == "GET" { 34 + xrpcType = xrpc.Query 35 + queryParams := make(map[string]any) 36 + for k, v := range c.QueryParams() { 37 + for _, vv := range v { 38 + queryParams[k] = vv 39 + } 40 + } 41 + err = client.Do(ctx, xrpcType, "application/json", lastSegment, queryParams, nil, &out) 42 + } else { 43 + xrpcType = xrpc.Procedure 44 + var body map[string]any 45 + if err := c.Bind(&body); err != nil { 46 + return c.JSON(http.StatusBadRequest, xrpc.XRPCError{ErrStr: "BadRequest", Message: fmt.Sprintf("invalid body: %s", err)}) 47 + } 48 + err = client.Do(ctx, xrpcType, "application/json", lastSegment, nil, body, &out) 49 + } 50 + 51 + if err != nil { 52 + log.Error(ctx, "upstream xrpc error", "error", err) 53 + return err 54 + } 55 + 56 + return c.JSON(200, out) 57 + }
+62 -25
yarn.lock
··· 53 53 languageName: node 54 54 linkType: hard 55 55 56 - "@aquareum/atproto-oauth-client-react-native@npm:^0.0.1": 57 - version: 0.0.1 58 - resolution: "@aquareum/atproto-oauth-client-react-native@npm:0.0.1" 59 - dependencies: 60 - "@atproto-labs/did-resolver": "npm:0.1.5" 61 - "@atproto-labs/handle-resolver-node": "npm:0.1.7" 62 - "@atproto-labs/simple-store": "npm:0.1.1" 63 - "@atproto-labs/simple-store-memory": "npm:0.1.1" 64 - "@atproto/did": "npm:0.1.3" 65 - "@atproto/jwk": "npm:0.1.1" 66 - "@atproto/jwk-jose": "npm:0.1.2" 67 - "@atproto/jwk-webcrypto": "npm:0.1.2" 68 - "@atproto/oauth-client": "npm:0.3.2" 69 - "@atproto/oauth-client-browser": "npm:0.3.2" 70 - "@atproto/oauth-types": "npm:0.2.1" 71 - abortcontroller-polyfill: "npm:^1.7.6" 72 - event-target-shim: "npm:^6.0.2" 73 - expo-sqlite: "npm:^15.0.3" 74 - jose: "npm:^5.2.0" 75 - react-native-quick-crypto: "npm:^0.7.7" 76 - checksum: 10/73263e06756f8acfc526a6e89d5e68df9b2930b839ba6748a498f1b7e5d5480d31e068037f95bd7b7d1611be8565da80f045d85506ced7d79e4e132f6182a371 77 - languageName: node 78 - linkType: hard 79 - 80 56 "@astrojs/compiler@npm:^2.11.0": 81 57 version: 2.12.0 82 58 resolution: "@astrojs/compiler@npm:2.12.0" ··· 8807 8783 languageName: node 8808 8784 linkType: hard 8809 8785 8786 + "@streamplace/atproto-oauth-client-react-native@workspace:*, @streamplace/atproto-oauth-client-react-native@workspace:js/atproto-oauth-client-react-native": 8787 + version: 0.0.0-use.local 8788 + resolution: "@streamplace/atproto-oauth-client-react-native@workspace:js/atproto-oauth-client-react-native" 8789 + dependencies: 8790 + "@atproto-labs/did-resolver": "npm:0.1.5" 8791 + "@atproto-labs/handle-resolver-node": "npm:0.1.7" 8792 + "@atproto-labs/simple-store": "npm:0.1.1" 8793 + "@atproto-labs/simple-store-memory": "npm:0.1.1" 8794 + "@atproto/did": "npm:0.1.3" 8795 + "@atproto/jwk": "npm:0.1.1" 8796 + "@atproto/jwk-jose": "npm:0.1.2" 8797 + "@atproto/jwk-webcrypto": "npm:0.1.2" 8798 + "@atproto/oauth-client": "npm:0.3.2" 8799 + "@atproto/oauth-client-browser": "npm:0.3.2" 8800 + "@atproto/oauth-types": "npm:0.2.1" 8801 + "@types/node": "npm:^22.10.1" 8802 + abortcontroller-polyfill: "npm:^1.7.6" 8803 + event-target-shim: "npm:^6.0.2" 8804 + expo-sqlite: "npm:^15.0.3" 8805 + jose: "npm:^5.2.0" 8806 + react-native-quick-crypto: "npm:^0.7.7" 8807 + typescript: "npm:^5.6.3" 8808 + languageName: unknown 8809 + linkType: soft 8810 + 8810 8811 "@streamplace/config-react-native-webrtc@workspace:js/config-react-native-webrtc": 8811 8812 version: 0.0.0-use.local 8812 8813 resolution: "@streamplace/config-react-native-webrtc@workspace:js/config-react-native-webrtc" ··· 11130 11131 dependencies: 11131 11132 undici-types: "npm:~6.19.8" 11132 11133 checksum: 10/9c73d4cbcbf9773a5986421025c26d6139d8ab960317b3062fbb449c00dbe8197230334be550ffcdb6059bd25f4cb903546bace905155628537283a80c2075d5 11134 + languageName: node 11135 + linkType: hard 11136 + 11137 + "@types/node@npm:^22.10.1": 11138 + version: 22.15.17 11139 + resolution: "@types/node@npm:22.15.17" 11140 + dependencies: 11141 + undici-types: "npm:~6.21.0" 11142 + checksum: 10/3f5870ec1ac16b1dd8e5817de81164df9b69e4cf19cce692cb7c9b1af1deaecfd98b591b56155fcc4aa582f7189a4fc0c8d7d3226fa0387403db615a12dd8cb6 11133 11143 languageName: node 11134 11144 linkType: hard 11135 11145 ··· 29556 29566 version: 0.0.0-use.local 29557 29567 resolution: "streamplace@workspace:js/app" 29558 29568 dependencies: 29559 - "@aquareum/atproto-oauth-client-react-native": "npm:^0.0.1" 29560 29569 "@atproto-labs/pipe": "npm:^0.1.0" 29561 29570 "@atproto/crypto": "npm:^0.4.2" 29562 29571 "@atproto/jwk-jose": "npm:^0.1.2" ··· 29581 29590 "@react-navigation/native": "npm:^6.1.18" 29582 29591 "@react-navigation/native-stack": "npm:^6.11.0" 29583 29592 "@reduxjs/toolkit": "npm:^2.3.0" 29593 + "@streamplace/atproto-oauth-client-react-native": "workspace:*" 29584 29594 "@tamagui/babel-plugin": "npm:^1.123.17" 29585 29595 "@tamagui/config": "npm:^1.123.17" 29586 29596 "@tamagui/lucide-icons": "npm:^1.123.17" ··· 30946 30956 languageName: node 30947 30957 linkType: hard 30948 30958 30959 + "typescript@npm:^5.6.3": 30960 + version: 5.8.3 30961 + resolution: "typescript@npm:5.8.3" 30962 + bin: 30963 + tsc: bin/tsc 30964 + tsserver: bin/tsserver 30965 + checksum: 10/65c40944c51b513b0172c6710ee62e951b70af6f75d5a5da745cb7fab132c09ae27ffdf7838996e3ed603bb015dadd099006658046941bd0ba30340cc563ae92 30966 + languageName: node 30967 + linkType: hard 30968 + 30949 30969 "typescript@npm:^5.7.2": 30950 30970 version: 5.7.3 30951 30971 resolution: "typescript@npm:5.7.3" ··· 30983 31003 tsc: bin/tsc 30984 31004 tsserver: bin/tsserver 30985 31005 checksum: 10/b61b8bb4b4d6a8a00f9d5f931f8c67070eed6ad11feabf4c41744a326987080bfc806a621596c70fbf2e5974eca3ed65bafeeeb22a078071bdfb51d8abd7c013 31006 + languageName: node 31007 + linkType: hard 31008 + 31009 + "typescript@patch:typescript@npm%3A^5.6.3#optional!builtin<compat/typescript>": 31010 + version: 5.8.3 31011 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=b45daf" 31012 + bin: 31013 + tsc: bin/tsc 31014 + tsserver: bin/tsserver 31015 + checksum: 10/98470634034ec37fd9ea61cc82dcf9a27950d0117a4646146b767d085a2ec14b137aae9642a83d1c62732d7fdcdac19bb6288b0bb468a72f7a06ae4e1d2c72c9 30986 31016 languageName: node 30987 31017 linkType: hard 30988 31018 ··· 31137 31167 version: 6.19.8 31138 31168 resolution: "undici-types@npm:6.19.8" 31139 31169 checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 31170 + languageName: node 31171 + linkType: hard 31172 + 31173 + "undici-types@npm:~6.21.0": 31174 + version: 6.21.0 31175 + resolution: "undici-types@npm:6.21.0" 31176 + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 31140 31177 languageName: node 31141 31178 linkType: hard 31142 31179