diff --git a/.gitignore b/.gitignore
index 66ef5546..28a38381 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ build-*
*.heap
/api
oom
+*.tsbuildinfo
diff --git a/Makefile b/Makefile
index 3408f285..f7d8d95f 100644
--- a/Makefile
+++ b/Makefile
@@ -112,13 +112,15 @@ js-lexicons:
&& sed -i.bak 's/AppBskyGraphBlock\.Main/AppBskyGraphBlock\.Record/' $$(find ./js/app/lexicons/types/place/stream -type f) \
&& sed -i.bak 's/PlaceStreamChatProfile\.Main/PlaceStreamChatProfile\.Record/' $$(find ./js/app/lexicons/types/place/stream -type f) \
&& sed -i.bak "s/import\ \*\ as\ AppBskyFeedDefs\ from\ '.\/defs'/import \{ AppBskyFeedDefs } from '@atproto\/api'/" $$(find ./js/app/lexicons/types -type f) \
+ && sed -i.bak "s/import\ \*\ as\ AppBskyActorDefs\ from\ '.\/defs'/import \{ AppBskyActorDefs } from '@atproto\/api'/" $$(find ./js/app/lexicons -type f) \
&& find . | grep bak$$ | xargs rm
.PHONY: md-lexicons
md-lexicons:
yarn exec lexmd \
lexicons/place/stream \
- js/docs/src/content/docs/lex-reference
+ js/docs/src/content/docs/lex-reference \
+ && $(MAKE) fix
.PHONY: lexgen
lexgen:
@@ -127,23 +129,17 @@ lexgen:
.PHONY: lexgen-types
lexgen-types:
- go run github.com/bluesky-social/indigo/cmd/lexgen --package streamplace \
- --types-import place.stream:stream.place/streamplace/pkg/streamplace \
- -outdir ./pkg/streamplace \
- --prefix place.stream \
+ go run github.com/bluesky-social/indigo/cmd/lexgen \
+ -outdir ./pkg/spxrpc \
--build-file util/lexgen-types.json \
+ --external-lexicons subprojects/atproto/lexicons \
lexicons/place/stream \
./subprojects/atproto/lexicons
-.PHONY: ci-lexicons
-ci-lexicons:
- $(MAKE) lexicons \
- && if ! git diff --exit-code >/dev/null; then echo "lexicons are out of date, run 'make lexicons' to fix"; exit 1; fi
-
.PHONY: lexgen-server
lexgen-server:
- mkdir -p ./pkg/spxrpc
- go run github.com/bluesky-social/indigo/cmd/lexgen --package spxrpc \
+ mkdir -p ./pkg/spxrpc \
+ && go run github.com/bluesky-social/indigo/cmd/lexgen \
--gen-server \
--types-import place.stream:stream.place/streamplace/pkg/streamplace \
--types-import app.bsky:github.com/bluesky-social/indigo/api/bsky \
@@ -151,10 +147,17 @@ lexgen-server:
--types-import chat.bsky:github.com/bluesky-social/indigo/api/chat \
--types-import tools.ozone:github.com/bluesky-social/indigo/api/ozone \
-outdir ./pkg/spxrpc \
- --prefix place.stream \
- --build-file util/lexgen-server.json \
+ --build-file util/lexgen-types.json \
+ --external-lexicons subprojects/atproto/lexicons \
+ --package spxrpc \
lexicons/place/stream \
- lexicons/app/bsky
+ lexicons/app/bsky \
+ lexicons/com/atproto
+
+.PHONY: ci-lexicons
+ci-lexicons:
+ $(MAKE) lexicons \
+ && if ! git diff --exit-code >/dev/null; then echo "lexicons are out of date, run 'make lexicons' to fix"; exit 1; fi
.PHONY: test
test:
diff --git a/go.mod b/go.mod
index ec4f8c7f..add53cbc 100644
--- a/go.mod
+++ b/go.mod
@@ -8,27 +8,32 @@ replace github.com/ThalesGroup/crypto11 => github.com/aquareum-tv/crypto11 v0.0.
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4
+replace github.com/AxisCommunications/go-dpop => github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4
+
+replace github.com/haileyok/atproto-oauth-golang => github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab
+
require (
firebase.google.com/go/v4 v4.14.1
git.stream.place/streamplace/c2pa-go v0.7.0
github.com/99designs/gqlgen v0.17.64
+ github.com/AxisCommunications/go-dpop v1.1.2
github.com/NYTimes/gziphandler v1.1.1
github.com/ThalesGroup/crypto11 v0.0.0-00010101000000-000000000000
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
- github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188
+ github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb
github.com/decred/dcrd/dcrec/secp256k1 v1.0.4
github.com/dunglas/httpsfv v1.0.2
github.com/ethereum/go-ethereum v1.14.7
github.com/go-git/go-git/v5 v5.12.0
github.com/go-gst/go-glib v1.4.0
github.com/go-gst/go-gst v1.4.0
+ github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/golang/glog v1.2.4
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
+ github.com/haileyok/atproto-oauth-golang v0.0.2
github.com/ipfs/go-cid v0.4.1
- github.com/ipfs/go-datastore v0.6.0
- github.com/ipfs/go-ipfs-blockstore v1.3.1
github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c
github.com/julienschmidt/httprouter v1.3.0
github.com/labstack/echo/v4 v4.13.3
@@ -52,12 +57,14 @@ require (
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
+ go.opentelemetry.io/otel/trace v1.35.0
go.uber.org/goleak v1.3.0
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
golang.org/x/image v0.22.0
golang.org/x/net v0.35.0
golang.org/x/sync v0.11.0
golang.org/x/term v0.29.0
+ golang.org/x/time v0.8.0
golang.org/x/tools v0.25.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
google.golang.org/api v0.189.0
@@ -130,7 +137,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
- github.com/gocql/gocql v0.0.0-00010101000000-000000000000 // indirect
+ github.com/gocql/gocql v1.7.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -153,6 +160,8 @@ require (
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
github.com/ipfs/go-blockservice v0.5.2 // indirect
+ github.com/ipfs/go-datastore v0.6.0 // indirect
+ github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
@@ -252,7 +261,6 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
- go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
@@ -261,7 +269,6 @@ require (
golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/text v0.22.0 // indirect
- golang.org/x/time v0.8.0 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect
google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
diff --git a/go.sum b/go.sum
index c3ad5c1f..c43f2917 100644
--- a/go.sum
+++ b/go.sum
@@ -74,6 +74,8 @@ github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsy
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk=
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA=
+github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb h1:qfkNGUq//RzFBRFNBVwRKcFwyXS+1jQ5VnLW9Jfh6Vc=
+github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
@@ -208,6 +210,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -387,6 +391,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:2n/HCxBM7oa5PNCPKIhV26EtJkaPXFfcVojPAT3ujTU=
github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:B9OPZOhZ3FIi6bu54lAgCMzXLh11Z7ilr3rOr/ClP+E=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds=
github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@@ -612,6 +618,10 @@ github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab h1:cXikoKjZxnUiRQ+IY4+3XU4eJL1g0JJ5TzRA2keUNBg=
+github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8=
+github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc=
+github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
diff --git a/js/app/.env.development b/js/app/.env.development
index f8372d14..9d84e273 100644
--- a/js/app/.env.development
+++ b/js/app/.env.development
@@ -1,3 +1,3 @@
EXPO_PUBLIC_STREAMPLACE_URL=http://127.0.0.1:38080
-EXPO_PUBLIC_WEB_TRY_LOCAL=false
+EXPO_PUBLIC_WEB_TRY_LOCAL=true
EXPO_USE_METRO_WORKSPACE_ROOT=1
\ No newline at end of file
diff --git a/js/app/components/login/login.tsx b/js/app/components/login/login.tsx
index 09d73a74..1b6a46d8 100644
--- a/js/app/components/login/login.tsx
+++ b/js/app/components/login/login.tsx
@@ -1,16 +1,31 @@
+import { AtpBaseClient } from "lexicons";
import NameColorPicker from "components/name-color-picker/name-color-picker";
import {
login,
logout,
+ selectIsReady,
selectLogin,
selectPDS,
selectUserProfile,
setPDS,
} from "features/bluesky/blueskySlice";
-import { useState } from "react";
-import { Keyboard } from "react-native";
+import { useEffect, useState } from "react";
+import { Keyboard, KeyboardAvoidingView } from "react-native";
import { useAppDispatch, useAppSelector } from "store/hooks";
-import { Button, Form, H3, Input, Sheet, Spinner, Text, View } from "tamagui";
+import {
+ Button,
+ Form,
+ H3,
+ H5,
+ Input,
+ Sheet,
+ Spinner,
+ Text,
+ View,
+} from "tamagui";
+import useStreamplaceNode from "hooks/useStreamplaceNode";
+import Loading from "components/loading/loading";
+import { useToastController } from "@tamagui/toast";
export default function Login() {
const dispatch = useAppDispatch();
@@ -18,11 +33,30 @@ export default function Login() {
const pds = useAppSelector(selectPDS);
const loginState = useAppSelector(selectLogin);
const [open, setOpen] = useState(false);
+ const [handle, setHandle] = useState("");
+ const isReady = useAppSelector(selectIsReady);
+ const toast = useToastController();
const onOpenChange = (open: boolean) => {
setOpen(open);
Keyboard.dismiss();
};
+ useEffect(() => {
+ if (loginState?.error) {
+ toast.show("Login error", {
+ message: loginState.error,
+ });
+ }
+ }, [loginState?.error]);
+
+ if (!isReady) {
+ return (
+
+
+
+ );
+ }
+
if (userProfile) {
return (
@@ -51,109 +85,52 @@ export default function Login() {
}
return (
-
-
- {/* {error} */}
-
-
-
- );
-}
-
-export function ChangePDS({
- open,
- onOpenChange,
-}: {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}) {
- const pds = useAppSelector(selectPDS);
- const dispatch = useAppDispatch();
- const [newURL, setNewURL] = useState("");
- return (
-
-
-
-
-
+
-
-
+
+
);
}
diff --git a/js/app/features/bluesky/blueskyProvider.tsx b/js/app/features/bluesky/blueskyProvider.tsx
index cfec6606..90454d28 100644
--- a/js/app/features/bluesky/blueskyProvider.tsx
+++ b/js/app/features/bluesky/blueskyProvider.tsx
@@ -6,6 +6,7 @@ import {
getProfile,
loadOAuthClient,
oauthCallback,
+ oauthError,
selectOAuthSession,
selectUserProfile,
} from "./blueskySlice";
@@ -31,7 +32,7 @@ export default function BlueskyProvider({
setLastLink(url);
if (url.includes("?")) {
const params = new URLSearchParams(url.split("?")[1]);
- if (params.has("code") && params.has("state") && params.has("iss")) {
+ if (params.has("error") || params.has("code")) {
dispatch(oauthCallback(url));
}
}
diff --git a/js/app/features/bluesky/blueskySlice.tsx b/js/app/features/bluesky/blueskySlice.tsx
index df51bb78..062cff5e 100644
--- a/js/app/features/bluesky/blueskySlice.tsx
+++ b/js/app/features/bluesky/blueskySlice.tsx
@@ -144,8 +144,24 @@ export const blueskySlice = createAppSlice({
},
),
+ oauthError: create.reducer(
+ (
+ state,
+ { payload }: { payload: { error: string; description: string } },
+ ) => {
+ return {
+ ...state,
+ login: {
+ loading: false,
+ error: payload.description || payload.error,
+ },
+ status: "loggedOut",
+ };
+ },
+ ),
+
login: create.asyncThunk(
- async (pds: string, thunkAPI) => {
+ async (handle: string, thunkAPI) => {
let { bluesky } = thunkAPI.getState() as {
bluesky: BlueskyState;
};
@@ -156,10 +172,10 @@ export const blueskySlice = createAppSlice({
if (!bluesky.client) {
throw new Error("No client");
}
- const u = await bluesky.client.authorize(pds);
+ const u = await bluesky.client.authorize(handle, {});
thunkAPI.dispatch(openLoginLink(u.toString()));
// cheeky 500ms delay so you don't see the text flash back
- await new Promise((resolve) => setTimeout(resolve, 500));
+ await new Promise((resolve) => setTimeout(resolve, 5000));
},
{
pending: (state) => {
@@ -182,6 +198,7 @@ export const blueskySlice = createAppSlice({
};
},
rejected: (state, action) => {
+ console.error("login rejected", action.error);
return {
...state,
login: {
@@ -215,6 +232,7 @@ export const blueskySlice = createAppSlice({
...state,
oauthSession: null,
pdsAgent: null,
+ status: "loggedOut",
};
},
rejected: (state) => {
@@ -253,6 +271,10 @@ export const blueskySlice = createAppSlice({
},
rejected: (state, action) => {
clearQueryParams();
+ return {
+ ...state,
+ status: "loggedOut",
+ };
// state.status = "failed";
},
},
@@ -266,6 +288,14 @@ export const blueskySlice = createAppSlice({
}
const params = new URLSearchParams(url.split("?")[1]);
if (!(params.has("code") && params.has("state") && params.has("iss"))) {
+ if (params.has("error")) {
+ thunkAPI.dispatch(
+ oauthError({
+ error: params.get("error") ?? "",
+ description: params.get("error_description") ?? "",
+ }),
+ );
+ }
throw new Error("Missing params, got: " + url);
}
const { bluesky } = thunkAPI.getState() as {
@@ -659,7 +689,7 @@ export const blueskySlice = createAppSlice({
};
},
rejected: (state, action) => {
- console.error("getProfile rejected", action.error);
+ console.error("createStreamKeyRecord rejected", action.error);
// state.status = "failed";
},
},
@@ -775,7 +805,7 @@ export const blueskySlice = createAppSlice({
};
},
rejected: (state, action) => {
- console.error("getProfile rejected", action.error);
+ console.error("createLivestreamRecord rejected", action.error);
return {
...state,
newLivestream: {
@@ -912,7 +942,7 @@ export const blueskySlice = createAppSlice({
};
},
rejected: (state, action) => {
- console.error("getProfile rejected", action.error);
+ console.error("createChatProfileRecord rejected", action.error);
return {
...state,
chatProfile: {
@@ -1048,6 +1078,7 @@ export const {
golivePost,
oauthCallback,
setPDS,
+ oauthError,
createStreamKeyRecord,
clearStreamKeyRecord,
createLivestreamRecord,
diff --git a/js/app/features/bluesky/blueskyTypes.tsx b/js/app/features/bluesky/blueskyTypes.tsx
index b1ac2d0c..2d98037a 100644
--- a/js/app/features/bluesky/blueskyTypes.tsx
+++ b/js/app/features/bluesky/blueskyTypes.tsx
@@ -1,4 +1,4 @@
-import { OAuthSession } from "@aquareum/atproto-oauth-client-react-native";
+import { OAuthSession } from "@streamplace/atproto-oauth-client-react-native";
import { Agent } from "@atproto/api";
import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
import { StreamKey } from "features/base/baseSlice";
diff --git a/js/app/features/bluesky/oauthClient.tsx b/js/app/features/bluesky/oauthClient.tsx
index 6638fcaf..f7a7984f 100644
--- a/js/app/features/bluesky/oauthClient.tsx
+++ b/js/app/features/bluesky/oauthClient.tsx
@@ -2,9 +2,10 @@ import {
ClientMetadata,
clientMetadataSchema,
ReactNativeOAuthClient,
-} from "@aquareum/atproto-oauth-client-react-native";
+} from "@streamplace/atproto-oauth-client-react-native";
import Constants from "expo-constants";
import { Platform } from "react-native";
+import { isWeb } from "tamagui";
export type StreamplaceOAuthClient = Omit<
ReactNativeOAuthClient,
@@ -58,14 +59,49 @@ export default async function createOAuthClient(
dpop_bound_access_tokens: true,
};
} else {
+ const redirectURI = isWeb
+ ? `${streamplaceUrl}/login`
+ : `${streamplaceUrl}/api/app-return`;
const res = await fetch(
- `${streamplaceUrl}/api/atproto-oauth/${Platform.OS}`,
+ `${streamplaceUrl}/oauth/downstream/client-metadata.json?redirect_uri=${encodeURIComponent(redirectURI)}`,
);
meta = await res.json();
}
clientMetadataSchema.parse(meta);
return new ReactNativeOAuthClient({
- handleResolver: "https://bsky.social", // backend instances should use a DNS based resolver
+ fetch: async (input, init) => {
+ // Normalize input to a Request object
+ let request: Request;
+ if (typeof input === "string" || input instanceof URL) {
+ request = new Request(input, init);
+ } else {
+ request = input;
+ }
+
+ // Lie to the oauth client and use our upstream server instead
+ if (
+ request.url.includes("plc.directory") ||
+ request.url.endsWith("did.json")
+ ) {
+ const res = await fetch(request, init);
+ if (!res.ok) {
+ return res;
+ }
+ const data = await res.json();
+ const service = data.service.find((s: any) => s.id === "#atproto_pds");
+ if (!service) {
+ return res;
+ }
+ service.serviceEndpoint = streamplaceUrl;
+ return new Response(JSON.stringify(data), {
+ status: res.status,
+ headers: res.headers,
+ });
+ }
+
+ return fetch(request, init);
+ },
+ handleResolver: streamplaceUrl,
responseMode: "query", // or "fragment" (frontend only) or "form_post" (backend only)
// These must be the same metadata as the one exposed on the
diff --git a/js/app/package.json b/js/app/package.json
index 500269e8..9cb58f87 100644
--- a/js/app/package.json
+++ b/js/app/package.json
@@ -25,7 +25,6 @@
"preset": "jest-expo"
},
"dependencies": {
- "@aquareum/atproto-oauth-client-react-native": "^0.0.1",
"@atproto-labs/pipe": "^0.1.0",
"@atproto/crypto": "^0.4.2",
"@atproto/jwk-jose": "^0.1.2",
@@ -40,6 +39,7 @@
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"@reduxjs/toolkit": "^2.3.0",
+ "@streamplace/atproto-oauth-client-react-native": "workspace:*",
"@tamagui/config": "^1.123.17",
"@tamagui/lucide-icons": "^1.123.17",
"@tamagui/toast": "^1.123.17",
diff --git a/js/atproto-oauth-client-react-native/.gitignore b/js/atproto-oauth-client-react-native/.gitignore
new file mode 100644
index 00000000..76921f5b
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+dist
+tsconfig.build.tsbuildinfo
diff --git a/js/atproto-oauth-client-react-native/README.md b/js/atproto-oauth-client-react-native/README.md
new file mode 100644
index 00000000..fce43837
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/README.md
@@ -0,0 +1,89 @@
+# atproto OAuth Client for React Native
+
+This package implements an atproto OAuth client usable on the React Native
+platform. It uses [react-native-quick-crypto] for cryptographic operations and
+[expo-sqlite] for persistence. Its usage is very similar to the atproto OAuth
+client for the browser, so refer to that [README] and [example] for general
+usage. Some differences are noted below.
+
+## expo-sqlite
+
+This library uses [expo-sqlite] to store the OAuth state and session data in a
+SQLite database. The schema is automatically created when the client is
+instantiated.
+
+Because this database is storing sensitive cryptographic keys, it is highly
+reccomended to use the optional SQLCipher extension. This can be accomplished in
+your app.json file:
+
+```json
+{
+ "expo": {
+ "plugins": [
+ [
+ "expo-sqlite",
+ {
+ "useSQLCipher": true
+ }
+ ]
+ ]
+ }
+}
+```
+
+## Login and session restore flow
+
+The basic login flow will involve popping up a web browser and allowing users to
+authenticate with their selected PDS. This can be accomplished with the
+`expo-web-browser` library:
+
+```tsx
+import { openAuthSessionAsync } from "expo-web-browser";
+
+// inside your login onPress, perhaps:
+const loginUrl = await oauthClient.authorize(pds);
+const res = await openAuthSessionAsync(loginUrl);
+if (res.type === "success") {
+ const params = new URLSearchParams(url.split("?")[1]);
+ const { session, state } = await oauthClient.callback(params);
+ console.log(`logged in as ${session.sub}`);
+}
+```
+
+## Development on localhost
+
+The atproto OAuth specification has a special case for development on localhost,
+but it is required to use a redirectUrl that returns to `127.0.0.1` or `[::1]`.
+This prevents the localhost OAuth flow from returning you directly to your app.
+As a workaround, you can host a static HTML server on 127.0.0.1 that recieves
+the incoming OAuth callback and then redirects to your app. (If you have a web
+version of your React Native app, you can just use that.) Such a redirect page
+might look something like this:
+
+```tsx
+import { useEffect } from "react";
+import { View, Text } from "react-native";
+
+export default function AppReturnScreen({ route }) {
+ useEffect(() => {
+ document.location.href = `com.example.app:/app-return${document.location.search}`;
+ }, []);
+ return (
+
+ Redirecting you back to the app...
+
+ );
+}
+```
+
+This flow will work on the iOS simulator and on Android devices provided you've
+forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
+instead need to set up TLS.
+
+[react-native-quick-crypto]:
+ https://github.com/margelo/react-native-quick-crypto
+[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
+[README]:
+ https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
+[example]:
+ https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
diff --git a/js/atproto-oauth-client-react-native/package.json b/js/atproto-oauth-client-react-native/package.json
new file mode 100644
index 00000000..251ae3fc
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@streamplace/atproto-oauth-client-react-native",
+ "version": "0.0.2",
+ "license": "MIT",
+ "description": "ATProto OAuth client for React Native",
+ "keywords": [
+ "atproto",
+ "oauth",
+ "client",
+ "node"
+ ],
+ "homepage": "https://atproto.com",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/bluesky-social/atproto",
+ "directory": "packages/oauth/oauth-client-react-native"
+ },
+ "type": "commonjs",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "@atproto-labs/did-resolver": "0.1.5",
+ "@atproto-labs/handle-resolver-node": "0.1.7",
+ "@atproto-labs/simple-store": "0.1.1",
+ "@atproto-labs/simple-store-memory": "0.1.1",
+ "@atproto/did": "0.1.3",
+ "@atproto/jwk": "0.1.1",
+ "@atproto/jwk-jose": "0.1.2",
+ "@atproto/jwk-webcrypto": "0.1.2",
+ "@atproto/oauth-client": "0.3.2",
+ "@atproto/oauth-client-browser": "0.3.2",
+ "@atproto/oauth-types": "0.2.1",
+ "abortcontroller-polyfill": "^1.7.6",
+ "event-target-shim": "^6.0.2",
+ "expo-sqlite": "^15.0.3",
+ "jose": "^5.2.0",
+ "react-native-quick-crypto": "^0.7.7"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.1",
+ "typescript": "^5.6.3"
+ },
+ "scripts": {
+ "build": "tsc --build tsconfig.build.json",
+ "postinstall": "yarn run build"
+ }
+}
diff --git a/js/atproto-oauth-client-react-native/src/index.ts b/js/atproto-oauth-client-react-native/src/index.ts
new file mode 100644
index 00000000..a5ad80d3
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/src/index.ts
@@ -0,0 +1,4 @@
+import "./polyfills";
+
+export * from "@atproto/oauth-client";
+export * from "./oauth-client-react-native";
diff --git a/js/atproto-oauth-client-react-native/src/oauth-client-react-native.native.ts b/js/atproto-oauth-client-react-native/src/oauth-client-react-native.native.ts
new file mode 100644
index 00000000..45185b08
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/src/oauth-client-react-native.native.ts
@@ -0,0 +1,188 @@
+import { SimpleStore } from "@atproto-labs/simple-store";
+import { jwkValidator } from "@atproto/jwk";
+import { JoseKey } from "@atproto/jwk-jose";
+import {
+ InternalStateData,
+ OAuthClient,
+ OAuthClientFetchMetadataOptions,
+ OAuthClientOptions,
+ OAuthSession,
+ Session,
+ SessionStore,
+ StateStore,
+} from "@atproto/oauth-client";
+import { JWK } from "jose";
+import QuickCrypto from "react-native-quick-crypto";
+import {
+ CryptoKey,
+ SubtleAlgorithm,
+} from "react-native-quick-crypto/lib/typescript/src/keys";
+import { JoseKeyStore, SQLiteKVStore } from "./sqlite-keystore";
+
+export type ReactNativeOAuthClientOptions = Omit<
+ OAuthClientOptions,
+ // Provided by this lib
+ | "runtimeImplementation"
+ // Provided by this lib but can be overridden
+ | "sessionStore"
+ | "stateStore"
+> & {
+ sessionStore?: SessionStore;
+ stateStore?: StateStore;
+ didStore?: SimpleStore;
+};
+
+export type ReactNativeOAuthClientFromMetadataOptions =
+ OAuthClientFetchMetadataOptions &
+ Omit;
+
+export class ReactNativeOAuthClient extends OAuthClient {
+ didStore: SimpleStore;
+
+ static async fromClientId(
+ options: ReactNativeOAuthClientFromMetadataOptions,
+ ) {
+ const clientMetadata = await OAuthClient.fetchMetadata(options);
+ return new ReactNativeOAuthClient({ ...options, clientMetadata });
+ }
+
+ constructor({
+ fetch,
+ responseMode = "query",
+
+ ...options
+ }: ReactNativeOAuthClientOptions) {
+ if (!options.stateStore) {
+ options.stateStore = new JoseKeyStore(
+ new SQLiteKVStore("state"),
+ );
+ }
+ if (!options.sessionStore) {
+ options.sessionStore = new JoseKeyStore(
+ new SQLiteKVStore("session"),
+ );
+ }
+ if (!options.didStore) {
+ options.didStore = new SQLiteKVStore("did");
+ }
+ super({
+ ...options,
+
+ sessionStore: options.sessionStore,
+ stateStore: options.stateStore,
+ fetch,
+ responseMode,
+ runtimeImplementation: {
+ createKey: async (algs): Promise => {
+ console.log("GOT HEREEEE!");
+ const errors: unknown[] = [];
+ for (const alg of algs) {
+ try {
+ let subtle = QuickCrypto?.webcrypto?.subtle;
+ const subalg = toSubtleAlgorithm(alg);
+ const keyPair = (await subtle.generateKey(subalg, true, [
+ "sign",
+ "verify",
+ ])) as CryptoKeyPair;
+
+ const ex = (await subtle.exportKey(
+ "jwk",
+ keyPair.privateKey as unknown as CryptoKey,
+ )) as JWK;
+ ex.alg = alg;
+ // these have trailing periods sometimes for some reason
+ for (const k of ["x", "y", "d"]) {
+ if (ex[k].endsWith(".")) {
+ ex[k] = ex[k].slice(0, -1);
+ }
+ }
+
+ // RNQC doesn't give us a kid, so let's do a quick hash of the key
+ const kid = QuickCrypto.createHash("sha256")
+ .update(JSON.stringify(ex))
+ .digest("hex");
+ const use = "sig";
+
+ return new JoseKey(jwkValidator.parse({ ...ex, kid, use }));
+ } catch (err) {
+ errors.push(err);
+ }
+ }
+ throw new Error("None of the algorithms worked");
+ },
+ getRandomValues: (length) =>
+ new Uint8Array(QuickCrypto.randomBytes(length)),
+ digest: (bytes, algorithm) =>
+ QuickCrypto.createHash(algorithm.name)
+ .update(bytes as unknown as ArrayBuffer)
+ .digest(),
+ },
+ clientMetadata: options.clientMetadata,
+ });
+ this.didStore = options.didStore;
+ }
+
+ async init(refresh?: boolean) {
+ const sub = await this.didStore.get(`(sub)`);
+ if (sub) {
+ try {
+ const session = await this.restore(sub, refresh);
+ return { session };
+ } catch (err) {
+ this.didStore.del(`(sub)`);
+ throw err;
+ }
+ }
+ }
+
+ async callback(params: URLSearchParams): Promise<{
+ session: OAuthSession;
+ state: string | null;
+ }> {
+ const { session, state } = await super.callback(params);
+ await this.didStore.set(`(sub)`, session.sub);
+ return { session, state };
+ }
+}
+
+export function toSubtleAlgorithm(
+ alg: string,
+ crv?: string,
+ options?: { modulusLength?: number },
+): SubtleAlgorithm {
+ switch (alg) {
+ case "PS256":
+ case "PS384":
+ case "PS512":
+ return {
+ name: "RSA-PSS",
+ hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`,
+ modulusLength: options?.modulusLength ?? 2048,
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ };
+ case "RS256":
+ case "RS384":
+ case "RS512":
+ return {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`,
+ modulusLength: options?.modulusLength ?? 2048,
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ };
+ case "ES256":
+ case "ES384":
+ return {
+ name: "ECDSA",
+ namedCurve: `P-${alg.slice(-3) as "256" | "384"}`,
+ };
+ case "ES512":
+ return {
+ name: "ECDSA",
+ namedCurve: "P-521",
+ };
+ default:
+ // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773
+
+ throw new TypeError(`Unsupported alg "${alg}"`);
+ }
+}
diff --git a/js/atproto-oauth-client-react-native/src/oauth-client-react-native.ts b/js/atproto-oauth-client-react-native/src/oauth-client-react-native.ts
new file mode 100644
index 00000000..2b3d2d65
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/src/oauth-client-react-native.ts
@@ -0,0 +1,4 @@
+// browser fallback
+// export * from "@atproto/oauth-client-browser";
+import { BrowserOAuthClient } from "@atproto/oauth-client-browser";
+export { BrowserOAuthClient as ReactNativeOAuthClient };
diff --git a/js/atproto-oauth-client-react-native/src/polyfills.native.ts b/js/atproto-oauth-client-react-native/src/polyfills.native.ts
new file mode 100644
index 00000000..433b4882
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/src/polyfills.native.ts
@@ -0,0 +1,36 @@
+import { Event, EventTarget } from "event-target-shim";
+import { install as installRNQC } from "react-native-quick-crypto";
+
+// Polyfill for the `throwIfAborted` method of the AbortController
+// used in @atproto/oauth-client
+import "abortcontroller-polyfill/dist/polyfill-patch-fetch";
+
+// Polyfill for jose. It tries to detect whether it's been passed a CryptoKey
+// instance, and isn't willing to accept RNQC's equivalent. So, this ensures that
+// `key instanceof CryptoKey` will always be true.
+// @ts-ignore
+global.CryptoKey = Object;
+
+// This is needed to populate the `crypto` global for jose's export here
+// https://github.com/panva/jose/blob/1e8b430b08a18a18883a69e7991832c9c602ca1a/src/runtime/browser/webcrypto.ts#L1
+installRNQC();
+
+// These two are needed for @atproto/oauth-client's `CustomEventTarget` to work.
+// @ts-ignore
+global.EventTarget = EventTarget;
+// @ts-ignore
+global.Event = Event;
+
+// And finally, this happens on React Native with every possible input:
+// URL.canParse("http://example.com") => false
+// I do not know why. Used in @atproto/oauth and @atproto/common-web
+if (!URL.canParse("http://example.com")) {
+ URL.canParse = (url: string | URL, base?: string) => {
+ try {
+ new URL(url, base);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+}
diff --git a/js/atproto-oauth-client-react-native/src/polyfills.ts b/js/atproto-oauth-client-react-native/src/polyfills.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/js/atproto-oauth-client-react-native/src/sqlite-keystore.ts b/js/atproto-oauth-client-react-native/src/sqlite-keystore.ts
new file mode 100644
index 00000000..65db7d12
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/src/sqlite-keystore.ts
@@ -0,0 +1,69 @@
+import { SimpleStore } from "@atproto-labs/simple-store";
+import { jwkValidator, Key } from "@atproto/jwk";
+import { JoseKey } from "@atproto/jwk-jose";
+import Storage from "expo-sqlite/kv-store";
+
+interface HasDPoPKey {
+ dpopKey: Key | undefined;
+}
+
+const NAMESPACE = `@@atproto/oauth-client-react-native`;
+
+/**
+ * An expo-sqlite store that handles serializing and deserializing
+ * our Jose DPoP keys. Wraps SQLiteKVStore or whatever other SimpleStore
+ * that a user might provide.
+ */
+export class JoseKeyStore {
+ private store: SimpleStore;
+ constructor(store: SimpleStore) {
+ this.store = store;
+ }
+
+ async get(key: string): Promise {
+ const itemStr = await this.store.get(key);
+ if (!itemStr) return undefined;
+ const item = JSON.parse(itemStr) as T;
+ if (item.dpopKey) {
+ item.dpopKey = new JoseKey(jwkValidator.parse(item.dpopKey));
+ }
+ return item;
+ }
+
+ async set(key: string, value: T): Promise {
+ if (value.dpopKey) {
+ value = {
+ ...value,
+ dpopKey: (value.dpopKey as JoseKey).privateJwk,
+ };
+ }
+ return await this.store.set(key, JSON.stringify(value));
+ }
+
+ async del(key: string): Promise {
+ return await this.store.del(key);
+ }
+}
+
+/**
+ * Simple wrapper around expo-sqlite's KVStore. Default implementation
+ * unless a user brings their own KV store.
+ */
+export class SQLiteKVStore implements SimpleStore {
+ private namespace: string;
+ constructor(namespace: string) {
+ this.namespace = `${NAMESPACE}:${namespace}`;
+ }
+
+ async get(key: string): Promise {
+ return (await Storage.getItem(`${this.namespace}:${key}`)) ?? undefined;
+ }
+
+ async set(key: string, value: string): Promise {
+ return await Storage.setItem(`${this.namespace}:${key}`, value);
+ }
+
+ async del(key: string): Promise {
+ return await Storage.removeItem(`${this.namespace}:${key}`);
+ }
+}
diff --git a/js/atproto-oauth-client-react-native/tsconfig.build.json b/js/atproto-oauth-client-react-native/tsconfig.build.json
new file mode 100644
index 00000000..35fdfad7
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../app/tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist"
+ },
+ "include": ["./src"]
+}
diff --git a/js/atproto-oauth-client-react-native/tsconfig.build.tsbuildinfo b/js/atproto-oauth-client-react-native/tsconfig.build.tsbuildinfo
deleted file mode 100644
index 35e6285d..00000000
--- a/js/atproto-oauth-client-react-native/tsconfig.build.tsbuildinfo
+++ /dev/null
@@ -1 +0,0 @@
-{"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"}
\ No newline at end of file
diff --git a/js/atproto-oauth-client-react-native/tsconfig.json b/js/atproto-oauth-client-react-native/tsconfig.json
new file mode 100644
index 00000000..e84b8178
--- /dev/null
+++ b/js/atproto-oauth-client-react-native/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "include": [],
+ "references": [{ "path": "./tsconfig.build.json" }]
+}
diff --git a/js/docs/package.json b/js/docs/package.json
index ccd904c3..24d51c91 100644
--- a/js/docs/package.json
+++ b/js/docs/package.json
@@ -5,7 +5,7 @@
"scripts": {
"dev": "astro dev --host 0.0.0.0 --port 38082",
"start": "astro dev --host 0.0.0.0 --port 38082",
- "build": "astro build && rm -rf ../app/dist/docs && cp -r dist ../app/dist/docs",
+ "build": "astro build && rm -rf ../app/dist/docs && mkdir -p ../app/dist && cp -r dist ../app/dist/docs",
"preview": "astro preview",
"astro": "astro"
},
diff --git a/js/docs/src/content/docs/lex-reference/account/place-stream-account-defs.md b/js/docs/src/content/docs/lex-reference/account/place-stream-account-defs.md
new file mode 100644
index 00000000..6ebd69be
--- /dev/null
+++ b/js/docs/src/content/docs/lex-reference/account/place-stream-account-defs.md
@@ -0,0 +1,43 @@
+---
+title: place.stream.account.defs
+description: Reference for the place.stream.account.defs lexicon
+---
+
+**Lexicon Version:** 1
+
+## Definitions
+
+
+
+### `loginResponse`
+
+**Type:** `object`
+
+**Properties:**
+
+| Name | Type | Req'd | Description | Constraints |
+| ------------- | -------- | ----- | ----------- | ------------- |
+| `redirectUrl` | `string` | ✅ | | Format: `uri` |
+
+---
+
+## Lexicon Source
+
+```json
+{
+ "lexicon": 1,
+ "id": "place.stream.account.defs",
+ "defs": {
+ "loginResponse": {
+ "type": "object",
+ "required": ["redirectUrl"],
+ "properties": {
+ "redirectUrl": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ }
+ }
+}
+```
diff --git a/js/docs/src/content/docs/lex-reference/account/place-stream-account-login.md b/js/docs/src/content/docs/lex-reference/account/place-stream-account-login.md
new file mode 100644
index 00000000..58000b51
--- /dev/null
+++ b/js/docs/src/content/docs/lex-reference/account/place-stream-account-login.md
@@ -0,0 +1,74 @@
+---
+title: place.stream.account.login
+description: Reference for the place.stream.account.login lexicon
+---
+
+**Lexicon Version:** 1
+
+## Definitions
+
+
+
+### `main`
+
+**Type:** `procedure`
+
+Get a redirect URL for the login flow.
+
+**Parameters:** _(None defined)_
+
+**Input:**
+
+- **Encoding:** `application/json`
+- **Schema:**
+
+**Schema Type:** `object`
+
+| Name | Type | Req'd | Description | Constraints |
+| ------------- | -------- | ----- | ------------------------------------------ | ----------- |
+| `handleOrDID` | `string` | ✅ | The handle or DID of the account to login. | |
+
+**Output:**
+
+- **Encoding:** `application/json`
+- **Schema:**
+
+**Schema Type:**
+[`place.stream.account.defs#loginResponse`](/lex-reference/place-stream-account-defs#loginresponse)
+
+---
+
+## Lexicon Source
+
+```json
+{
+ "lexicon": 1,
+ "id": "place.stream.account.login",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Get a redirect URL for the login flow.",
+ "input": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["handleOrDID"],
+ "properties": {
+ "handleOrDID": {
+ "type": "string",
+ "description": "The handle or DID of the account to login."
+ }
+ }
+ }
+ },
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "ref",
+ "ref": "place.stream.account.defs#loginResponse"
+ }
+ }
+ }
+ }
+}
+```
diff --git a/lexicons/app/bsky/actor/getProfile.json b/lexicons/app/bsky/actor/getProfile.json
new file mode 100644
index 00000000..15b0fcc2
--- /dev/null
+++ b/lexicons/app/bsky/actor/getProfile.json
@@ -0,0 +1,28 @@
+{
+ "lexicon": 1,
+ "id": "app.bsky.actor.getProfile",
+ "defs": {
+ "main": {
+ "type": "query",
+ "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.",
+ "parameters": {
+ "type": "params",
+ "required": ["actor"],
+ "properties": {
+ "actor": {
+ "type": "string",
+ "format": "at-identifier",
+ "description": "Handle or DID of account to fetch profile of."
+ }
+ }
+ },
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "ref",
+ "ref": "app.bsky.actor.defs#profileViewDetailed"
+ }
+ }
+ }
+ }
+}
diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json
new file mode 100644
index 00000000..69751a52
--- /dev/null
+++ b/lexicons/com/atproto/identity/resolveHandle.json
@@ -0,0 +1,37 @@
+{
+ "lexicon": 1,
+ "id": "com.atproto.identity.resolveHandle",
+ "defs": {
+ "main": {
+ "type": "query",
+ "description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.",
+ "parameters": {
+ "type": "params",
+ "required": ["handle"],
+ "properties": {
+ "handle": {
+ "type": "string",
+ "format": "handle",
+ "description": "The handle to resolve."
+ }
+ }
+ },
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["did"],
+ "properties": {
+ "did": { "type": "string", "format": "did" }
+ }
+ }
+ },
+ "errors": [
+ {
+ "name": "HandleNotFound",
+ "description": "The resolution process confirmed that the handle does not resolve to any DID."
+ }
+ ]
+ }
+ }
+}
diff --git a/package.json b/package.json
index ee606c01..d70fcab0 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"check:workspaces": "cd js/app && yarn run check",
"fix": "git ls-files | xargs prettier --write --ignore-unknown",
"postinstall": "husky && make js-lexicons",
- "build": "yarn workspaces foreach --parallel --all run build",
+ "build": "yarn workspaces foreach -t --parallel --all run build",
"prepare": "husky",
"release": "lerna publish --force-publish",
"precommit": "make precommit",
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 7a644480..e472ecbe 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -12,7 +12,6 @@ import (
"net/http/httputil"
"net/url"
"os"
- "slices"
"strings"
"sync"
"time"
@@ -37,6 +36,7 @@ import (
"stream.place/streamplace/pkg/mist/mistconfig"
"stream.place/streamplace/pkg/model"
"stream.place/streamplace/pkg/notifications"
+ "stream.place/streamplace/pkg/oproxy"
"stream.place/streamplace/pkg/spmetrics"
"stream.place/streamplace/pkg/spxrpc"
"stream.place/streamplace/pkg/streamplace"
@@ -121,15 +121,32 @@ func (fs AppHostingFS) Open(name string) (http.File, error) {
// api/playback/iame.li/hls/source/000000000000.ts
func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) {
+ var xrpc http.Handler
xrpc, err := spxrpc.NewServer(a.CLI, a.Model)
if err != nil {
return nil, err
}
+ op := oproxy.New(&oproxy.Config{
+ Host: a.CLI.PublicHost,
+ CreateOAuthSession: a.Model.CreateOAuthSession,
+ UpdateOAuthSession: a.Model.UpdateOAuthSession,
+ LoadOAuthSession: a.Model.LoadOAuthSession,
+ Scope: "atproto transition:generic",
+ UpstreamJWK: a.CLI.JWK,
+ DownstreamJWK: a.CLI.AccessJWK,
+ })
+
+ xrpc = op.OAuthMiddleware(xrpc)
router := httprouter.New()
+ router.Handler("GET", "/oauth/*anything", op.Handler())
+ router.Handler("POST", "/oauth/*anything", op.Handler())
+ router.Handler("GET", "/.well-known/oauth-authorization-server", op.Handler())
+ router.Handler("GET", "/.well-known/oauth-protected-resource", op.Handler())
apiRouter := httprouter.New()
apiRouter.HandlerFunc("POST", "/api/notification", a.HandleNotification(ctx))
// old clients
router.HandlerFunc("GET", "/app-updates", a.HandleAppUpdates(ctx))
+
// new ones
apiRouter.HandlerFunc("GET", "/api/manifest", a.HandleAppUpdates(ctx))
apiRouter.GET("/api/desktop-updates/:platform/:architecture/:version/:buildTime/:file", a.HandleDesktopUpdates(ctx))
@@ -156,9 +173,6 @@ func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) {
apiRouter.GET("/api/segment/recent", a.HandleRecentSegments(ctx))
apiRouter.GET("/api/segment/recent/:repoDID", a.HandleUserRecentSegments(ctx))
apiRouter.GET("/api/bluesky/resolve/:handle", a.HandleBlueskyResolve(ctx))
- for _, platform := range atproto.AllowedPlatforms {
- apiRouter.GET(fmt.Sprintf("/api/atproto-oauth/%s", platform), a.HandleATProtoOAuth(ctx, platform))
- }
apiRouter.GET("/api/live-users", a.HandleLiveUsers(ctx))
apiRouter.GET("/api/view-count/:user", a.HandleViewCount(ctx))
apiRouter.NotFound = a.HandleAPI404(ctx)
@@ -591,28 +605,6 @@ func (a *StreamplaceAPI) HandleBlueskyResolve(ctx context.Context) httprouter.Ha
}
}
-func (a *StreamplaceAPI) HandleATProtoOAuth(ctx context.Context, platform string) httprouter.Handle {
- return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
- host, _, err := net.SplitHostPort(req.Host)
- if err != nil {
- host = req.Host
- }
- if !slices.Contains(atproto.AllowedPlatforms, platform) {
- apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil)
- return
- }
-
- meta := atproto.GetMetadata(host, platform, a.CLI.AppBundleID)
- bs, err := json.Marshal(meta)
- if err != nil {
- apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(bs)
- }
-}
-
type ChatResponse struct {
Post *bsky.FeedPost `json:"post"`
Repo *model.Repo `json:"repo"`
@@ -778,7 +770,7 @@ func (a *StreamplaceAPI) getLimiter(ip string) *rate.Limiter {
limiter, exists := a.limiters[ip]
if !exists {
// 5 actions per second with a burst of 3
- limiter = rate.NewLimiter(rate.Limit(10.0), 8)
+ limiter = rate.NewLimiter(rate.Limit(20.0), 16)
a.limiters[ip] = limiter
}
diff --git a/pkg/api/api_internal.go b/pkg/api/api_internal.go
index fd656ced..8d5e9194 100644
--- a/pkg/api/api_internal.go
+++ b/pkg/api/api_internal.go
@@ -423,6 +423,20 @@ func (a *StreamplaceAPI) InternalHandler(ctx context.Context) (http.Handler, err
w.Write(bs)
})
+ router.GET("/oauth-sessions", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
+ sessions, err := a.Model.ListOAuthSessions()
+ if err != nil {
+ errors.WriteHTTPInternalServerError(w, "unable to get oauth sessions", err)
+ return
+ }
+ bs, err := json.Marshal(sessions)
+ if err != nil {
+ errors.WriteHTTPInternalServerError(w, "unable to marshal oauth sessions", err)
+ return
+ }
+ w.Write(bs)
+ })
+
router.POST("/notification-blast", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var payload notificationpkg.NotificationBlast
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
diff --git a/pkg/api/app-return.html b/pkg/api/app-return.html
index 4425916b..3fdf1a27 100644
--- a/pkg/api/app-return.html
+++ b/pkg/api/app-return.html
@@ -37,7 +37,7 @@
// authorized twice? I don't know why. I spent two hours trying to figure out why
// without luck. You're welcome to try more if you want! In the meantime, using
// an annoying button makes it work every time.
- // document.location.href = `${appBundleId}:/${window.location.search}`;
+ document.location.href = `${appBundleId}:/${window.location.search}`;
document.querySelector("button").addEventListener("click", () => {
document.location.href = `${appBundleId}:/${window.location.search}`;
});
diff --git a/pkg/api/playback.go b/pkg/api/playback.go
index d5fe07c0..02e78bc2 100644
--- a/pkg/api/playback.go
+++ b/pkg/api/playback.go
@@ -226,6 +226,11 @@ func (a *StreamplaceAPI) HandleWebRTCIngest(ctx context.Context) httprouter.Hand
}
addrBytes = decoded[:32]
didBytes = decoded[32:]
+ priv, err = atcrypto.ParsePrivateBytesK256(addrBytes)
+ if err != nil {
+ errors.WriteHTTPUnauthorized(w, "invalid authorization key (not valid atcrypto)", err)
+ return
+ }
}
key, _ := secp256k1.PrivKeyFromBytes(addrBytes)
@@ -234,6 +239,11 @@ func (a *StreamplaceAPI) HandleWebRTCIngest(ctx context.Context) httprouter.Hand
return
}
var signer crypto.Signer = key.ToECDSA()
+ pub, err := priv.PublicKey()
+ if err != nil {
+ apierrors.WriteHTTPUnauthorized(w, "invalid authorization key (could not parse as atcrypto)", err)
+ return
+ }
did := string(didBytes)
@@ -248,6 +258,15 @@ func (a *StreamplaceAPI) HandleWebRTCIngest(ctx context.Context) httprouter.Hand
apierrors.WriteHTTPUnauthorized(w, "user is not allowed to stream", err)
return
}
+ signingKey, err := a.Model.GetSigningKey(ctx, pub.DIDKey(), repo.DID)
+ if err != nil {
+ apierrors.WriteHTTPUnauthorized(w, "signing key not found", err)
+ return
+ }
+ if signingKey == nil {
+ apierrors.WriteHTTPUnauthorized(w, "signing key not found", nil)
+ return
+ }
} else {
atkey, err := atproto.ParsePubKey(signer.Public())
if err != nil {
diff --git a/pkg/atproto/atproto.go b/pkg/atproto/atproto.go
index f76537f7..dc97cd4b 100644
--- a/pkg/atproto/atproto.go
+++ b/pkg/atproto/atproto.go
@@ -13,11 +13,8 @@ import (
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/repo"
- "github.com/bluesky-social/indigo/util"
"github.com/bluesky-social/indigo/xrpc"
"github.com/ipfs/go-cid"
- "github.com/ipfs/go-datastore"
- blockstore "github.com/ipfs/go-ipfs-blockstore"
"go.opentelemetry.io/otel"
"stream.place/streamplace/pkg/aqhttp"
"stream.place/streamplace/pkg/constants"
@@ -69,7 +66,7 @@ type mstNode struct {
}
func (atsync *ATProtoSynchronizer) SyncBlueskyRepo(ctx context.Context, handle string, mod model.Model) (*model.Repo, error) {
- ctx = log.WithLogValues(ctx, "func", "SyncBlueskyRepo")
+ ctx = log.WithLogValues(ctx, "func", "SyncBlueskyRepo", "handle", handle)
// Get handle-specific lock and ensure synchronized access
ident, err := ResolveIdent(ctx, handle)
@@ -96,7 +93,6 @@ func (atsync *ATProtoSynchronizer) SyncBlueskyRepo(ctx context.Context, handle s
DID: ident.DID.String(),
PDS: ident.PDSEndpoint(),
Version: "",
- RootCID: "",
Handle: ident.Handle.String(),
}
err = mod.UpdateRepo(&newRepo)
@@ -130,47 +126,10 @@ func (atsync *ATProtoSynchronizer) SyncBlueskyRepo(ctx context.Context, handle s
log.Log(ctx, "got diff", "bytes", len(repoBytes))
- bs := blockstore.NewBlockstore(datastore.NewMapDatastore())
- root, err := repo.IngestRepo(ctx, bs, bytes.NewReader(repoBytes))
- if err != nil {
- return nil, fmt.Errorf("failed to ingest repo for %s: %w", ident.DID.String(), err)
- }
- log.Log(ctx, "ingested repo", "root", root)
- if oldRepo != nil && oldRepo.RootCID != "" {
- oldRoot, err := cid.Decode(oldRepo.RootCID)
- if err != nil {
- return nil, fmt.Errorf("failed to decode old root CID for %s: %w", ident.DID.String(), err)
- }
- if oldRoot.Equals(root) {
- log.Debug(ctx, "no changes to repo", "root", root)
- return oldRepo, nil
- }
- }
-
r, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(repoBytes))
if err != nil {
return nil, fmt.Errorf("failed to parse repo CAR data for %s: %w", ident.DID.String(), err)
}
-
- mstNodes := map[string]mstNode{}
- err = r.ForEach(ctx, "", func(k string, v cid.Cid) error {
- nsid, rkey, err := syntax.ParseRepoPath(k)
- if err != nil {
- log.Warn(ctx, "failed to parse repo path", "k", k, "err", err)
- return err
- }
- hash := v.Hash().HexString()
- log.Debug(ctx, "got mst node", "cid", v, "rkey", rkey, "nsid", nsid, "hash", hash)
- mstNodes[hash] = mstNode{
- rkey: rkey,
- collection: nsid,
- }
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("failed to iterate over repo: %w", err)
- }
-
// extract DID from repo commit
sc := r.SignedCommit()
signerDID, err := syntax.ParseDID(sc.Did)
@@ -181,46 +140,34 @@ func (atsync *ATProtoSynchronizer) SyncBlueskyRepo(ctx context.Context, handle s
return nil, fmt.Errorf("signer DID %s does not match identity %s", signerDID, ident.DID.String())
}
- bs = r.Blockstore()
- cst := util.CborStore(bs)
- allKeys, err := bs.AllKeysChan(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get all keys: %w", err)
- }
- for k := range allKeys {
- blk, err := bs.Get(ctx, k)
+ err = r.ForEach(ctx, "", func(k string, v cid.Cid) error {
+ nsid, rkey, err := syntax.ParseRepoPath(k)
if err != nil {
- return nil, fmt.Errorf("failed to get block for key %s: %w", k, err)
+ log.Warn(ctx, "failed to parse repo path", "k", k, "err", err)
+ return fmt.Errorf("could not parse repo path %s: %w", k, err)
}
- rec := map[string]any{}
- err = cst.Get(ctx, k, &rec)
+ _, bs, err := r.GetRecordBytes(ctx, k)
if err != nil {
- return nil, fmt.Errorf("failed to get block for key %s: %w", k, err)
- }
- typ, ok := rec["$type"]
- if !ok {
- log.Debug(ctx, "record type not found", "key", k)
- continue
+ log.Warn(ctx, "failed to get record bytes", "k", k, "rkey", rkey, "err", err)
+ return fmt.Errorf("could not retrieve record bytes for %s (rkey: %s): %w", k, rkey, err)
}
- log.Debug(ctx, "record type", "key", k, "type", typ)
- hash := k.Hash().HexString()
- node, ok := mstNodes[hash]
- if !ok {
- log.Warn(ctx, "no mst node found for record", "key", k, "hash", hash)
- continue
- }
- rawData := blk.RawData()
- err = atsync.handleCreateUpdate(ctx, signerDID.String(), node.rkey, &rawData, k.String(), node.collection)
+ log.Debug(ctx, "record type", "key", k, "type", nsid.String())
+ err = atsync.handleCreateUpdate(ctx, signerDID.String(), rkey, bs, v.String(), nsid)
if err != nil {
log.Warn(ctx, "failed to handle create update", "err", err)
+ // invalid CBOR and stuff should get ignored, so
+ // return fmt.Errorf("failed to process record update for %s (type: %s): %w", k, nsid.String(), err)
}
+ return nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to iterate over repo: %w", err)
}
newRepo := model.Repo{
DID: ident.DID.String(),
PDS: ident.PDSEndpoint(),
Version: sc.Rev,
- RootCID: root.String(),
Handle: ident.Handle.String(),
}
err = mod.UpdateRepo(&newRepo)
diff --git a/pkg/atproto/client_metadata.go b/pkg/atproto/client_metadata.go
deleted file mode 100644
index bedc4fa7..00000000
--- a/pkg/atproto/client_metadata.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package atproto
-
-import (
- "fmt"
-)
-
-var AllowedPlatforms = []string{"ios", "android", "web"}
-
-type OAuthClientMetadata struct {
- RedirectURIs []string `json:"redirect_uris"`
- ResponseTypes []string `json:"response_types,omitempty"`
- GrantTypes []string `json:"grant_types,omitempty"`
- Scope string `json:"scope,omitempty"`
- TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
- TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"`
- UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"`
- UserinfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"`
- JwksURI string `json:"jwks_uri,omitempty"`
- ApplicationType string `json:"application_type,omitempty"` // "web" or "native"
- SubjectType string `json:"subject_type,omitempty"` // "public" or "pairwise"
- RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"`
- IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"`
- AuthorizationSignedResponseAlg string `json:"authorization_signed_response_alg,omitempty"`
- AuthorizationEncryptedResponseEnc string `json:"authorization_encrypted_response_enc,omitempty"`
- AuthorizationEncryptedResponseAlg string `json:"authorization_encrypted_response_alg,omitempty"`
- ClientID string `json:"client_id,omitempty"`
- ClientName string `json:"client_name,omitempty"`
- ClientURI string `json:"client_uri,omitempty"`
- PolicyURI string `json:"policy_uri,omitempty"`
- TosURI string `json:"tos_uri,omitempty"`
- LogoURI string `json:"logo_uri,omitempty"`
- DefaultMaxAge int `json:"default_max_age,omitempty"`
- RequireAuthTime *bool `json:"require_auth_time,omitempty"`
- Contacts []string `json:"contacts,omitempty"`
- TLSClientCertificateBoundAccessTokens *bool `json:"tls_client_certificate_bound_access_tokens,omitempty"`
- DPoPBoundAccessTokens *bool `json:"dpop_bound_access_tokens,omitempty"`
- AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"`
- // Jwks *JWKSet `json:"jwks,omitempty"` // You'll need to define JWKSet type
-}
-
-func boolPtr(b bool) *bool {
- return &b
-}
-
-func GetMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata {
- meta := &OAuthClientMetadata{
- ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/%s", host, platform),
- ClientURI: fmt.Sprintf("https://%s", host),
- // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
- Scope: "atproto transition:generic",
- TokenEndpointAuthMethod: "none",
- ClientName: "Streamplace",
- ResponseTypes: []string{"code"},
- GrantTypes: []string{"authorization_code", "refresh_token"},
- DPoPBoundAccessTokens: boolPtr(true),
- }
- if platform == "web" {
- meta.RedirectURIs = []string{fmt.Sprintf("https://%s/login", host)}
- meta.ApplicationType = "web"
- } else {
- meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)}
- meta.ApplicationType = "native"
- }
- return meta
-}
-
-// clientMetadata: {
-// client_id: "http://localhost?scope=atproto%20transition:generic",
-// redirect_uris: ["http://127.0.0.1:38081"],
-// scope: "atproto transition:generic",
-// token_endpoint_auth_method: "none",
-// // jwks_uri: "https://my-app.example/jwks.json",
-// client_name: "Loopback client",
-// response_types: ["code"],
-// grant_types: ["authorization_code", "refresh_token"],
-// application_type: "native",
-// dpop_bound_access_tokens: true,
-// },
diff --git a/pkg/atproto/jwks.go b/pkg/atproto/jwks.go
new file mode 100644
index 00000000..e5c5e02d
--- /dev/null
+++ b/pkg/atproto/jwks.go
@@ -0,0 +1,45 @@
+package atproto
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+
+ oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers"
+ "github.com/lestrrat-go/jwx/v2/jwk"
+ "stream.place/streamplace/pkg/log"
+)
+
+func EnsureJWK(ctx context.Context, fPath string) (jwk.Key, error) {
+ var key jwk.Key
+ _, err := os.Stat(fPath)
+ if err == nil {
+ b, err := os.ReadFile(fPath)
+ if err != nil {
+ return nil, err
+ }
+ key, err = jwk.ParseKey(b)
+ if err != nil {
+ return nil, err
+ }
+ } else if os.IsNotExist(err) {
+ key, err = oauth_helpers.GenerateKey(nil)
+ if err != nil {
+ return nil, err
+ }
+
+ b, err := json.Marshal(key)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := os.WriteFile(fPath, b, 0600); err != nil {
+ return nil, err
+ }
+ log.Log(ctx, "generated JWK", "path", fPath)
+ } else {
+ return nil, err
+ }
+
+ return key, nil
+}
diff --git a/pkg/atproto/sync.go b/pkg/atproto/sync.go
index 7fee72d2..a5cfa262 100644
--- a/pkg/atproto/sync.go
+++ b/pkg/atproto/sync.go
@@ -2,6 +2,7 @@ package atproto
import (
"context"
+ "errors"
"fmt"
"reflect"
"time"
@@ -19,7 +20,7 @@ import (
)
func (atsync *ATProtoSynchronizer) handleCreateUpdate(ctx context.Context, userDID string, rkey syntax.RecordKey, recCBOR *[]byte, cid string, collection syntax.NSID) error {
- ctx = log.WithLogValues(ctx, "func", "handleCreateUpdate")
+ ctx = log.WithLogValues(ctx, "func", "handleCreateUpdate", "userDID", userDID, "rkey", rkey.String(), "cid", cid, "collection", collection.String())
now := time.Now()
r, err := atsync.Model.GetRepo(userDID)
if err != nil {
@@ -32,11 +33,14 @@ func (atsync *ATProtoSynchronizer) handleCreateUpdate(ctx context.Context, userD
}
d, err := data.UnmarshalCBOR(*recCBOR)
if err != nil {
- return fmt.Errorf("failed to parse record CBOR: %w", err)
+ return fmt.Errorf("failed to unmarhsal record CBOR: %w", err)
}
cb, err := lexutil.CborDecodeValue(*recCBOR)
- if err != nil {
- return fmt.Errorf("failed to parse record CBOR: %w", err)
+ if errors.Is(err, lexutil.ErrUnrecognizedType) {
+ log.Debug(ctx, "unrecognized record type", "key", rkey.String(), "type", err)
+ return nil
+ } else if err != nil {
+ return fmt.Errorf("failed to decode record CBOR: %w", err)
}
switch rec := cb.(type) {
case *bsky.GraphFollow:
diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go
index aa1ac1e3..16d8cfb2 100644
--- a/pkg/cmd/streamplace.go
+++ b/pkg/cmd/streamplace.go
@@ -145,6 +145,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error {
fs.StringVar(&cli.RelayHost, "relay-host", "wss://bsky.network", "websocket url for relay firehose")
fs.Bool("insecure", false, "DEPRECATED, does nothing.")
fs.StringVar(&cli.Color, "color", "", "'true' to enable colorized logging, 'false' to disable")
+ fs.StringVar(&cli.PublicHost, "public-host", "", "public host for this streamplace node (excluding https:// e.g. stream.place)")
fs.BoolVar(&cli.Thumbnail, "thumbnail", true, "enable thumbnail generation")
fs.BoolVar(&cli.SmearAudio, "smear-audio", false, "enable audio smearing to create 'perfect' segment timestamps")
fs.BoolVar(&cli.ExternalSigning, "external-signing", false, "enable external signing via exec (prevents potential memory leak)")
@@ -288,6 +289,21 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error {
return err
}
}
+
+ jwkPath := cli.DataFilePath([]string{"jwk.json"})
+ jwk, err := atproto.EnsureJWK(ctx, jwkPath)
+ if err != nil {
+ return err
+ }
+ cli.JWK = jwk
+
+ accessJWKPath := cli.DataFilePath([]string{"access-jwk.json"})
+ accessJWK, err := atproto.EnsureJWK(ctx, accessJWKPath)
+ if err != nil {
+ return err
+ }
+ cli.AccessJWK = accessJWK
+
b := bus.NewBus()
atsync := &atproto.ATProtoSynchronizer{
CLI: &cli,
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 03e7931f..315c9c46 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -91,9 +91,10 @@ type CLI struct {
SmearAudio bool
ExternalSigning bool
TracingEndpoint string
+ PublicHost string
JWK jwk.Key
-
- dataDirFlags []*string
+ AccessJWK jwk.Key
+ dataDirFlags []*string
}
var STREAMPLACE_SCHEME_PREFIX = "streamplace://"
diff --git a/pkg/model/model.go b/pkg/model/model.go
index d2d2961a..04e0b303 100644
--- a/pkg/model/model.go
+++ b/pkg/model/model.go
@@ -13,12 +13,15 @@ import (
slogGorm "github.com/orandin/slog-gorm"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
+ "stream.place/streamplace/pkg/config"
"stream.place/streamplace/pkg/log"
+ "stream.place/streamplace/pkg/oproxy"
"stream.place/streamplace/pkg/streamplace"
)
type DBModel struct {
- DB *gorm.DB
+ DB *gorm.DB
+ CLI *config.CLI
}
type Model interface {
@@ -79,6 +82,11 @@ type Model interface {
CreateChatProfile(ctx context.Context, profile *ChatProfile) error
GetChatProfile(ctx context.Context, repoDID string) (*ChatProfile, error)
+
+ CreateOAuthSession(id string, session *oproxy.OAuthSession) error
+ LoadOAuthSession(id string) (*oproxy.OAuthSession, error)
+ UpdateOAuthSession(id string, session *oproxy.OAuthSession) error
+ ListOAuthSessions() ([]oproxy.OAuthSession, error)
}
func MakeDB(dbURL string) (Model, error) {
@@ -135,6 +143,7 @@ func MakeDB(dbURL string) (Model, error) {
Block{},
ChatMessage{},
ChatProfile{},
+ oproxy.OAuthSession{},
} {
err = db.AutoMigrate(model)
if err != nil {
diff --git a/pkg/model/oauth_session.go b/pkg/model/oauth_session.go
new file mode 100644
index 00000000..e482809a
--- /dev/null
+++ b/pkg/model/oauth_session.go
@@ -0,0 +1,42 @@
+package model
+
+import (
+ "errors"
+
+ "gorm.io/gorm"
+ "stream.place/streamplace/pkg/oproxy"
+)
+
+func (m *DBModel) CreateOAuthSession(id string, session *oproxy.OAuthSession) error {
+ return m.DB.Create(session).Error
+}
+
+func (m *DBModel) LoadOAuthSession(id string) (*oproxy.OAuthSession, error) {
+ var session oproxy.OAuthSession
+ if err := m.DB.Where("downstream_dpop_jkt = ?", id).First(&session).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &session, nil
+}
+
+func (m *DBModel) UpdateOAuthSession(id string, session *oproxy.OAuthSession) error {
+ res := m.DB.Model(&oproxy.OAuthSession{}).Where("downstream_dpop_jkt = ?", id).Updates(session)
+ if res.Error != nil {
+ return res.Error
+ }
+ if res.RowsAffected == 0 {
+ return errors.New("no rows affected")
+ }
+ return nil
+}
+
+func (m *DBModel) ListOAuthSessions() ([]oproxy.OAuthSession, error) {
+ var sessions []oproxy.OAuthSession
+ if err := m.DB.Find(&sessions).Error; err != nil {
+ return nil, err
+ }
+ return sessions, nil
+}
diff --git a/pkg/oproxy/dpop_helpers.go b/pkg/oproxy/dpop_helpers.go
new file mode 100644
index 00000000..f86791b0
--- /dev/null
+++ b/pkg/oproxy/dpop_helpers.go
@@ -0,0 +1,197 @@
+package oproxy
+
+import (
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "encoding/base64"
+ "encoding/json"
+ "math/big"
+ "strings"
+
+ "github.com/AxisCommunications/go-dpop"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// all of this code borrowed from https://github.com/AxisCommunications/go-dpop
+// MIT license
+func keyFunc(t *jwt.Token) (interface{}, error) {
+ // Return the required jwkHeader header. See https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
+ // Used to validate the signature of the DPoP proof.
+ jwkHeader := t.Header["jwk"]
+ if jwkHeader == nil {
+ return nil, dpop.ErrMissingJWK
+ }
+
+ jwkMap, ok := jwkHeader.(map[string]interface{})
+ if !ok {
+ return nil, dpop.ErrMissingJWK
+ }
+
+ return parseJwk(jwkMap)
+}
+
+// Parses a JWK and inherently strips it of optional fields
+func parseJwk(jwkMap map[string]interface{}) (interface{}, error) {
+ // Ensure that JWK kty is present and is a string.
+ kty, ok := jwkMap["kty"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+ switch kty {
+ case "EC":
+ // Ensure that the required fields are present and are strings.
+ x, ok := jwkMap["x"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+ y, ok := jwkMap["y"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+ crv, ok := jwkMap["crv"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+
+ // Decode the coordinates from Base64.
+ //
+ // According to RFC 7518, they are Base64 URL unsigned integers.
+ // https://tools.ietf.org/html/rfc7518#section-6.3
+ xCoordinate, err := base64urlTrailingPadding(x)
+ if err != nil {
+ return nil, err
+ }
+ yCoordinate, err := base64urlTrailingPadding(y)
+ if err != nil {
+ return nil, err
+ }
+
+ // Read the specified curve of the key.
+ var curve elliptic.Curve
+ switch crv {
+ case "P-256":
+ curve = elliptic.P256()
+ case "P-384":
+ curve = elliptic.P384()
+ case "P-521":
+ curve = elliptic.P521()
+ default:
+ return nil, dpop.ErrUnsupportedCurve
+ }
+
+ return &ecdsa.PublicKey{
+ X: big.NewInt(0).SetBytes(xCoordinate),
+ Y: big.NewInt(0).SetBytes(yCoordinate),
+ Curve: curve,
+ }, nil
+ case "RSA":
+ // Ensure that the required fields are present and are strings.
+ e, ok := jwkMap["e"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+ n, ok := jwkMap["n"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+
+ // Decode the exponent and modulus from Base64.
+ //
+ // According to RFC 7518, they are Base64 URL unsigned integers.
+ // https://tools.ietf.org/html/rfc7518#section-6.3
+ exponent, err := base64urlTrailingPadding(e)
+ if err != nil {
+ return nil, err
+ }
+ modulus, err := base64urlTrailingPadding(n)
+ if err != nil {
+ return nil, err
+ }
+ return &rsa.PublicKey{
+ N: big.NewInt(0).SetBytes(modulus),
+ E: int(big.NewInt(0).SetBytes(exponent).Uint64()),
+ }, nil
+ case "OKP":
+ // Ensure that the required fields are present and are strings.
+ x, ok := jwkMap["x"].(string)
+ if !ok {
+ return nil, dpop.ErrInvalidProof
+ }
+
+ publicKey, err := base64urlTrailingPadding(x)
+ if err != nil {
+ return nil, err
+ }
+
+ return ed25519.PublicKey(publicKey), nil
+ case "OCT":
+ return nil, dpop.ErrUnsupportedKeyAlgorithm
+ default:
+ return nil, dpop.ErrUnsupportedKeyAlgorithm
+ }
+}
+
+// Borrowed from MicahParks/keyfunc See: https://github.com/MicahParks/keyfunc/blob/master/keyfunc.go#L56
+//
+// base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant
+// JWKS contain padding at the end values for base64url encoded public keys.
+//
+// Trailing padding is required to be removed from base64url encoded keys.
+// RFC 7517 Section 1.1 defines base64url the same as RFC 7515 Section 2:
+// https://datatracker.ietf.org/doc/html/rfc7517#section-1.1
+// https://datatracker.ietf.org/doc/html/rfc7515#section-2
+func base64urlTrailingPadding(s string) ([]byte, error) {
+ s = strings.TrimRight(s, "=")
+ return base64.RawURLEncoding.DecodeString(s)
+}
+
+// Strips eventual optional members of a JWK in order to be able to compute the thumbprint of it
+// https://datatracker.ietf.org/doc/html/rfc7638#section-3.2
+func getThumbprintableJwkJSONbytes(jwk map[string]interface{}) ([]byte, error) {
+ minimalJwk, err := parseJwk(jwk)
+ if err != nil {
+ return nil, err
+ }
+ jwkHeaderJSONBytes, err := getKeyStringRepresentation(minimalJwk)
+ if err != nil {
+ return nil, err
+ }
+ return jwkHeaderJSONBytes, nil
+}
+
+// Returns the string representation of a key in JSON format.
+func getKeyStringRepresentation(key interface{}) ([]byte, error) {
+ var keyParts interface{}
+ switch key := key.(type) {
+ case *ecdsa.PublicKey:
+ // Calculate the size of the byte array representation of an elliptic curve coordinate
+ // and ensure that the byte array representation of the key is padded correctly.
+ bits := key.Curve.Params().BitSize
+ keyCurveBytesSize := bits/8 + bits%8
+
+ keyParts = map[string]interface{}{
+ "kty": "EC",
+ "crv": key.Curve.Params().Name,
+ "x": base64.RawURLEncoding.EncodeToString(key.X.FillBytes(make([]byte, keyCurveBytesSize))),
+ "y": base64.RawURLEncoding.EncodeToString(key.Y.FillBytes(make([]byte, keyCurveBytesSize))),
+ }
+ case *rsa.PublicKey:
+ keyParts = map[string]interface{}{
+ "kty": "RSA",
+ "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.E)).Bytes()),
+ "n": base64.RawURLEncoding.EncodeToString(key.N.Bytes()),
+ }
+ case ed25519.PublicKey:
+ keyParts = map[string]interface{}{
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "x": base64.RawURLEncoding.EncodeToString(key),
+ }
+ default:
+ return nil, dpop.ErrUnsupportedKeyAlgorithm
+ }
+
+ return json.Marshal(keyParts)
+}
diff --git a/pkg/oproxy/helpers.go b/pkg/oproxy/helpers.go
new file mode 100644
index 00000000..1d28ff17
--- /dev/null
+++ b/pkg/oproxy/helpers.go
@@ -0,0 +1,99 @@
+package oproxy
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/AxisCommunications/go-dpop"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/google/uuid"
+)
+
+func boolPtr(b bool) *bool {
+ return &b
+}
+
+func codeUUID(prefix string) string {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("%s-%s", prefix, uu.String())
+}
+
+var urnPrefix = "urn:ietf:params:oauth:request_uri:"
+
+const UUID_LENGTH = 37
+
+func makeURN(jkt string) string {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("%s%s-%s", urnPrefix, uu.String(), jkt)
+}
+
+// urn --> jkt, uu
+func parseURN(urn string) (string, string, error) {
+ if !strings.HasPrefix(urn, urnPrefix) {
+ return "", "", fmt.Errorf("invalid URN: %s", urn)
+ }
+ withoutPrefix := urn[len(urnPrefix):]
+ uu := withoutPrefix[:UUID_LENGTH]
+ suffix := withoutPrefix[UUID_LENGTH:]
+ return suffix, uu, nil
+}
+
+func makeState(jkt string) string {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("%s-%s", uu.String(), jkt)
+}
+
+func parseState(state string) (string, string, error) {
+ if len(state) < UUID_LENGTH {
+ return "", "", fmt.Errorf("invalid state: %s", state)
+ }
+ uu := state[:UUID_LENGTH]
+ suffix := state[UUID_LENGTH:]
+ return suffix, uu, nil
+}
+
+func makeNonce() string {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("nonce-%s", uu.String())
+}
+
+// returns jkt, nonce, error
+func getJKT(dpopJWT string) (string, string, error) {
+ var claims dpop.ProofTokenClaims
+ token, err := jwt.ParseWithClaims(dpopJWT, &claims, keyFunc)
+ if err != nil {
+ return "", "", err
+ }
+ jwk, ok := token.Header["jwk"].(map[string]any)
+ if !ok {
+ return "", "", fmt.Errorf("missing jwk in DPoP JWT header")
+ }
+ jwkJSONbytes, err := getThumbprintableJwkJSONbytes(jwk)
+ if err != nil {
+ // keyFunc used with parseWithClaims should ensure that this can not happen but better safe than sorry.
+ return "", "", errors.Join(dpop.ErrInvalidProof, err)
+ }
+ h := sha256.New()
+ _, err = h.Write(jwkJSONbytes)
+ if err != nil {
+ return "", "", errors.Join(dpop.ErrInvalidProof, err)
+ }
+ b64URLjwkHash := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
+
+ return b64URLjwkHash, claims.Nonce, nil
+}
diff --git a/pkg/oproxy/oauth_0_metadata.go b/pkg/oproxy/oauth_0_metadata.go
new file mode 100644
index 00000000..73a4f4ac
--- /dev/null
+++ b/pkg/oproxy/oauth_0_metadata.go
@@ -0,0 +1,155 @@
+package oproxy
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/haileyok/atproto-oauth-golang/helpers"
+ "github.com/labstack/echo/v4"
+)
+
+func (o *OProxy) HandleOAuthAuthorizationServer(c echo.Context) error {
+ c.Response().Header().Set("Access-Control-Allow-Origin", "*")
+ c.Response().Header().Set("Content-Type", "application/json")
+ c.Response().WriteHeader(200)
+ json.NewEncoder(c.Response().Writer).Encode(generateOAuthServerMetadata(o.host))
+ return nil
+}
+
+func (o *OProxy) HandleOAuthProtectedResource(c echo.Context) error {
+ return c.JSON(200, map[string]interface{}{
+ "resource": fmt.Sprintf("https://%s", o.host),
+ "authorization_servers": []string{
+ fmt.Sprintf("https://%s", o.host),
+ },
+ "scopes_supported": []string{},
+ "bearer_methods_supported": []string{
+ "header",
+ },
+ "resource_documentation": "https://atproto.com",
+ })
+}
+
+func (o *OProxy) HandleClientMetadataUpstream(c echo.Context) error {
+ meta := o.GetUpstreamMetadata()
+ return c.JSON(200, meta)
+}
+
+func (o *OProxy) HandleJwksUpstream(c echo.Context) error {
+ pubKey, err := o.upstreamJWK.PublicKey()
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "could not get public key")
+ }
+ return c.JSON(200, helpers.CreateJwksResponseObject(pubKey))
+}
+
+func (o *OProxy) HandleClientMetadataDownstream(c echo.Context) error {
+ redirectURI := c.QueryParam("redirect_uri")
+ meta, err := o.GetDownstreamMetadata(redirectURI)
+ if err != nil {
+ return err
+ }
+ return c.JSON(200, meta)
+}
+
+func (o *OProxy) GetUpstreamMetadata() *OAuthClientMetadata {
+ // publicKey, err := o.upstreamJWK.PublicKey()
+ // if err != nil {
+ // panic(err)
+ // }
+ // jwks := jwk.NewSet()
+ // err = jwks.AddKey(publicKey)
+ // if err != nil {
+ // panic(err)
+ // }
+ // ro := helpers.CreateJwksResponseObject(publicKey)
+ meta := &OAuthClientMetadata{
+ ClientID: fmt.Sprintf("https://%s/oauth/upstream/client-metadata.json", o.host),
+ JwksURI: fmt.Sprintf("https://%s/oauth/upstream/jwks.json", o.host),
+ ClientURI: fmt.Sprintf("https://%s", o.host),
+ // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
+ Scope: "atproto transition:generic",
+ TokenEndpointAuthMethod: "private_key_jwt",
+ ClientName: "Streamplace",
+ ResponseTypes: []string{"code"},
+ GrantTypes: []string{"authorization_code", "refresh_token"},
+ DPoPBoundAccessTokens: boolPtr(true),
+ TokenEndpointAuthSigningAlg: "ES256",
+ RedirectURIs: []string{fmt.Sprintf("https://%s/oauth/return", o.host)},
+ // Jwks: ro,
+ }
+ return meta
+}
+
+func generateOAuthServerMetadata(host string) map[string]any {
+ oauthServerMetadata := map[string]any{
+ "issuer": fmt.Sprintf("https://%s", host),
+ "request_parameter_supported": true,
+ "request_uri_parameter_supported": true,
+ "require_request_uri_registration": true,
+ "scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
+ "subject_types_supported": []string{"public"},
+ "response_types_supported": []string{"code"},
+ "response_modes_supported": []string{"query", "fragment", "form_post"},
+ "grant_types_supported": []string{"authorization_code", "refresh_token"},
+ "code_challenge_methods_supported": []string{"S256"},
+ "ui_locales_supported": []string{"en-US"},
+ "display_values_supported": []string{"page", "popup", "touch"},
+ "authorization_response_iss_parameter_supported": true,
+ "request_object_encryption_alg_values_supported": []string{},
+ "request_object_encryption_enc_values_supported": []string{},
+ "jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host),
+ "authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host),
+ "token_endpoint": fmt.Sprintf("https://%s/oauth/token", host),
+ "token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
+ "revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host),
+ "introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host),
+ "pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host),
+ "require_pushed_authorization_requests": true,
+ "client_id_metadata_document_supported": true,
+ "request_object_signing_alg_values_supported": []string{
+ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
+ "ES256", "ES256K", "ES384", "ES512", "none",
+ },
+ "token_endpoint_auth_signing_alg_values_supported": []string{
+ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
+ "ES256", "ES256K", "ES384", "ES512",
+ },
+ "dpop_signing_alg_values_supported": []string{
+ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
+ "ES256", "ES256K", "ES384", "ES512",
+ },
+ }
+ return oauthServerMetadata
+}
+
+func (o *OProxy) GetDownstreamMetadata(redirectURI string) (*OAuthClientMetadata, error) {
+ meta := &OAuthClientMetadata{
+ ClientID: fmt.Sprintf("https://%s/oauth/downstream/client-metadata.json", o.host),
+ ClientURI: fmt.Sprintf("https://%s", o.host),
+ // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
+ Scope: "atproto transition:generic",
+ TokenEndpointAuthMethod: "none",
+ ClientName: "Streamplace",
+ ResponseTypes: []string{"code"},
+ GrantTypes: []string{"authorization_code", "refresh_token"},
+ DPoPBoundAccessTokens: boolPtr(true),
+ RedirectURIs: []string{fmt.Sprintf("https://%s/login", o.host), fmt.Sprintf("https://%s/api/app-return", o.host)},
+ ApplicationType: "web",
+ }
+ if redirectURI != "" {
+ found := false
+ for _, uri := range meta.RedirectURIs {
+ if uri == redirectURI {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", redirectURI))
+ }
+ meta.RedirectURIs = []string{redirectURI}
+ }
+ return meta, nil
+}
diff --git a/pkg/oproxy/oauth_1_par.go b/pkg/oproxy/oauth_1_par.go
new file mode 100644
index 00000000..34bfd435
--- /dev/null
+++ b/pkg/oproxy/oauth_1_par.go
@@ -0,0 +1,178 @@
+package oproxy
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "slices"
+
+ "github.com/AxisCommunications/go-dpop"
+ "github.com/labstack/echo/v4"
+ "go.opentelemetry.io/otel"
+)
+
+type PAR struct {
+ ClientID string `json:"client_id"`
+ RedirectURI string `json:"redirect_uri"`
+ CodeChallenge string `json:"code_challenge"`
+ CodeChallengeMethod string `json:"code_challenge_method"`
+ State string `json:"state"`
+ LoginHint string `json:"login_hint"`
+ ResponseMode string `json:"response_mode"`
+ ResponseType string `json:"response_type"`
+ Scope string `json:"scope"`
+}
+
+type PARResponse struct {
+ RequestURI string `json:"request_uri"`
+ ExpiresIn int `json:"expires_in"`
+}
+
+var ErrFirstNonce = echo.NewHTTPError(http.StatusBadRequest, "first time seeing this key, come back with a nonce")
+
+func (o *OProxy) HandleOAuthPAR(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthPAR")
+ defer span.End()
+ c.Response().Header().Set("Access-Control-Allow-Origin", "*")
+ var par PAR
+ if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+ }
+
+ dpopHeader := c.Request().Header.Get("DPoP")
+ if dpopHeader == "" {
+ return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
+ }
+
+ resp, err := o.NewPAR(ctx, c, &par, dpopHeader)
+ if errors.Is(err, ErrFirstNonce) {
+ res := map[string]interface{}{
+ "error": "use_dpop_nonce",
+ "error_description": "Authorization server requires nonce in DPoP proof",
+ }
+ return c.JSON(http.StatusBadRequest, res)
+ } else if err != nil {
+ return err
+ }
+ return c.JSON(http.StatusCreated, resp)
+}
+
+func (o *OProxy) NewPAR(ctx context.Context, c echo.Context, par *PAR, dpopHeader string) (*PARResponse, error) {
+ jkt, nonce, err := getJKT(dpopHeader)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get JKT from DPoP header header=%s: %s", dpopHeader, err))
+ }
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to load OAuth session: %s", err))
+ }
+ // special case - if this is the first request, we need to send it back for a new nonce
+ if session == nil {
+ _, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{
+ Nonce: nonce, // normally this would be bad! but on the first request we're revalidating nonce anyway
+ TimeWindow: &dpopTimeWindow,
+ })
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse DPoP header: %s", err))
+ }
+ newNonce := makeNonce()
+ err = o.createOAuthSession(jkt, &OAuthSession{
+ DownstreamDPoPJKT: jkt,
+ DownstreamDPoPNonce: newNonce,
+ })
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth session: %s", err))
+ }
+ // come back later, nerd
+ c.Response().Header().Set("DPoP-Nonce", newNonce)
+ return nil, ErrFirstNonce
+ }
+ if session.DownstreamDPoPNonce != nonce {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid nonce")
+ }
+ proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{
+ Nonce: session.DownstreamDPoPNonce,
+ TimeWindow: &dpopTimeWindow,
+ })
+ // Check the error type to determine response
+ if err != nil {
+ // if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
+ // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil)
+ // return
+ // }
+ // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err)
+ // return
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid DPoP proof: %s", err))
+ }
+ if proof.PublicKey() != jkt {
+ panic("invalid code path: parsed DPoP proof twice and got different keys?!")
+ }
+
+ clientMetadata, err := o.GetDownstreamMetadata(par.RedirectURI)
+ if err != nil {
+ return nil, err
+ }
+ if par.ClientID != clientMetadata.ClientID {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid client_id: expected %s, got %s", clientMetadata.ClientID, par.ClientID))
+ }
+
+ if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", par.RedirectURI))
+ }
+
+ if par.CodeChallengeMethod != "S256" {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid code challenge method: expected S256, got %s", par.CodeChallengeMethod))
+ }
+
+ if par.ResponseMode != "query" {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response mode: expected query, got %s", par.ResponseMode))
+ }
+
+ if par.ResponseType != "code" {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response type: expected code, got %s", par.ResponseType))
+ }
+
+ if par.Scope != o.scope {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope")
+ }
+
+ if par.LoginHint == "" {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS")
+ }
+
+ if par.State == "" {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required")
+ }
+
+ if par.Scope != o.scope {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope))
+ }
+
+ urn := makeURN(jkt)
+
+ newNonce := makeNonce()
+
+ err = o.updateOAuthSession(jkt, &OAuthSession{
+ DownstreamDPoPJKT: jkt,
+ DownstreamDPoPNonce: newNonce,
+ DownstreamPARRequestURI: urn,
+ DownstreamCodeChallenge: par.CodeChallenge,
+ DownstreamState: par.State,
+ DownstreamRedirectURI: par.RedirectURI,
+ Handle: par.LoginHint,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not create oauth session: %w", err)
+ }
+ c.Response().Header().Set("DPoP-Nonce", newNonce)
+
+ resp := &PARResponse{
+ RequestURI: urn,
+ ExpiresIn: int(dpopTimeWindow.Seconds()),
+ }
+
+ return resp, nil
+}
diff --git a/pkg/oproxy/oauth_2_authorize.go b/pkg/oproxy/oauth_2_authorize.go
new file mode 100644
index 00000000..794b43e4
--- /dev/null
+++ b/pkg/oproxy/oauth_2_authorize.go
@@ -0,0 +1,165 @@
+package oproxy
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ oauth "github.com/haileyok/atproto-oauth-golang"
+ "github.com/haileyok/atproto-oauth-golang/helpers"
+ "github.com/labstack/echo/v4"
+ "go.opentelemetry.io/otel"
+)
+
+func (o *OProxy) HandleOAuthAuthorize(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthAuthorize")
+ defer span.End()
+ c.Response().Header().Set("Access-Control-Allow-Origin", "*")
+ requestURI := c.QueryParam("request_uri")
+ if requestURI == "" {
+ return echo.NewHTTPError(http.StatusBadRequest, "request_uri is required")
+ }
+ clientID := c.QueryParam("client_id")
+ if clientID == "" {
+ return echo.NewHTTPError(http.StatusBadRequest, "client_id is required")
+ }
+ redirectURL, err := o.Authorize(ctx, requestURI, clientID)
+ if err != nil {
+ // we're a redirect; if we fail we need to send the user back
+ jkt, _, err := parseURN(requestURI)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err))
+ }
+
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err))
+ }
+
+ u, err := url.Parse(session.DownstreamRedirectURI)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err))
+ }
+ q := u.Query()
+ q.Set("error", "authorize_failed")
+ q.Set("error_description", err.Error())
+ u.RawQuery = q.Encode()
+ return c.Redirect(http.StatusTemporaryRedirect, u.String())
+ }
+ return c.Redirect(http.StatusTemporaryRedirect, redirectURL)
+}
+
+// downstream --> upstream transition; attempt to send user to the upstream auth server
+func (o *OProxy) Authorize(ctx context.Context, requestURI, clientID string) (string, *echo.HTTPError) {
+ downstreamMeta, err := o.GetDownstreamMetadata("")
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get downstream metadata: %s", err))
+ }
+ if downstreamMeta.ClientID != clientID {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("client ID mismatch: %s != %s", downstreamMeta.ClientID, clientID))
+ }
+
+ jkt, _, err := parseURN(requestURI)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err))
+ }
+
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err))
+ }
+
+ if session == nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no session found for jkt=%s", jkt))
+ }
+
+ if session.Status() != OAuthSessionStatePARCreated {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in par-created state: %s", session.Status()))
+ }
+
+ if session.DownstreamPARRequestURI != requestURI {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("request URI mismatch: %s != %s", session.DownstreamPARRequestURI, requestURI))
+ }
+
+ now := time.Now()
+ session.DownstreamPARUsedAt = &now
+ err = o.updateOAuthSession(jkt, session)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
+ }
+
+ upstreamMeta := o.GetUpstreamMetadata()
+ oclient, err := oauth.NewClient(oauth.ClientArgs{
+ ClientJwk: o.upstreamJWK,
+ ClientId: upstreamMeta.ClientID,
+ RedirectUri: upstreamMeta.RedirectURIs[0],
+ })
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth client: %s", err))
+ }
+
+ did, err := ResolveHandle(ctx, session.Handle)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve handle '%s': %s", session.DID, err))
+ }
+
+ service, err := ResolveService(ctx, did)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve service for DID '%s': %s", did, err))
+ }
+
+ authserver, err := oclient.ResolvePdsAuthServer(ctx, service)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve PDS auth server for service '%s': %s", service, err))
+ }
+
+ authmeta, err := oclient.FetchAuthServerMetadata(ctx, authserver)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to fetch auth server metadata from '%s': %s", authserver, err))
+ }
+
+ k, err := helpers.GenerateKey(nil)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate DPoP key: %s", err))
+ }
+
+ state := makeState(jkt)
+
+ opts := oauth.ParAuthRequestOpts{
+ State: state,
+ }
+ parResp, err := oclient.SendParAuthRequest(ctx, authserver, authmeta, session.Handle, upstreamMeta.Scope, k, opts)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to send PAR auth request to '%s': %s", authserver, err))
+ }
+
+ jwkJSON, err := json.Marshal(k)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to marshal DPoP key to JSON: %s", err))
+ }
+
+ u, err := url.Parse(authmeta.AuthorizationEndpoint)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse auth server metadata: %s", err))
+ }
+ u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(upstreamMeta.ClientID), parResp.RequestUri)
+ str := u.String()
+
+ session.DID = did
+ session.PDSUrl = service
+ session.UpstreamState = parResp.State
+ session.UpstreamAuthServerIssuer = authserver
+ session.UpstreamPKCEVerifier = parResp.PkceVerifier
+ session.UpstreamDPoPNonce = parResp.DpopAuthserverNonce
+ session.UpstreamDPoPPrivateJWK = string(jwkJSON)
+
+ err = o.updateOAuthSession(jkt, session)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
+ }
+
+ return str, nil
+}
diff --git a/pkg/oproxy/oauth_3_return.go b/pkg/oproxy/oauth_3_return.go
new file mode 100644
index 00000000..7c17b92d
--- /dev/null
+++ b/pkg/oproxy/oauth_3_return.go
@@ -0,0 +1,156 @@
+package oproxy
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/bluesky-social/indigo/api/atproto"
+ "github.com/bluesky-social/indigo/xrpc"
+ oauth "github.com/haileyok/atproto-oauth-golang"
+ "github.com/labstack/echo/v4"
+ "github.com/lestrrat-go/jwx/v2/jwk"
+ "go.opentelemetry.io/otel"
+)
+
+func (o *OProxy) HandleOAuthReturn(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthReturn")
+ defer span.End()
+ code := c.QueryParam("code")
+ iss := c.QueryParam("iss")
+ state := c.QueryParam("state")
+ errorCode := c.QueryParam("error")
+ errorDescription := c.QueryParam("error_description")
+ var httpError *echo.HTTPError
+ var redirectURL string
+ if errorCode != "" {
+ httpError = echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("%s (%s)", errorDescription, errorCode))
+ } else {
+ redirectURL, httpError = o.Return(ctx, code, iss, state)
+ }
+ if httpError != nil {
+ // we're a redirect; if we fail we need to send the user back
+ jkt, _, err := parseState(state)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err))
+ }
+
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err))
+ }
+
+ u, err := url.Parse(session.DownstreamRedirectURI)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err))
+ }
+ q := u.Query()
+ q.Set("error", "return_failed")
+ q.Set("error_description", httpError.Error())
+ u.RawQuery = q.Encode()
+ return c.Redirect(http.StatusTemporaryRedirect, u.String())
+ }
+ return c.Redirect(http.StatusTemporaryRedirect, redirectURL)
+}
+
+func (o *OProxy) Return(ctx context.Context, code string, iss string, state string) (string, *echo.HTTPError) {
+ upstreamMeta := o.GetUpstreamMetadata()
+ oclient, err := oauth.NewClient(oauth.ClientArgs{
+ ClientJwk: o.upstreamJWK,
+ ClientId: upstreamMeta.ClientID,
+ RedirectUri: upstreamMeta.RedirectURIs[0],
+ })
+
+ jkt, _, err := parseState(state)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse state: %s", err))
+ }
+
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get OAuth session: %s", err))
+ }
+ if session == nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no OAuth session found for state: %s", state))
+ }
+
+ if session.Status() != OAuthSessionStateUpstream {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in upstream state: %s", session.Status()))
+ }
+
+ if session.UpstreamState != state {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("state mismatch: %s != %s", session.UpstreamState, state))
+ }
+
+ if iss != session.UpstreamAuthServerIssuer {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("issuer mismatch: %s != %s", iss, session.UpstreamAuthServerIssuer))
+ }
+
+ key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK))
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to parse DPoP private JWK: %s", err))
+ }
+
+ itResp, err := oclient.InitialTokenRequest(ctx, code, iss, session.UpstreamPKCEVerifier, session.UpstreamDPoPNonce, key)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to request initial token: %s", err))
+ }
+ now := time.Now()
+
+ if itResp.Sub != session.DID {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("sub mismatch: %s != %s", itResp.Sub, session.DID))
+ }
+
+ if itResp.Scope != upstreamMeta.Scope {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("scope mismatch: %s != %s", itResp.Scope, upstreamMeta.Scope))
+ }
+
+ downstreamCode, err := generateAuthorizationCode()
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate downstream code: %s", err))
+ }
+
+ expiry := now.Add(time.Second * time.Duration(itResp.ExpiresIn)).UTC()
+ session.UpstreamAccessToken = itResp.AccessToken
+ session.UpstreamAccessTokenExp = &expiry
+ session.UpstreamRefreshToken = itResp.RefreshToken
+ session.DownstreamAuthorizationCode = downstreamCode
+
+ authArgs := &oauth.XrpcAuthedRequestArgs{
+ Did: session.DID,
+ AccessToken: session.UpstreamAccessToken,
+ PdsUrl: session.PDSUrl,
+ Issuer: session.UpstreamAuthServerIssuer,
+ DpopPdsNonce: session.UpstreamDPoPNonce,
+ DpopPrivateJwk: key,
+ }
+
+ xrpcClient := &oauth.XrpcClient{
+ OnDpopPdsNonceChanged: func(did, newNonce string) {},
+ }
+
+ // brief check to make sure we can actually do stuff
+ var out atproto.ServerCheckAccountStatus_Output
+ if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.server.checkAccountStatus", nil, nil, &out); err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to check account status: %s", err))
+ }
+
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
+ }
+
+ u, err := url.Parse(session.DownstreamRedirectURI)
+ if err != nil {
+ return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err))
+ }
+ q := u.Query()
+ q.Set("iss", fmt.Sprintf("https://%s", o.host))
+ q.Set("state", session.DownstreamState)
+ q.Set("code", session.DownstreamAuthorizationCode)
+ u.RawQuery = q.Encode()
+
+ return u.String(), nil
+}
diff --git a/pkg/oproxy/oauth_4_token.go b/pkg/oproxy/oauth_4_token.go
new file mode 100644
index 00000000..b46d0d21
--- /dev/null
+++ b/pkg/oproxy/oauth_4_token.go
@@ -0,0 +1,221 @@
+package oproxy
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/AxisCommunications/go-dpop"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/google/uuid"
+ "github.com/labstack/echo/v4"
+ "go.opentelemetry.io/otel"
+)
+
+type TokenRequest struct {
+ GrantType string `json:"grant_type"`
+ RedirectURI string `json:"redirect_uri"`
+ Code string `json:"code"`
+ CodeVerifier string `json:"code_verifier"`
+ ClientID string `json:"client_id"`
+ RefreshToken string `json:"refresh_token"`
+}
+
+type RevokeRequest struct {
+ Token string `json:"token"`
+ ClientID string `json:"client_id"`
+}
+
+type TokenResponse struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ RefreshToken string `json:"refresh_token"`
+ Scope string `json:"scope"`
+ ExpiresIn int `json:"expires_in"`
+ Sub string `json:"sub"`
+}
+
+var OAuthTokenExpiry = time.Hour * 24
+
+var dpopTimeWindow = time.Duration(30 * time.Second)
+
+func (o *OProxy) HandleOAuthToken(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthToken")
+ defer span.End()
+ var tokenRequest TokenRequest
+ if err := json.NewDecoder(c.Request().Body).Decode(&tokenRequest); err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err))
+ }
+
+ dpopHeader := c.Request().Header.Get("DPoP")
+ if dpopHeader == "" {
+ return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
+ }
+
+ res, err := o.Token(ctx, &tokenRequest, dpopHeader)
+ if err != nil {
+ return err
+ }
+ jkt, _, err := getJKT(dpopHeader)
+ if err != nil {
+ return err
+ }
+ sess, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return err
+ }
+ sess.DownstreamDPoPNonce = makeNonce()
+ err = o.updateOAuthSession(sess.DownstreamDPoPJKT, sess)
+ if err != nil {
+ return err
+ }
+ c.Response().Header().Set("DPoP-Nonce", sess.DownstreamDPoPNonce)
+
+ return c.JSON(http.StatusOK, res)
+}
+
+func (o *OProxy) Token(ctx context.Context, tokenRequest *TokenRequest, dpopHeader string) (*TokenResponse, error) {
+ proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/token"}, dpop.ParseOptions{
+ Nonce: "",
+ TimeWindow: &dpopTimeWindow,
+ })
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof")
+ }
+
+ jkt := proof.PublicKey()
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not get oauth session: %s", err))
+ }
+
+ if tokenRequest.GrantType == "authorization_code" {
+ return o.AccessToken(ctx, tokenRequest, session)
+ } else if tokenRequest.GrantType == "refresh_token" {
+ return o.RefreshToken(ctx, tokenRequest, session)
+ }
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "unsupported grant type")
+}
+
+func (o *OProxy) AccessToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) {
+ if session.Status() != OAuthSessionStateDownstream {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in downstream state: %s", session.Status()))
+ }
+
+ // Hash the code verifier using SHA-256
+ hasher := sha256.New()
+ hasher.Write([]byte(tokenRequest.CodeVerifier))
+ codeChallenge := hasher.Sum(nil)
+
+ encodedChallenge := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(codeChallenge)
+
+ if session.DownstreamCodeChallenge != encodedChallenge {
+ return nil, fmt.Errorf("invalid code challenge")
+ }
+
+ if session.DownstreamAuthorizationCode != tokenRequest.Code {
+ return nil, fmt.Errorf("invalid authorization code")
+ }
+
+ accessToken, err := o.generateJWT(session)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate access token: %w", err)
+ }
+
+ refreshToken, err := generateRefreshToken()
+ if err != nil {
+ return nil, fmt.Errorf("could not generate refresh token: %w", err)
+ }
+
+ session.DownstreamAccessToken = accessToken
+ session.DownstreamRefreshToken = refreshToken
+
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ return nil, fmt.Errorf("could not update downstream session: %w", err)
+ }
+
+ return &TokenResponse{
+ AccessToken: accessToken,
+ TokenType: "DPoP",
+ RefreshToken: refreshToken,
+ Scope: "atproto transition:generic",
+ ExpiresIn: int(OAuthTokenExpiry.Seconds()),
+ Sub: session.DID,
+ }, nil
+}
+
+func (o *OProxy) RefreshToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) {
+
+ if session.Status() != OAuthSessionStateReady {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "session is not in ready state")
+ }
+
+ if session.DownstreamRefreshToken != tokenRequest.RefreshToken {
+ return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid refresh token")
+ }
+
+ newJWT, err := o.generateJWT(session)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate new access token: %w", err)
+ }
+
+ session.DownstreamAccessToken = newJWT
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ return nil, fmt.Errorf("could not update downstream session: %w", err)
+ }
+
+ return &TokenResponse{
+ AccessToken: newJWT,
+ TokenType: "DPoP",
+ RefreshToken: session.DownstreamRefreshToken,
+ Scope: "atproto transition:generic",
+ ExpiresIn: int(OAuthTokenExpiry.Seconds()),
+ Sub: session.DID,
+ }, nil
+}
+
+func (o *OProxy) generateJWT(session *OAuthSession) (string, error) {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ return "", err
+ }
+ downstreamMeta, err := o.GetDownstreamMetadata("")
+ if err != nil {
+ return "", err
+ }
+ now := time.Now()
+ token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
+ "jti": uu.String(),
+ "sub": session.DID,
+ "exp": now.Add(OAuthTokenExpiry).Unix(),
+ "iat": now.Unix(),
+ "nbf": now.Unix(),
+ "cnf": map[string]any{
+ "jkt": session.DownstreamDPoPJKT,
+ },
+ "aud": fmt.Sprintf("did:web:%s", o.host),
+ "scope": downstreamMeta.Scope,
+ "client_id": downstreamMeta.ClientID,
+ "iss": fmt.Sprintf("https://%s", o.host),
+ })
+
+ var rawKey any
+ if err := o.downstreamJWK.Raw(&rawKey); err != nil {
+ return "", err
+ }
+
+ tokenString, err := token.SignedString(rawKey)
+
+ if err != nil {
+ return "", err
+ }
+
+ return tokenString, nil
+}
diff --git a/pkg/oproxy/oauth_5_revoke.go b/pkg/oproxy/oauth_5_revoke.go
new file mode 100644
index 00000000..134d402a
--- /dev/null
+++ b/pkg/oproxy/oauth_5_revoke.go
@@ -0,0 +1,56 @@
+package oproxy
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/AxisCommunications/go-dpop"
+ "github.com/labstack/echo/v4"
+ "go.opentelemetry.io/otel"
+)
+
+func (o *OProxy) HandleOAuthRevoke(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthRevoke")
+ defer span.End()
+ var revokeRequest RevokeRequest
+ if err := json.NewDecoder(c.Request().Body).Decode(&revokeRequest); err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err))
+ }
+ dpopHeader := c.Request().Header.Get("DPoP")
+ if dpopHeader == "" {
+ return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
+ }
+ err := o.Revoke(ctx, dpopHeader, &revokeRequest)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not handle oauth revoke: %s", err))
+ }
+ return c.JSON(http.StatusOK, map[string]interface{}{})
+}
+
+func (o *OProxy) Revoke(ctx context.Context, dpopHeader string, revokeRequest *RevokeRequest) error {
+ proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/revoke"}, dpop.ParseOptions{
+ Nonce: "",
+ TimeWindow: &dpopTimeWindow,
+ })
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof")
+ }
+
+ session, err := o.loadOAuthSession(proof.PublicKey())
+ if err != nil {
+ return fmt.Errorf("could not get downstream session: %w", err)
+ }
+
+ now := time.Now()
+ session.RevokedAt = &now
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ return fmt.Errorf("could not update downstream session: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/oproxy/oauth_middleware.go b/pkg/oproxy/oauth_middleware.go
new file mode 100644
index 00000000..6c147548
--- /dev/null
+++ b/pkg/oproxy/oauth_middleware.go
@@ -0,0 +1,234 @@
+package oproxy
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/AxisCommunications/go-dpop"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/labstack/echo/v4"
+)
+
+var OAuthSessionContextKey = oauthSessionContextKeyType{}
+
+type oauthSessionContextKeyType struct{}
+
+var OProxyContextKey = oproxyContextKeyType{}
+
+type oproxyContextKeyType struct{}
+
+func GetOAuthSession(ctx context.Context) (*OAuthSession, *XrpcClient) {
+ o, ok := ctx.Value(OProxyContextKey).(*OProxy)
+ if !ok {
+ return nil, nil
+ }
+ session, ok := ctx.Value(OAuthSessionContextKey).(*OAuthSession)
+ if !ok {
+ return nil, nil
+ }
+ client, err := o.GetXrpcClient(session)
+ if err != nil {
+ return nil, nil
+ }
+ return session, client
+}
+
+func (o *OProxy) OAuthMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // todo: see what these were set to before it got to us.
+ w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP")
+ w.Header().Set("Access-Control-Allow-Methods", "*")
+ w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce")
+
+ ctx := r.Context()
+ session, err := o.getOAuthSession(r, w)
+ if err != nil {
+ if errors.Is(err, dpop.ErrIncorrectNonce) {
+ // w.Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce", error_description="Invalid nonce"`)
+ w.Header().Set("content-type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ bs, _ := json.Marshal(map[string]interface{}{
+ "error": "use_dpop_nonce",
+ "error_description": "Authorization server requires nonce in DPoP proof",
+ })
+ w.Write(bs)
+ return
+ }
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ if session == nil {
+ next.ServeHTTP(w, r)
+ return
+ }
+ ctx = context.WithValue(ctx, OAuthSessionContextKey, session)
+ ctx = context.WithValue(ctx, OProxyContextKey, o)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
+func getMethod(method string) (dpop.HTTPVerb, error) {
+ switch method {
+ case "POST":
+ return dpop.POST, nil
+ case "GET":
+ return dpop.GET, nil
+ }
+ return "", fmt.Errorf("invalid method")
+}
+
+func (o *OProxy) getOAuthSession(r *http.Request, w http.ResponseWriter) (*OAuthSession, error) {
+
+ authHeader := r.Header.Get("Authorization")
+ if authHeader == "" {
+ return nil, nil
+ }
+ if !strings.HasPrefix(authHeader, "DPoP ") {
+ return nil, fmt.Errorf("invalid authorization header (must start with DPoP)")
+ }
+ token := strings.TrimPrefix(authHeader, "DPoP ")
+
+ dpopHeader := r.Header.Get("DPoP")
+ if dpopHeader == "" {
+ return nil, fmt.Errorf("missing DPoP header")
+ }
+
+ dpopMethod, err := getMethod(r.Method)
+ if err != nil {
+ return nil, fmt.Errorf("invalid method: %w", err)
+ }
+
+ u, err := url.Parse(r.URL.String())
+ if err != nil {
+ return nil, fmt.Errorf("invalid url: %w", err)
+ }
+ u.Scheme = "https"
+ u.Host = r.Host
+ u.RawQuery = ""
+ u.Fragment = ""
+
+ jkt, nonce, err := getJKT(dpopHeader)
+
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return nil, fmt.Errorf("could not get oauth session: %w", err)
+ }
+ if session == nil {
+ return nil, fmt.Errorf("oauth session not found")
+ }
+ if session.RevokedAt != nil {
+ return nil, fmt.Errorf("oauth session revoked")
+ }
+ if session.DownstreamDPoPNonce != nonce {
+ 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"`)
+ w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce)
+ return nil, dpop.ErrIncorrectNonce
+ }
+
+ session.DownstreamDPoPNonce = makeNonce()
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ return nil, fmt.Errorf("could not update downstream session: %w", err)
+ }
+ w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce)
+
+ proof, err := dpop.Parse(dpopHeader, dpopMethod, u, dpop.ParseOptions{
+ Nonce: nonce,
+ TimeWindow: &dpopTimeWindow,
+ })
+ // Check the error type to determine response
+ if err != nil {
+ if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
+ // Return 'invalid_dpop_proof'
+ return nil, fmt.Errorf("invalid DPoP proof: %w", err)
+ }
+ return nil, fmt.Errorf("error validating proof proof: %w", err)
+ }
+
+ // Hash the token with base64 and SHA256
+ // Get the access token JWT (introspect if needed)
+ // Parse the access token JWT and verify the signature
+ // Hash the access token with SHA-256
+ hasher := sha256.New()
+ hasher.Write([]byte(token))
+ hash := hasher.Sum(nil)
+
+ // Encode the hash in URL-safe base64 format without padding
+ // accessTokenHash := base64.RawURLEncoding.EncodeToString(hash)
+ accessTokenHash := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash)
+ pubKey, err := o.downstreamJWK.PublicKey()
+ if err != nil {
+ return nil, fmt.Errorf("could not get access jwk public key: %w", err)
+ }
+ var pubKeyECDSA ecdsa.PublicKey
+ err = pubKey.Raw(&pubKeyECDSA)
+ if err != nil {
+ return nil, fmt.Errorf("could not get access jwk public key: %w", err)
+ }
+
+ // Parse the access token JWT
+ claims := &dpop.BoundAccessTokenClaims{}
+ accessTokenJWT, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) {
+ return &pubKeyECDSA, nil
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("could not parse access token: %w", err)
+ }
+
+ err = proof.Validate([]byte(accessTokenHash), accessTokenJWT)
+ // Check the error type to determine response
+ if err != nil {
+ return nil, fmt.Errorf("invalid proof: %w", err)
+ }
+
+ return session, nil
+}
+
+func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ dpopHeader := c.Request().Header.Get("DPoP")
+ if dpopHeader == "" {
+ return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header")
+ }
+
+ jkt, _, err := getJKT(dpopHeader)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+ }
+
+ session, err := o.loadOAuthSession(jkt)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+ }
+
+ c.Set("session", session)
+ return next(c)
+ }
+}
+
+func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ err := next(c)
+ if err == nil {
+ return nil
+ }
+ httpError, ok := err.(*echo.HTTPError)
+ if ok {
+ o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal)
+ return err
+ }
+ o.slog.Error("unhandled error", "error", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+}
diff --git a/pkg/oproxy/oauth_session.go b/pkg/oproxy/oauth_session.go
new file mode 100644
index 00000000..261f1345
--- /dev/null
+++ b/pkg/oproxy/oauth_session.go
@@ -0,0 +1,156 @@
+package oproxy
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ oauth "github.com/haileyok/atproto-oauth-golang"
+ "github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+var refreshWhenRemaining = time.Minute * 59
+
+// OAuthSession stores authentication data needed during the OAuth flow
+type OAuthSession struct {
+ DID string `json:"did" gorm:"column:repo_did;index"`
+ Handle string `json:"handle" gorm:"column:handle;index"` // possibly also did if they have no handle
+ PDSUrl string `json:"pds_url" gorm:"column:pds_url;index"`
+
+ // Upstream fields
+ UpstreamState string `json:"upstream_state" gorm:"column:upstream_state;index"`
+ UpstreamAuthServerIssuer string `json:"upstream_auth_server_issuer" gorm:"column:upstream_auth_server_issuer"`
+ UpstreamPKCEVerifier string `json:"upstream_pkce_verifier" gorm:"column:upstream_pkce_verifier"`
+ UpstreamDPoPNonce string `json:"upstream_dpop_nonce" gorm:"column:upstream_dpop_nonce"`
+ UpstreamDPoPPrivateJWK string `json:"upstream_dpop_private_jwk" gorm:"column:upstream_dpop_private_jwk;type:text"`
+ UpstreamAccessToken string `json:"upstream_access_token" gorm:"column:upstream_access_token"`
+ UpstreamAccessTokenExp *time.Time `json:"upstream_access_token_exp" gorm:"column:upstream_access_token_exp"`
+ UpstreamRefreshToken string `json:"upstream_refresh_token" gorm:"column:upstream_refresh_token"`
+
+ // Downstream fields
+ DownstreamDPoPNonce string `json:"downstream_dpop_nonce" gorm:"column:downstream_dpop_nonce"`
+ DownstreamDPoPJKT string `json:"downstream_dpop_jkt" gorm:"column:downstream_dpop_jkt;primaryKey"`
+ DownstreamAccessToken string `json:"downstream_access_token" gorm:"column:downstream_access_token;index"`
+ DownstreamRefreshToken string `json:"downstream_refresh_token" gorm:"column:downstream_refresh_token;index"`
+ DownstreamAuthorizationCode string `json:"downstream_authorization_code" gorm:"column:downstream_authorization_code;index"`
+ DownstreamState string `json:"downstream_state" gorm:"column:downstream_state"`
+ DownstreamScope string `json:"downstream_scope" gorm:"column:downstream_scope"`
+ DownstreamCodeChallenge string `json:"downstream_code_challenge" gorm:"column:downstream_code_challenge"`
+ DownstreamPARRequestURI string `json:"downstream_par_request_uri" gorm:"column:downstream_par_request_uri"`
+ DownstreamPARUsedAt *time.Time `json:"downstream_par_used_at" gorm:"column:downstream_par_used_at"`
+ DownstreamRedirectURI string `json:"downstream_redirect_uri" gorm:"column:downstream_redirect_uri"`
+
+ RevokedAt *time.Time `json:"revoked_at" gorm:"column:revoked_at"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// for gorm. this is prettier than "o_auth_sessions"
+func (o *OAuthSession) TableName() string {
+ return "oauth_sessions"
+}
+
+type OAuthSessionStatus string
+
+const (
+ // We've gotten the first request and sent it back for a new nonce
+ OAuthSessionStatePARPending OAuthSessionStatus = "par-pending"
+ // PAR has been created, but not yet used
+ OAuthSessionStatePARCreated OAuthSessionStatus = "par-created"
+ // PAR has been used, but maybe upstream will fail for some reason
+ OAuthSessionStatePARUsed OAuthSessionStatus = "par-used"
+ // PAR has been used, we're waiting to hear back from upstream
+ OAuthSessionStateUpstream OAuthSessionStatus = "upstream"
+ // Upstream came back, we've issued the user a code but it hasn't been used yet
+ OAuthSessionStateDownstream OAuthSessionStatus = "downstream"
+ // Code has been used, everything is good
+ OAuthSessionStateReady OAuthSessionStatus = "ready"
+ // For any reason we're done. Revoked or expired
+ OAuthSessionStateRejected OAuthSessionStatus = "rejected"
+)
+
+func (o *OAuthSession) Status() OAuthSessionStatus {
+ if o.RevokedAt != nil {
+ return OAuthSessionStateRejected
+ }
+ if o.DownstreamAccessToken != "" {
+ return OAuthSessionStateReady
+ }
+ if o.DownstreamAuthorizationCode != "" {
+ return OAuthSessionStateDownstream
+ }
+ if o.UpstreamDPoPPrivateJWK != "" {
+ return OAuthSessionStateUpstream
+ }
+ if o.DownstreamPARUsedAt != nil {
+ return OAuthSessionStatePARUsed
+ }
+ if o.DownstreamPARRequestURI != "" {
+ return OAuthSessionStatePARCreated
+ }
+ if o.DownstreamDPoPNonce != "" {
+ return OAuthSessionStatePARPending
+ }
+ bs, _ := json.Marshal(o)
+ fmt.Printf("unknown oauth session status: %s\n", string(bs))
+ // todo: this should never happen, log a warning? panic?
+ return OAuthSessionStateRejected
+}
+
+func (o *OProxy) loadOAuthSession(jkt string) (*OAuthSession, error) {
+ session, err := o.userLoadOAuthSession(jkt)
+ if err != nil {
+ return nil, err
+ }
+ if session == nil {
+ return nil, nil
+ }
+ if session.Status() != OAuthSessionStateReady {
+ return session, nil
+ }
+ if session.UpstreamAccessTokenExp.Sub(time.Now()) > refreshWhenRemaining {
+ return session, nil
+ }
+
+ upstreamMeta := o.GetUpstreamMetadata()
+
+ oclient, err := oauth.NewClient(oauth.ClientArgs{
+ ClientJwk: o.upstreamJWK,
+ ClientId: upstreamMeta.ClientID,
+ RedirectUri: upstreamMeta.RedirectURIs[0],
+ })
+
+ dpopKey, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK))
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse upstream dpop private key: %w", err)
+ }
+
+ // refresh upstream before returning
+ resp, err := oclient.RefreshTokenRequest(context.Background(), session.UpstreamRefreshToken, session.UpstreamAuthServerIssuer, session.UpstreamDPoPNonce, dpopKey)
+ if err != nil {
+ // revoke, probably
+ o.slog.Error("failed to refresh upstream token, revoking downstream session", "error", err)
+ now := time.Now()
+ session.RevokedAt = &now
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ o.slog.Error("after upstream token refresh, failed to revoke downstream session", "error", err)
+ }
+ return nil, fmt.Errorf("failed to refresh upstream token: %w", err)
+ }
+
+ exp := time.Now().Add(time.Second * time.Duration(resp.ExpiresIn)).UTC()
+ session.UpstreamAccessToken = resp.AccessToken
+ session.UpstreamAccessTokenExp = &exp
+ session.UpstreamRefreshToken = resp.RefreshToken
+
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update downstream session after upstream token refresh: %w", err)
+ }
+
+ o.slog.Debug("refreshed upstream token", "session", session.DownstreamDPoPJKT)
+
+ return session, nil
+}
diff --git a/pkg/oproxy/oproxy.go b/pkg/oproxy/oproxy.go
new file mode 100644
index 00000000..067d3aef
--- /dev/null
+++ b/pkg/oproxy/oproxy.go
@@ -0,0 +1,74 @@
+package oproxy
+
+import (
+ "log/slog"
+ "net/http"
+ "os"
+
+ "github.com/labstack/echo/v4"
+ "github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+type OProxy struct {
+ createOAuthSession func(id string, session *OAuthSession) error
+ updateOAuthSession func(id string, session *OAuthSession) error
+ userLoadOAuthSession func(id string) (*OAuthSession, error)
+ e *echo.Echo
+ host string
+ scope string
+ upstreamJWK jwk.Key
+ downstreamJWK jwk.Key
+ slog *slog.Logger
+}
+
+type Config struct {
+ CreateOAuthSession func(id string, session *OAuthSession) error
+ UpdateOAuthSession func(id string, session *OAuthSession) error
+ LoadOAuthSession func(id string) (*OAuthSession, error)
+ Host string
+ Scope string
+ UpstreamJWK jwk.Key
+ DownstreamJWK jwk.Key
+ Slog *slog.Logger
+}
+
+func New(conf *Config) *OProxy {
+ e := echo.New()
+ mySlog := conf.Slog
+ if mySlog == nil {
+ mySlog = slog.New(slog.NewTextHandler(os.Stderr, nil))
+ }
+ o := &OProxy{
+ createOAuthSession: conf.CreateOAuthSession,
+ updateOAuthSession: conf.UpdateOAuthSession,
+ userLoadOAuthSession: conf.LoadOAuthSession,
+ e: e,
+ host: conf.Host,
+ scope: conf.Scope,
+ upstreamJWK: conf.UpstreamJWK,
+ downstreamJWK: conf.DownstreamJWK,
+ slog: mySlog,
+ }
+ o.e.GET("/.well-known/oauth-authorization-server", o.HandleOAuthAuthorizationServer)
+ o.e.GET("/.well-known/oauth-protected-resource", o.HandleOAuthProtectedResource)
+ o.e.POST("/oauth/par", o.HandleOAuthPAR)
+ o.e.GET("/oauth/authorize", o.HandleOAuthAuthorize)
+ o.e.GET("/oauth/return", o.HandleOAuthReturn)
+ o.e.POST("/oauth/token", o.DPoPNonceMiddleware(o.HandleOAuthToken))
+ o.e.POST("/oauth/revoke", o.DPoPNonceMiddleware(o.HandleOAuthRevoke))
+ o.e.GET("/oauth/upstream/client-metadata.json", o.HandleClientMetadataUpstream)
+ o.e.GET("/oauth/upstream/jwks.json", o.HandleJwksUpstream)
+ o.e.GET("/oauth/downstream/client-metadata.json", o.HandleClientMetadataDownstream)
+ o.e.Use(o.ErrorHandlingMiddleware)
+ return o
+}
+
+func (o *OProxy) Handler() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP")
+ w.Header().Set("Access-Control-Allow-Methods", "*")
+ w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce")
+ o.e.ServeHTTP(w, r)
+ })
+}
diff --git a/pkg/oproxy/resolution.go b/pkg/oproxy/resolution.go
new file mode 100644
index 00000000..55637953
--- /dev/null
+++ b/pkg/oproxy/resolution.go
@@ -0,0 +1,124 @@
+package oproxy
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "strings"
+
+ "github.com/bluesky-social/indigo/atproto/syntax"
+)
+
+// mostly borrowed from github.com/haileyok/atproto-oauth-golang, MIT license
+func ResolveHandle(ctx context.Context, handle string) (string, error) {
+ var did string
+
+ _, err := syntax.ParseHandle(handle)
+ if err != nil {
+ return "", err
+ }
+
+ recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
+ if err == nil {
+ for _, rec := range recs {
+ if strings.HasPrefix(rec, "did=") {
+ did = strings.Split(rec, "did=")[1]
+ break
+ }
+ }
+ }
+
+ if did == "" {
+ req, err := http.NewRequestWithContext(
+ ctx,
+ "GET",
+ fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
+ nil,
+ )
+ if err != nil {
+ return "", err
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ io.Copy(io.Discard, resp.Body)
+ return "", fmt.Errorf("unable to resolve handle")
+ }
+
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ maybeDid := string(b)
+
+ if _, err := syntax.ParseDID(maybeDid); err != nil {
+ return "", fmt.Errorf("unable to resolve handle")
+ }
+
+ did = maybeDid
+ }
+
+ return did, nil
+}
+
+func ResolveService(ctx context.Context, did string) (string, error) {
+ type Identity struct {
+ Service []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ ServiceEndpoint string `json:"serviceEndpoint"`
+ } `json:"service"`
+ }
+
+ var ustr string
+ if strings.HasPrefix(did, "did:plc:") {
+ ustr = fmt.Sprintf("https://plc.directory/%s", did)
+ } else if strings.HasPrefix(did, "did:web:") {
+ ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:"))
+ } else {
+ return "", fmt.Errorf("did was not a supported did type")
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
+ if err != nil {
+ return "", err
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ io.Copy(io.Discard, resp.Body)
+ return "", fmt.Errorf("could not find identity in plc registry")
+ }
+
+ var identity Identity
+ if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
+ return "", err
+ }
+
+ var service string
+ for _, svc := range identity.Service {
+ if svc.ID == "#atproto_pds" {
+ service = svc.ServiceEndpoint
+ }
+ }
+
+ if service == "" {
+ return "", fmt.Errorf("could not find atproto_pds service in identity services")
+ }
+
+ return service, nil
+}
diff --git a/pkg/oproxy/token_generation.go b/pkg/oproxy/token_generation.go
new file mode 100644
index 00000000..6aca1897
--- /dev/null
+++ b/pkg/oproxy/token_generation.go
@@ -0,0 +1,23 @@
+package oproxy
+
+import (
+ "fmt"
+
+ "github.com/google/uuid"
+)
+
+func generateRefreshToken() (string, error) {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("refresh-%s", uu.String()), nil
+}
+
+func generateAuthorizationCode() (string, error) {
+ uu, err := uuid.NewV7()
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("code-%s", uu.String()), nil
+}
diff --git a/pkg/oproxy/types.go b/pkg/oproxy/types.go
new file mode 100644
index 00000000..e57776fa
--- /dev/null
+++ b/pkg/oproxy/types.go
@@ -0,0 +1,35 @@
+package oproxy
+
+import "github.com/haileyok/atproto-oauth-golang/helpers"
+
+type OAuthClientMetadata struct {
+ RedirectURIs []string `json:"redirect_uris"`
+ ResponseTypes []string `json:"response_types,omitempty"`
+ GrantTypes []string `json:"grant_types,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
+ TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"`
+ UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"`
+ UserinfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"`
+ JwksURI string `json:"jwks_uri,omitempty"`
+ ApplicationType string `json:"application_type,omitempty"` // "web" or "native"
+ SubjectType string `json:"subject_type,omitempty"` // "public" or "pairwise"
+ RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"`
+ IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"`
+ AuthorizationSignedResponseAlg string `json:"authorization_signed_response_alg,omitempty"`
+ AuthorizationEncryptedResponseEnc string `json:"authorization_encrypted_response_enc,omitempty"`
+ AuthorizationEncryptedResponseAlg string `json:"authorization_encrypted_response_alg,omitempty"`
+ ClientID string `json:"client_id,omitempty"`
+ ClientName string `json:"client_name,omitempty"`
+ ClientURI string `json:"client_uri,omitempty"`
+ PolicyURI string `json:"policy_uri,omitempty"`
+ TosURI string `json:"tos_uri,omitempty"`
+ LogoURI string `json:"logo_uri,omitempty"`
+ DefaultMaxAge int `json:"default_max_age,omitempty"`
+ RequireAuthTime *bool `json:"require_auth_time,omitempty"`
+ Contacts []string `json:"contacts,omitempty"`
+ TLSClientCertificateBoundAccessTokens *bool `json:"tls_client_certificate_bound_access_tokens,omitempty"`
+ DPoPBoundAccessTokens *bool `json:"dpop_bound_access_tokens,omitempty"`
+ AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"`
+ Jwks *helpers.JwksResponseObject `json:"jwks,omitempty"`
+}
diff --git a/pkg/oproxy/xrpc_client.go b/pkg/oproxy/xrpc_client.go
new file mode 100644
index 00000000..e57460b0
--- /dev/null
+++ b/pkg/oproxy/xrpc_client.go
@@ -0,0 +1,62 @@
+package oproxy
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/bluesky-social/indigo/xrpc"
+ oauth "github.com/haileyok/atproto-oauth-golang"
+ "github.com/labstack/echo/v4"
+ "github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+var xrpcClient *oauth.XrpcClient
+
+type XrpcClient struct {
+ client *oauth.XrpcClient
+ authArgs *oauth.XrpcAuthedRequestArgs
+}
+
+func (o *OProxy) GetXrpcClient(session *OAuthSession) (*XrpcClient, error) {
+ key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK))
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse DPoP private JWK: %w", err)
+ }
+ authArgs := &oauth.XrpcAuthedRequestArgs{
+ Did: session.DID,
+ AccessToken: session.UpstreamAccessToken,
+ PdsUrl: session.PDSUrl,
+ Issuer: session.UpstreamAuthServerIssuer,
+ DpopPdsNonce: session.UpstreamDPoPNonce,
+ DpopPrivateJwk: key,
+ }
+
+ xrpcClient := &oauth.XrpcClient{
+ OnDpopPdsNonceChanged: func(did, newNonce string) {
+ sess, err := o.loadOAuthSession(session.DownstreamDPoPJKT)
+ if err != nil {
+ o.slog.Error("failed to get OAuth session in OnDpopPdsNonceChanged", "error", err)
+ return
+ }
+ sess.UpstreamDPoPNonce = newNonce
+ err = o.updateOAuthSession(session.DownstreamDPoPJKT, sess)
+ if err != nil {
+ o.slog.Error("failed to update OAuth session in OnDpopPdsNonceChanged", "error", err)
+ }
+ },
+ }
+ return &XrpcClient{client: xrpcClient, authArgs: authArgs}, nil
+}
+
+func (c *XrpcClient) Do(ctx context.Context, kind xrpc.XRPCRequestType, inpenc, method string, params map[string]any, bodyobj any, out any) error {
+ err := c.client.Do(ctx, c.authArgs, kind, inpenc, method, params, bodyobj, out)
+ if err == nil {
+ return nil
+ }
+ xErr, ok := err.(*xrpc.Error)
+ if !ok {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ return echo.NewHTTPError(xErr.StatusCode, xErr.Error())
+}
diff --git a/pkg/spxrpc/app_bsky_actor.go b/pkg/spxrpc/app_bsky_actor.go
new file mode 100644
index 00000000..09a2ad8e
--- /dev/null
+++ b/pkg/spxrpc/app_bsky_actor.go
@@ -0,0 +1,27 @@
+package spxrpc
+
+import (
+ "context"
+ "net/http"
+
+ appbskytypes "github.com/bluesky-social/indigo/api/bsky"
+ "github.com/bluesky-social/indigo/xrpc"
+ "github.com/labstack/echo/v4"
+ "stream.place/streamplace/pkg/oproxy"
+)
+
+func (s *Server) handleAppBskyActorGetProfile(ctx context.Context, actor string) (*appbskytypes.ActorDefs_ProfileViewDetailed, error) {
+ session, client := oproxy.GetOAuthSession(ctx)
+ if session == nil {
+ return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found")
+ }
+
+ // brief check to make sure we can actually do stuff
+ var out appbskytypes.ActorDefs_ProfileViewDetailed
+ err := client.Do(ctx, xrpc.Query, "application/json", "app.bsky.actor.getProfile", map[string]any{"actor": actor}, nil, &out)
+ if err != nil {
+ return nil, err
+ }
+
+ return &out, nil
+}
diff --git a/pkg/spxrpc/com_atproto_identity.go b/pkg/spxrpc/com_atproto_identity.go
new file mode 100644
index 00000000..d48e73de
--- /dev/null
+++ b/pkg/spxrpc/com_atproto_identity.go
@@ -0,0 +1,16 @@
+package spxrpc
+
+import (
+ "context"
+
+ comatprototypes "github.com/bluesky-social/indigo/api/atproto"
+ "stream.place/streamplace/pkg/oproxy"
+)
+
+func (s *Server) handleComAtprotoIdentityResolveHandle(ctx context.Context, handle string) (*comatprototypes.IdentityResolveHandle_Output, error) {
+ did, err := oproxy.ResolveHandle(ctx, handle)
+ if err != nil {
+ return nil, err
+ }
+ return &comatprototypes.IdentityResolveHandle_Output{Did: did}, nil
+}
diff --git a/pkg/spxrpc/spxrpc.go b/pkg/spxrpc/spxrpc.go
index c32ef702..6a540071 100644
--- a/pkg/spxrpc/spxrpc.go
+++ b/pkg/spxrpc/spxrpc.go
@@ -29,6 +29,12 @@ func NewServer(cli *config.CLI, model model.Model) (*Server, error) {
if err != nil {
return nil, err
}
+ err = s.RegisterHandlersComAtproto(e)
+ if err != nil {
+ return nil, err
+ }
+ e.GET("/xrpc/*", s.HandleWildcard)
+ e.POST("/xrpc/*", s.HandleWildcard)
return s, nil
}
diff --git a/pkg/spxrpc/stubs.go b/pkg/spxrpc/stubs.go
index ab3b31b0..9be497a3 100644
--- a/pkg/spxrpc/stubs.go
+++ b/pkg/spxrpc/stubs.go
@@ -3,6 +3,7 @@ package spxrpc
import (
"strconv"
+ comatprototypes "github.com/bluesky-social/indigo/api/atproto"
appbskytypes "github.com/bluesky-social/indigo/api/bsky"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/otel"
@@ -10,10 +11,25 @@ import (
)
func (s *Server) RegisterHandlersAppBsky(e *echo.Echo) error {
+ e.GET("/xrpc/app.bsky.actor.getProfile", s.HandleAppBskyActorGetProfile)
e.GET("/xrpc/app.bsky.feed.getFeedSkeleton", s.HandleAppBskyFeedGetFeedSkeleton)
return nil
}
+func (s *Server) HandleAppBskyActorGetProfile(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleAppBskyActorGetProfile")
+ defer span.End()
+ actor := c.QueryParam("actor")
+ var out *appbskytypes.ActorDefs_ProfileViewDetailed
+ var handleErr error
+ // func (s *Server) handleAppBskyActorGetProfile(ctx context.Context,actor string) (*appbskytypes.ActorDefs_ProfileViewDetailed, error)
+ out, handleErr = s.handleAppBskyActorGetProfile(ctx, actor)
+ if handleErr != nil {
+ return handleErr
+ }
+ return c.JSON(200, out)
+}
+
func (s *Server) HandleAppBskyFeedGetFeedSkeleton(c echo.Context) error {
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleAppBskyFeedGetFeedSkeleton")
defer span.End()
@@ -45,9 +61,24 @@ func (s *Server) RegisterHandlersChatBsky(e *echo.Echo) error {
}
func (s *Server) RegisterHandlersComAtproto(e *echo.Echo) error {
+ e.GET("/xrpc/com.atproto.identity.resolveHandle", s.HandleComAtprotoIdentityResolveHandle)
return nil
}
+func (s *Server) HandleComAtprotoIdentityResolveHandle(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleComAtprotoIdentityResolveHandle")
+ defer span.End()
+ handle := c.QueryParam("handle")
+ var out *comatprototypes.IdentityResolveHandle_Output
+ var handleErr error
+ // func (s *Server) handleComAtprotoIdentityResolveHandle(ctx context.Context,handle string) (*comatprototypes.IdentityResolveHandle_Output, error)
+ out, handleErr = s.handleComAtprotoIdentityResolveHandle(ctx, handle)
+ if handleErr != nil {
+ return handleErr
+ }
+ return c.JSON(200, out)
+}
+
func (s *Server) RegisterHandlersPlaceStream(e *echo.Echo) error {
e.GET("/xrpc/place.stream.graph.getFollowingUser", s.HandlePlaceStreamGraphGetFollowingUser)
return nil
diff --git a/pkg/spxrpc/wildcard.go b/pkg/spxrpc/wildcard.go
new file mode 100644
index 00000000..4768f04b
--- /dev/null
+++ b/pkg/spxrpc/wildcard.go
@@ -0,0 +1,57 @@
+package spxrpc
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/bluesky-social/indigo/xrpc"
+ "github.com/labstack/echo/v4"
+ "go.opentelemetry.io/otel"
+ "stream.place/streamplace/pkg/log"
+ "stream.place/streamplace/pkg/oproxy"
+)
+
+func (s *Server) HandleWildcard(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleWildcard")
+ defer span.End()
+
+ session, client := oproxy.GetOAuthSession(ctx)
+ if session == nil {
+ return echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found")
+ }
+
+ var out map[string]any
+
+ // Get the last path segment in the URL
+ path := c.Request().URL.Path
+ segments := strings.Split(path, "/")
+ lastSegment := segments[len(segments)-1]
+
+ var xrpcType xrpc.XRPCRequestType
+ var err error
+ if c.Request().Method == "GET" {
+ xrpcType = xrpc.Query
+ queryParams := make(map[string]any)
+ for k, v := range c.QueryParams() {
+ for _, vv := range v {
+ queryParams[k] = vv
+ }
+ }
+ err = client.Do(ctx, xrpcType, "application/json", lastSegment, queryParams, nil, &out)
+ } else {
+ xrpcType = xrpc.Procedure
+ var body map[string]any
+ if err := c.Bind(&body); err != nil {
+ return c.JSON(http.StatusBadRequest, xrpc.XRPCError{ErrStr: "BadRequest", Message: fmt.Sprintf("invalid body: %s", err)})
+ }
+ err = client.Do(ctx, xrpcType, "application/json", lastSegment, nil, body, &out)
+ }
+
+ if err != nil {
+ log.Error(ctx, "upstream xrpc error", "error", err)
+ return err
+ }
+
+ return c.JSON(200, out)
+}
diff --git a/yarn.lock b/yarn.lock
index e15e25f3..24e96dd6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -53,30 +53,6 @@ __metadata:
languageName: node
linkType: hard
-"@aquareum/atproto-oauth-client-react-native@npm:^0.0.1":
- version: 0.0.1
- resolution: "@aquareum/atproto-oauth-client-react-native@npm:0.0.1"
- dependencies:
- "@atproto-labs/did-resolver": "npm:0.1.5"
- "@atproto-labs/handle-resolver-node": "npm:0.1.7"
- "@atproto-labs/simple-store": "npm:0.1.1"
- "@atproto-labs/simple-store-memory": "npm:0.1.1"
- "@atproto/did": "npm:0.1.3"
- "@atproto/jwk": "npm:0.1.1"
- "@atproto/jwk-jose": "npm:0.1.2"
- "@atproto/jwk-webcrypto": "npm:0.1.2"
- "@atproto/oauth-client": "npm:0.3.2"
- "@atproto/oauth-client-browser": "npm:0.3.2"
- "@atproto/oauth-types": "npm:0.2.1"
- abortcontroller-polyfill: "npm:^1.7.6"
- event-target-shim: "npm:^6.0.2"
- expo-sqlite: "npm:^15.0.3"
- jose: "npm:^5.2.0"
- react-native-quick-crypto: "npm:^0.7.7"
- checksum: 10/73263e06756f8acfc526a6e89d5e68df9b2930b839ba6748a498f1b7e5d5480d31e068037f95bd7b7d1611be8565da80f045d85506ced7d79e4e132f6182a371
- languageName: node
- linkType: hard
-
"@astrojs/compiler@npm:^2.11.0":
version: 2.12.0
resolution: "@astrojs/compiler@npm:2.12.0"
@@ -8807,6 +8783,31 @@ __metadata:
languageName: node
linkType: hard
+"@streamplace/atproto-oauth-client-react-native@workspace:*, @streamplace/atproto-oauth-client-react-native@workspace:js/atproto-oauth-client-react-native":
+ version: 0.0.0-use.local
+ resolution: "@streamplace/atproto-oauth-client-react-native@workspace:js/atproto-oauth-client-react-native"
+ dependencies:
+ "@atproto-labs/did-resolver": "npm:0.1.5"
+ "@atproto-labs/handle-resolver-node": "npm:0.1.7"
+ "@atproto-labs/simple-store": "npm:0.1.1"
+ "@atproto-labs/simple-store-memory": "npm:0.1.1"
+ "@atproto/did": "npm:0.1.3"
+ "@atproto/jwk": "npm:0.1.1"
+ "@atproto/jwk-jose": "npm:0.1.2"
+ "@atproto/jwk-webcrypto": "npm:0.1.2"
+ "@atproto/oauth-client": "npm:0.3.2"
+ "@atproto/oauth-client-browser": "npm:0.3.2"
+ "@atproto/oauth-types": "npm:0.2.1"
+ "@types/node": "npm:^22.10.1"
+ abortcontroller-polyfill: "npm:^1.7.6"
+ event-target-shim: "npm:^6.0.2"
+ expo-sqlite: "npm:^15.0.3"
+ jose: "npm:^5.2.0"
+ react-native-quick-crypto: "npm:^0.7.7"
+ typescript: "npm:^5.6.3"
+ languageName: unknown
+ linkType: soft
+
"@streamplace/config-react-native-webrtc@workspace:js/config-react-native-webrtc":
version: 0.0.0-use.local
resolution: "@streamplace/config-react-native-webrtc@workspace:js/config-react-native-webrtc"
@@ -11133,6 +11134,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/node@npm:^22.10.1":
+ version: 22.15.17
+ resolution: "@types/node@npm:22.15.17"
+ dependencies:
+ undici-types: "npm:~6.21.0"
+ checksum: 10/3f5870ec1ac16b1dd8e5817de81164df9b69e4cf19cce692cb7c9b1af1deaecfd98b591b56155fcc4aa582f7189a4fc0c8d7d3226fa0387403db615a12dd8cb6
+ languageName: node
+ linkType: hard
+
"@types/normalize-package-data@npm:^2.4.0":
version: 2.4.4
resolution: "@types/normalize-package-data@npm:2.4.4"
@@ -29556,7 +29566,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "streamplace@workspace:js/app"
dependencies:
- "@aquareum/atproto-oauth-client-react-native": "npm:^0.0.1"
"@atproto-labs/pipe": "npm:^0.1.0"
"@atproto/crypto": "npm:^0.4.2"
"@atproto/jwk-jose": "npm:^0.1.2"
@@ -29581,6 +29590,7 @@ __metadata:
"@react-navigation/native": "npm:^6.1.18"
"@react-navigation/native-stack": "npm:^6.11.0"
"@reduxjs/toolkit": "npm:^2.3.0"
+ "@streamplace/atproto-oauth-client-react-native": "workspace:*"
"@tamagui/babel-plugin": "npm:^1.123.17"
"@tamagui/config": "npm:^1.123.17"
"@tamagui/lucide-icons": "npm:^1.123.17"
@@ -30946,6 +30956,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@npm:^5.6.3":
+ version: 5.8.3
+ resolution: "typescript@npm:5.8.3"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10/65c40944c51b513b0172c6710ee62e951b70af6f75d5a5da745cb7fab132c09ae27ffdf7838996e3ed603bb015dadd099006658046941bd0ba30340cc563ae92
+ languageName: node
+ linkType: hard
+
"typescript@npm:^5.7.2":
version: 5.7.3
resolution: "typescript@npm:5.7.3"
@@ -30986,6 +31006,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin":
+ version: 5.8.3
+ resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=b45daf"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10/98470634034ec37fd9ea61cc82dcf9a27950d0117a4646146b767d085a2ec14b137aae9642a83d1c62732d7fdcdac19bb6288b0bb468a72f7a06ae4e1d2c72c9
+ languageName: node
+ linkType: hard
+
"typescript@patch:typescript@npm%3A^5.7.2#optional!builtin":
version: 5.7.3
resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=b45daf"
@@ -31140,6 +31170,13 @@ __metadata:
languageName: node
linkType: hard
+"undici-types@npm:~6.21.0":
+ version: 6.21.0
+ resolution: "undici-types@npm:6.21.0"
+ checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14
+ languageName: node
+ linkType: hard
+
"undici@npm:6.19.7":
version: 6.19.7
resolution: "undici@npm:6.19.7"