From ceade16215f2d0e03d78c5ea5632f77b60c1238c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 8 Mar 2025 18:19:30 -0500 Subject: [PATCH 01/23] sync --- packages/openauth/src/client.ts | 153 +++++++++++++++++++++----- packages/openauth/src/issuer.ts | 32 ++---- packages/openauth/test/client.test.ts | 35 +++--- packages/openauth/test/issuer.test.ts | 37 +++---- packages/openauth/test/scrap.test.ts | 85 -------------- 5 files changed, 162 insertions(+), 180 deletions(-) delete mode 100644 packages/openauth/test/scrap.test.ts diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index c4e282a3..4a5dce29 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -116,7 +116,7 @@ export type Challenge = { /** * Configure the client. */ -export interface ClientInput { +export interface ClientInput { /** * The client ID. This is just a string to identify your app. * @@ -141,6 +141,12 @@ export interface ClientInput { * ``` */ issuer?: string + + /** + * Optionally specify the subjects that are used by the issuer. + */ + subjects?: S + /** * Optionally, override the internally used fetch function. * @@ -319,7 +325,11 @@ export interface VerifyResult { * Has the same shape as the subjects you defined when creating the issuer. */ subject: { - [type in keyof T]: { type: type; properties: v1.InferOutput } + [type in keyof T]: { + id: string + type: type + properties: v1.InferOutput + } }[keyof T] } @@ -343,7 +353,7 @@ export interface VerifyError { /** * An instance of the OpenAuth client contains the following methods. */ -export interface Client { +export interface Client { /** * Start the autorization flow. For example, in SSR sites. * @@ -532,11 +542,38 @@ export interface Client { * } * ``` */ - verify( - subjects: T, + verify( token: string, options?: VerifyOptions, - ): Promise | VerifyError> + ): Promise | VerifyError> + + /** + * Decode a JWT token without verifying its signature. + * + * ```ts + * const decoded = client.decode(token, subjects) + * ``` + * + * This returns the decoded token's subject if successful. + * + * ```ts + * if (!decoded.err) { + * console.log(decoded.subject.properties) + * } + * ``` + * + * Or if it fails, it returns an error. + * + * ```ts + * if (decoded.err) { + * // handle error + * } + * ``` + */ + decode( + token: string, + subjects: T, + ): DecodeSuccess | DecodeError } /** @@ -544,7 +581,37 @@ export interface Client { * * @param input - Configure the client. */ -export function createClient(input: ClientInput): Client { +/** + * Returned when the decode is successful. + */ +export interface DecodeSuccess { + /** + * This is always `false` when the decode is successful. + */ + err: false + /** + * The decoded subject from the token. + */ + subject: { + id: string + type: keyof T + properties: v1.InferOutput + } +} + +/** + * Returned when the decode fails. + */ +export interface DecodeError { + /** + * The type of error that occurred. + */ + err: InvalidAccessTokenError +} + +export function createClient( + input: ClientInput, +): Client { const jwksCache = new Map>() const issuerCache = new Map() const issuer = input.issuer || process.env.OPENAUTH_ISSUER @@ -695,7 +762,6 @@ export function createClient(input: ClientInput): Client { } }, async verify( - subjects: T, token: string, options?: VerifyOptions, ): Promise | VerifyError> { @@ -708,33 +774,32 @@ export function createClient(input: ClientInput): Client { }>(token, jwks, { issuer, }) - const validated = await subjects[result.payload.type][ - "~standard" - ].validate(result.payload.properties) - if (!validated.issues && result.payload.mode === "access") - return { - aud: result.payload.aud as string, - subject: { - type: result.payload.type, - properties: validated.value, - } as any, + let properties = result.payload.properties + if (input.subjects) { + const schema = input.subjects[result.payload.type as keyof S] + const validation = await schema["~standard"].validate(properties) + if (validation.issues) { + throw new InvalidSubjectError() } + properties = validation.value + } return { - err: new InvalidSubjectError(), + aud: result.payload.aud as string, + subject: { + id: result.payload.sub, + type: result.payload.type, + properties: properties, + } as any, } } catch (e) { if (e instanceof errors.JWTExpired && options?.refresh) { const refreshed = await this.refresh(options.refresh) if (refreshed.err) return refreshed - const verified = await result.verify( - subjects, - refreshed.tokens!.access, - { - refresh: refreshed.tokens!.refresh, - issuer, - fetch: options?.fetch, - }, - ) + const verified = await result.verify(refreshed.tokens!.access, { + refresh: refreshed.tokens!.refresh, + issuer, + fetch: options?.fetch, + }) if (verified.err) return verified verified.tokens = refreshed.tokens return verified @@ -744,6 +809,38 @@ export function createClient(input: ClientInput): Client { } } }, + + decode( + token: string, + subjects: T, + ): DecodeSuccess | DecodeError { + try { + const payload = decodeJwt(token) + if ( + !payload || + typeof payload.type !== "string" || + typeof payload.sub !== "string" + ) { + return { err: new InvalidAccessTokenError() } + } + + const type = payload.type as keyof T + if (!subjects[type]) { + return { err: new InvalidAccessTokenError() } + } + + return { + err: false, + subject: { + id: payload.sub, + type, + properties: payload.properties as v1.InferOutput, + }, + } + } catch (e) { + return { err: new InvalidAccessTokenError() } + } + }, } return result } diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index d3bec881..a052c161 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -152,13 +152,13 @@ export interface OnSuccessResponder< */ subject( type: Type, + id: string, properties: Extract["properties"], opts?: { ttl?: { access?: number refresh?: number } - subject?: string }, ): Promise } @@ -515,18 +515,13 @@ export function issuer< async success(ctx: Context, properties: any, successOpts) { return await input.success( { - async subject(type, properties, subjectOpts) { + async subject(type, id, properties, subjectOpts) { const authorization = await getAuthorization(ctx) - const subject = subjectOpts?.subject - ? subjectOpts.subject - : await resolveSubject(type, properties) - await successOpts?.invalidate?.( - await resolveSubject(type, properties), - ) + await successOpts?.invalidate?.(id) if (authorization.response_type === "token") { const location = new URL(authorization.redirect_uri) const tokens = await generateTokens(ctx, { - subject, + subject: id, type: type as string, properties, clientID: authorization.client_id, @@ -551,7 +546,7 @@ export function issuer< { type, properties, - subject, + subject: id, redirectURI: authorization.redirect_uri, clientID: authorization.client_id, pkce: authorization.pkce, @@ -634,18 +629,6 @@ export function issuer< .encrypt(await encryptionKey().then((k) => k.public)) } - async function resolveSubject(type: string, properties: any) { - const jsonString = JSON.stringify(properties) - const encoder = new TextEncoder() - const data = encoder.encode(jsonString) - const hashBuffer = await crypto.subtle.digest("SHA-1", data) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - return `${type}:${hashHex.slice(0, 16)}` - } - async function generateTokens( ctx: Context, value: { @@ -979,11 +962,10 @@ export function issuer< }) return input.success( { - async subject(type, properties, opts) { + async subject(type, id, properties, opts) { const tokens = await generateTokens(c, { type: type as string, - subject: - opts?.subject || (await resolveSubject(type, properties)), + subject: id, properties, clientID: clientID.toString(), ttl: { diff --git a/packages/openauth/test/client.test.ts b/packages/openauth/test/client.test.ts index a60e3fbc..aa2195b9 100644 --- a/packages/openauth/test/client.test.ts +++ b/packages/openauth/test/client.test.ts @@ -9,7 +9,7 @@ import { afterAll, mock, } from "bun:test" -import { object, string } from "valibot" +import { object } from "valibot" import { issuer } from "../src/issuer.js" import { createClient } from "../src/client.js" import { @@ -20,9 +20,7 @@ import { MemoryStorage } from "../src/storage/memory.js" import { createSubjects } from "../src/subject.js" const subjects = createSubjects({ - user: object({ - userID: string(), - }), + user: object({}), }) let storage = MemoryStorage() @@ -31,9 +29,7 @@ const auth = issuer({ subjects, allow: async () => true, success: async (ctx) => { - return ctx.subject("user", { - userID: "123", - }) + return ctx.subject("user", "123", {}) }, ttl: { access: 60, @@ -78,10 +74,11 @@ describe("verify", () => { clientID: "123", fetch: (a, b) => Promise.resolve(auth.request(a, b)), }) - const [verifier, authorization] = await client.pkce( + const authorization = await client.authorize( "https://client.example.com/callback", + "code", ) - let response = await auth.request(authorization) + let response = await auth.request(authorization.url) response = await auth.request(response.headers.get("location")!, { headers: { cookie: response.headers.get("set-cookie")!, @@ -92,7 +89,7 @@ describe("verify", () => { const exchanged = await client.exchange( code!, "https://client.example.com/callback", - verifier, + authorization.challenge.verifier, ) if (exchanged.err) throw exchanged.err tokens = exchanged.tokens @@ -100,14 +97,13 @@ describe("verify", () => { test("success", async () => { const refreshSpy = spyOn(client, "refresh") - const verified = await client.verify(subjects, tokens.access) + const verified = await client.verify(tokens.access) expect(verified).toStrictEqual({ aud: "123", subject: { + id: "123", type: "user", - properties: { - userID: "123", - }, + properties: {}, }, }) expect(refreshSpy).not.toBeCalled() @@ -116,7 +112,7 @@ describe("verify", () => { test("success after refresh", async () => { const refreshSpy = spyOn(client, "refresh") setSystemTime(Date.now() + 1000 * 6000 + 1000) - const verified = await client.verify(subjects, tokens.access, { + const verified = await client.verify(tokens.access, { refresh: tokens.refresh, }) expect(verified).toStrictEqual({ @@ -127,10 +123,9 @@ describe("verify", () => { refresh: expectNonEmptyString, }, subject: { + id: "123", type: "user", - properties: { - userID: "123", - }, + properties: {}, }, }) expect(refreshSpy).toBeCalled() @@ -138,7 +133,7 @@ describe("verify", () => { test("failure with expired access token", async () => { setSystemTime(Date.now() + 1000 * 6000 + 1000) - const verified = await client.verify(subjects, tokens.access) + const verified = await client.verify(tokens.access) expect(verified).toStrictEqual({ err: expect.any(InvalidAccessTokenError), }) @@ -146,7 +141,7 @@ describe("verify", () => { test("failure with invalid refresh token", async () => { setSystemTime(Date.now() + 1000 * 6000 + 1000) - const verified = await client.verify(subjects, tokens.access, { + const verified = await client.verify(tokens.access, { refresh: "foo", }) expect(verified).toStrictEqual({ diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index be303d77..cfbc9f61 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -14,9 +14,7 @@ import { MemoryStorage } from "../src/storage/memory.js" import { Provider } from "../src/provider/provider.js" const subjects = createSubjects({ - user: object({ - userID: string(), - }), + user: object({}), }) let storage = MemoryStorage() @@ -52,9 +50,7 @@ const issuerConfig = { }, success: async (ctx, value) => { if (value.provider === "dummy") { - return ctx.subject("user", { - userID: "123", - }) + return ctx.subject("user", "123", {}) } throw new Error("Invalid provider: " + value.provider) }, @@ -75,6 +71,7 @@ describe("code flow", () => { test("success", async () => { const client = createClient({ issuer: "https://auth.example.com", + subjects, clientID: "123", fetch: (a, b) => Promise.resolve(auth.request(a, b)), }) @@ -108,13 +105,12 @@ describe("code flow", () => { refresh: expectNonEmptyString, expiresIn: 60, }) - const verified = await client.verify(subjects, tokens.access) + const verified = await client.verify(tokens.access) if (verified.err) throw verified.err expect(verified.subject).toStrictEqual({ + id: "123", type: "user", - properties: { - userID: "123", - }, + properties: {}, }) }) }) @@ -144,14 +140,13 @@ describe("client credentials flow", () => { access_token: expectNonEmptyString, refresh_token: expectNonEmptyString, }) - const verified = await client.verify(subjects, tokens.access_token) + const verified = await client.verify(tokens.access_token) expect(verified).toStrictEqual({ aud: "myuser", subject: { + id: "123", type: "user", - properties: { - userID: "123", - }, + properties: {}, }, }) }) @@ -227,14 +222,13 @@ describe("refresh token", () => { expect(refreshed.access_token).not.toEqual(tokens.access) expect(refreshed.refresh_token).not.toEqual(tokens.refresh) - const verified = await client.verify(subjects, refreshed.access_token) + const verified = await client.verify(refreshed.access_token) expect(verified).toStrictEqual({ aud: "123", subject: { + id: "123", type: "user", - properties: { - userID: "123", - }, + properties: {}, }, }) }) @@ -254,14 +248,13 @@ describe("refresh token", () => { expect(refreshed.access_token).not.toEqual(tokens.access) expect(refreshed.refresh_token).not.toEqual(tokens.refresh) - const verified = await client.verify(subjects, refreshed.access_token) + const verified = await client.verify(refreshed.access_token) expect(verified).toStrictEqual({ aud: "123", subject: { + id: "123", type: "user", - properties: { - userID: "123", - }, + properties: {}, }, }) }) diff --git a/packages/openauth/test/scrap.test.ts b/packages/openauth/test/scrap.test.ts deleted file mode 100644 index 61e2d407..00000000 --- a/packages/openauth/test/scrap.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { expect, test } from "bun:test" -import { issuer } from "../src/issuer.js" -import { MemoryStorage } from "../src/storage/memory.js" -import { object, string } from "valibot" -import { createSubjects } from "../src/subject.js" -import { createClient } from "../src/client.js" - -const subjects = createSubjects({ - user: object({ - userID: string(), - }), -}) - -const auth = issuer({ - storage: MemoryStorage(), - subjects, - allow: async () => true, - success: async (ctx) => { - return ctx.subject("user", { - userID: "123", - }) - }, - ttl: { - access: 1, - }, - providers: { - dummy: { - type: "dummy", - init(route, ctx) { - route.get("/authorize", async (c) => { - return ctx.success(c, { - email: "foo@bar.com", - }) - }) - }, - }, - }, -}) - -test("code flow", async () => { - const client = createClient({ - issuer: "https://auth.example.com", - clientID: "123", - fetch: (a, b) => Promise.resolve(auth.request(a, b)), - }) - const [verifier, authorization] = await client.pkce( - "https://client.example.com/callback", - ) - let response = await auth.request(authorization) - expect(response.status).toBe(302) - response = await auth.request(response.headers.get("location")!, { - headers: { - cookie: response.headers.get("set-cookie")!, - }, - }) - expect(response.status).toBe(302) - const location = new URL(response.headers.get("location")!) - const code = location.searchParams.get("code") - expect(code).not.toBeNull() - const exchanged = await client.exchange( - code!, - "https://client.example.com/callback", - verifier, - ) - if (exchanged.err) throw exchanged.err - expect(exchanged.tokens.access).toBeTruthy() - expect(exchanged.tokens.refresh).toBeTruthy() - const verified = await client.verify(subjects, exchanged.tokens.access) - if (verified.err) throw verified.err - expect(verified.subject.type).toBe("user") - if (verified.subject.type !== "user") throw new Error("Invalid subject") - expect(verified.subject.properties.userID).toBe("123") - await new Promise((resolve) => setTimeout(resolve, 2000)) - const failed = await client.verify(subjects, exchanged.tokens.access) - expect(failed.err).toBeInstanceOf(Error) - const next = await client.verify(subjects, exchanged.tokens.access, { - refresh: exchanged.tokens.refresh, - }) - if (next.err) throw next.err - expect(next.tokens?.access).toBeDefined() - expect(next.tokens?.refresh).toBeDefined() - expect(next.tokens?.access).not.toEqual(exchanged.tokens.access) - expect(next.tokens?.refresh).not.toEqual(exchanged.tokens.refresh) - await client.verify(subjects, next.tokens!.access!) -}) From 6bf80e55edb4cf9876245470e502b54c9006c538 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 9 Mar 2025 18:04:18 -0400 Subject: [PATCH 02/23] solidjs --- bun.lockb | Bin 255216 -> 257680 bytes package.json | 3 +- packages/openauth/src/client.ts | 4 - packages/openauth/src/issuer.ts | 9 +- packages/solid/package.json | 27 +++++ packages/solid/src/index.tsx | 179 ++++++++++++++++++++++++++++++++ packages/solid/tsconfig.json | 22 ++++ 7 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 packages/solid/package.json create mode 100644 packages/solid/src/index.tsx create mode 100644 packages/solid/tsconfig.json diff --git a/bun.lockb b/bun.lockb index 7f4b865820733fdfc374b2177bd115d17dacfb6e..c7a00e825b25aee79dca2a44389238c580a9686c 100755 GIT binary patch delta 48080 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 K5&Z&X*8c$?Z_E7v delta 46809 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 decode( token: string, - subjects: T, ): DecodeSuccess | DecodeError { try { const payload = decodeJwt(token) @@ -825,9 +824,6 @@ export function createClient( } const type = payload.type as keyof T - if (!subjects[type]) { - return { err: new InvalidAccessTokenError() } - } return { err: false, diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index a052c161..6c868ca2 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -667,7 +667,8 @@ export function issuer< value.ttl.refresh, ) } - const accessTimeUsed = Math.floor((value.timeUsed ?? Date.now()) / 1000) + const expiry = + Math.floor((value.timeUsed ?? Date.now()) / 1000) + value.ttl.access return { access: await new SignJWT({ mode: "access", @@ -677,7 +678,7 @@ export function issuer< iss: issuer(ctx), sub: value.subject, }) - .setExpirationTime(Math.floor(accessTimeUsed + value.ttl.access)) + .setExpirationTime(expiry) .setProtectedHeader( await signingKey().then((k) => ({ alg: k.alg, @@ -686,9 +687,7 @@ export function issuer< })), ) .sign(await signingKey().then((item) => item.private)), - expiresIn: Math.floor( - accessTimeUsed + value.ttl.access - Date.now() / 1000, - ), + expiresIn: Math.floor(expiry - Date.now() / 1000), refresh: [value.subject, refreshToken].join(":"), } } diff --git a/packages/solid/package.json b/packages/solid/package.json new file mode 100644 index 00000000..004648f7 --- /dev/null +++ b/packages/solid/package.json @@ -0,0 +1,27 @@ +{ + "name": "@openauthjs/solid", + "version": "0.4.2", + "type": "module", + "scripts": { + "build": "tsc" + }, + "sideEffects": false, + "devDependencies": { + "@tsconfig/node22": "22.0.0", + "typescript": "5.6.3" + }, + "exports": { + ".": "./dist/index.jsx" + }, + "peerDependencies": { + "solid-js": "^1.8.0" + }, + "dependencies": { + "@solid-primitives/storage": "^4.3.1", + "@openauthjs/openauth": "workspace:*" + }, + "files": [ + "src", + "dist" + ] +} diff --git a/packages/solid/src/index.tsx b/packages/solid/src/index.tsx new file mode 100644 index 00000000..39f8f518 --- /dev/null +++ b/packages/solid/src/index.tsx @@ -0,0 +1,179 @@ +import { createClient } from "@openauthjs/openauth/client" +import { makePersisted } from "@solid-primitives/storage" +import { + batch, + createContext, + createEffect, + createMemo, + createSignal, + onMount, + ParentProps, + Show, + untrack, + useContext, +} from "solid-js" +import { createStore, produce } from "solid-js/store" + +interface Storage { + subjects: Record< + string, + { + id: string + refresh: string + } + > + current?: string +} + +interface Context { + subjects: Record + current?: SubjectInfo + switch(id: string): void + logout(id: string): void + authorize(): void +} + +interface SubjectInfo { + id: string + access(): Promise +} + +interface AuthContextOpts { + issuer: string + clientID: string +} + +const context = createContext() + +export function OpenAuthProvider(props: ParentProps) { + const client = createClient({ + issuer: props.issuer, + clientID: props.clientID, + }) + const [storage, setStorage] = makePersisted( + createStore({ + subjects: {}, + }), + { + name: `${props.issuer}.auth`, + }, + ) + + const [init, setInit] = createSignal(false) + + onMount(async () => { + const hash = new URLSearchParams(window.location.search.substring(1)) + const code = hash.get("code") + const state = hash.get("state") + if (code && state) { + const oldState = sessionStorage.getItem("openauth.state") + const verifier = sessionStorage.getItem("openauth.verifier") + const redirect = sessionStorage.getItem("openauth.redirect") + if (redirect && verifier && oldState === state) { + const result = await client.exchange(code, redirect, verifier) + if (!result.err) { + const id = result.tokens.refresh.split(":").slice(0, -1).join(":") + batch(() => { + setStorage("subjects", id, { + id: id, + refresh: result.tokens.refresh, + }) + setStorage("current", id) + }) + } + } + } + setInit(true) + }) + + async function authorize(redirectPath?: string) { + const redirect = new URL( + window.location.origin + (redirectPath ?? "/"), + ).toString() + const authorize = await client.authorize(redirect, "code", { + pkce: true, + }) + sessionStorage.setItem("openauth.state", authorize.challenge.state) + sessionStorage.setItem("openauth.redirect", redirect) + if (authorize.challenge.verifier) + sessionStorage.setItem("openauth.verifier", authorize.challenge.verifier) + window.location.href = authorize.url + } + + const accessCache = new Map() + async function access(id: string) { + const subject = storage.subjects[id] + const existing = accessCache.get(id) + const access = await client.refresh(subject.refresh, { + access: existing, + }) + if (access.err) { + ctx().logout(id) + throw access.err + } + if (access.tokens) { + setStorage("subjects", id, "refresh", access.tokens.refresh) + accessCache.set(id, access.tokens.access) + } + return access.tokens?.access || existing! + } + + const ctx = createMemo(() => { + console.log("recomputing subject context") + const subjects: Record = {} + for (const [key, value] of Object.entries(storage.subjects)) { + subjects[key] = { + get id() { + return value.id + }, + async access() { + return untrack(() => access(key)) + }, + } + } + return { + subjects, + get current() { + return subjects[storage.current!] + }, + switch(id: string) { + if (!storage.subjects[id]) return + setStorage("current", id) + }, + authorize, + logout(id: string) { + if (!storage.subjects[id]) return + setStorage( + produce((s) => { + delete s.subjects[id] + if (s.current === id) s.current = Object.keys(s.subjects)[0] + }), + ) + }, + } + }) + + createEffect(() => { + if (!init()) return + if (storage.current) return + const [first] = Object.keys(storage.subjects) + if (first) { + setStorage("current", first) + return + } + authorize() + }) + + return ( + + {props.children} + + ) +} + +export function useOpenAuth() { + const result = useContext(context) + if (!result) throw new Error("no auth context") + return result +} + diff --git a/packages/solid/tsconfig.json b/packages/solid/tsconfig.json new file mode 100644 index 00000000..29492476 --- /dev/null +++ b/packages/solid/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "skipLibCheck": true + }, + "include": [ + "src" + ] +} From 57977c50341ad81a3e8cdb4750d610d5c3b5e836 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 9 Mar 2025 21:00:02 -0400 Subject: [PATCH 03/23] sync --- .changeset/config.json | 3 +- bun.lockb | Bin 257680 -> 257696 bytes examples/client/cloudflare-api/package.json | 2 +- examples/client/sveltekit/package.json | 2 +- examples/quickstart/sst/package.json | 2 +- examples/quickstart/standalone/package.json | 2 +- package.json | 3 +- packages/solid/CHANGELOG.md | 51 ++++++++++++++++++++ scripts/snapshot | 45 +++++++++++++++++ 9 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 packages/solid/CHANGELOG.md create mode 100755 scripts/snapshot diff --git a/.changeset/config.json b/.changeset/config.json index d5dfa3d2..7f8e1712 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,10 +2,11 @@ "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", "changelog": "@changesets/cli/changelog", "commit": "./commit.cjs", - "fixed": [["@openauthjs/openauth"]], + "fixed": [["@openauthjs/openauth", "@openauthjs/solid"]], "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": ["@openauthjs/example-*"] } + diff --git a/bun.lockb b/bun.lockb index c7a00e825b25aee79dca2a44389238c580a9686c..3a85a743a710d0259a2bf26d2291726a45896f7d 100755 GIT binary patch delta 32756 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 EAEe6#`v3p{ delta 32627 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 diff --git a/examples/client/cloudflare-api/package.json b/examples/client/cloudflare-api/package.json index cd283a40..6ed4c2c6 100644 --- a/examples/client/cloudflare-api/package.json +++ b/examples/client/cloudflare-api/package.json @@ -1,5 +1,5 @@ { - "name": "cloudflare-api", + "name": "@openauthjs/cloudflare-api", "version": "0.0.0", "private": true } diff --git a/examples/client/sveltekit/package.json b/examples/client/sveltekit/package.json index 92b35a4a..f7935f6e 100644 --- a/examples/client/sveltekit/package.json +++ b/examples/client/sveltekit/package.json @@ -11,7 +11,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { - "@openauthjs/openauth": "^0.4.3", + "@openauthjs/openauth": "workspace:*", "@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", diff --git a/examples/quickstart/sst/package.json b/examples/quickstart/sst/package.json index ff7a4767..be91cc98 100644 --- a/examples/quickstart/sst/package.json +++ b/examples/quickstart/sst/package.json @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@openauthjs/openauth": "^0.3.2", + "@openauthjs/openauth": "workspace:*", "hono": "^4.6.16", "next": "15.1.4", "react": "^19.0.0", diff --git a/examples/quickstart/standalone/package.json b/examples/quickstart/standalone/package.json index e3c5bc65..f3d2ef0c 100644 --- a/examples/quickstart/standalone/package.json +++ b/examples/quickstart/standalone/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@openauthjs/openauth": "^0.3.2", + "@openauthjs/openauth": "workspace:*", "next": "15.1.4", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/package.json b/package.json index 2d6060c1..ea94f1a0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "examples/client/*" ], "scripts": { - "release": "bun run --filter=\"@openauthjs/openauth\" build && bun run --filter=\"@openauthjs/solid\" build && changeset publish" + "release": "bun run --filter=\"@openauthjs/openauth\" build && bun run --filter=\"@openauthjs/solid\" build && changeset publish", + "publish:version": "changeset publish" }, "devDependencies": { "@tsconfig/node22": "22.0.0", diff --git a/packages/solid/CHANGELOG.md b/packages/solid/CHANGELOG.md new file mode 100644 index 00000000..c77f7a35 --- /dev/null +++ b/packages/solid/CHANGELOG.md @@ -0,0 +1,51 @@ +# @openauthjs/solid + +## 0.0.0-20250310005931 + +### Patch Changes + +- Snapshot release 20250309205929 +- Updated dependencies + - @openauthjs/openauth@0.0.0-20250310005931 + +## 0.0.0-20250309205835-20250310005837 + +### Patch Changes + +- Snapshot release 20250309205835 +- Updated dependencies + - @openauthjs/openauth@0.0.0-20250309205835-20250310005837 + +## 0.0.0-20250309184932-20250309224934 + +### Patch Changes + +- Snapshot release 20250309184918 +- Snapshot release 20250309184932 +- Updated dependencies +- Updated dependencies + - @openauthjs/openauth@0.0.0-20250309184932-20250309224934 + +## 0.0.0-20250309183911-20250309223913 + +### Patch Changes + +- Snapshot release 20250309183911 +- Updated dependencies + - @openauthjs/openauth@0.0.0-20250309183911-20250309223913 + +## 0.0.0-20250309183848-20250309223850 + +### Patch Changes + +- Snapshot release 20250309183848 +- Updated dependencies [ec8ca65] +- Updated dependencies + - @openauthjs/openauth@0.0.0-20250309183848-20250309223850 + +## 0.0.0-20250309183546.b99be98-20250309223547 + +### Patch Changes + +- Updated dependencies [ec8ca65] + - @openauthjs/openauth@0.0.0-20250309183546.b99be98-20250309223547 diff --git a/scripts/snapshot b/scripts/snapshot new file mode 100755 index 00000000..db53f987 --- /dev/null +++ b/scripts/snapshot @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -e + +SNAPSHOT_ID=$(date +%Y%m%d%H%M%S) + +echo "Creating snapshot release: ${SNAPSHOT_ID}" + +# Create a temporary changeset file to force a version bump +echo "Creating temporary changeset..." +mkdir -p .changeset +cat > .changeset/snapshot-${SNAPSHOT_ID}.md << EOF +--- +"@openauthjs/openauth": patch +"@openauthjs/solid": patch +--- + +Snapshot release ${SNAPSHOT_ID} +EOF + +# First build the packages +echo "Building packages..." +bun run --filter="@openauthjs/openauth" build +bun run --filter="@openauthjs/solid" build + +# Use changesets to create a snapshot release +echo "Creating snapshot release with ID: ${SNAPSHOT_ID}" +bun changeset version --snapshot + +# Fix workspace protocol in solid package that changesets replaces +echo "Fixing workspace protocol reference..." +sed -i 's/"@openauthjs\/openauth": "[^"]*"/"@openauthjs\/openauth": "workspace:*"/g' packages/solid/package.json + +# Publish the snapshot versions +echo "Publishing snapshot versions..." +bun changeset publish --tag snapshot + +# Reset versions in package.json files after snapshot publish +echo "Resetting package versions..." +git checkout -- packages/openauth/package.json packages/solid/package.json + +# Remove the temporary changeset file +rm -f .changeset/snapshot-${SNAPSHOT_ID}.md + +echo "Snapshot release completed: ${SNAPSHOT_ID}" From 8ce3d32bfd7c4ea60f85af14fbc906acf3c27c58 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:35:14 -0400 Subject: [PATCH 04/23] sync --- packages/openauth/src/issuer.ts | 4 +- packages/solid/src/index.tsx | 81 +++++++++++++++------------------ scripts/snapshot | 18 ++------ 3 files changed, 41 insertions(+), 62 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 6c868ca2..c1863b30 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -208,7 +208,7 @@ export const aws = awsHandle export interface IssuerInput< Providers extends Record>, - Subjects extends SubjectSchema, + Subjects extends SubjectSchema = SubjectSchema, Result = { [key in keyof Providers]: Prettify< { @@ -236,7 +236,7 @@ export interface IssuerInput< * }) * ``` */ - subjects: Subjects + subjects?: Subjects /** * The storage adapter that you want to use. * diff --git a/packages/solid/src/index.tsx b/packages/solid/src/index.tsx index 39f8f518..2609542e 100644 --- a/packages/solid/src/index.tsx +++ b/packages/solid/src/index.tsx @@ -4,12 +4,10 @@ import { batch, createContext, createEffect, - createMemo, createSignal, onMount, ParentProps, Show, - untrack, useContext, } from "solid-js" import { createStore, produce } from "solid-js/store" @@ -17,25 +15,23 @@ import { createStore, produce } from "solid-js/store" interface Storage { subjects: Record< string, - { - id: string - refresh: string - } + SubjectInfo > current?: string } interface Context { - subjects: Record - current?: SubjectInfo + all: Record + subject?: SubjectInfo switch(id: string): void logout(id: string): void - authorize(): void + access(): Promise + authorize(redirectPath?: string): void } interface SubjectInfo { id: string - access(): Promise + refresh: string } interface AuthContextOpts { @@ -108,7 +104,7 @@ export function OpenAuthProvider(props: ParentProps) { access: existing, }) if (access.err) { - ctx().logout(id) + ctx.logout(id) throw access.err } if (access.tokens) { @@ -118,40 +114,35 @@ export function OpenAuthProvider(props: ParentProps) { return access.tokens?.access || existing! } - const ctx = createMemo(() => { - console.log("recomputing subject context") - const subjects: Record = {} - for (const [key, value] of Object.entries(storage.subjects)) { - subjects[key] = { - get id() { - return value.id - }, - async access() { - return untrack(() => access(key)) - }, - } - } - return { - subjects, - get current() { - return subjects[storage.current!] - }, - switch(id: string) { - if (!storage.subjects[id]) return - setStorage("current", id) - }, - authorize, - logout(id: string) { - if (!storage.subjects[id]) return - setStorage( - produce((s) => { - delete s.subjects[id] - if (s.current === id) s.current = Object.keys(s.subjects)[0] - }), - ) - }, + + const ctx: Context = { + get all() { + return storage.subjects + }, + get subject() { + if (!storage.current) return + return storage.subjects[storage.current!] + }, + switch(id: string) { + if (!storage.subjects[id]) return + setStorage("current", id) + }, + authorize, + logout(id: string) { + if (!storage.subjects[id]) return + setStorage( + produce((s) => { + delete s.subjects[id] + if (s.current === id) s.current = Object.keys(s.subjects)[0] + }), + ) + }, + async access(id?: string) { + id = id || storage.current + if (!id) return + return access(id || storage.current!) } - }) + } createEffect(() => { if (!init()) return @@ -166,7 +157,7 @@ export function OpenAuthProvider(props: ParentProps) { return ( - {props.children} + {props.children} ) } diff --git a/scripts/snapshot b/scripts/snapshot index db53f987..87573057 100755 --- a/scripts/snapshot +++ b/scripts/snapshot @@ -6,18 +6,6 @@ SNAPSHOT_ID=$(date +%Y%m%d%H%M%S) echo "Creating snapshot release: ${SNAPSHOT_ID}" -# Create a temporary changeset file to force a version bump -echo "Creating temporary changeset..." -mkdir -p .changeset -cat > .changeset/snapshot-${SNAPSHOT_ID}.md << EOF ---- -"@openauthjs/openauth": patch -"@openauthjs/solid": patch ---- - -Snapshot release ${SNAPSHOT_ID} -EOF - # First build the packages echo "Building packages..." bun run --filter="@openauthjs/openauth" build @@ -33,13 +21,13 @@ sed -i 's/"@openauthjs\/openauth": "[^"]*"/"@openauthjs\/openauth": "workspace:* # Publish the snapshot versions echo "Publishing snapshot versions..." -bun changeset publish --tag snapshot +(cd packages/openauth && bun publish --tag snapshot) +(cd packages/solid && bun publish --tag snapshot) + # Reset versions in package.json files after snapshot publish echo "Resetting package versions..." git checkout -- packages/openauth/package.json packages/solid/package.json -# Remove the temporary changeset file -rm -f .changeset/snapshot-${SNAPSHOT_ID}.md echo "Snapshot release completed: ${SNAPSHOT_ID}" From 0a2404b3ef504b92eb4d1cb76c25811ded81a584 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:39:29 -0400 Subject: [PATCH 05/23] openauth 1.0 - couple of breaking changes to look for: --- .changeset/pretty-bobcats-cheat.md | 6 ++++++ .changeset/small-olives-confess.md | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/pretty-bobcats-cheat.md create mode 100644 .changeset/small-olives-confess.md diff --git a/.changeset/pretty-bobcats-cheat.md b/.changeset/pretty-bobcats-cheat.md new file mode 100644 index 00000000..0d8e5d09 --- /dev/null +++ b/.changeset/pretty-bobcats-cheat.md @@ -0,0 +1,6 @@ +--- +"@openauthjs/openauth": major +"@openauthjs/solid": major +--- + +openauth 1.0 - couple of breaking changes to look for: diff --git a/.changeset/small-olives-confess.md b/.changeset/small-olives-confess.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/small-olives-confess.md @@ -0,0 +1,2 @@ +--- +--- From 7a389df4866d5671889400ff2f96c83b8da2e3ac Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:39:52 -0400 Subject: [PATCH 06/23] sync --- .changeset/small-olives-confess.md | 2 -- examples/client/cloudflare-api/package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 .changeset/small-olives-confess.md diff --git a/.changeset/small-olives-confess.md b/.changeset/small-olives-confess.md deleted file mode 100644 index a845151c..00000000 --- a/.changeset/small-olives-confess.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/examples/client/cloudflare-api/package.json b/examples/client/cloudflare-api/package.json index 6ed4c2c6..eee1b4ae 100644 --- a/examples/client/cloudflare-api/package.json +++ b/examples/client/cloudflare-api/package.json @@ -1,5 +1,5 @@ { - "name": "@openauthjs/cloudflare-api", + "name": "@openauthjs/example-cloudflare-api", "version": "0.0.0", "private": true } From 52c3a20148ac10dde9d396c42626d4e68702a298 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:44:50 -0400 Subject: [PATCH 07/23] sync --- .changeset/pretty-bobcats-cheat.md | 6 ------ packages/openauth/src/issuer.ts | 8 ++++++-- packages/openauth/test/issuer.test.ts | 5 +++-- packages/solid/src/index.tsx | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) delete mode 100644 .changeset/pretty-bobcats-cheat.md diff --git a/.changeset/pretty-bobcats-cheat.md b/.changeset/pretty-bobcats-cheat.md deleted file mode 100644 index 0d8e5d09..00000000 --- a/.changeset/pretty-bobcats-cheat.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@openauthjs/openauth": major -"@openauthjs/solid": major ---- - -openauth 1.0 - couple of breaking changes to look for: diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index c1863b30..ed764068 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -515,15 +515,19 @@ export function issuer< async success(ctx: Context, properties: any, successOpts) { return await input.success( { - async subject(type, id, properties, subjectOpts) { + async subject(type, id, unvalidated, subjectOpts) { const authorization = await getAuthorization(ctx) + const properties = unvalidated + if (input.subjects[type]) { + const validated = await input.success[type](properties) + } await successOpts?.invalidate?.(id) if (authorization.response_type === "token") { const location = new URL(authorization.redirect_uri) const tokens = await generateTokens(ctx, { subject: id, type: type as string, - properties, + properties: validated, clientID: authorization.client_id, ttl: { access: subjectOpts?.ttl?.access ?? ttlAccess, diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index cfbc9f61..174aeb21 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -50,9 +50,10 @@ const issuerConfig = { }, success: async (ctx, value) => { if (value.provider === "dummy") { - return ctx.subject("user", "123", {}) + return ctx.subject("1", { + userID: "1", + }) } - throw new Error("Invalid provider: " + value.provider) }, } const auth = issuer(issuerConfig) diff --git a/packages/solid/src/index.tsx b/packages/solid/src/index.tsx index 2609542e..2c3e4cc7 100644 --- a/packages/solid/src/index.tsx +++ b/packages/solid/src/index.tsx @@ -25,7 +25,7 @@ interface Context { subject?: SubjectInfo switch(id: string): void logout(id: string): void - access(): Promise + access(id?: string): Promise authorize(redirectPath?: string): void } From 10e1840a0fd698ffd24860179d583b1571ccf1f9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:48:25 -0400 Subject: [PATCH 08/23] 1.0 --- .changeset/lemon-vans-thank.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/lemon-vans-thank.md diff --git a/.changeset/lemon-vans-thank.md b/.changeset/lemon-vans-thank.md new file mode 100644 index 00000000..7a240047 --- /dev/null +++ b/.changeset/lemon-vans-thank.md @@ -0,0 +1,6 @@ +--- +"@openauthjs/openauth": patch +"@openauthjs/solid": patch +--- + +1.0 From 1930f33533eddd5db7ab2e133f4dae196669f3b1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:48:28 -0400 Subject: [PATCH 09/23] sync --- packages/openauth/src/issuer.ts | 16 ++++++++++++---- scripts/snapshot | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index ed764068..5d7959d7 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -517,9 +517,17 @@ export function issuer< { async subject(type, id, unvalidated, subjectOpts) { const authorization = await getAuthorization(ctx) - const properties = unvalidated - if (input.subjects[type]) { - const validated = await input.success[type](properties) + let properties: any = unvalidated + const validator = input.subjects?.[type] + if (validator) { + const validated = + await validator["~standard"].validate(properties) + if (validated.issues) { + throw new Error( + validated.issues.map((i) => i.message).join("\n"), + ) + } + properties = validated.value } await successOpts?.invalidate?.(id) if (authorization.response_type === "token") { @@ -527,7 +535,7 @@ export function issuer< const tokens = await generateTokens(ctx, { subject: id, type: type as string, - properties: validated, + properties, clientID: authorization.client_id, ttl: { access: subjectOpts?.ttl?.access ?? ttlAccess, diff --git a/scripts/snapshot b/scripts/snapshot index 87573057..3c1477a1 100755 --- a/scripts/snapshot +++ b/scripts/snapshot @@ -8,8 +8,8 @@ echo "Creating snapshot release: ${SNAPSHOT_ID}" # First build the packages echo "Building packages..." -bun run --filter="@openauthjs/openauth" build -bun run --filter="@openauthjs/solid" build +(cd packages/openauth && bun run build) +(cd packages/solid && bun run build) # Use changesets to create a snapshot release echo "Creating snapshot release with ID: ${SNAPSHOT_ID}" From eed98eb7cbecf2587d396278e542c551d688dc66 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 15:52:09 -0400 Subject: [PATCH 10/23] sync --- scripts/snapshot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/snapshot b/scripts/snapshot index 3c1477a1..f21d436d 100755 --- a/scripts/snapshot +++ b/scripts/snapshot @@ -27,7 +27,7 @@ echo "Publishing snapshot versions..." # Reset versions in package.json files after snapshot publish echo "Resetting package versions..." -git checkout -- packages/openauth/package.json packages/solid/package.json +git checkout . echo "Snapshot release completed: ${SNAPSHOT_ID}" From 573cdbdda1272793967de4bc94a693920c43f670 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 17:48:11 -0400 Subject: [PATCH 11/23] add react context --- packages/react/CHANGELOG.md | 7 ++ packages/react/package.json | 30 +++++ packages/react/src/index.tsx | 221 +++++++++++++++++++++++++++++++++++ packages/react/tsconfig.json | 21 ++++ 4 files changed, 279 insertions(+) create mode 100644 packages/react/CHANGELOG.md create mode 100644 packages/react/package.json create mode 100644 packages/react/src/index.tsx create mode 100644 packages/react/tsconfig.json diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md new file mode 100644 index 00000000..52a57997 --- /dev/null +++ b/packages/react/CHANGELOG.md @@ -0,0 +1,7 @@ +# @openauthjs/react + +## 0.1.0 + +### Minor Changes + +- Initial release \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 00000000..db7f9831 --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,30 @@ +{ + "name": "@openauthjs/react", + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "tsc" + }, + "sideEffects": false, + "devDependencies": { + "@tsconfig/node22": "22.0.0", + "@types/react": "^18.2.0", + "typescript": "5.6.3" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "peerDependencies": { + "react": "^18.2.0" + }, + "dependencies": { + "@openauthjs/openauth": "workspace:*" + }, + "files": [ + "src", + "dist" + ] +} \ No newline at end of file diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx new file mode 100644 index 00000000..63a08458 --- /dev/null +++ b/packages/react/src/index.tsx @@ -0,0 +1,221 @@ +import { createClient } from "@openauthjs/openauth/client" +import { + createContext, + useContext, + useEffect, + useState, + ReactNode, + useCallback, + Dispatch, + SetStateAction, + useMemo, +} from "react" + +interface Storage { + subjects: Record< + string, + SubjectInfo + > + current?: string +} + +interface Context { + all: Record + subject?: SubjectInfo + switch(id: string): void + logout(id: string): void + access(id?: string): Promise + authorize(redirectPath?: string): void +} + +interface SubjectInfo { + id: string + refresh: string +} + +interface AuthContextOpts { + issuer: string + clientID: string + children: ReactNode +} + +const AuthContext = createContext(undefined) + +const STORAGE_PREFIX = "openauth" + +function usePersistedState(key: string, initialState: T): [T, Dispatch>] { + const [state, setState] = useState(() => { + try { + const item = localStorage.getItem(key) + return item ? JSON.parse(item) : initialState + } catch (error) { + console.error('Error reading from localStorage:', error) + return initialState + } + }) + + useEffect(() => { + try { + localStorage.setItem(key, JSON.stringify(state)) + } catch (error) { + console.error('Error writing to localStorage:', error) + } + }, [key, state]) + + return [state, setState] +} + +export function OpenAuthProvider(props: AuthContextOpts) { + const client = useMemo(() => { + return createClient({ + issuer: props.issuer, + clientID: props.clientID, + }) + }, [props.issuer, props.clientID]) + + const storageKey = `${props.issuer}.auth` + const [storage, setStorage] = usePersistedState(storageKey, { subjects: {} }) + + const [initialized, setInitialized] = useState(false) + const accessCache = useMemo(() => new Map(), []) + + useEffect(() => { + const handleCode = async () => { + const hash = new URLSearchParams(window.location.search.substring(1)) + const code = hash.get("code") + const state = hash.get("state") + if (code && state) { + const oldState = sessionStorage.getItem(`${STORAGE_PREFIX}.state`) + const verifier = sessionStorage.getItem(`${STORAGE_PREFIX}.verifier`) + const redirect = sessionStorage.getItem(`${STORAGE_PREFIX}.redirect`) + if (redirect && verifier && oldState === state) { + const result = await client.exchange(code, redirect, verifier) + if (!result.err) { + const id = result.tokens.refresh.split(":").slice(0, -1).join(":") + setStorage(prevStorage => ({ + ...prevStorage, + subjects: { + ...prevStorage.subjects, + [id]: { + id: id, + refresh: result.tokens.refresh, + } + }, + current: id + })) + } + } + } + setInitialized(true) + } + + handleCode() + }, [client]) + + const authorize = useCallback(async (redirectPath?: string) => { + const redirect = new URL( + window.location.origin + (redirectPath ?? "/"), + ).toString() + const authorize = await client.authorize(redirect, "code", { + pkce: true, + }) + sessionStorage.setItem(`${STORAGE_PREFIX}.state`, authorize.challenge.state) + sessionStorage.setItem(`${STORAGE_PREFIX}.redirect`, redirect) + if (authorize.challenge.verifier) + sessionStorage.setItem(`${STORAGE_PREFIX}.verifier`, authorize.challenge.verifier) + window.location.href = authorize.url + }, [client]) + + const getAccess = useCallback(async (id: string) => { + const subject = storage.subjects[id] + const existing = accessCache.get(id) + const access = await client.refresh(subject.refresh, { + access: existing, + }) + if (access.err) { + ctx.logout(id) + throw access.err + } + if (access.tokens) { + const tokens = access.tokens + setStorage(prev => ({ + ...prev, + subjects: { + ...prev.subjects, + [id]: { + ...prev.subjects[id], + refresh: tokens.refresh + } + } + })) + accessCache.set(id, tokens.access) + return tokens.access + } + return existing! + }, [client, storage.subjects, accessCache]) + + const ctx: Context = { + get all() { + return storage.subjects + }, + get subject() { + if (!storage.current) return undefined + return storage.subjects[storage.current] + }, + switch(id: string) { + if (!storage.subjects[id]) return + setStorage(prev => ({ + ...prev, + current: id + })) + }, + authorize, + logout(id: string) { + if (!storage.subjects[id]) return + setStorage(prev => { + const newSubjects = { ...prev.subjects } + delete newSubjects[id] + + return { + ...prev, + subjects: newSubjects, + current: prev.current === id ? Object.keys(newSubjects)[0] : prev.current + } + }) + }, + async access(id?: string) { + const targetId = id || storage.current + if (!targetId) return undefined + return getAccess(targetId) + } + } + + useEffect(() => { + if (!initialized) return + if (storage.current) return + const subjects = Object.keys(storage.subjects) + if (subjects.length > 0) { + setStorage(prev => ({ + ...prev, + current: subjects[0] + })) + return + } + }, [initialized, storage.current, storage.subjects]) + + if (!initialized) { + return null + } + + return ( + + {props.children} + + ) +} + +export function useOpenAuth() { + const context = useContext(AuthContext) + if (!context) throw new Error("useOpenAuth must be used within an OpenAuthProvider") + return context +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 00000000..0b2696d0 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true + }, + "include": [ + "src" + ] +} \ No newline at end of file From a10e583c2dcff0bc155152a0c806ff42a1e5f9f5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 10 Mar 2025 17:51:07 -0400 Subject: [PATCH 12/23] react --- .changeset/lemon-vans-thank.md | 1 + bun.lockb | Bin 257696 -> 258256 bytes package.json | 3 +-- packages/react/package.json | 7 ++++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.changeset/lemon-vans-thank.md b/.changeset/lemon-vans-thank.md index 7a240047..c68293de 100644 --- a/.changeset/lemon-vans-thank.md +++ b/.changeset/lemon-vans-thank.md @@ -1,6 +1,7 @@ --- "@openauthjs/openauth": patch "@openauthjs/solid": patch +"@openauthjs/react": patch --- 1.0 diff --git a/bun.lockb b/bun.lockb index 3a85a743a710d0259a2bf26d2291726a45896f7d..615d2c866975400122e9ef9605bdf2a14fb2f6f1 100755 GIT binary patch delta 46158 zcmeFadz?+>|M$PwW;2_d$zjMjXBmu{G0Zm3XHjxK3Qmj{`*Gio-|wIH!)spG`*ppq^I=_US$k&r$s+Hb zDYBq`tvgEQ8CGL^)!2w9uLj!uzOdGUNhRY>9hk7COIrNWy`_7tXmKHrPoMiTdsV#c z$GiQG;r=M#^JRRBAf7NOdGh3xE#o%B*)g_Xsq)bZnnr;JH1hnI?&Ja&Rpk831RQF~yee#lO;N~gCW zsbhIu#4C7cO1+eE!^iu6$?Nl#!u$C`K3{S85x6kio*yOPYuGAvcxt`msgoxq4Nn<2 ztlpFfz5x_1y@$sW>WvvcG--13G4d;h{-m3aKjSI;id7^1do)#J#F+6zhNSs?cT$#; zkA+p~YLun;QLyYGlahuer}}(nJiSIqw_0hi;>RXW8j;-4=POIR@<~apr>uR$Xp8a* zq8EkFV-qZ%afN~;B;M%u=0SU%zoNC%KI|`D>%3Rz@ZgP{GB#;ky_8|$wqy}sk+f^< zVtGoZ|5ny*;T%*l%}9ea4!1r>vX!4sbU02nEwtQX%Lu7rJ> ziB=i*VN+B_Hug>M_$iZzC6Ak&?kh}P)D#P*morWiTLs?d&B4j!qCN@_-LKf1K%4(Y z&mx`j8#jJfvX9RaFP}-U8rBllkhZ9z3bpcOq>dkxGOXUH)QGCCLu&G*@pmST@eNH) zot!oy*|!46(j>4*$arOp8lUQ9Jh+-$u}*eM-ik>_tGmVSgEiyE-|X^GSk-+UT|*o8 zD@$tiM^&#n+iJLelaiB$PNv#NEjNC3O}CV@7W1nd?ZY>Gp^KjJN+YAb7QN=OJU_R6Q;b{8AH5?wyuy- z2WDnu+yra%CX7#=Ovm}&wO0g6r%!C+PO5>hCQ(ONT^Q@>)jYc}d?Wg=(XQWlSd;Mx zEdL#_;@5iiW3Yzg9#0<)EB{_zd>feb=^0U;5D6=T2w09kG*vcDmYh7HUP`KO z1reHki(t*>JS|;&1gwO8U`?BciPv-)0W)JV`ojg`@Xiq4uzJ|V@|RAx+PJZu@zR`s z6kE9sACokBa`G_W=;X9JRlS6^u3vNfREy-)DU(yir23LmCr(M8l;(?T=cY?eWz$Gz zobgjPZ9rFTlZK9;q;}qfuG*bw@923l3WV(E^WPK~?C92hWKwE9${z812e-V#=$aLk zI=T5LjdH5vTZ64>yU?~GN~d2=a69{RxFqTxSd%UbR_cpgTn@)ShpxeiW^mM6gRRyl zbaQJxDz#o((%3P+q)8(tBuz?9Ziud0hc8H7Zgunf7^ZdU&NiMzcRWN!a=Z&xMUHlN zXURZpt*RFAO>nq-b~JZa)w8hT({6WH+s@nERg{EX27NKM^8NGRpFywWOZR20BA{jc z2pK8C0&MlWE&{69-LzDzY$&XPy1}a8&^~TUy2I*`-hJJ5Q36(hCHlF``ZBiCw}ds# z>v~)fF4P~P78#Id#^v7bz_l9a?%LnMYVk=}1ssNzaoxdgM#-r|lg1i9Jxa0MS$NOMabUmzoUIv%c0H=;jnlyoo z`iyr6Bo3~GLpfMI^Bb${Cis-c*>DByRj~H?xgL*)Rk2>MCQ1uf1=faDU`da!jdgqA zOL0089}>{O?DY60kC(z4f)i8RA(_lZ$0+$8Mc2MFbo`j{ld$)*a5eBZ-sP=ySn>DM zLg~{yo&al24x8%6kHAh3B7`ptL#Ct-8K36!O#@1J7hSC}j)$wmTy!apE}!NOMPs-K z`ps}*xGY=8{^buqw8fIiPaWlE&B%-B7AS9`aWYzenEUC*UeD6W+N( zuAkx7C-u(cF_W2JYwvOMSq7_~^I=VmS@xM5Di)i9rP_>!)!cq|u|hR!8S>C*b-mY} zPHkY-;m(xF$;0X3(PNUQs52k5lL}QV_UL_X+TyT=;F_ITsC4@Gv)uMfCNGUBv%{Gw zNvV@3FK=eSdENK$HYy(W!)Wj))1H6v<2=+2Ot54iO$jxGI1SpBpdR(pP%>*inc zVb?y7U4jZ_oIsFcZ9A%P#l$^WT1vYgaT~nJ1&hVx-SE$j&ZYI{n5+-Pk3zJB0J$8}R1l!&!eRkGV}5 z086(Px_J+SwF*1J<={q-yEWfKY3k5VuvPbiuHxqz-nw~SaWvq61RZ+JRSop-Sbbn1xDki)!EIN=Y3%D%H0HTVpUER*Oe0^D4C5J+8HZHPMcuSAw^!a2rq( zJqo)Z9E4L=x;;|{)@17D+4bSd*cMz`+rzo1-Ihl^>tB)fmhAC(4Xpj? zW74UDJ65{|{!E3`!r4?@J@w)mx6AWA?^fU*SUoubR)ZfWK3y4Cd%;~Yd0-X%pS5lQ z*>H926|h>G2G@k=kWd-i|FY{h30A>9Uvc$2u+>8$Sov3iE5dKCbBC@#$n~E-m;P5x z_rB_OdFl0T4O2&^j85_Se0esw8Lmaw*nSCX(!C3pgIjKNC+#YafAM%y@`&WAq&L{A z=$rU!NWWpsRng-f?|Gn|TVVKJu_F<3+yHCMOdHQtkI&Pd-9F2W=N|E{)G@&qnr-!oLSLHn3m@yl$Nli}K74u)pVGsp_wemR_%r03iZgl_}F z$NlgzKYU*jzV8U%M}%(^!uK!X`<3v0iFbdI5xz~Blsr;*ioUM<++O7lh?9x^Q;Fi~ z)AqZ4@aHENebF@l83)|?Fa=hJhL6tUiI+XxOW)n&p(B&VjAlBt_v{3?0Dj^8emmq& zcrU+T#;}y(!!>`_FDlt0Z_NT*7QcDC-`Sq!N>wj5v+8XJ7kqK-RM`c)9&2{r)~OxN zpZj`xnfPB)9=Lt@`U_<~39LVM(*-MH!P-z?)2(~U^?NIQlHZ(}Xtl5>%nRCAYKE-V zc3iEHHQP?(=V&{NpR4RE{Jdhv)ec!Pc3SOF@VW6|6bbnJPqnVV{(oVZ26f|h6i{G~D$5==0E4PIFg>JB$Hb@9|V>DH< ze73bWHo`Lkwncu2?JEsJ{vw6!rcnvOHmrmWPPSH5OmG`kE61W>e}TgGFHs3rKRd2r z$a>yRYZwZC&Z<*ABUH0MdDe6jyLs*S;7CFV=x+U=^(?240-s~WI$q_u-nm{@lh_E1 z)=pX3(_`~uG}S-^&k$8x3QGG~};b+Md=l!MfefY8nbY$W=o9OckGrjlfVX^4f_TVf@$C$I)$ z(Jmb0ZgLBzhV^0s4`B7SXVi}O-&E1wl9&*@ol9h30%oz4C=085L=wQSF#YolVUefE{MA?t!2*Dl1c zr15jHoyE@&>?`d;{_@rBrtK4~zIIyskoBOQ)jkybg2PY?Co`?P8aIbW+1eGG-!pP1 zK|~gcW@L^MyojQ@IO7$pQ6pzsG>EZA*l8U@fl_L*dAXDSdXu@dJD+fq_mWIAr-UsJ%su=p^EiS8WYsgA+><+g$ zHhH~D$n`10zBj^&y^~O~6FNa?kP~Ve6%H*Xtnk-sZ2!_d!5U-7^#}!KF($3;<~`yAZxL$igbJ|> zw|7E43AwS0bF?#rIyDk0Zz6CpS4HA1dmr?{|Y6YA=uJwwRLg#q<)A>^jr zO32OcT8`F^R=Fu`LT-xVggQF;Rc#hdJDQN|w<$-vM##y}->SJit#^XIV1m7+cY?LW zzS27stlEOKbdO(M795SmKxm>ihzTsgYNov+cvPXBPArjVH|?9??~-Uw>zfdql}L@8 zIPEt3u&B27u=+9n;w|l_{SpEb8GvS*-Hq%93xg3@t(^ojSH;R8bM5)fV}em!lv(Nw zpv7Lb5-Y);(U*&*Lg@LNO}HYdwVd3eNlz>{P6rhmi_Z7r4tSo<0VCpuHm;{xccb6k zuREfs^%2hH;z=yEGOttmF{~!`j3)8^!fowe1||eM&`K3V(^kYrV5rs3h7vqPP&MMT z6&>Tx)6V{7P(rX(J2yKnoE(YLv2^J!U}q=AMqud5L92->NEOs)?xk*|9hVdeEZ#L?|5iKtk~JDkuz_Lh`{z$0iK>>0!3178#BXwQg?_c!TbPaBmGOz+|I z@y~6z^|gIvR4CB&HlObfdq(g0Kv^#I?VZq4LIa)9uY_)QLfzjQU(CC=p z5iC_G=v?@V+~LkTr&)oXSZ>~H3Ek@EC=Vx}#9nSn@=1uXrrUAjLe_daZCof=ytjKS z3OMJ+SL`dqJZQ&_4_SxoG=7e^v-tVCePw*eAKlMxIw2vjq@T_h%_qcLKiF9lLjJ`5 z_AkgQnFH()=!v^xO#nB24Qj48fLT#18T4!fX3I$sZc27Xg zjYV)7mWIt4TMqhZsoY>B*;`U|$8se#6nHg>M%gnmo8=|Ik(_a_GQ^#3+yTVI1n$7% zd_(E)5$dZl{dI@h)23+C&zcenW({?_#Tos;4_Gao>LlA~cZP!h8RiZl^D!|d7@N!p zJJr$A_g>HP*;bF3;2A7!0Pgvs!EkpKIvY)32v!RfZ9Q$rO$`M;!lu!6oResi6!)1_5$8_fz$h+}q;#z+qjOtnSXzQ^+}l`8SqKSIiXUMoehE% zLvupw2yvN2yP6Z~FeR)lB*Z0lMttx~LfREsLhM2{?X-JC!M=C8nzPjh7h!48*mRo4 z1V6-T;np=JCRpt*cfa-7+3jP3saVR(y?0q>UzrsOeuKtU98ITE^1m+>cycO!?zK{P z6dz$m2bx}kX74Gn;S~MKrKoT6C1txmD;LO zl_ZC~N_utVaN^x{-1n#UZNuZn|AX|i|Cm0XkDB>za=V>{72s~iSN2cm@jrFgJ2TwD z&TDg74BmW?J5F4~$HxR;!}2B$4=jGP;~okHnsBq!-nsR^hfoVD>)e-XpM8kiSTuDF z)11k2bB22kbMC7HcVKaXyo)kW`9D47K33l@=c0;Zn=~&uN z>2U7WH|1E`&c4IC!^ul=lkfHU*d*}el)9!zA- z4+U<&kK*mCwc-O$65>i2=oY-c&rmXn0kVy;5m zE0?(6O~O(b920DZcCG1k3y+@m3@fSvYeD0q+HLRi`=bm5hE`6`LPJ8D78d zI)S1ci*pHggmve-JC^2RY{Y}^0meN;zJSG2Ay*x#tcTcSu;@ejq7PO}$5Px9EROH) z*abhpx;-bZ@xyK$y~buR7E5P5ccVCmrCgXT)Vtk0pKpj`DbF=nY7-S>Bu-$d^LQ3R zrHee0GlE=@+F|t|F2c#>eXQ14?jfi2eAjZ8V6Yz+XBp*5y0uuEH=H7P8dzt6bL!yQ znNCP8brxjsC{_#>^OE`(&UD-6)_(|=YQ=S$K6%`;+_?Q%ot<=QTg^w^TnaiH_Gqle z+6n^82yszlMMw77$YBQ|j#42#hWmg`C6r zeNVV^$1U_dEKXT0tins&$3SjHU&2z=T+8=lZmS2Dn|UIQcpXc#!SyV>)LjV9(Iqed ztFxLESVPEd+!aC^A=kg&Q{j20**XzRYtucWy)`p3ldv30ZsXv^AmNZ^Wqo-AUPbjk_9MYde-ElG9tk^3UhAq<%~w z6N_gCS~G;S*4+bU)wMbuYRo#m;PXwkXAEl=LBOrE2C3$Y;i_o4K8Q8RZS>mM2n_YK z`v7#t8RYocKa|^GrZh~&quIYVg;RzruKSwA-Ff_Q?VKm$Hbi!6D+pDP3Kx& zu(~kR~7b97GW-3T>AcP=s!GqKd( z>(`atD4L3H$=9&l!PVsNw#l{J`^jZkDuHJJ+>hto9G=4LEP(_pPE&Q_tql9h`cU8y z_AU0+_3^>q2(==Wv&4cuv)mqGX)t;ijoql)F%hS*x?<&2El_t0MK~ikoKOSDQ|FV% zvFc+{Eh@4Ls|gkjzdP1Wdo2`fw$-gD@q=Ol_hNBd!~yOdLJ4>}Cy`*GZEjWE|550M zl}KDEXVovjYT@L`&ie-Y=5CR{d7cr@?%qDzEsO&Jop&#mW+-FYA~pg;Eg|Y|$M7o3Oo-j- zMme{();K$Bdnm99t*gC#dwk#rLTtN)LT`pc59Nf8=7g&5(o+WcjU~hZlF-JS(AAt! zo895qc{!mYIiU)B!m-IYp%)3Yb?cH7ihC=prRRip6Jk@K+~D4Ds7p>LGbeN;9151* z=N^U&+19JE5g0A#p`dNW$L7W0EEykf9kbJRg@QNicYEFK=UG@y9arrh+p#o0Jg;I8 zyW&~-94q>Oo2mOO>oF{?KqjvqmSKgkxH9uZtm;8`^6==eZES>RI2(_(%)YXRn{zaG z#gsf0UiQkSEtV?dY(0StEcY6`jgZ>o-0}rVyzT77%bG)pJ-{d>n4;-D1B{ zGjCi;pYuP?{QM)A$Hvq7i2Z($<29CxHvnX z+$?|klH3NI;ssVhiH^@7S!K5Zif`@3{}tyWt|Q0`x&ryL_d1{4tlIPd%9s7u`M~K; zf}WU4c!#SytPJ~lc5YS%{XJc*bOStFtO^bFY;gdL1WGpw=p$CT(LQ_B=>kpFJ;n`yK?n%!U%m0+er#(G4tD>KK@n^mGvub}~@B>hWmw-OvA|S7F^|%yV z*0cX#ar*zOjB@9ro+|HEG&h$)Z|vzFr~g^_|Jd&KUp4w4RX}4IM@y>0{b8+&A!?aj z=ySc4$A3tA?fffN)Np=iz(;z1{}YyLikEIQEPIT{V?7>c>3YM5fa5S8RzVYCeZ&e* z^6WpbJ@4JpK@FetV9!4EdFfh8o93mN=A{uUINh_we!I|F_oARZMG+ZZtXSJe_;Fiq zmP`0C+CQ;s@qm|KZk9C1)8XQJk2wX#Wi0i_udqJ<#B%w~ z^UKZ3M@6bsKR?u8L0I|TAkkwrqZqcN5`Mii=>~E*k3Hl4(&RyU@2-lM#DvGN}cOB&0M0`Ofj{)ep5 zp6>a>Zg)vg#^Eoh{uL{J27XHG)Y@Y$t2v08HuGU!A(zU@uJ@(8$1KBA>g7`ZH!Qyu zp8wMxuk`pCFTGf^;ssdEdBxMkO25vtVZQc=umeH;xzmgIzhPyuiyz`W{Lq=(B6Ah& zS0R63E-xhzN)e$1rD6G$^X&4l5>}Atv4WL6y)vxbqz0_?wLHDHXV>-YdayoX^-Q#9 zM{6}IVT>n;wT_#4wpbNt?s0;ri=~G=Tdb~b1#6%?dip=HT++MnQyF#jxErjDZiD&f zyTju?uv$6@mj7T_6(0iYBUZZMo<0(mUkc1W-xz)rgzu8!G3jsgrFlYb4%#msb2qB{ zz1ZBWV&{1JU$Kgv>&540=?{9kSp7U7R<&(d*P~^y{FdioYAWIxPgn)(BbLLna4~oj zEWfRuzRlz9uqwL4<7~Jn_F-5bv4S7)L+Ous{ISQMyyP=Gy5_Kn~Lp#CLm+|0k?;GrV+S+4p$1SPh)%>G#5l zx}P6@Ukzrt7co~VpTA`L?=er$&85-Tdir0m zb4vdw2a^9|2LG4wZu$RM0agBGeyC?&^;(jfHTSo8@!Mhb#Ot1)Sn0Ao`@dLgMG1C# z4*$gRd(+EskLUkq#zkL_aS#rNxjcbCKb|V|@AD~o#XT#ztO?#g1PtT=@cH*@M=YB3FUAEHy`+Um1M&*7krT&qv{`vR$lx9yweyBGp!8+Le`+SN{ z;6USb%fHX3{(U~>oOJ(vKK1YODffEv@AIjDpHKb!e9E~>{^MgLdHlr#Fel;P>;Hd0 zpKAaA>iN|A#TOgjVk!)=I+-$+tzqVc_pC&7sEk$6?5%7SGKVTzove9gVr8qBnOX(m zNM(firg9alm$ks8i89S$(W9nTRme6oMUR=|qJ<`^8uYlCD>CMcXpxDl4lOpBq9@FG z(Grt*GxVfcELv(Vik>nZYCy}(3ej?NMYO_nuL(VE)`(Ubs}_}us7>XPYf-sXW}Sr1 z5=zxZc+Mo%#&NaD60I@C>Ojw%6wz9fEqcL}tqZ+qCW>A%`$R9B%JraEOqyt&ISiR& z^~pB6KH07}GwY*oFvmq3P1G&WYi6!!lQ|>WY~mU~Ste7o#he#yHHlHsHnUi?-CPvC zZaOrCc9<2SH_R1Lw&~u8B6~Nc$c>FC@=aqkR$HUh*2W0C%{mF2C6tOrc*`V3BaCc< zuv@}DQ>+O>nHYpAO%M*4YzccMRF6S8WG2QSOpQf2BHxHiR}>Dw?|mj4&kb~DB-e%p6wB?nHB93R(C)Mbg(-4Ez9)iVBKMQcSPBUV&ySb zM_eL0AtZN12$*#eHcKeg2_e5p>Vz<|Gs12OK~t455a!&9a8^Po6Ll*>Y=S(DivVNnl+OA;bYVh@D&w;`&)X0xniaPptiBx~a63X} z)BSdY-aQdEN~mhAo(K_lASCxhsBYFt*es#c9SAi{(j5pRdm-$WP|FnSg;1t9!jxVJ zbxgK|y%MVTMyO{d_C}c62jPf>TTJCX2sQg6%X zR%fe&UVQImbu`&SpiZXjP@?w^#ec?7{JWTa2v%29c^K5qq={}dheh2@tz@W&nJK!> z92ebgqJ~2~&0NtP=8UM9i5mg+HkqP6=Det{NgN6FGmAz2%|+1w(;)>KXjX^@nJc2f zru!%;$*d6#G1h2ksOcjbX4Z+4O>hh}+$4!cm@LSQ97{7QjHMYVCS@!_nQ;h*B#bs? z$El$bW{g7^YxYT)Iv%0kc!cpLZ9GEF2?!@8Of1MI$ZgWvI!*rNTn#+^P2oJS#9fTxwmYrPX7F8H6?IX!9&kp}jGRgoHAvL!MwajQ zBER!+Wv(eUm1Jeo$Y;t_@_EQ)OV}%+dK$t!GcgTe>NJES66TxA(-3M-N0>bgA=4a| za7;q{WY5B5uY~INA-rfN-iI)CHo_4JFPqA< z5o+F#Fncz_I&)aUF$vN4Bdj+w??;&P0K!=b8%@*$2(fbz7CwNm$()gJPC}bG2w5g` z4#J|j2$v*mHHmW(+CPY}YA(Wdb5X)&2|XV~*kM*Yh_L!0gup`x*{1tL2)!Rh*eKym zV?B%zF%Kd6VT9dgorKL2O3g!f%OuT182JdoZVCHLu}2Wf%tx5=2*LrAEn%;O>hlo} znThierY=A@BHzMrOoZ7B5DuHe5{^lT&O~_M%*;fX^C-eu2}exSqX@A!!oo)p zj+!$P&PixvBOEuGHo~IE5H3kLVGKQiQV- zE}5vM2(eEgEL@84vpFN-+*4Lz)AlK@_g75jQwWQe;c@9HJbpEa%MjWxM_9EC;i|bP z;j)CD%Mq@b70VG;uRsW_&{4u~daTe<;%StPC^||!jZ4Hzgyg3Y0%o0r%@RtjM96QF zRw9gi24S~^pegnYLYY+vQ=UO6XtE{jl~8>ZLLoD86~feK2_2D8#8iG3q2_Z4v!6vM zW)4d@CL#Jcgc4@va|m-*Bb=2`%0#V3h+TuQa5X|{b4J2B32oLOlr@=a5Eea;a7jX> zNqiom{aS=o&m&YY7bRSl&~q(9MYCcp!s-_g0xuv`Hr-!9==~zXMhR7o^&&#VO9;s? zB2+i)By5&Y>Lr94Cg~-FkuM|cmQc$Sdl{k3D+p6wMyO-5CG3?@{S}0IX5uRdQ`aFJ zk#LKtybht}s|d5#Aw-$O5{^lTeifmSnfWThob?E2B}AL3^$4*W5EiaSh%sj*oRiRI z145k1+<>rXBf=#K@g{L2Li^VcR&7LRZZ1l=ETQLX2qClLHH6ih5CWSJ5>59_2)#EW zY?RQ-Sep?dvJjFtBeXH=By5&YDhr{VNy44!ClY@lO!5xvP3DS*luW)NfC`U*`hI~>>g;WnFyJwZ>g<&sQ7qO z`7MN+d)3yr5GI<#5{^lT-iwfGX6{9pvrlc6FvUdeLx|mvuy7y3UFM90a}wI@M@Tc7 z`wX^gVTiXZgc)^Ygw>6Z(i}LY(_tm`Hod1@D667 zO@}|QcKG?_DS1uCdsc+MSg}nHzGt=cTWdBQ`oKDnmv3>^bYh=8Zbbwd)Zz_8Lz2?c zf|JIN8N(km@qM)E&~fX4RjOiR=hauI%<%77P1^M02`j%p@OyKwG~<+3ApC3o2escc z@DrK*=->|dCRUs$~yx54LdtCix_@qs*!U-eQgwRV%`Bdh421$o-|RKHn4j$axbLf7x<6WmBa{|Cmxv&=9Qmw;?&+ zc0JAWZl5a6{G5#XR&#@&g#7yp(zv2IxmB?GZd%*PAK9QltX_BK(|D$_?)Jy)A=~fQ z+>hjz-v4&(cD)>1K|#M0QJ>bZ{-aQDF+AXDZ9GlyX5HgyZ9Pq|303#BcAlnJi8cf0 zAzyn>)Qg0AgG-+do~Hkatn4VfpzCRRWwVN>bwcBx^UC;T1^9G0N3$>J`h+J@Ho0QSVrv^R(`srtd18_Ou?JrWaK8WhwQBekY{f`qN9U<2}Fd z{~zkB1^Oa|KKj2z1xu*?ah^EF6V(IFJxwpeE2wW?weYlYo~Bp+^|dg4RG6Yw_!gk< zo#1JDspKA@&qPlvOSpps^}qhOjcQ*Gv;+D~@f;%whu`?S)6?`a-D}=UczR1;1y=z2 z+d%qEMdP1-Q@-<>rnG6EUq!;-dJWMF`ss441kQP)rl*3H!8}jXgi^2ySnFv@s9;qv z8>p2To>q-;N1#?}(#o$o=mpfsdp+%DwO_9ts+F@mu?FEc(A3KNJgp|-ZD?xcY)`92 z_yIJ1?)S9XglD0t#D|8qUD9_FivRp19btv+FW^|Tsz z$kT2ite0F>frmX!?>6d-Sju>wr$rHt^D6j=r|FeP<)=LKy#`A&f3W(9&+>;%LWX}_ zx?b+r`d4=nw3A3;qTxo(E7dvw5GoU2f-KMd!RMF7yJs|1Mh>k!8LFk z>;rqie()ps2wVVK_9ws(;4APO_*nbGI|RN4p8)LxAAs}V6!->cA2B+x|CywN1VwVYFd*1y)f_J-}meGZ}sHvrL~31|o!ff&#j)CP4yz4rX53F?D7Kwl5l z?sW*f4c-Co0&QMRK|E*%v~h(%3y=s}nu)XgCCr>z{<?qXd|1^R<&6M(kZ^JZVPH7Ww-)t2{5TC$ z-SsZ~qd>3VE1v~G|9$rY=neXSzMvn_7ZplV=`x@Wn2D{g0Hp7r%d$aPPzM!{z!yLlps&DZ;0&lhf)C*jz!9KtY3hQs0O-ZK`@t-5FUSDX!QG${z10|KtGx?* zDwqt$ffS4JA0>lGeKB_cNCN%AATSWD1S`PP;7PC)tOmOB>1*E~gNMN^&=m{;gTVmM z8}tFKKnoB8{b|TRFbK>|XDq6Ks-PMe29klktkr_?83q3bd;!jb1t1eV3__p;-gY$a^^p()#U=+5ll1ZQ@zUd{Xg)aO-a0B=rrysyYa1}fX^!?l! zAezKYzy!kj7O^gwdM$JWn1|jEOac0KZ4!Cu3tkOC6ler=%cQq;T2pvi&@M0I-;qF9 zFph}vU;>y7rhtm*l|W@s1ylvqfW8;>3-}fM2CjnN!8NcD7_bN|22X$`;7Kq8+ynFl zv0FiRt^ZC0c9ZEuYCai^21Bujfn;!r*8Bv129JS>pg%rCXki>_?*dan9KKCK9dH5r zBKQH6#x4a4fg<1;v}eI9V1vfL7f2<;F<=A^`o8U(XuH53@D?}#J_G&10FVTZQGt`- zRd9*4W8qpr2hNQk4YUVsKq~o;2OB^#b_&pUYs{{>{<>BT^UYj;>GXOOd<$p*qCi8? z2-T+&Gj%l;N1=4&6@{pGezY6-|tJ9Bo zS6;s5sPC)@I!;N{H;r|S+5&F(oW~MwM!f8Ibq3>e?kSy=ZmzY4xH%xaAE=^Q^LK&{ z;30gcfjOJLdC-4bo{Pwm=8?mBmO#tuRY_LE8(YkknC?)>^Xu(z_Z{Du)~y{@9&p>BPA39+JRL# z^0;5+aGP)oiC7r~F<7jOlra_SBBf?6J+ z_h_Zhg0I19-0wi$tUoR0*9Mjss2d|-C6rS^!aHFl)B=~qfS}V8vK7`9R^1woR|fJ|C30u1cwM1YncTVx z7H&pw2daY#pghoouBcy6Pz|Fhr~+;Rh`!38k{6aOA8}it%v6y!u(plXAnenMa0o<$ zSkMHt1c^Y~OAVkj4M1&BOIPKZ1hf~`2ilkFfVx17MNZnElwo6_N~r=WP?{>z!n0LL z6&@})hOqKbngkH8WHZ8XKnLfh>HKJ}0FW4VQq9AT(n28IdhH=&fmr(n|D3&{BY_7A zYk!zbI2B9+6To;d4yd8GfgYecFq3V6nd;tSpd3;T%4rlHnlUNxaI?<#cXE&N{jf)X zkwD8rmuD>v?V#Ep^P}J`@ETC>Z2;@Q%V0f_kN8y}JseI~V5295o!-RW4Yq+TKwY{C$ak~H z3QOAxpy<3nzdw4*PvY zSgrdKoClu(ua(X(i1`@%G&lv6M!kM6ogZr5XW&zC2B<~h%KV4$S@5}sFJLA9)?;O& zf>aTuk$n-!Pnv8s=trP(@-c^ghJOM-fb{SAq0E&)W2>+Vm4m`cc*zUrC9#%TIJ2wh zzXADQ0lxyJQ-zi9FF*~u48moo67u!z^l-otRS9J#4i}<>t{q2WsYx6SygFx4t|g52Q%1*#hb-R0@-?gHTmINZg$>kEg~-374=0Cl%2sjk+r zY7QtL&7q5q_E&+*Ln}pkIX$;f1tdn2iMnMS&=BihyNU7rF=SY-Grh$F!2Xnmo{#`CgD(}eA`gy%8WcT zOfhfn>U65mmrWk7lu!3GC7200e{XsD8_!<(F7!yxO8F||T#lH#;7IuC^reU9$2Tln zDc@j}(kQDvk0Gm{UK=s$yCRkHJ&K}&j>4*B_|HgI%BKsT8f4K0CE@Xbo4@__zAql4 z3c3;%L()tVhyNyJrF>d}g;6vm6w_hf)cTo2E4)}KpC*oCbOhqem+@)e^orjssBnL! zd^*$6pBYPFdNHHZTg`6#{ZQ+RmGWr_)Sz+Whb7Z&-?>Wlr@o!o|BFi1@~t9DwaS8v z!d32lVDGGUBP&N(4I9>PP@n0y&!jK&m*vH)#moGStWV7$tVn%ZNd5VYRpY>x<&|FY zTLWWoZ^G5iSj+uvCV5Ty<^D4MvUyFy%lRoN|a@`lGr+CDy|@p^W>qf*;f3E8t3cYx{*8@ z)NfS3A$y@Y{32B-5-{Ud_&fW{1sKUS^%?UkM%hI_;MQF8yx?2!An=J?b8E|F96xdYLn)agY{ zcSOu^{G6WKZxUDfM_8|#%;I@&yori!C%+rW^0%WM{cUc4dyZ2BC(BpO$(0nnhc}}9 zjk;Z9_*C@qp@mta7`f`+=@W zMK`=fZqe>68(_LTOYNL&>YIJf`YZBfj?=~Slr{e6{Ee;eP0VxtnbBj5yB!l1dZzl_ zvx=1?JtJN}j$MZs<^1;N+b1k~h#dC$JTB8 zXdji!LJ`c^PZo zyCkj0v<~Zo*ZRw$JM-RKn{IC9Z(+;Rdx93>P-U69~BOQ`) zaq`7FnSIaP=C?FSnL)nyP1Fk%)yH%adD-W#b0glcbhe4`_VCArp4&Aiy}NaUcZS?9 zjWl#l&2p<{z2=!QFVWOL>_30pNxW_}5ijHC<`CYtB2Tl)RiTYrdeuHR`D)YRbVh^v zY*jfM)gSgGFDE~*2Y&Q2GpcY!Q}`8s=kUfd+01{1`LMZ?+rcXjEJ|B8>b(?JcWnLW z`cVyh|Gn7mY1p7X>n-+IbCRTgu^MaB-TM4$imhY9WSIl6(&e9;LF*{#&zGDk`J!oz zmPrd(m#OFf6WjUjfdy7n(Qe_)@fGGplJZrgx7PVb_#>*C2Cw=vBFl0qmj8o0vWlMg z__NtHtwJc8)n19$%+e41WzFxeGTlBkr%(CoRBea{yGKSRSQo&R2RrPIF>Cr%u|i(j z!Di-qf1}7xx%OyTEvueUx6bV3AtkMZ=2`^YOqzGr(~f=B-OcJhrf~eOsb#-*XBsoo zca#{dpfB^)-9GBn=d3Mn7xN7@6*kb0@uqt&I%-B^MNUdi8akQO;lKIZfW08jnY(Ul zBDd9W8yEiL&^2=$+@9_B_dUzYQIqv2u;YH3d_%!BJChmHh^*PWqied&yfM(HLe~nj z_K|5^{V4W7-!`+1YLyS+pvKJE`s{mU?mAqzrsdR%q}^(o4>tH4mLH0ThT)FM-=_7s zR_aZ%@b=HUYnmz>{SCd5_nVq#0%pm-s;7BvqrXz*YO>NY5C5I(DsExS?5}yb)4!y| zpC@BU-bY;anm^u}YfikzR9k3@Z1OMh-&EVYu!*@|r?&ZIlfO)KTV`Z=^4)mt>+cJc z+h0Ae)wq7cSnZKRh|&3^`xD=O+hY0Q->=8aH1#$!EK70Fp(5Yp_lAvm`v?DZhpi?F z2kRp<9V&kXKP}3K=3jksQ`KT$UiXWvV_w-zB@*hmIsGTGdc>txt#@7b=uZr@FJr*I ziEUbUoblfEm<)4Om00dMJUX*{pL_1SRP(ySE)$jItgTK^`Csv?Kt>g&b+gXieI(<$ zUzxgw<5ZdQ4ePo+QRLIZQFVVi@Y{8du650ZEPunuM6THd$Rl-b``~E5M-68xJ;-89a zu;u;pL;GHjiHJ5&ZSj|B68@X=g-W+R|I|x&zIELr{Kw*V`o3*<^Lc-Z>oIMj&DW%j z?8P$G>G8y@;ccTH|0w&q$7o}f%2TqEQ_1jOuV33XWx%9RjVG^rJ!V$in5Seu$7`(_ zyOp+v|K$Cc!B1cNFf%Lhy7LjU0tc?DTcPsdzf?ab?+YUb6>B>6x?kZY=E7DQR*loB zDrd)kU3JvFC+1xD2>%uP?6aAxOO$T+^YxgHO-#Zz8a9x$y0%3WOQ^HidhFnJkBMgD zHfr&Rc?gPpwz<0?haV5U+OuBlS--_pkJa^IhuN{sU#Hq>9Ey?ApZB<;)&*j80&9_A z3U61D32s(r=A4+kuIv5?CIbgJ4l%yFOyYKme8}{J${YN&OC;ZO{;o-V&geLyGY~~a z*E9>rDf&4)I9X(*b{^exaI1S~Qd9dp^#d7ZPGdki*@zV0vQFAy>>z0RD!DdcXP zOBPpqy^HJdr8#`T+MV_igywz7);PX~7Y_S*?wmc*$11;Bd>@x* z?~?C2l+V-BZ9$(M-v*vM^-WER(?y5J#(i3v<;sb#i)B+@5A#hnr8`I4x~9=if8|K4 zjeGsQ<++46{xf@sZWg$}Yw#X5<9GVI_!HWi*LPC6xOV2)P8z?#6nv9=8G7&LH~qD% zwQ29($(?}JXTxWRV+|E4mrkerW(E8nED}!K1^RdjsrpPYLN}WvY zU9`P<$PC=Yn9N;VqyKVZI0arbr+4|e)^szE@8%Nq z_k)qZ8{l|EcDdD^gljAJe)^Z$*Qz=F=Nue6nW#N<*vF=m$eCtk`94qj9)F{M$d|r( zdyl_1O}nzkUzu;2M{3L$n~D1%Zw7eF^{>~M%3fubD%DOib}w{>H|hO#;{NxG_s{FU zsAb*GU8ebJufGD{0(eMO$epKd5mxN=*NOaxtj!mD{XKczzd4B`|Dro}QfbV8q__?I zLm|f-jY*Tky-&_r+Rb90Jp!KQN)RZHF0bL~t1A{U*iIoAYd9R5drw&Q@mQE&_ojark@ zW%KI+HKh*^?l2Fmuh=m(s>3Yujj7)-nmeyreN2OcO#AM5Xs5itLqeyOKhC-8dgvBx zs2O_DU#GwX9Q5R*L|>EfzQ1I?1}yW6ea$QHdEvT!%|S9M-?Fd!_^0`vh?=)Wyv4%^ z&M3~Oci+bkaWN`^gO1BLmiSNSnseG`J0&@j{xUIo%2a*#v$xfV9F_NaElcz>MGv!- zy}~Q0@W0Q8YCTCwVQ=%sVSiqK_W|ZzMAlNb4>+Ft-}VnKuyTOA{=95W#@jf$jk+;U z1K%rV+1t$8*Ujm}&>=HG^s)H{G4gAY>T1?;8*Ehy5dZ)-eb2^)%{054wciBwb=9hnM zhF^BuFK2Kyd6XYekF>m8=GrW0GvXWNKUsqExe=PQT`jVPl0cR%tX)6BL zEmqN-TP*ihl3#eCo1)0SzbSHW#s2yhr_2$5Q+|WTZAUn#j4&&X(8LL5D-<~`#ho>6 zpTD-I{JNG!{G6qomE-W+6KO}?oWWW=hjHh}x{m}pB(3|pF z+`~DS@O@??4%R2;A*lRi{Pei-_^&_jDe%o_v#F4YSrfeLbeOc~N<{ zz(Ehy*K8g9<;r`9-+A4EFXO2W0u(VkoVwq-rOe_`2u zek+<&Ip-7K4b#l)A9FiWcAD$auV2Y|w^ms7VP1;~rj6crGcmdan)lSg_e))>IP!W- zV^i=XW7Eo1KgkpHy=ER(`4&xH z3ROEC&Hf$-&MvBz`y9+_XbPV8SN8VS&hA*XX*+iH(O}PWmET0JWri!(ZR^ z&4P&^JI!^D+y_jzGwjyhEt#qJ35U&orqd@>cQn}+AW{2O<6b&eF<*WXMY|97rkIQ8 zIRY&D#NRb?7G64y?ECVk)89Y5;sd<&kcykI`Nkq^_PVj3av6_Wi(^3?w?2|~{FiTE zEsdk@b(n{Ltb=X~513hJ*ltIe!)N?6Bir2T&Y0F8zPb4G9c46wy(f%L-evg}AP;=X z*c$WQr~W~~&a>R-GT)nCpP^ne#m-XMW1n%jbo@)30-yVv_>13XT7K?tm}D3gJ!M+> z^vcpn+w#mUYE`|D|II~7y)${tDMi}_f0e5yQEtU>p@e8+LZY7T&a@p!lqtl{q^#Ahjs3? zmYw5ZlK%m>>75oPbSU=5sE3_SbLQ#aj1JxDJ;82Ll(g#b$6J^Cy+hoF{iNl;)wIwe zy_hp+m(KcjXY3>1P%zJKe!w(4&&A#yG=JlUW;`Puoj?K&a`O@I4!iq-QM%7+sKJZ! zeNSg=&3={e#_t*J#C-MPs~7G%qLZ_Co9fOY{SU{N_)Wr3 zH}fr)kJ(rD@)+HQItMKt%$g7md= zpu=A5H%%80$p1hw9C)aq3(+2vt&EPF6HxhY@Y8a=)_7vq_G`Z^iXX=qZ6^H3K+&(* zIm^y*AEV^EC%=h)Y0|g4zVif&DCs&rI}lerT!-_-k>S}wss-LhiUq-k59t)SAQ+#X$Rl5p_4h2QG+-Hmxl=ZW&U z&5zzgIyJxT;iF|5Z~RrqFKrQ2>L@X4_Y=?l@Zj#Em&XyKg-FbKb5%KdGw?SP^$jb& z_=BeNH#D;LgYL;;?(E-onUq^=k<5Dzyxdsd=JY$i0nM>~H1iAQDH~awR63EYzqxd& zhnEa}fmG~cYWW}h!l`PS?4QZ%&pFP&h`7~szr^}{_gmKI9y90${+}-IcRcz(VYYsE zJxy&hHkajP7 zUzkETM_!%pHm%N@p=W=ZvT~)@!!*s!&0lJP>3@+m;w4*dXhs+RjgOjB7r6~^$0?kD z>+k(t`9i<_J-gX_bA}1buax;!6AJUoMf1t*$^78n^RQ`#Q|B$1Z z^Ajy{JD*<9)w7%LTt0blddiH=Ma%NRqjTgEz1_Ly>AiKl5NGu-F~CzN4*b z4IAD&_<9>EEH?kEl4}pH@rdF{mWZ2>n}^7~-z6a#OuXss5U;qNH6oZsJq8VukRU;f zp&k*Xh+)RGIjzT#q+x=hN>d_@h@`=2lRl=Vktk-Wk*ib{iA4I_Z}-bLH?C^)&))Cb zvuDqq*PcD!$?klr%S`H{l`1g|w-k9l!=&+hgf*MIKbqr}u)VK7=`y+nwUi>G(np|I zGH)@tdZ6ch8rBG^KB0=|dUJ$EzQB`MERC(xHBC-0)M_RA zc96oV2gy(APsU7co(JjMdKj_)K@p>0>3ZYIzDpl=))zj8TscWGRA>O5l0ZqBv?=*g zEahKNNjQ}PlX5Ooh^V(^^PG=&T^OfNDdBF=;091C3I3fj8{l;yxObZ%2QisjU^YE$ zKwQ@+TkN<`O23Rs?DB#ul~lf0uT1h>t}Kd{aS2FKE&@I?l++NO5Ym0DU$u_Ufdka9 z4Kr&*v|x?{U^9&5oD{o!l1t2`o=w^glXP zW;(^`{AD2}xy-QpQ@EP%;(3c9iu;L>4okF3mqEqCJ;_^pDcGG#*P65)PZD zx6%>H&2aTmM1ml#$USX)i2uF4y3{hMs1?>&M3rbPzX6V)Gf#$x7Mjbi$LVnF7#Ul$ zKx^nRk!H*)c^0x~xsAIZDgRenh~_J}Dm$8&qwt!Y%Ig3ftfE=dq=7k@g%G*40@2fw z1Y!5mE;T)s<|B-?1(m`BD#0hjS^LG-rjnicDWY;lRfG|=A1+s!1^{j<2KTtnqG}p0 z=&V{!72GFpp;|PWB1pZUgKZ%`F^AKxXv%Mk!A>EZzolfu%P6H+5?W1<4D1c1_;Zms z8`f)8Np@rY_aIm*uw;h-Rt(6l^9^=q=Ben$b>H#Cs0_$n8+*xpwT!AfVF?bqEbeN; zUT2aKmbK#-LiyY5y*42GgC(mm2|hLQUHo-hFO_;>xlVROHLz-)^)9Vz2O3(0OcLfO zb@B$2RRVd2&!w0m+v7z!4Kbp*M;r0v#!l0|_V95pL5mkH`YpCI;s6n{=$3oCIs*;{ zNOJQPxoC)p61_oF4jsVk)_MtVUtfM=c11n14X`yx1Vw+Z?0Jb41T>stL3lGoaslE{ z9jHveNrT7w7dK40m2@+IjyjVXnI}+zk@dudZ#71?4L4s^`XKt<;ls?z__Jb+y(z=| zIA{1^jN!)vPN#E+Rqx9t`n{ZHh@=^QtP2W8-t&Xp zL;~609a(ao`#tFg8LH`|0J`bNIw4qU1eh|QNUZSiw)?T&VtViZg6nyI{6`G+_Xi$a z>ow7z1zNJ9ApBrcU0t~U=zxO3JgJA%rxoRhR!RkgwFbXA#NCXPN0}p@L9%15QAKgaej6Nex>2X*6=Z8nu5> zPyjUhG8h&BfT_dFds;gA9M%Dx7HFtctfwUy>bzt^1yJiNv}jhDbXg<>ri6a8J2Gws z|AH>6o?O%eYqJ}upHp^N9gQ$O6N~gs1Q%qnO6jk#>K8Dle>=o1vv0cjq6<; zdfH+EMZQD+o;z{(RrKK8&9y&?+698lH4-5D2Ycw6B;yQy`WG}30Jvst7H`H-6>rjM z7aHrztHM_73uZ=F4ZScLqj=%~Xj9R{S^D;g0~NiDzlsc}Y9kVZ#5a?Cc>Sv=gH@d1 zlTNgO<^s;JRo19`SSZcJ#=5#ti3#hsQ=N$gg-`PlDWkIOry7bUPdOBaX9I}1lajW` zxZthIWIS`N%t@S;9ye~JWsrrznZe1`(o{2xpetsUNa<$Qg(}U=;FYv;LEPe)<*~hE zk{9LmQrJA#05xSFk(3;}G-hSW!h{ul)fai2EzC98b5+vv#1+Xg^JBxq^0o|R>sovA zVlxRtBagGL6ugPGrua?Fj~3=Kg-+V=SZBkdn~eofsEy609X7Twulp!wipX2DiG3Yw zm|QEUEUP734eMrd(dzNF>U*u$o>FVrAP=ohoxZAJ!3Gbz7<~PYyxTR1XPPhS>O~E; z?7YF_f#^?tKo{+-k4H1EXLdH-tl`os+{+|%#6y~QpAFvt=BioHwP-tov04fKMkH?e xX+bNz*UId%5vwI~57BZ#XvO?mbvzuaYG+#EyOtr=D(98pnG5DTa%>QUeCH8VRzkNTY7`~2SbFZYKl*SfEDuU=-&WHx#ILZP+i z3oWi$_4>jgC*OW`!+nPv-Pk|0#FC{arvC8b7s8GY^Z5dBZ8$&tipTpsZlA~X+mMl2IpTK2 zTW~53D>KXX`ztqCPI{^HVP!F7?8p%VQ-`H|o7+t~Zulst9%o6SqSnAlJ-|-(mq;r| zQpfW65~-9+|I`|(BLAqrawK~CyAD%L1P)Z%2?;9_l)Uh>`wXZR4k^e{N zh2bOEWKksj4NoY2tJ|9!?X6aM|D$%6Rif$`=U*R2O8Tn-*Ne%yeR5#!Q)pJAzO=U{p{eK%YYUhmDpcI2Wy3Ju*`*qT5Yf1xiS zo$?zoazKiY&p0oiHn18N32R6TR#b)V@}-X*IV^QRjUi*-Mv%kUlrbYGBoFiTA3Jv3 zq){opk?1ARfAsq_Zs|itj&(AwdAnP&Qg&+Y@*Ou;a*H*vW?bVtT#klS-392HA|b!e zZgczNG`Xr@idAuQ892djURVV@3CnQ_TpXV6#Six4yLfg}SQWfc*_|9I zV~3@V7{|g6y0d;8tVuO2Wx@zes!g80%;Pn1z}J&)DkWu9jnuKeCJ35Lu}%cDInlNA zG<7q)fvss$op?=;JTNmP{pSRCnuK=miZEZ6*_6lCAPIPlOw}Hcw$Bj!F z;2WAUX@aVE9z8$t@8PFfq>LRuE_K*gU&`3g<5R{=@||trrb`*iCXwRvW#Xr9>fF+u zgvtF!j!`=gpsRM1TRD2J^vwzO-n``+Y)W!#KR9`84ay$03R~rkYVFR77vX%^$wQp# z_*%7br)@nuBD_SJAy;+wEVvk)0+)lk!J1Qx+qoQyZ-FlVc?^znt&gqNpKtHhddS!s zlahxI^CgcNG%9(_*pw`E)jD(m`l5rI-;}$ZR-`%Gb~4>jjf~`YE3Ar4=;+RpUmCls z>I1kOdZ>H)U{}Cy?!}kv;;ydb&h9GmVV5L64qN&Daq!d8EBMlU>CFg~#i0fnDM2ml z0IW@46+2H$waQ+CwY;~$s-V@~ZOPScZjbzct#z>tR)O1kxXaqWN`C=e%iyHP2jK#} z5RQ`p8&0}`HEWB@akBh^?V|3RcBd!}1$s z`}37ZJ4$ZlvAXtiH;rM~nuhaXZM_}(yUm?~t%1CPT?!sDz^!&?Sj~D1T|AU2s?|GW zEafvTdwcnXy6ZV?#Xkm9>$LO^1hl#arHmLmX+(dY4jF72zN@rd+vj?k&AyzzmVQ+Q zmcpw3TE81Vg2MsszA{5x+w%Bws$0=4xFr62JYEm0q6^?#HNX=Is9(koP98IgjOvVZ z2c`&I0sUK6mwM<7tQoe;rU_unH~(tH4~a2KdY2E}w;qry(3w z0Dc};MIQC|A&)1(8j2m`-Ju!BhR106hN5di^dC8FpTl&}uDZ|Dwhh|TAQ%!_b`N6P; zzn6XOmhutpu~c?5SWT&CM-`}CbrgAMBr4Bzr%X9GfIT5~T*^Q?Z|Ja;@#?7l_UHoT zBZkg)(_We7cJEnxU4as5C$ZF;cI2hex)=WnaPru3W0(-W{BzuC)h~H$N{#yUeBV6e z+UH=6cCLBu==Z=@eXq=Q^;77|cM`1joIzLq{spdm1iL5|O5cGX$6R(|!Sab~urxPT zKkPPmvBy(k&6P2*I^e5CZgy|Os>V{%snZYH>k5`AwS~;JMT{SjIxv-M4^0_AXt2*0 zXJ-{Gk@j_l8#~T+2W&8W3*OvNIBTovQnxAfVd>{!<=uFh+w|LDP54`uyER`!X_~Kl zuvPbsuK4%3%`EQF)FAnUi534!}(}xIJ@iy*oNpJUcHOMf^GR z5@`eu6Hv?ZJ>h12d86xa3RXccz-q}Fk7vT#dv=me6;Rtx)3arM-Lo82z| z0M=}~_bIn0lVCM?Bzl@M{^eiP}?=qMES54P#bG!U1%sOB9aLjL>KNZ(9zU6il(M;)a*Atb=Ux3=;$6g z(ua=lp%Z%OBpy1Uhi(}{w+f+K0`FEJE&I$LI_igx^r54C=-wlAZxOmz2;B;V?omSb zB%yl`?;avObSp3>Ww7r4e3diZ-r+8X(}$g>Sdp~OC)_^xoNtmmT!dSM#|4;N)RLnGR8&T>{RBUnsvbr`!qe5C~P<9|whsLQ~`pPhZb)Z&+xZVxZ@VOV(S;*}z1+}8Qk?U%}3^>6>M z!iRaon_MWrGmyBtrHShAZ?dFnxIfDdR1Nye+DZKEW6$MhGdq)?)9paDp#Kd!iJyh- zxz&P!-g$h!7F5x)v--ydwqP~I$|cMCCXd~^dICx3Ru2Xy=Hs*btrj(BXg1GR#I3uq0UJTfRG(w>VpiZM|& zEIXoRtbdCgs2#LE&2P7^oe-$PaH>vu?1*Zyff1e+Cd>bn9f%HEmu|6JM<)czu@;h? zd?TV`1Iw`Pax6-=J}zMAtCJ9@$6D)v=(8hY;_SI~f`J1lYC^aw7^jx4^>ChTroc(v(frMb-VKmj;=k(TV zSPikLCizE1x|Y+b@IF}WiPI{rZS&w%kLKKkBIj?gyo~6Argmm97?@Ya&4A($#D!yY zA$@K;t3hm_Bv)X$)7FNufo@p6u$-wM*n!mti+15zq?}tWHLMXEb{|$Zdm(f0hjR9b z#DqYN@;+ZHC)vBiPY5&Gz>KKDa`&ZmdP z#%G^uc3ZZ`C@&`DaVH+-f9Em!4;}^ZC@7DpP$^?P4tOg^zk9MZSxu}8*=^>m;Cw8P%U1FE;z?Q5ncK-&bwieasU2t= z44gyL#=g;HT z3H5eD`+{EDAQ$x z+-iJ9$n|T-)yUPJAat+N27V!=Mzg$GpmBDfQ!p@d-zn#Y94_o&1gva?|Exs=0pm5c29m z$jQ(8sF9todxG^;W4m?t1b=sXZuel|2uqc<#?C>P1@bj<=Y%ug!Wv>V(((^XP$;_- zw>7a(^hmJ2XkzE^YK*=>_YcuTwZ4sI| zX|(k%#-adrwvLTs1FvJTf*C|sOjuK{zX^7Mp7DWnh0yajd+R|g2FSgl2ENOV(=nzF zS<~%a+z>2zI>(3br?Hfa>Dbn-%4Gy~K)AC|;+wlQWs}_$mm8zLy|8|~^=xx{Uf%@k zQgb^Wm+L!du3GGD5P@L?l|P$w+c(_?M zi(TNEM&ShR#?zS}fr4$_t1shLn~}nbvkSz=`zPChfx*BbY$k)}>u<;Yf#r-U7n00@ zLH{Z{Fen&!hZxr{V-e@6K|!lmdriNa)~pcy2pmYFs#m z#+s%xf1kl}lhCfPA7we+)qqN?g-+>#*;w8jeI_m(gSDJqP~&f1zuRs-Bq6LuM~;DZ zfkE+M^9i-K7dD8u&UCc%4dsN`$>-yrCM#R=e0%QDVA#6|_t*ux$A|6itYxQABd*nb zoX|=_U7XNYgjy&Rs7GI@qU=;dV*}%`RH1-#!F~=))5~d6*mqcN-mUM^=qvA)gxq{S zB&3uyEFsok+Rhvi^mnoYBZGmh_qxX!rXYJ^2Yc?wpueh}$JEVr<|YEGE6XWd)*AT#I94r`SL`md3(8CY;97 zs&lrIF#kYjdeO>6J9A<%Y!Wu3QX@X_A|YnK78G+T{~+Br_>=6+Nx{J4LD^@>hOuEs zuMFqEtvD9C&;P zS3w+I>*wEDowzGy8Yzyu0Ssm-M%D{C4CT>on9s+2_wrbc#R~GQFR@ttuEn)=?zCW7 zmyynuykUIUK0^KNQw`$-=#R|J+2r$fJ;IWrijJHgc$K<-{fV`l}AR(;$ zB&NH)@V+>6!3wKzT-d+q{mFVX6wv0E@1e#5Gb<=pg!)k;_Ya?>9eOQ;dLd(nU1 z&RiG_+(45rDQCt8s>}!-&2@F?hNVj!XA&mk;%rN|*~hW&aq?1JyO};83m?ynfpOs& z8WftxZg~x>p0n1HW`$;RMuS-G^oxUGHD*(wU4Y7sC#1qS+w_VH$Eb(l?qe5b+pRMa z{N?Pq8Nsl@4>*@&o^rfEh~XaBD4c-2nf&*~1}Z!ln)b4KVrh?M32{@{)DGCez(zDJ zEBb+L{`NWA#1~eN51&p*y_kJZv;#rg5~mLC)xzew+md_NUUr^)=j%RudH_p9Z3_68xVhxlVI-^>Ry{59u!)44I1_lcw2*(;7g)75aDg(mn@=9+z%~nu zt%Z7J5>j8grCCee)NVUEVYRZ`r8deaXO4=1pcj=IoCeO9!eb zXF{Bv^ms6^AB9^e9v60`%-^sQ9ZNH<+ZxR;jc@O@oKf67Jo$<+Y(^Y}k;eybUt2n~;_`1H3qR*BCs1>b+aK=sI|{3w8^=Sz zU0Ch0+^Rhn2;b|L;7*YsR&B>ur<9RcHL<8!bZpovEaz7Z{x|Kwu3(_}KDVZ5&>FfLnbx-h`+|YA z7emFWxw|}zvvX{$_4SMPiG2xy%7@%?X$=?GL0Inm(ZlWySasZ5)3!67XSlQ3M;&&H z<2b-L_QX;zxWCv~g{8J|>0-6LiRD$6*${EWjic)PVxXm+bTAk;9gU|E2jjzDCDhgl zl{^{>4ag2{$qxOSP;1Ap#Y@g5kF*bGhu+Q(l{*%S9he<@E<5ynb|~THkl%xZTDWz| z4i$MNq;<;iPi6q(}5ap4$2 z3@+C^=lVC6#)ShBBj4~<*K+nB?wRJk5)5od)0A@8&X-uK5M#_^uJWhcICmH7j-?9m ztD4$z;TTOY+&jE4ubPSEGC4e=s)Kp zR(uo3`aP=-O@ZR?^5XxD^AMNBdm+Bu1lj>5VDoZ5Iaw9x2$XSWC)Q>8vxz(Dx_Y`; z`E~c~oUHUcJzcEyy>$7|rx#F-`pMuU4g-UL5~c!u#D06>$r6dOhUo_%v9cTP@d#KS zu~LuncnqwM*l(XdSu0J}c>UlbR^=zj;PYp!+@^rMU?xzhvw%Kg#XkU~KM2%@`JTPN zA3p1CYKMsDP(H0l;<4`TQNrpX-=Y!M(8TeU$Hf z_Ibj7kGTLipZ|u{!XrS_3`1j|DQ+v|D#@R@*m>qVg-l#z4b@H`Q&6t!#rKAG1fb~`iK=A z<=J8tFviozdb(J_(8EQs{3dw%{eEx#-R}t#J)Q*XBi6<<&9if|r0JgicdP=_J-?hR zX@CnP#0t*$Y_T$0=-FZgANK5=ENKxx)MHCz*ax!8r)j%ci>3YaNm!+A zftBkvd3mhhc7BL=c)Syq-!51)?|{cgVSRG4q?h<1KE@CEp9~E&0q2VFy62DuE8{m| zeR8s-x4ihbVcG9^_Pellt4pw!#+Qox18dx_p{pCd^V`;&MeJMN3>VVmecelvla=I0 zbXDnRSbb$-D2p(O9;?WF*phDX^qegJLY|(Jr5CZhNl(DZpqS?tm(L`AXbsMUHP7b4x!Bv=`>gZbyX+vCo# zTG}0!e-Bs{?*;24R=WEfo}H65ZKt9u{WLFLtYErl z=VawG)6+dp`<%!fMGvSl6^iVEO4=ywX>D_8M3pvEtXl5%5+x zx3=J&Uc?I??}Am+Js$6a3uB*z^${!h8b4If8y>&y@i~}(zVrN0f$z)kSoR0l8lsD^ z=AZ8p0d?gySOtFz>m$}A{25k@RhlyJ!?N?h%IFqYe#Kz^`AYIbldHUE-v-OSiXuH$ z5!JEN6j9rAh=JwPKtDW|VN z{P%nDf5~Y$$nk)e;UO=<_P=>dMsg>;_L+Ykm(dA&e5TI$=W*FTkIVjfT*l+7oX^j+FmgU7 z!*p!v|2!_!Es=WSpT}i-Y^I+0=W!XGz(g`1@~Am1VQ`GvS|8z<8C_p(t*^F9c*R7;A{>|SKrBM0$&xTJ79l1M;iQ=n zhfpOB;i807Cb|K_SqaM;Ae=TAB+O}m(7Yi+mdR*{5Z4glx`a1PVm!hn2^-@P&X{Ww zR>UK8ZG`ZSS=R`mRU?G3#t844j*Ss+NZ2XiU&fz+u&FUZN&>=pvsFU31cc&2gbz$| z5FtE>a7@Al6VU`=w}kOc5I#1CB@AwYP$?1Nq8XitP%;tWjD*ijR8xfG5*}!ZaM@%@ znAj8{<}QRQX2x9zRqjH#DB(*J-3;NZgk{YTzBU&m%xQ+uyg9-(lhGU@t~tVW3E!H; z76_LlY;1vW-CUEfq6I?NmIyzXbuAHEwL}POh47>4*b3o>gq;$8HvS}pO|1}8k`R6| zTP1W$LMYza-^TjQXZp4F-($jCqZ~u=`%Od}Ty{$s-v%L>2vyo6T$E72M0Y?qD`8m&ghJ+mggG4$ zn%|8OVKVMUh`Sr%x`d)8u_MAI2^%{i6gSr-tmuf)wG%=Kv#t|Dt4;`Eoe@fzj-3&1 zNZ2VM()ha|Z0d}V(gmTc*(#x17lh(n5z3q7t_a~>5spcSG7VF`op zL8x>uLM1c$UWAhOBAk&>*+g|iI4Ee~Y=ZzrU?N$xQ0cX4Kkb ziQ1T|14vM100|ZjAcuD5T?Bu76P*HeFmpwBn+u|jrolj{lgSWuHkUS;EMdKv!^sJH1Z>SMNw`kKH{=suGyN;Z2${Y=C# zsJ}@S4KRmADW=qLXrLJ_8e}pdb9^|>s4;?Oq?$=15GIa5I4@zSsX9^(9jS(nL>O+~ zm2g%<<537B&D>E4b4DS2DPgo}Fd89lG{UOU2xHA<36~_a8-p-j-(q0c$55*-W9i(9 zX5Cn-Gs*lYnru3bgQl3xqN&C|9-3ylL#Eq!GC44wOwvqX0z&u%gy9nqW|%z^c1tLG zKf)}NdOyP8`@JzVhwk@p)H_jr->-#!mH!d8><5JKET2-hWS zH;Ho)E=kxp2VtkVCSk=KgsyWDcA0f^5n9bf;7j>!baa@9a6`gQ3HywHKEkGX2r2Us z4w$VHy3I!@z5wAxle_>Sd;!8S35QL@LWJEC#xF!TY7R>nybz(%!wARB=!X$XK8$ci z!Yd|f5yEi^4=h5+G+7cRE<%V|jBwJ-Sd36*F~UU&r%ZGP!dVH+G7wIi3lipJAT(cs zkYzHKAjB;}xGv#MlV~GclCaT6IAg9!SYab{U5fCIS+^9S)l!78WeD$@j>`~kNZ2Xi zU&g;2Vbd~%l;sHL%~lEBmLn852p^bagAi^Ij!C#+B32;mmN0$=!pG*YguyEiDm{X5 z(Tsisq2wb7XC!=PqE;dtm+-(!gv%yN!o-ybF^?i#F*6=TsPZVnMG0S;=*JMwN?7(7 z!q?`4ggK8PG+%{q&19@Xh+BnlUBb5}aW%pv2^&`);dP4MYypBkDY7r_}Tc^A#7TUkg^Wp7qeADw{-}`*XtPJH~rS@7_lDZ7>bS& z8*tezVf+S!T;{NZ!5a`NJ&q7&Mn8^F@^OSS67rg;jR?mjJg^ZVV6r4k+=vkK1VVl@ z;|YW+Pas^BP{2e#iEviJvL_J=nF|u;Jc-bJ6GDW^*n|+b3E{egq9$=O!X*hCHzO1` z*CedijL`Kdgc4@mQwXh|LI`^rp_J+PG{OxDJ0(OK|1$`io<>M{2BECkDxupm2*sa8 zC~uOVMF@Wu;h2Od6Y(6vZVBU`L%7WxmN57^gi6mNR5GKVM=1F`!WjvbP1F{I;}Ra& zf>70DNtn0=A!aK=bu(itLY1uu7bVm%(c2KtN?5iHp_aKIVa_&$=GzgXO~!VFxa|nn zCDb*EI}k2O*ti2B#$1!IVh2Lkod~gJ-A;s7I}yTOKxkk(zJPE;!cGbC#=i?;(+dbG zyAT?itrEKJLMXl)A!w3!BZTipI3^*{MC?J>En)l~guBdP34`|_RN9Ns+>G9fP;xKA z83`>-)INmc5+2xxkYutXOx%YMvmc?2nXw#zBO*g9z6pbT)}EB3zQN@kNBL<{AQ%JLwQ~uURMRW_}cPHysZ{Jz0Fop9}_qV^)<<&`^+9uvWa*J>St0#{mo&~08{E1lwwAU2AWLKAQSa6G}ufM zrJ5|!5L5LPXsDSX8fM-V4L8xpsovRF)Yjuve5AP`Va{>2H4|a9$;d>A%T!w>j5UcT z5H3mBcmiR(xh7%7352dE5$-qZP9n5Ai4gWG!X(r2RfHQ7c1oCH{HG8$y^4@>3SpYr zDxupcgyOFuq?zQ`5W-(WI3{6+i8#$jYRLyPLe&!GNZ`w8bb^mEA@Ht6%kCm^)WsI)% zL_5op$AZ)T@UWTu%Dd36#0&oX)(h>d{Z7Qxb3~l3!t0Qs+yY}p4jaawWbsYf_3SzS zNq_N!d}&J=sv4pHRo1s_;d}nPR@kb>yfo)#XqORdR&*6h3h=KQDkX5r8NSvMPLXynH-8(pH} zFQ@QQq4Jo56bSt<^ADP=Oa6Q9*Q*qhmW?ef|JkK!tsP*RE%-`j6);{`el}1*r6#)h@Yf?$7?G{nn@# z&2#7cAE^#yer151KKspI!2OSGdD%3Re0OEM=RfM-#h^c0nn7uh=P!N~4wKKSaC3JF zs~{`~&9sTx4Op_p!doodB5`1LpUBQ4Q6 zy83)Cx-O2bpfJVL?t=BOE0;;0W);<&V!nx9RC6y%uUVG&v=*MGS15M@eOh{&-eL^B zGS3z{CN8u~DX#8_tU;ifbt%A0mqu%BHPO*I2d756AeZ|xC+e+14 z?{~iMX`$ad>YeY8JT3InzP@eouBUbM((3KluYjH$cCvU=PY+S`j%}*vC{`gwfW8Z& z&k#>jYwLL0P){p{R^QWxd78e2*1*$-dzyaT8hX`dgr_-gN;3DH7fMHZqTXm>Iyj$E zo>mI0m8a=#f7QM;XyIw&Jikcn(CdbJ%U^ZS8+=ULw8uSdx~JWSHXEpse2>fhD&_4M zNkFYk_Z%w`z6Yq4`c|*N3~L4L6BA%>_t;6XL%a?pzj4VwQ{znRV6$FO`ium zt(w|D4N)zB&=ac@zD&+)`9q#|C*jbyp5}O34Ya56s|f3R5UNZ~pl{vY4(559UY^t| zwyMBUqHLFF@Xi17)`jYSHp##RtHTPDwBsj?LKu8pU38zgc;U|G_7_GjaImZ zL*1|LmYaG`%eyp)1Z9Ahb_Eax^i|6X;3M!c_yk-8p8I}uk=ZqH1N;D3+rF=Xmh)xsGPnS~09wvRz;*B{I0cS_AHkd8C^!UO0$+i* zz~?|q{tWmMTm;{Pw?U@n(WeB?0qp^=fsesi@EOn^a2lKd{{ruU55QG$5@Z4WpID!O zOW+DP488>)g8kq*upR6GJHZC94ru7-X&=$pYS@+l8!QFOfhMXZrY7Pwy6kIk6^y{v zelZ+qe;Eo=!G7XO!PP-EPz%%scY+!q8q@^0gF8TFP?4qNs{$&4FUUX}*UR7)a2#X; zZCrIhJrD!5X~h9;R1HBqXbciS5HtZz!CjykXbxI{mY@|#0{JN2hD+YySspPxJ00@+%y7OF>xRkGywI1 zmeXCD1VNhL2DAb#KoV#TT7q_iSXf{vZYDJDPe={~<63 z3_{bd81=UP0-!GoOak+OUZ8&h=x^C|23ZLreC7A|M}F z1u9dZ-s=nl+9Q913cwa91oD8qAQ#9D0w6!g2W|m{K?Lvv4qv{4U<~<=1>-<(&>!^C z_MNJugCuj%m*_; zJCF?ef?nWW&ymaf7z66! zTMtwMpJQJEUxEniLLe^)fYoSg!BgNljekclh75;-WE}Km;sa<0!69%M90U5o*FE4~ z&=b5t1>OPA03DA9!M6b&PIrKbpe1Mq^lC;bcn%D}9t`FZE?u0$ZUrSkNl*%u29bXA z)I6(ZS{1@oK{Zev+zE7={TI9h%m&lJc{1D!W`j0BXT#P&=RuwSbl%$mbjH*1?J{XD zfq#Pq;AzkuU!9!92l3&{HnGk(Zgdut_#I9k}{g_&jPRUZrDJ#Y(!4 zKS{jyi3flxtBI_Ap%u_YZ1S!j=Ubg~wfl*1OSBe1op1io4PR%tb}q35IzJeu@;Ihe(DtsmxiV&(1lSKOZh?0 zFe<$+dZ9R76hrZfD*&2+G@XcawN%ET1O%O)kgc$;r0UjCyfTo#Dv>i|#cR%}GCB1_ z_zSnAHwU+YNWdyjb7pf{jEW!%Q~+gwwDO>w7Y_M|n*n8}irfX)22DZ8Cy{Vtpo43D zP!BW#K@bfp0i~&_t8WzomBAgL2DlSwp9$?bS}gLZ1(abOph~F%Do~m#lHl2DhzbuC z979-nC`}^}s$@gLu{ty8C>;mlJ&uJ#PO7=%=xNG0)cTf$wZjev3TxlspR+e45zwA6 z7mOu528;$H!3Z!MsIg}8QmaCx)`Yw#LfNF0$|03F&6dID%u=fjZxgW5nX=2Q@~sCF z90ar=bV<^p7zz*b^ii;K*a*}CkArb&+Pj4N3C{vkedgh1)~#u}XorfQNjM#-f2M;O zAZI#-pFq>m$bdQEA;3T1bg%%ZAD4lpK;_%8{P^e01oioP!dmc4{6158xm7l89^zaO ziWJMCC{X2=V{5c^7F!7(0ZOw1s8AK4_)w*!Dcwf&$H4~hFjx&wAkA}xV+reaRkx|tbW2(l=(hA3B6U&T3_5|QK&U0E;LF&0M4?0DSA@R= zSAYsQ3zYd8a187K>b-5?dGIXQ3gjc+0;H#f4Doi5)9EnwQLq>62I|tCKt3;ctgy5_ zK>o6o<{&r#lx9EJ2NWk;en-F|jlY~;^h7zl1YQFtfm$4@@d?7pI1?NPuK;Cs3cL#B zr?B!6zYb0VpiHF+Z-G#&-XZ+9r>py<3-1DD9P+zNSgpGR zJ_GNAP$U0E_#8M7-UElV-qr1&0=4b}_z-*m)S^&jJ|X-u_{hUWSOtFJvGPzss)*9a z{u;Gl#CjY2WeV8n_CSxe{n>6;`2&S6B(Z^}@Py(NYU#_A~lVK>j~~ zAA!=T!pipsP{Y0lp)ynneABXpP=qp2C6t*sREVEKeLw~M27U#h0)HX=ZxAXhXF>7_ z7ZKWv~z}YQMkHMh#;(c_pb=&fkR!KH%D0A9l#C;>TXq1J*{EY98f-6 zH2#H&ECdRI0zfy2`GLAoJ)lbcvF3lJd)?|3rfv_lRJzj04&|j`OVhZh+tgBpLkTo_ zN>B+s1Sk%Q0zI)%3srMvD4(2mG2(J+Im0>qRi5%-rKC9-l_a4mAW={Cw!qH=&G1NU z&1i+yrCKR_K^gSW^3vv%bH!-0Q5P%C5un0DOE45yp7e)x9;!?r3RD2)fE(d*$gb$c z-2qnuy6L;!L&#tIfbvlq<+YyqdMB($l)577ailIhS~Ecya`@f!5>e*!-2|&-( z*5lL^ZUS@x3W8hF^h~8Ts0DNpN<^;;Q?~QGQw3GTC<}D{2tCic8@~>q6Br7HfK*`W zt+raF=>bVsp#B;TpM^E0W$*C9)6l1aDPS_tW2H%8BGB#W{Xp6npvO)Vz& zM(MCxePb0r_)IL7%Y3`qs+3j*qcriMKNMLZPd=1#DEGn@;OfI>wLB2fw{L|!O;L)Y z=qB%WxLHQIj*njX?yCxU?nfzwaspP`x7x3&*LZWnsS0^?gH9Kw>#j~QM_+35eu2;H zFQ|}5_i#m-3F&odYbAK?p(lR)I{0wc3VBMQs8S>0NO*nPs#A;N>y)aHryEKM`FbAx zHm!d;e8|^@D&(1mqJmDts!HfjWmd?ed8=w`Wfy}-hTZYi&$B<9PZjj1ha!+P-Ni|n z6KiJlFZ*nTJlYZpqUc=0u`d0i9%<#jTwM0S3VAfU=+bm`FMXBX=9Nab{(7p}wF-H9 zm}zURQUUe3%2{I8U`DR@{7&SldZ1118Q+Cl!;;6P=&xfxW8PeA)s5WeIjpIaerNRu zQu|`R7y`83RA~U?`(Z(_3QUkbn>cSlZS{) zOb`d&e4FtMen;?A$LufiS>?-j-o4ZDt1G`Z%|iUFOS#Ra^;TQ!+uY{+_0~YYWqNNQ zd1=#*y6%I&G5G{iJx1=^3fW5^O z*ld-m|3(paFy{|gx^@4#%nzs=WvGWf@?u`R^U-thT}!qmhMv>h`lg8Kwb`m;{Zhm% zh9g6N$GGc#U;Osd;^gn0JnCwQ_L}n)5~(Sz1(Ca3oi3;HJar6*7*iKBG)KKfBf4eovdb^?-vfWCL{OmS&N1pfMp2Gin z@8bum`2U5X8hIt2HRX4(H#h}XzfBiSZidrK!MY%Acs1#Gtl8YXqQ3x&lGQZ(c2M7a z=4&b%vw*9jcB;lHAARs!^4OM^-`nqfh8QizPffx2mnN3_LVf0JM!t=v@lKi$eTSQ2 z>)f|r9@e95ch`Xy_!^thIIvnpWj2ygVKVwW&z%Q`y#EPnQhmW}_sy$pdc9zkEVHz- zn^WkIOIPtnH*m{ZTiGne!`f2WJoN(O`ch@{zU=dr%?)g?yR93QO^aPtonrsl?SU$0 z!7i%;ucZ}whbnb5uk5lOEqazNQmZp+^;mev&c+Rj`SY3ayRCSC6|;3Wom05WIl)0pXM}ylZY|P#(IYc+`x(nPHQ{1a^ZFjEWQ@O>d%^Cw@~f|!tj+rM zrbh*0Sf%N2*V*^m!f)>N-;8Nw%I>8bdw5B=SIXVH_N51k-gMx_JRJBQ+)${@Ui=vO z^c#I1nO^U)6TjW`J73ML-Ag5Y#)Cml@0~fid9&8j-ni*ezPfwPimm)?T%Dg^Z*emw zXueb>`s1LZQPdn|@i<&8U5h z%$L>8ynT#JX>))Ckt4YdXg*dyvtdlu&~3e(y^h_QL$JA~jJufp`|%6?0ryS`uU9Wq zHsZv?yDnf?|t)f{~k9TLVuFIcGH!8cjYN$-Hh2=&pf=}Dp^1D zN8XS4zG`{LWvj_ekIuSBPYDpuAM zI=~7F{b~BFb%o2mRONDqn-1O|n@_J+q)J`?{Eye&jOk-~lQc5)SLyrR`tLxY^2NTt z=`hD+;6SfG4V8J8OPkJ}bt0zSb760%emDJ&#+VNe(871{;5wXs_+rMUq9t1XaMR;5 zF*+$8|31C^)W^HpH)DQ@F*Og;!qA_xuiy677uGG=G5MxP4U>G3S~NFPM19P%gI4u~ z>5biM;UABbh5d_&(T?!}tjRQXf7hp*X_YjuAF|q8?c5dWND*29r`g6^HjJ*8V^mA|%~COO5jAgY_M57YEmv*s15*91S^ z6cjJKb>HMgBUU(m&VuM*8XsYgT4qK=W!B)Q!^EM2pKWfreSkV!$1g^GQ_vhbLW$e( z(EdAd&-K`_hDlH3p`#lOb+RZGxd{jLa@_J7OQVh!p-R)e(~aw3txq()jxuaF65Rp5 zx8qywdViC7!K*u6k;g1L%2~8JHP*g*Psfw(lMC;71qVh;edU}!+gWRynLA(NSP*TR zzht#5bH2IjKfG$Weorm!*TM38XLjG&7UsE^*hV^=3omgC;C68-Q~a0}5%{E~y9rG; zwT`hm&bDx?KeX}EIiufvPla-YR-t4v;28V&swB6V_O*_zFLmN*F+6m<$755H*(Q%s zt=-AcZ}jbro8&K~BboY$QF*cT@|PJtcUX((MyY4oz05eZGJ{@b7-pGAWi2*4U$%By zS?$fRSE#l-h}QY`W-q4oMF;cMD|F!PVeW?L2W;Z_F>OM%}DlnDei3Tic%qcN?!l zn|0=K0x=o8jvh^|7(K`Gb4`m=wmxlKXFWG?Cws?@?TlTe>2N}*9bGIU+6bKXQqHzw;8i(`E^^98w-IgZS;mh?L++!|YcYS;6ObZpH!G2H!J z?Pe;xMmd&g@S0UUU!m^q0@~Kyj6k*yb~mHWSj8&Tt{Lk)Nt~`Vw=8MiC1Unj4uqU4 zoX>^sW6n@&%XN7v{8?}^Xi*;a?7FJCiDe@kt`2x|RH`sZ+F6nAc z(le2NJ(kA&<~aQ6NN7H~8JV_kTJ@~ZaE3-_%bT3wyfJV(Ur%4?`VwX9_&W4=JMPk^ zd4=A7JI^Smdz{WMZ5EwpDQxRu;@)Dz{j*H1xB8p4Z|Tz9-yA$+-C8!r*Li?DmG4XH zpTFYVuII>)=MfsrAtve!Mb`5TO>sx{!mp>Et5UCkdmVNTu#QV9FSn9q{(Ro4wQkEw zmYL54iJ)Iw@BHn<_j`S)lY}>Rzpq)z$Uo0ZbJ*>e-_QM0{5KlGx7nzhyugm++z*!e zpYN%>d#69YXL{>xtD$?()Xv|;^ybznGAY%)%vw)eJuz_b?&^Ak7_CzTzeUf`y;WcH zG*t9I=iyNE&O26A%tSKKNpwc^nQ_Ic+ud^eqm=`ly?_|)Ijg%heA_zOzue83^`^#I z%HD%RNgNhm-SldU39sIE(;>?w<6vD*HB-;BcUwc;ovqb_zSeg>*^Sd~sEjh^`Lmo* z>f=zDoDOc>eyr8AJ6aXfajBjj^|dg+kWjIF}>fl;$wymcMl$ef1H}0JGhh0 z)Y&Y!Cz?Qv=E8@Ei~lZlgXUx}7<q?#Z`S%}uvtZp@IO_(A{bjH@Am@oD;tE&1=-K9) z%p3*&&Hb-??KAg$OvCOn3qR&J0_9AhPi|%xQ`y}0i8Y8DlBYkh`UWb^a-Tb$G<{XG zJ1_Dy{j53lDdD9Txgt50C}Cc{Xw|o_%rd`TwCdCxMJMU8*s}E-N+j>kHMg+;-{ca7 zGIm1BuyI^;uPnbXsNurpB@6p6O*4HyWy+PE?RL#jI3J!(8{<%rr0-nowKv!17dIJkX82MS$!(@HuOq5z* zumesZl@7b5pHFHgg52H!$2Chp6IkMz#_P=r&JI=XxyDf<<*{OfL6WIE#D^+)6yGSa5oBI})!^>+UJ2pQ=i5VhQ5)+nPRI26TU(Q7{~ zs(iI3PclMRdUsNC1^iNeSMbwGX8Ro_iZ6JyzaC3zS7gu4Wum^Oz)9ws$QuXGI%O99 z@YjjiIDh9C%ENCGbLaZc9NTu7&QUzTV8mQSqk_-|P~khw{nyw+-Eut9ynl_$(wK+c@qFgZyTjk= z^?65f(9?d3m}-9chDzP}4PJ}!(u6H}Wm~}xqdN4&%iTPEt4%Uqp)8l;94h6I>3f52 zdV7)E$JICYzxe(54I6G&(JdhID;#v*?A_(l`j5YIQkPqAF=SVlPHJa;oncmf%Wcu` z24nemY;SRJ?s{{NVsqTkOVuP;2>W6uBZALH5V2A%Kqda)Jmg8zRSx%S{1k0?I(t|Z)$ zCXvUz_q+8<1wn(jszF4WgiujhQ?JleqCzwgQLhmiZyjS&b_3Iza9`uG2-Dh; zhQYoJ!8H6S26^QiyiZRb8-t90uBUF*SRWpNE?*yhU6_AyeNj%JPFHd!&D@O)+=>gU z!BTrVRSiQ3%@meqd1LMP=2KSI={lHdfgop$&bl$HCVT{CJO?C(zI=j53f+6AKNhX+ z4mrfIWPGX{o@7Cla40^MqWWDZWw-)e?L<`^yS_sfQlDqg=vhj_5Vo6JM97KEPfuJn z`xl=s(l4JTEN=L~pA&Q$6zaY~6Mq%vxxk3qHV9TB8fOpB~nL zEy-OeB{gGMtg|WFg_&^`z_nw%mrVtnB^Ro722>*}l@?4YhaG84Q!py#!VpHWwfZ1J zDanjkU(l*6P(A5hUS)>Cjuq0BvdfD?b2{@6G%x zmfDKjIJ@eIXJ{B#YkdKY;-mku}4y2SM>x)!hpRWxg zt}#ah9lcZ5=L(aGNcpJsj!}Km^+cUCMDb8!^TbkXuUwZs_t;-Nt0Fx&iB=3UwUe5X$GCYpR7x~Xp=k%3+UKJblNt#RZVW&za5ZYs?175aWzSi6b z9pPsx;)Z-BpH4etx@&;w55(-_kNq-J!uY}=?MYa?bZXUpUow@iiKfn~X5}x$mF`p5 zEyVN-%NOr8!`9|sxU1%K(l({XNJp$o5V;+`Mj5MjQ>clV?dCngJ^J21A6}8w!%3Gr z5D>m!H#tw*HLUzui4GY;t4z#dn*fA2f#9| z$1y-7luR8wAYK}c^#G3-Xmy=OgF&!e0oc|!ml{>cEso!(TY_6Zn&1iQ@6jAj)*0X1 z&GBR_a4!?sk_9Pk4+>>e)vmKX$O(BD^TQ7l;Bh<6Z;4(io`rvV5G{V9=OmT9!@{7; z4!K_qsH}hvwKP&O)pEK`j|hZ~gKtFVE&S{d5O_8Tbk#b&p|V6z*3o&>DKGX;%ZAwn zfAaQbZ5v#0A;T$6Rpg<4Fb7DFCwfELBwFIlg6uIRVxN>f? z<)Rj+{cfjZyt6y3JWw$l)^w;46CD9rdnTCUQDb?Ng2@v+f@ky0Bo51XTO%P&JwiGk zFnn`bhoIEN7dDXw8}vd~^OGmtN1W^xuOpo8Nuj>r>ygB`yl7Hn_jQ*_bqt3k9qO>D zvunJH8Y#GA|0h_GrY@}x>|tHmIhkT z3ZQvtDM2U2;iV?gP?+2<{1PS^7Z=<>`_mpE*arcD=Rlk;J&l-Fc&1RA2@pO6q!}P1 zFI2rXH_3f8Ao8&HE!8wiTV3>8QFkj?iK8KSDAA|H5LIzwMqce(63z_`nJzFfnig<8 zf$o@Bm0l2?5qN3^ZZsJb^twn29;Oo(hLr|C3$s#)!Xjz8!n)ymmj@NrIylfnq@7Nz z-21HTgW)^+qiupnf2XD=xW`3ZjzXK8V$RgDUrib~z&6_|XmuV`Wo0yn1 zGj391Y)X9B*yIEXv$1)_H8wWdP&~60d)%4|LU4BR>&BE~{1@fq9X}&6;q%nMDKkP- z=Oo9^2vn~aXjV8&aGjNuGId6B?8Nxs0Jz&X>W^~jRLT6!S~0#>dao7Q)8tCln{q2z8b6SAT_d0oB=cQ1%2TUy&*DGZ$DjKhT6vd+Obf&R-cS}4sNQ30#r9)R wpw7dIR${Le)XxGQKmFaplPlSR>^H~LD&)V%dw!JH3gWdyv07WtKXbO`KV(N7fdBvi diff --git a/package.json b/package.json index ea94f1a0..141f5935 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,7 @@ "module": "index.ts", "type": "module", "workspaces": [ - "packages/openauth", - "packages/solid", + "packages/*", "examples/issuer/*", "examples/client/*" ], diff --git a/packages/react/package.json b/packages/react/package.json index db7f9831..465b414b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -8,7 +8,7 @@ "sideEffects": false, "devDependencies": { "@tsconfig/node22": "22.0.0", - "@types/react": "^18.2.0", + "@types/react": "^19.0.0", "typescript": "5.6.3" }, "exports": { @@ -18,7 +18,7 @@ } }, "peerDependencies": { - "react": "^18.2.0" + "react": "^19.0.0" }, "dependencies": { "@openauthjs/openauth": "workspace:*" @@ -27,4 +27,5 @@ "src", "dist" ] -} \ No newline at end of file +} + From e9b49e78b79cc7b109df4c8f14c7edbb6fd9e238 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 11 Mar 2025 15:11:32 -0400 Subject: [PATCH 13/23] sync --- packages/solid/src/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/solid/src/index.tsx b/packages/solid/src/index.tsx index 2c3e4cc7..fe47f610 100644 --- a/packages/solid/src/index.tsx +++ b/packages/solid/src/index.tsx @@ -152,7 +152,6 @@ export function OpenAuthProvider(props: ParentProps) { setStorage("current", first) return } - authorize() }) return ( From f6bc00565e11b4cb7994aa65fa1d3136a9e8acd0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 13 Mar 2025 13:21:38 -0400 Subject: [PATCH 14/23] sync --- packages/react/src/index.tsx | 81 +++++++++++++++++++++++++----------- packages/solid/src/index.tsx | 37 +++++++++------- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 63a08458..7c583b8c 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -62,6 +62,20 @@ function usePersistedState(key: string, initialState: T): [T, Dispatch { + const handleStorageChange = (event: StorageEvent) => { + if (event.key === key && event.newValue !== null) { + try { + setState(JSON.parse(event.newValue)) + } catch (error) { + console.error('Error parsing storage value:', error) + } + } + } + window.addEventListener('storage', handleStorageChange) + return () => window.removeEventListener('storage', handleStorageChange) + }, [key]) + return [state, setState] } @@ -77,7 +91,6 @@ export function OpenAuthProvider(props: AuthContextOpts) { const [storage, setStorage] = usePersistedState(storageKey, { subjects: {} }) const [initialized, setInitialized] = useState(false) - const accessCache = useMemo(() => new Map(), []) useEffect(() => { const handleCode = async () => { @@ -105,6 +118,10 @@ export function OpenAuthProvider(props: AuthContextOpts) { })) } } + const url = new URL(window.location.href) + url.searchParams.delete('code') + url.searchParams.delete('state') + window.history.replaceState({}, '', url) } setInitialized(true) } @@ -126,33 +143,47 @@ export function OpenAuthProvider(props: AuthContextOpts) { window.location.href = authorize.url }, [client]) + const accessCache = useMemo(() => new Map(), []) + const pendingRequests = useMemo(() => new Map>(), []) const getAccess = useCallback(async (id: string) => { - const subject = storage.subjects[id] - const existing = accessCache.get(id) - const access = await client.refresh(subject.refresh, { - access: existing, - }) - if (access.err) { - ctx.logout(id) - throw access.err + const existingRequest = pendingRequests.get(id) + if (existingRequest) { + return existingRequest } - if (access.tokens) { - const tokens = access.tokens - setStorage(prev => ({ - ...prev, - subjects: { - ...prev.subjects, - [id]: { - ...prev.subjects[id], - refresh: tokens.refresh - } + const request = (async () => { + try { + const subject = storage.subjects[id] + const existing = accessCache.get(id) + const access = await client.refresh(subject.refresh, { + access: existing, + }) + if (access.err) { + ctx.logout(id) + return } - })) - accessCache.set(id, tokens.access) - return tokens.access - } - return existing! - }, [client, storage.subjects, accessCache]) + if (access.tokens) { + const tokens = access.tokens + setStorage(prev => ({ + ...prev, + subjects: { + ...prev.subjects, + [id]: { + ...prev.subjects[id], + refresh: tokens.refresh + } + } + })) + accessCache.set(id, tokens.access) + return tokens.access + } + return existing! + } finally { + pendingRequests.delete(id) + } + })() + pendingRequests.set(id, request) + return request + }, [client, storage.subjects, accessCache, pendingRequests]) const ctx: Context = { get all() { diff --git a/packages/solid/src/index.tsx b/packages/solid/src/index.tsx index fe47f610..e9425d06 100644 --- a/packages/solid/src/index.tsx +++ b/packages/solid/src/index.tsx @@ -97,21 +97,30 @@ export function OpenAuthProvider(props: ParentProps) { } const accessCache = new Map() + const pendingRequests = new Map>() async function access(id: string) { - const subject = storage.subjects[id] - const existing = accessCache.get(id) - const access = await client.refresh(subject.refresh, { - access: existing, - }) - if (access.err) { - ctx.logout(id) - throw access.err - } - if (access.tokens) { - setStorage("subjects", id, "refresh", access.tokens.refresh) - accessCache.set(id, access.tokens.access) - } - return access.tokens?.access || existing! + const pending = pendingRequests.get(id) + if (pending) return pending + const promise = (async () => { + const existing = accessCache.get(id) + const subject = storage.subjects[id] + const access = await client.refresh(subject.refresh, { + access: existing, + }) + if (access.err) { + pendingRequests.delete(id) + ctx.logout(id) + return + } + if (access.tokens) { + setStorage("subjects", id, "refresh", access.tokens.refresh) + accessCache.set(id, access.tokens.access) + } + pendingRequests.delete(id) + return access.tokens?.access || existing! + })() + pendingRequests.set(id, promise) + return promise } From 9c995cf2af30de8f1f3c86581dbfa43be7186ed9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 13 Mar 2025 13:22:32 -0400 Subject: [PATCH 15/23] sync --- packages/openauth/src/issuer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 5d7959d7..35183d76 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -1115,7 +1115,7 @@ export function issuer< issuer: issuer(c), }) - const validated = await input.subjects[result.payload.type][ + const validated = await input.subjects![result.payload.type][ "~standard" ].validate(result.payload.properties) From d51b5f235dd8022b323370f4a8e13d531f5f9a14 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 13 Mar 2025 13:58:06 -0400 Subject: [PATCH 16/23] sync --- examples/issuer/bun/issuer.ts | 2 +- packages/openauth/script/build.ts | 0 packages/openauth/src/issuer.ts | 26 +++++++++++++++----------- packages/react/src/index.tsx | 9 +++++++-- 4 files changed, 23 insertions(+), 14 deletions(-) mode change 100644 => 100755 packages/openauth/script/build.ts diff --git a/examples/issuer/bun/issuer.ts b/examples/issuer/bun/issuer.ts index 55d4c86d..68b046f4 100644 --- a/examples/issuer/bun/issuer.ts +++ b/examples/issuer/bun/issuer.ts @@ -34,7 +34,7 @@ export default issuer({ }, success: async (ctx, value) => { if (value.provider === "password") { - return ctx.subject("user", { + return ctx.subject("user", "123", { id: await getUser(value.email), }) } diff --git a/packages/openauth/script/build.ts b/packages/openauth/script/build.ts old mode 100644 new mode 100755 diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 35183d76..66bb0bfd 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -153,7 +153,9 @@ export interface OnSuccessResponder< subject( type: Type, id: string, - properties: Extract["properties"], + properties: [Extract["properties"]] extends [never] + ? Record + : Extract["properties"], opts?: { ttl?: { access?: number @@ -1115,18 +1117,20 @@ export function issuer< issuer: issuer(c), }) - const validated = await input.subjects![result.payload.type][ - "~standard" - ].validate(result.payload.properties) - - if (!validated.issues && result.payload.mode === "access") { - return c.json(validated.value as SubjectSchema) + let validated = result.payload.properties + const schema = input.subjects?.[result.payload.type] + if (schema) { + const result = await schema["~standard"].validate(validated) + if (result.issues) { + return c.json({ + error: "invalid_token", + error_description: "Invalid token", + }) + } + validated = result.value } - return c.json({ - error: "invalid_token", - error_description: "Invalid token", - }) + return c.json(validated as SubjectSchema) }) app.onError(async (err, c) => { diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 7c583b8c..3e031710 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -23,7 +23,7 @@ interface Context { all: Record subject?: SubjectInfo switch(id: string): void - logout(id: string): void + logout(id?: string): void access(id?: string): Promise authorize(redirectPath?: string): void } @@ -37,6 +37,7 @@ interface AuthContextOpts { issuer: string clientID: string children: ReactNode + onExpiry?: (id: string) => Promise } const AuthContext = createContext(undefined) @@ -159,6 +160,8 @@ export function OpenAuthProvider(props: AuthContextOpts) { }) if (access.err) { ctx.logout(id) + if (props.onExpiry) await props.onExpiry(id) + else authorize() return } if (access.tokens) { @@ -201,7 +204,9 @@ export function OpenAuthProvider(props: AuthContextOpts) { })) }, authorize, - logout(id: string) { + logout(id?: string) { + id = id ?? storage.current + if (!id) return if (!storage.subjects[id]) return setStorage(prev => { const newSubjects = { ...prev.subjects } From d0bf74b703ef7c30ef9a515accdade5b09800e4c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 13 Mar 2025 14:07:55 -0400 Subject: [PATCH 17/23] sync --- packages/react/src/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 3e031710..8108e4b7 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -19,7 +19,7 @@ interface Storage { current?: string } -interface Context { +export interface Context { all: Record subject?: SubjectInfo switch(id: string): void @@ -37,7 +37,7 @@ interface AuthContextOpts { issuer: string clientID: string children: ReactNode - onExpiry?: (id: string) => Promise + onExpiry?: (id: string, ctx: Context) => Promise } const AuthContext = createContext(undefined) @@ -160,7 +160,7 @@ export function OpenAuthProvider(props: AuthContextOpts) { }) if (access.err) { ctx.logout(id) - if (props.onExpiry) await props.onExpiry(id) + if (props.onExpiry) await props.onExpiry(id, ctx) else authorize() return } From e9950e1c64ac97ef33eb6fec9c35d1755526d6d7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 13 Mar 2025 14:19:50 -0400 Subject: [PATCH 18/23] sync --- packages/openauth/src/client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index 41a8feb5..a3f8bf0d 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -328,7 +328,9 @@ export interface VerifyResult { [type in keyof T]: { id: string type: type - properties: v1.InferOutput + properties: v1.InferOutput extends unknown + ? Record + : v1.InferOutput } }[keyof T] } From 366b14d8a043aec3fd35c203a297d03bc5242830 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 13 Mar 2025 14:28:28 -0400 Subject: [PATCH 19/23] sync --- .changeset/config.json | 3 +-- scripts/snapshot | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 7f8e1712..844e69b6 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,11 +2,10 @@ "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", "changelog": "@changesets/cli/changelog", "commit": "./commit.cjs", - "fixed": [["@openauthjs/openauth", "@openauthjs/solid"]], + "fixed": [["@openauthjs/openauth", "@openauthjs/solid", "@openauthjs/react"]], "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": ["@openauthjs/example-*"] } - diff --git a/scripts/snapshot b/scripts/snapshot index f21d436d..78e71845 100755 --- a/scripts/snapshot +++ b/scripts/snapshot @@ -10,6 +10,7 @@ echo "Creating snapshot release: ${SNAPSHOT_ID}" echo "Building packages..." (cd packages/openauth && bun run build) (cd packages/solid && bun run build) +(cd packages/react && bun run build) # Use changesets to create a snapshot release echo "Creating snapshot release with ID: ${SNAPSHOT_ID}" @@ -23,6 +24,7 @@ sed -i 's/"@openauthjs\/openauth": "[^"]*"/"@openauthjs\/openauth": "workspace:* echo "Publishing snapshot versions..." (cd packages/openauth && bun publish --tag snapshot) (cd packages/solid && bun publish --tag snapshot) +(cd packages/react && bun publish --tag snapshot) # Reset versions in package.json files after snapshot publish From 4fb4a8ff59bdd2e89260a21af09d2b9c33f02dd6 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 22 Mar 2025 18:46:50 -0400 Subject: [PATCH 20/23] provider opt --- .changeset/long-snakes-happen.md | 6 ++++++ packages/react/src/index.tsx | 13 ++++++++++--- packages/solid/src/index.tsx | 12 +++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 .changeset/long-snakes-happen.md diff --git a/.changeset/long-snakes-happen.md b/.changeset/long-snakes-happen.md new file mode 100644 index 00000000..24caf852 --- /dev/null +++ b/.changeset/long-snakes-happen.md @@ -0,0 +1,6 @@ +--- +"@openauthjs/react": patch +"@openauthjs/solid": patch +--- + +provider opt diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 8108e4b7..0e452493 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -19,13 +19,18 @@ interface Storage { current?: string } +export interface AuthorizeOptions { + redirectPath?: string + provider?: string +} + export interface Context { all: Record subject?: SubjectInfo switch(id: string): void logout(id?: string): void access(id?: string): Promise - authorize(redirectPath?: string): void + authorize(opts?: AuthorizeOptions): void } interface SubjectInfo { @@ -40,6 +45,7 @@ interface AuthContextOpts { onExpiry?: (id: string, ctx: Context) => Promise } + const AuthContext = createContext(undefined) const STORAGE_PREFIX = "openauth" @@ -130,12 +136,13 @@ export function OpenAuthProvider(props: AuthContextOpts) { handleCode() }, [client]) - const authorize = useCallback(async (redirectPath?: string) => { + const authorize = useCallback(async (opts?: AuthorizeOptions) => { const redirect = new URL( - window.location.origin + (redirectPath ?? "/"), + window.location.origin + (opts?.redirectPath ?? "/"), ).toString() const authorize = await client.authorize(redirect, "code", { pkce: true, + provider: opts?.provider, }) sessionStorage.setItem(`${STORAGE_PREFIX}.state`, authorize.challenge.state) sessionStorage.setItem(`${STORAGE_PREFIX}.redirect`, redirect) diff --git a/packages/solid/src/index.tsx b/packages/solid/src/index.tsx index e9425d06..22777ae1 100644 --- a/packages/solid/src/index.tsx +++ b/packages/solid/src/index.tsx @@ -26,7 +26,12 @@ interface Context { switch(id: string): void logout(id: string): void access(id?: string): Promise - authorize(redirectPath?: string): void + authorize(opts?: AuthorizeOptions): void +} + +export interface AuthorizeOptions { + redirectPath?: string + provider?: string } interface SubjectInfo { @@ -82,12 +87,13 @@ export function OpenAuthProvider(props: ParentProps) { setInit(true) }) - async function authorize(redirectPath?: string) { + async function authorize(opts?: AuthorizeOptions) { const redirect = new URL( - window.location.origin + (redirectPath ?? "/"), + window.location.origin + (opts?.redirectPath ?? "/"), ).toString() const authorize = await client.authorize(redirect, "code", { pkce: true, + provider: opts?.provider, }) sessionStorage.setItem("openauth.state", authorize.challenge.state) sessionStorage.setItem("openauth.redirect", redirect) From ba745eb5ee675856d57cc09bc435a504b59b021c Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Sat, 12 Apr 2025 12:09:32 -0400 Subject: [PATCH 21/23] Add OpenAuth client in golang --- packages/openauth-go/client/client.go | 547 +++++++++++++++++++++ packages/openauth-go/go.mod | 20 + packages/openauth-go/go.sum | 36 ++ packages/openauth-go/internal/util/pkce.go | 60 +++ packages/openauth-go/subject/subject.go | 5 + 5 files changed, 668 insertions(+) create mode 100644 packages/openauth-go/client/client.go create mode 100644 packages/openauth-go/go.mod create mode 100644 packages/openauth-go/go.sum create mode 100644 packages/openauth-go/internal/util/pkce.go create mode 100644 packages/openauth-go/subject/subject.go diff --git a/packages/openauth-go/client/client.go b/packages/openauth-go/client/client.go new file mode 100644 index 00000000..8cd1de20 --- /dev/null +++ b/packages/openauth-go/client/client.go @@ -0,0 +1,547 @@ +package client + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" + "github.com/toolbeam/openauth/internal/util" + "github.com/toolbeam/openauth/subject" +) + +var ( + ErrInvalidAuthorizationCode = errors.New("invalid authorization code") + ErrInvalidRefreshToken = errors.New("invalid refresh token") + ErrInvalidAccessToken = errors.New("invalid access token") + ErrInvalidSubject = errors.New("invalid subject") + ErrUnknownState = errors.New("The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.") +) + +// WellKnown is the well-known information for an OAuth 2.0 authorization server. +type WellKnown struct { + // JWKsUri is the URI to the JWKS endpoint. + JWKsUri string `json:"jwks_uri"` + // TokenEndpoint is the URI to the token endpoint. + TokenEndpoint string `json:"token_endpoint"` + // AuthorizationEndpoint is the URI to the authorization endpoint. + AuthorizationEndpoint string `json:"authorization_endpoint"` +} + +// Tokens is the tokens returned by the auth server. +type Tokens struct { + // Access is the access token. + Access string `json:"access_token"` + // Refresh is the refresh token. + Refresh string `json:"refresh_token"` + // ExpiresIn is the number of seconds until the access token expires. + ExpiresIn int `json:"expires_in"` +} + +// Challenge is the challenge that you can use to verify the code. +type Challenge struct { + // State is the state that was sent to the redirect URI. + State string + // Verifier is the verifier that was sent to the redirect URI. + Verifier string +} + +// AuthorizeOptions is the options for the authorize endpoint. +type AuthorizeOptions struct { + // Enable the PKCE flow. This is for SPA apps. + PKCE bool + // Provider is the provider to use for the OAuth flow. + Provider string +} + +// AuthorizeResult is the result of the authorize endpoint. +type AuthorizeResult struct { + // The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps. + Challenge Challenge + // The URL to redirect the user to. This starts the OAuth flow. + URL string +} + +// ExchangeSuccess is the success result of the exchange endpoint. +type ExchangeSuccess struct { + Tokens Tokens +} + +type RefreshOptions struct { + // Optionally, pass in the access token. + Access string +} + +type RefreshSuccess struct { + Tokens *Tokens +} + +type VerifyOptions struct { + // Optionally, pass in the refresh token. + Refresh string + // Optionally, override the internally used HTTP client. + HTTPClient *http.Client + // @internal + issuer string + // @internal + audience string +} + +type VerifyResult struct { + Tokens *Tokens + // @internal + aud string + Subject *Subject +} + +type Subject struct { + ID string + Type string + Properties any +} + +type ExchangeOptions struct { + Verifier string +} + +type DecodeSuccess struct { + Subject *Subject +} + +type ClientInput struct { + // The client ID. This is just a string to identify your app. + ClientID string + // The URL of your OpenAuth server. + Issuer string + httpClient *http.Client + // The schema of the subject. + SubjectSchema subject.SubjectSchemas +} + +type Client struct { + clientID string + issuer string + httpClient *http.Client + issuerCache map[string]WellKnown + jwksCache map[string]jwk.Set + mu sync.RWMutex + subjectSchema *subject.SubjectSchemas +} + +func NewClient(input ClientInput) (*Client, error) { + httpClient := input.httpClient + if httpClient == nil { + httpClient = http.DefaultClient + } + if input.Issuer == "" { + input.Issuer = os.Getenv("OPENAUTH_ISSUER") + } + if input.Issuer == "" { + return nil, errors.New("issuer is required") + } + return &Client{ + clientID: input.ClientID, + issuer: input.Issuer, + httpClient: httpClient, + issuerCache: map[string]WellKnown{}, + jwksCache: map[string]jwk.Set{}, + mu: sync.RWMutex{}, + subjectSchema: &input.SubjectSchema, + }, nil +} + +// getIssuer fetches the well-known configuration from the issuer URL and caches it. +func (c *Client) getIssuer() (WellKnown, error) { + c.mu.RLock() + defer c.mu.RUnlock() + if cached, ok := c.issuerCache[c.issuer]; ok { + return cached, nil + } + resp, err := c.httpClient.Get(fmt.Sprintf("%s/.well-known/oauth-authorization-server", c.issuer)) + if err != nil { + return WellKnown{}, fmt.Errorf("failed to fetch well-known config: %w", err) + } + defer resp.Body.Close() + var config WellKnown + if err := json.NewDecoder(resp.Body).Decode(&config); err != nil { + return WellKnown{}, fmt.Errorf("failed to decode well-known config: %w", err) + } + c.issuerCache[c.issuer] = config + return config, nil +} + +// getJWKS fetches the JWKS from the issuer URL and caches it. +func (c *Client) getJWKS() (jwk.Set, error) { + wellKnown, err := c.getIssuer() + if err != nil { + return nil, fmt.Errorf("failed to get well-known config: %w", err) + } + c.mu.Lock() + defer c.mu.Unlock() + if cached, ok := c.jwksCache[wellKnown.JWKsUri]; ok { + return cached, nil + } + set, err := jwk.Fetch(context.Background(), wellKnown.JWKsUri) + if err != nil { + return nil, fmt.Errorf("failed to fetch JWKS: %w", err) + } + c.jwksCache[wellKnown.JWKsUri] = set + return set, nil +} + +// Start the authorization flow. +// +// This takes a redirect URI and the type of flow you want to use. The redirect URI is the +// location where the user will be redirected after the flow is complete. +// +// Supports both the `code` and `token` flows. We recommend using the `code` flow as it's +// more secure. +// +// For SPA apps, we recommend using the PKCE flow. +// +// result, err := client.Authorize(redirectURI, "code", &AuthorizeOptions{ +// PKCE: true, +// }) +// +// This returns a redirect URL and a challenge that you can use to verify the code. +func (c *Client) Authorize(redirectURI string, response string, opts *AuthorizeOptions) (*AuthorizeResult, error) { + result := &AuthorizeResult{} + u, err := url.Parse(c.issuer) + if err != nil { + return nil, fmt.Errorf("failed to parse issuer: %w", err) + } + u.Path = "/authorize" + result.Challenge.State = uuid.New().String() + query := url.Values{ + "client_id": {c.clientID}, + "redirect_uri": {redirectURI}, + "response_type": {response}, + "state": {result.Challenge.State}, + } + if opts != nil && opts.Provider != "" { + query.Set("provider", opts.Provider) + } + if opts != nil && opts.PKCE && response == "code" { + verifier, challenge, method, err := util.GeneratePKCE() + if err != nil { + return nil, fmt.Errorf("failed to generate PKCE: %w", err) + } + query.Set("code_challenge_method", method) + query.Set("code_challenge", challenge) + result.Challenge.Verifier = verifier + } + u.RawQuery = query.Encode() + result.URL = u.String() + return result, nil +} + +// Exchange the code for access and refresh tokens. +// +// You call this after the user has been redirected back to your app after the OAuth flow. +// +// For SSR sites, the code is returned in the query parameter. +// +// result, err := client.Exchange(code, redirectURI) +// +// For SPA sites, the code is returned as a part of the redirect URL hash. +// +// result, err := client.Exchange(code, redirectURI, &ExchangeOptions{ +// Verifier: verifier, +// }) +// +// This returns the access and refresh tokens. Or if it fails, it returns an error that +// you can handle depending on the error. +// +// if err != nil { +// if errors.Is(err, ErrInvalidAuthorizationCode) { +// // handle invalid code error +// } else { +// // handle other errors +// } +// } +func (c *Client) Exchange(code string, redirectURI string, opts *ExchangeOptions) (*ExchangeSuccess, error) { + endpoint := c.issuer + "/token" + data := url.Values{} + data.Set("code", code) + data.Set("redirect_uri", redirectURI) + data.Set("grant_type", "authorization_code") + data.Set("client_id", c.clientID) + if opts != nil && opts.Verifier != "" { + data.Set("code_verifier", opts.Verifier) + } + req, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("resp: %+v\n", resp.StatusCode) + return nil, ErrInvalidAuthorizationCode + } + + var tokens Tokens + if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil { + return nil, fmt.Errorf("failed to decode tokens: %w", err) + } + + return &ExchangeSuccess{Tokens: tokens}, nil +} + +// Refresh implements the token if they have expires. This is used in an SPA app to maintain the +// session, without logging the user out. +// +// result, err := client.Refresh(refreshToken, &RefreshOptions{}) +// +// Can optionally take the access token as well. If passed in, this will skip the refresh +// if the access token is still valid. +// +// result, err := client.Refresh(refreshToken, &RefreshOptions{ +// Access: accessToken, +// }) +// +// This returns the refreshed tokens only if they've been refreshed. +// +// if result.Tokens != nil { +// // tokens are refreshed +// } +// +// Or if it fails, it returns an error that you can handle depending on the error. +// +// if err != nil { +// if errors.Is(err, ErrInvalidRefreshToken) { +// // handle invalid refresh token error +// } else { +// // handle other errors +// } +// } +func (c *Client) Refresh(refreshToken string, opts *RefreshOptions) (*RefreshSuccess, error) { + if opts != nil && opts.Access != "" { + parsed, err := jwt.ParseInsecure([]byte(opts.Access)) + if err != nil { + return nil, ErrInvalidAccessToken + } + exp, ok := parsed.Expiration() + if ok && exp.After(time.Now().Add(time.Second*30)) { + return &RefreshSuccess{}, nil + } + } + + issuerEndpoint := c.issuer + "/token" + data := url.Values{} + data.Set("grant_type", "refresh_token") + data.Set("refresh_token", refreshToken) + + req, err := http.NewRequest("POST", issuerEndpoint, strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, ErrInvalidRefreshToken + } + + var tokens Tokens + if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil { + return nil, ErrInvalidRefreshToken + } + + return &RefreshSuccess{Tokens: &tokens}, nil +} + +// Verify the token in the incoming request. +// +// This is typically used for SSR sites where the token is stored in an HTTP only cookie. And +// is passed to the server on every request. +// +// result, err := client.Verify(token, &VerifyOptions{}) +// +// This optionally takes the refresh token as well. If passed in, it'll automatically +// refresh the access token if it has expired. +// +// result, err := client.Verify(token, &VerifyOptions{ +// Refresh: refreshToken, +// }) +// +// This returns the decoded subjects from the access token. And the tokens if they've been +// refreshed. +// +// if result.Tokens != nil { +// // tokens are refreshed +// } +// +// Or if it fails, it returns an error that you can handle depending on the error. +// +// if err != nil { +// if errors.Is(err, ErrInvalidRefreshToken) { +// // handle invalid refresh token error +// } else { +// // handle other errors +// } +// } +func (c *Client) Verify(token string, options *VerifyOptions) (*VerifyResult, error) { + jwks, err := c.getJWKS() + if err != nil { + return nil, fmt.Errorf("failed to get JWKS: %w", err) + } + + var opts []jwt.ParseOption + opts = append(opts, jwt.WithKeySet(jwks)) + if options.issuer != "" { + opts = append(opts, jwt.WithIssuer(options.issuer)) + } + if options.audience != "" { + opts = append(opts, jwt.WithAudience(options.audience)) + } + + parsed, err := jwt.ParseString(token, opts...) + if err != nil { + // Check if token is expired and we have a refresh token + if options.Refresh != "" && errors.Is(err, jwt.TokenExpiredError()) { + refreshed, err := c.Refresh(options.Refresh, &RefreshOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to refresh token: %w", err) + } + if refreshed.Tokens == nil { + panic("should have tokens when refreshing without access token") + } + + // Recursively verify the new token + verified, err := c.Verify(refreshed.Tokens.Access, &VerifyOptions{ + Refresh: refreshed.Tokens.Refresh, + issuer: options.issuer, + audience: options.audience, + }) + if err != nil { + return nil, fmt.Errorf("failed to verify refreshed token: %w", err) + } + verified.Tokens = refreshed.Tokens + return verified, nil + } + return nil, fmt.Errorf("failed to parse token: %w", err) + } + + // Get standard claims + sub, ok := parsed.Subject() + if !ok || sub == "" { + return nil, errors.New("missing subject") + } + + audiences, ok := parsed.Audience() + if !ok || len(audiences) == 0 { + return nil, errors.New("missing audience") + } + + // Get private claims + var mode interface{} + if err := parsed.Get("mode", &mode); err != nil { + return nil, errors.New("missing mode claim") + } + modeStr, ok := mode.(string) + if !ok || modeStr != "access" { + return nil, errors.New("invalid token mode") + } + + var typ interface{} + if err := parsed.Get("type", &typ); err != nil { + return nil, errors.New("missing type claim") + } + typeStr, ok := typ.(string) + if !ok { + return nil, errors.New("invalid type format") + } + + var props interface{} + if err := parsed.Get("properties", &props); err != nil { + return nil, errors.New("missing properties") + } + validator, ok := (*c.subjectSchema)[typeStr] + if !ok { + return nil, errors.New("missing validator for type") + } + properties, err := validator(props) + if err != nil { + return nil, err + } + + return &VerifyResult{ + Tokens: nil, // Only set if token was refreshed + aud: audiences[0], + Subject: &Subject{ + ID: sub, + Type: typeStr, + Properties: properties, + }, + }, nil +} + +// Decode a JWT token without verifying its signature. +// +// This is typically used for SSR sites where the token is stored in an HTTP only cookie. And +// is passed to the server on every request. +// +// result, err := client.Decode(token) +// +// This returns the decoded token's subject if successful. +// +// if err != nil { +// // handle error +// } +func (c *Client) Decode(token string) (*DecodeSuccess, error) { + parsed, err := jwt.ParseInsecure([]byte(token)) + if err != nil { + return nil, ErrInvalidAccessToken + } + + sub, ok := parsed.Subject() + if !ok || sub == "" { + return nil, ErrInvalidAccessToken + } + + var typ interface{} + if err := parsed.Get("type", &typ); err != nil { + return nil, ErrInvalidAccessToken + } + typeStr, ok := typ.(string) + if !ok { + return nil, ErrInvalidAccessToken + } + + var props interface{} + if err := parsed.Get("properties", &props); err != nil { + return nil, ErrInvalidAccessToken + } + validator, ok := (*c.subjectSchema)[typeStr] + if !ok { + return nil, errors.New("missing validator for type") + } + properties, err := validator(props) + if err != nil { + return nil, err + } + + return &DecodeSuccess{ + Subject: &Subject{ID: sub, Type: typeStr, Properties: properties}, + }, nil +} diff --git a/packages/openauth-go/go.mod b/packages/openauth-go/go.mod new file mode 100644 index 00000000..82de0357 --- /dev/null +++ b/packages/openauth-go/go.mod @@ -0,0 +1,20 @@ +module github.com/toolbeam/openauth + +go 1.23.5 + +require ( + github.com/google/uuid v1.6.0 + github.com/lestrrat-go/jwx/v3 v3.0.0 +) + +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sys v0.31.0 // indirect +) diff --git a/packages/openauth-go/go.sum b/packages/openauth-go/go.sum new file mode 100644 index 00000000..58b145d8 --- /dev/null +++ b/packages/openauth-go/go.sum @@ -0,0 +1,36 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 h1:pzDjP9dSONCFQC/AE3mWUnHILGiYPiMKzQIS+weKJXA= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1/go.mod h1:wdsgouffPvWPEYh8t7PRH/PidR5sfVqt0na4Nhj60Ms= +github.com/lestrrat-go/jwx/v3 v3.0.0 h1:IRnFNdZx5dJHjTpPVkYqP6TRahJI2Z9v43UwEDJcj6U= +github.com/lestrrat-go/jwx/v3 v3.0.0/go.mod h1:ak32WoNtHE0aLowVWBcCvXngcAnW4tuC0YhFwOr/kwc= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packages/openauth-go/internal/util/pkce.go b/packages/openauth-go/internal/util/pkce.go new file mode 100644 index 00000000..7efaa3d1 --- /dev/null +++ b/packages/openauth-go/internal/util/pkce.go @@ -0,0 +1,60 @@ +package util + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" +) + +const ( + PKCEMethodS256 = "S256" + PKCEMethodPlain = "plain" + PKCEDefaultLength = 64 +) + +func generateVerifier(length int) (string, error) { + buffer := make([]byte, length) + _, err := rand.Read(buffer) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(buffer), nil +} + +func generateChallenge(verifier, method string) (string, error) { + if method == PKCEMethodPlain { + return verifier, nil + } + hasher := sha256.New() + hasher.Write([]byte(verifier)) + hash := hasher.Sum(nil) + return base64.URLEncoding.EncodeToString(hash), nil +} + +func GeneratePKCE(length ...int) (verifier, challenge, method string, err error) { + l := PKCEDefaultLength + if len(length) > 0 { + l = length[0] + } + if l < 43 || l > 128 { + return "", "", "", errors.New("code verifier length must be between 43 and 128 characters") + } + verifier, err = generateVerifier(l) + if err != nil { + return "", "", "", err + } + challenge, err = generateChallenge(verifier, PKCEMethodS256) + if err != nil { + return "", "", "", err + } + return verifier, challenge, PKCEMethodS256, nil +} + +func ValidatePKCE(verifier, challenge, method string) (bool, error) { + generatedChallenge, err := generateChallenge(verifier, method) + if err != nil { + return false, err + } + return generatedChallenge == challenge, nil +} diff --git a/packages/openauth-go/subject/subject.go b/packages/openauth-go/subject/subject.go new file mode 100644 index 00000000..2e9785c4 --- /dev/null +++ b/packages/openauth-go/subject/subject.go @@ -0,0 +1,5 @@ +package subject + +type SubjectValidator[T any] func(data any) (T, error) + +type SubjectSchemas map[string]SubjectValidator[any] From 70786a13f668b66538ebb6e48d1c095c21cca1e3 Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Sat, 12 Apr 2025 19:59:41 -0400 Subject: [PATCH 22/23] add go client example --- examples/client/go/issuer.ts | 34 +++++++ examples/client/go/package.json | 9 ++ examples/client/go/src/go.mod | 26 ++++++ examples/client/go/src/go.sum | 58 ++++++++++++ examples/client/go/src/main.go | 151 +++++++++++++++++++++++++++++++ examples/client/go/sst-env.d.ts | 19 ++++ examples/client/go/sst.config.ts | 29 ++++++ examples/client/go/subjects.ts | 8 ++ examples/client/go/tsconfig.json | 10 ++ 9 files changed, 344 insertions(+) create mode 100644 examples/client/go/issuer.ts create mode 100644 examples/client/go/package.json create mode 100644 examples/client/go/src/go.mod create mode 100644 examples/client/go/src/go.sum create mode 100644 examples/client/go/src/main.go create mode 100644 examples/client/go/sst-env.d.ts create mode 100644 examples/client/go/sst.config.ts create mode 100644 examples/client/go/subjects.ts create mode 100644 examples/client/go/tsconfig.json diff --git a/examples/client/go/issuer.ts b/examples/client/go/issuer.ts new file mode 100644 index 00000000..fcaad24f --- /dev/null +++ b/examples/client/go/issuer.ts @@ -0,0 +1,34 @@ +import { handle } from "hono/aws-lambda" +import { issuer } from "@openauthjs/openauth/issuer" +import { CodeProvider } from "@openauthjs/openauth/provider/code" +import { CodeUI } from "@openauthjs/openauth/ui/code" +import { subjects } from "./subjects" + +async function getUser(email: string) { + // Get user from database + // Return user ID + return "123" +} + +const app = issuer({ + subjects, + providers: { + code: CodeProvider( + CodeUI({ + async sendCode(claims, code) { + console.log("sendCode", claims, code) + }, + }), + ), + }, + success: async (ctx, value) => { + if (value.provider === "code") { + const id = await getUser(value.claims.email) + return ctx.subject("user", id, { id }) + } + throw new Error("Invalid provider") + }, +}) + +// @ts-ignore +export const handler = handle(app) diff --git a/examples/client/go/package.json b/examples/client/go/package.json new file mode 100644 index 00000000..29e80402 --- /dev/null +++ b/examples/client/go/package.json @@ -0,0 +1,9 @@ +{ + "name": "@openauthjs/example-client-go", + "type": "module", + "version": "0.0.0", + "dependencies": { + "@openauthjs/openauth": "workspace:*", + "sst": "3.5.1" + } +} diff --git a/examples/client/go/src/go.mod b/examples/client/go/src/go.mod new file mode 100644 index 00000000..5e42761a --- /dev/null +++ b/examples/client/go/src/go.mod @@ -0,0 +1,26 @@ +module example-client-go + +go 1.23.5 + +require ( + github.com/aws/aws-lambda-go v1.48.0 + github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 + github.com/sst/sst/v3 v3.13.10 + github.com/toolbeam/openauth v0.0.0 +) + +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.0 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sys v0.31.0 // indirect +) + +replace github.com/toolbeam/openauth => ../../../../packages/openauth-go diff --git a/examples/client/go/src/go.sum b/examples/client/go/src/go.sum new file mode 100644 index 00000000..a2b440f4 --- /dev/null +++ b/examples/client/go/src/go.sum @@ -0,0 +1,58 @@ +github.com/aws/aws-lambda-go v1.48.0 h1:1aZUYsrJu0yo5fC4z+Rba1KhNImXcJcvHu763BxoyIo= +github.com/aws/aws-lambda-go v1.48.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 h1:pzDjP9dSONCFQC/AE3mWUnHILGiYPiMKzQIS+weKJXA= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1/go.mod h1:wdsgouffPvWPEYh8t7PRH/PidR5sfVqt0na4Nhj60Ms= +github.com/lestrrat-go/jwx/v3 v3.0.0 h1:IRnFNdZx5dJHjTpPVkYqP6TRahJI2Z9v43UwEDJcj6U= +github.com/lestrrat-go/jwx/v3 v3.0.0/go.mod h1:ak32WoNtHE0aLowVWBcCvXngcAnW4tuC0YhFwOr/kwc= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sst/sst/v3 v3.13.10 h1:DohLPpekXerhU8YHM9M8kbx8YB3W4o7tW2qO2b/Ro9g= +github.com/sst/sst/v3 v3.13.10/go.mod h1:t0+Krbn45bPEvo19BNbP3TACUS6J7TLbM04YvuoX3Gg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/client/go/src/main.go b/examples/client/go/src/main.go new file mode 100644 index 00000000..7bf4cd7b --- /dev/null +++ b/examples/client/go/src/main.go @@ -0,0 +1,151 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + "github.com/sst/sst/v3/sdk/golang/resource" + "github.com/toolbeam/openauth/client" + "github.com/toolbeam/openauth/subject" +) + +func getOrigin(u *url.URL) string { + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) +} + +type UserSubject struct { + Id string `json:"id"` +} + +func main() { + + authUrl, err := resource.Get("Auth", "url") + if err != nil { + panic(err) + } + var authUrlString string + authUrlString = authUrl.(string) + if authUrlString == "" { + panic("authUrl is empty") + } + + // setup the openauth client + authClient, err := client.NewClient(client.ClientInput{ + ClientID: "lambda-api-go", + Issuer: authUrlString, + SubjectSchema: subject.SubjectSchemas{ + "user": func(properties any) (any, error) { + user, ok := properties.(map[string]any) + if !ok { + return nil, errors.New("invalid user type") + } + if user["id"] == nil { + return nil, errors.New("id is required") + } + // can do other validation here if there are other properties + return UserSubject{Id: user["id"].(string)}, nil + }, + }, + }) + + if err != nil { + panic(err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + accessCookie, err := r.Cookie("access_token") + if err != nil { + http.Redirect(w, r, "/authorize", http.StatusSeeOther) + return + } + verified, err := authClient.Verify(accessCookie.Value, &client.VerifyOptions{}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if verified.Tokens != nil { + http.SetCookie(w, &http.Cookie{ + Name: "access_token", + Value: verified.Tokens.Access, + MaxAge: 34560000, + SameSite: http.SameSiteStrictMode, + Path: "/", + HttpOnly: true, + }) + http.SetCookie(w, &http.Cookie{ + Name: "refresh_token", + Value: verified.Tokens.Refresh, + MaxAge: 34560000, + SameSite: http.SameSiteStrictMode, + Path: "/", + HttpOnly: true, + }) + } + w.Header().Set("Content-Type", "application/json") + // can do check on the type of the subject to get the correct type to cast to + if verified.Subject.Type != "user" { + http.Error(w, "invalid subject type", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(verified.Subject.Properties.(UserSubject)) + }) + mux.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) { + origin := getOrigin(r.URL) + fmt.Printf("origin: %+v\n", origin) + redirectURI := origin + "/callback" + authorize, err := authClient.Authorize(redirectURI, "code", &client.AuthorizeOptions{}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, authorize.URL, http.StatusSeeOther) + }) + + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + origin := getOrigin(r.URL) + if origin == "" { + http.Error(w, "Origin header is required", http.StatusBadRequest) + return + } + redirectURI := origin + "/callback" + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "Code is required", http.StatusBadRequest) + return + } + exchanged, err := authClient.Exchange(code, redirectURI, &client.ExchangeOptions{}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Printf("exchanged: %+v\n", exchanged) + fmt.Printf("exchanged.Tokens: %+v\n", exchanged.Tokens) + fmt.Printf("exchanged.Tokens.Access: %+v\n", exchanged.Tokens.Access) + fmt.Printf("exchanged.Tokens.Refresh: %+v\n", exchanged.Tokens.Refresh) + http.SetCookie(w, &http.Cookie{ + Name: "access_token", + Value: exchanged.Tokens.Access, + MaxAge: 34560000, + SameSite: http.SameSiteStrictMode, + Path: "/", + HttpOnly: true, + }) + http.SetCookie(w, &http.Cookie{ + Name: "refresh_token", + Value: exchanged.Tokens.Refresh, + MaxAge: 34560000, + SameSite: http.SameSiteStrictMode, + Path: "/", + HttpOnly: true, + }) + http.Redirect(w, r, origin, http.StatusSeeOther) + }) + + lambda.Start(httpadapter.NewV2(mux).ProxyWithContext) +} diff --git a/examples/client/go/sst-env.d.ts b/examples/client/go/sst-env.d.ts new file mode 100644 index 00000000..f3f05451 --- /dev/null +++ b/examples/client/go/sst-env.d.ts @@ -0,0 +1,19 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "ApiGo": { + "name": string + "type": "sst.aws.Function" + "url": string + } + "Auth": { + "type": "sst.aws.Auth" + "url": string + } + } +} diff --git a/examples/client/go/sst.config.ts b/examples/client/go/sst.config.ts new file mode 100644 index 00000000..ff07d950 --- /dev/null +++ b/examples/client/go/sst.config.ts @@ -0,0 +1,29 @@ +/// + +export default $config({ + app(input) { + return { + name: "openauth", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + + providers: { + aws: { + region: "us-east-1", + }, + }, + } + }, + async run() { + const auth = new sst.aws.Auth("Auth", { + issuer: "./issuer.handler", + }) + + const apiGo = new sst.aws.Function("ApiGo", { + url: true, + runtime: "go", + handler: "./src", + link: [auth], + }) + }, +}) diff --git a/examples/client/go/subjects.ts b/examples/client/go/subjects.ts new file mode 100644 index 00000000..479fefad --- /dev/null +++ b/examples/client/go/subjects.ts @@ -0,0 +1,8 @@ +import { object, string } from "valibot" +import { createSubjects } from "@openauthjs/openauth/subject" + +export const subjects = createSubjects({ + user: object({ + id: string(), + }), +}) diff --git a/examples/client/go/tsconfig.json b/examples/client/go/tsconfig.json new file mode 100644 index 00000000..0ccdf2e4 --- /dev/null +++ b/examples/client/go/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} From 1342dd20a3001eed7b4d9b165b3d8bba17305153 Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Sat, 12 Apr 2025 20:03:44 -0400 Subject: [PATCH 23/23] clean up --- examples/client/go/src/main.go | 59 +++++++++++++--------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/examples/client/go/src/main.go b/examples/client/go/src/main.go index 7bf4cd7b..c6afb7ad 100644 --- a/examples/client/go/src/main.go +++ b/examples/client/go/src/main.go @@ -70,22 +70,7 @@ func main() { return } if verified.Tokens != nil { - http.SetCookie(w, &http.Cookie{ - Name: "access_token", - Value: verified.Tokens.Access, - MaxAge: 34560000, - SameSite: http.SameSiteStrictMode, - Path: "/", - HttpOnly: true, - }) - http.SetCookie(w, &http.Cookie{ - Name: "refresh_token", - Value: verified.Tokens.Refresh, - MaxAge: 34560000, - SameSite: http.SameSiteStrictMode, - Path: "/", - HttpOnly: true, - }) + setCookies(w, verified.Tokens.Access, verified.Tokens.Refresh) } w.Header().Set("Content-Type", "application/json") // can do check on the type of the subject to get the correct type to cast to @@ -97,7 +82,6 @@ func main() { }) mux.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) { origin := getOrigin(r.URL) - fmt.Printf("origin: %+v\n", origin) redirectURI := origin + "/callback" authorize, err := authClient.Authorize(redirectURI, "code", &client.AuthorizeOptions{}) if err != nil { @@ -124,28 +108,29 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } - fmt.Printf("exchanged: %+v\n", exchanged) - fmt.Printf("exchanged.Tokens: %+v\n", exchanged.Tokens) - fmt.Printf("exchanged.Tokens.Access: %+v\n", exchanged.Tokens.Access) - fmt.Printf("exchanged.Tokens.Refresh: %+v\n", exchanged.Tokens.Refresh) - http.SetCookie(w, &http.Cookie{ - Name: "access_token", - Value: exchanged.Tokens.Access, - MaxAge: 34560000, - SameSite: http.SameSiteStrictMode, - Path: "/", - HttpOnly: true, - }) - http.SetCookie(w, &http.Cookie{ - Name: "refresh_token", - Value: exchanged.Tokens.Refresh, - MaxAge: 34560000, - SameSite: http.SameSiteStrictMode, - Path: "/", - HttpOnly: true, - }) + + setCookies(w, exchanged.Tokens.Access, exchanged.Tokens.Refresh) http.Redirect(w, r, origin, http.StatusSeeOther) }) lambda.Start(httpadapter.NewV2(mux).ProxyWithContext) } + +func setCookies(w http.ResponseWriter, access, refresh string) { + http.SetCookie(w, &http.Cookie{ + Name: "access_token", + Value: access, + MaxAge: 34560000, + SameSite: http.SameSiteStrictMode, + Path: "/", + HttpOnly: true, + }) + http.SetCookie(w, &http.Cookie{ + Name: "refresh_token", + Value: refresh, + MaxAge: 34560000, + SameSite: http.SameSiteStrictMode, + Path: "/", + HttpOnly: true, + }) +}