diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts index 1a654a9..ada728a 100644 --- a/apps/api/src/auth.ts +++ b/apps/api/src/auth.ts @@ -1,6 +1,6 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { genericOAuth } from "better-auth/plugins"; +import { bearer, deviceAuthorization, genericOAuth } from "better-auth/plugins"; import { expo } from "@better-auth/expo"; import { drizzleSchema } from "@money/shared/db"; import { db } from "./db"; @@ -37,6 +37,8 @@ export const auth = betterAuth({ scopes: ["profile", "email"], } ] - }) + }), + deviceAuthorization(), + bearer(), ] }); diff --git a/apps/api/src/zero.ts b/apps/api/src/zero.ts index 8817369..ec54755 100644 --- a/apps/api/src/zero.ts +++ b/apps/api/src/zero.ts @@ -99,6 +99,8 @@ const createMutators = (authData: AuthData | null) => { id: randomUUID(), userId: authData.user.id, token: data.access_token, + logoUrl: "", + name: "" }); }, diff --git a/apps/expo/app/_layout.tsx b/apps/expo/app/_layout.tsx index e3ceeee..ea3f929 100644 --- a/apps/expo/app/_layout.tsx +++ b/apps/expo/app/_layout.tsx @@ -45,6 +45,7 @@ export default function RootLayout() { + diff --git a/apps/expo/app/approve.tsx b/apps/expo/app/approve.tsx new file mode 100644 index 0000000..c3f29da --- /dev/null +++ b/apps/expo/app/approve.tsx @@ -0,0 +1,23 @@ +import { authClient } from "@/lib/auth-client"; +import { useLocalSearchParams } from "expo-router"; +import { useEffect } from "react"; +import { Text } from "react-native"; + +export default function Page() { + const { code } = useLocalSearchParams<{code: string }>(); + const { isPending, data } = authClient.useSession(); + if (isPending) return Loading...; + if (!isPending && !data) return Please log in; + + useEffect(() => { + authClient.device.approve({ + userCode: code, + }); + + }, []); + + return + Approving: {code} + +} + diff --git a/apps/expo/lib/auth-client.ts b/apps/expo/lib/auth-client.ts index f59dd4b..94eb257 100644 --- a/apps/expo/lib/auth-client.ts +++ b/apps/expo/lib/auth-client.ts @@ -1,5 +1,5 @@ import { createAuthClient } from "better-auth/react"; -import { genericOAuthClient } from "better-auth/client/plugins"; +import { deviceAuthorizationClient, genericOAuthClient } from "better-auth/client/plugins"; import { expoClient } from "@better-auth/expo/client"; import * as SecureStore from "expo-secure-store"; import { BASE_URL } from "@money/shared"; @@ -13,5 +13,6 @@ export const authClient = createAuthClient({ storage: SecureStore, }), genericOAuthClient(), + deviceAuthorizationClient(), ] }); diff --git a/apps/tui/lib/auth-client.ts b/apps/tui/lib/auth-client.ts new file mode 100644 index 0000000..5050398 --- /dev/null +++ b/apps/tui/lib/auth-client.ts @@ -0,0 +1,11 @@ +import { createAuthClient } from "better-auth/client"; +import { deviceAuthorizationClient } from "better-auth/client/plugins"; + +export const authClient = createAuthClient({ + baseURL: "http://laptop:3000", + plugins: [ + deviceAuthorizationClient(), + ] +}); + + diff --git a/apps/tui/package.json b/apps/tui/package.json index 7e085ea..b4e7356 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -9,10 +9,12 @@ "react": "*" }, "dependencies": { - "@money/ui": "workspace:*", "@money/shared": "workspace:*", + "@money/ui": "workspace:*", "@opentui/core": "^0.1.39", "@opentui/react": "^0.1.39", + "@types/qrcode": "^1.5.6", + "qrcode": "^1.5.4", "react-native": "^0.82.1", "react-native-opentui": "workspace:*" }, diff --git a/apps/tui/src/auth.tsx b/apps/tui/src/auth.tsx new file mode 100644 index 0000000..cfeebc3 --- /dev/null +++ b/apps/tui/src/auth.tsx @@ -0,0 +1,79 @@ +import { authClient } from "@/lib/auth-client"; +import { use, useEffect, useState } from "react"; +import { AuthContext } from "./auth/context"; +import * as Code from "./auth/code"; + +const CLIENT_ID = "koon-family"; + +export function Auth() { + const { setAuth } = use(AuthContext); + + const [code, setCode] = useState(); + const [error, setError] = useState(); + + const pollForToken = async (code: string, interval = 5) => { + const { data, error } = await authClient.device.token({ + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + device_code: code, + client_id: CLIENT_ID, + fetchOptions: { headers: { "user-agent": "My CLI" } }, + }); + + if (data?.access_token) { + const { data: sessionData, error } = await authClient.getSession({ + fetchOptions: { + auth: { + type: "Bearer", + token: data.access_token + } + } + }); + if (error) return setError(error.message); + if (!sessionData) return setError("No data"); + + setAuth({ + token: data.access_token, + auth: sessionData, + }); + } + + if (error) { + if (error.error === "authorization_pending") { + setTimeout(() => pollForToken(code, interval), interval * 1000); + } else if (error.error === "slow_down") { + setTimeout(() => pollForToken(code, interval + 5), (interval + 5) * 1000); + } else { + setError(`${error}`); + } + } + } + + async function getCode() { + try { + const { data, error } = await authClient.device.code({ + client_id: CLIENT_ID, + scope: "openid profile email", + }); + if (error) return setError(error.error_description); + if (!data) return setError("No data returned"); + + setCode(data.user_code); + + pollForToken(data?.device_code); + + } catch (e) { + setError(`${e}`); + } + } + + + useEffect(() => { + getCode(); + }, []); + + if (error) return + + return !code ? : ; +} + + diff --git a/apps/tui/src/auth/code.tsx b/apps/tui/src/auth/code.tsx new file mode 100644 index 0000000..56365ea --- /dev/null +++ b/apps/tui/src/auth/code.tsx @@ -0,0 +1,48 @@ +import { QR } from "@/util/qr"; +import type { ReactNode } from "react"; + +function CodeDisplay({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + + +export function Display({ code }: { code: string }) { + const URL = `https://money.koon.us/approve?code=${code}`; + + return + {QR(URL)} + + Welcome to Koon Money + Go to: {URL} + Code: {code} + + +} + +export function Loading() { + return + + + Welcome to Koon Money + You need to login first + Loading login information + + +} + +export function Error({ msg }: { msg: string }) { + return + + Could not login + {msg} + + +} + + diff --git a/apps/tui/src/auth/context.ts b/apps/tui/src/auth/context.ts new file mode 100644 index 0000000..5c54944 --- /dev/null +++ b/apps/tui/src/auth/context.ts @@ -0,0 +1,19 @@ +import { type AuthData } from "@money/shared/auth"; +import { createContext } from "react"; + +export type AuthType = { + auth: AuthData; + token: string; +}; + +export type AuthContextType = { + auth: AuthType | null; + setAuth: (auth: AuthContextType['auth']) => void; +}; + +export const AuthContext = createContext({ + auth: null, + setAuth: () => {} +}); + + diff --git a/apps/tui/src/index.tsx b/apps/tui/src/index.tsx index e5b5e05..444ef90 100644 --- a/apps/tui/src/index.tsx +++ b/apps/tui/src/index.tsx @@ -1,28 +1,57 @@ -import { RGBA, TextAttributes, createCliRenderer } from "@opentui/core"; +import { createCliRenderer } from "@opentui/core"; import { createRoot } from "@opentui/react"; import { App, type Route } from "@money/ui"; import { ZeroProvider } from "@rocicorp/zero/react"; import { schema } from '@money/shared'; -import { useState } from "react"; +import { use, useState } from "react"; +import { writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "fs"; +import { join } from "path"; +import { homedir } from "os"; +import { Auth } from "./auth"; +import { AuthContext, type AuthType } from "./auth/context"; const userID = "anon"; const server = "http://laptop:4848"; -const auth = undefined; +// const auth = undefined; + +const PATH = join(homedir(), ".local", "share", "money"); + +function Main({ auth: initalAuth }: { auth: AuthType | null }) { + const [auth, setAuth] = useState(initalAuth); -function Main() { return ( - + { + if (auth) { + mkdirSync(PATH, { recursive: true }); + writeFileSync(PATH + "/auth.json", JSON.stringify(auth)); + setAuth(auth); + } else { + rmSync(PATH + "token"); + } + } }}> + {auth ? : } + + ); +} + + + +function Authed() { + const { auth } = use(AuthContext); + return ( + ); } function Router() { + const { auth } = use(AuthContext); const [route, setRoute] = useState("/"); return ( @@ -30,4 +59,5 @@ function Router() { } const renderer = await createCliRenderer(); -createRoot(renderer).render(
); +const auth = existsSync(PATH + "/auth.json") ? JSON.parse(readFileSync(PATH + "/auth.json", 'utf8')) as AuthType : null; +createRoot(renderer).render(
); diff --git a/apps/tui/tsconfig.json b/apps/tui/tsconfig.json index 7c23f5a..7b40acf 100644 --- a/apps/tui/tsconfig.json +++ b/apps/tui/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "paths": { - "react-native": ["../react-native-opentui"] + "react-native": ["../react-native-opentui"], + "@/*": ["./*"] }, // Environment setup & latest features "lib": ["ESNext"], diff --git a/apps/tui/util/qr.ts b/apps/tui/util/qr.ts new file mode 100644 index 0000000..778a7fb --- /dev/null +++ b/apps/tui/util/qr.ts @@ -0,0 +1,26 @@ +import QRCode from "qrcode"; + +export function QR(value: string): string { + const qr = QRCode.create(value, { + errorCorrectionLevel: "L", + version: 3, + }); + + const m = qr.modules.data; + const size = qr.modules.size; + + // Use half-block characters to compress vertically + // Upper half = '▀', lower half = '▄', full = '█', empty = ' ' + let out = ""; + for (let y = 0; y < size; y += 2) { + for (let x = 0; x < size; x++) { + const top = m[y * size + x]; + const bottom = m[(y + 1) * size + x]; + out += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " "; + } + out += "\n"; + } + return out; +} + + diff --git a/packages/shared/src/db/schema/private.ts b/packages/shared/src/db/schema/private.ts index e9e27b3..15185c9 100644 --- a/packages/shared/src/db/schema/private.ts +++ b/packages/shared/src/db/schema/private.ts @@ -5,6 +5,7 @@ import { text, timestamp, uniqueIndex, + integer, } from "drizzle-orm/pg-core"; import { users } from "./public"; @@ -93,3 +94,19 @@ export const auditLogs = pgTable("audit_log", { action: text("action").notNull(), }); +export const deviceCodes = pgTable("deviceCode", { + id: text("id").primaryKey(), + deviceCode: text("device_code").notNull(), + userCode: text("user_code").notNull(), + userId: text("user_id").references(() => users.id, { + onDelete: "set null", + }), + clientId: text("client_id"), + scope: text("scope"), + status: text("status").notNull(), + expiresAt: timestamp("expires_at"), + lastPolledAt: timestamp("last_polled_at"), + pollingInterval: integer("polling_interval"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index b8e8a84..99479a5 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -3,6 +3,7 @@ import { Transactions } from "./transactions"; import { View, Text } from "react-native"; import { Settings } from "./settings"; import { useKeyboard } from "./useKeyboard"; +import type { AuthData } from "@money/shared/auth"; const PAGES = { @@ -41,10 +42,8 @@ type Routes = { export type Route = Routes; -type Auth = any; - interface RouterContextType { - auth: Auth; + auth: AuthData | null; route: Route; setRoute: (route: Route) => void; } @@ -58,7 +57,7 @@ export const RouterContext = createContext({ type AppProps = { - auth: Auth; + auth: AuthData | null; route: Route; setRoute: (page: Route) => void; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50383a1..402410c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,12 @@ importers: '@opentui/react': specifier: ^0.1.39 version: 0.1.39(react@19.1.0)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: '*' version: 19.1.0 @@ -2854,6 +2860,9 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react@19.1.17': resolution: {integrity: sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==} @@ -3527,6 +3536,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3684,6 +3696,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -3744,6 +3760,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -5705,6 +5724,10 @@ packages: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + pngjs@6.0.0: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} @@ -5826,6 +5849,11 @@ packages: resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} hasBin: true + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -6052,6 +6080,9 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requireg@0.2.2: resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} engines: {node: '>= 4.0.0'} @@ -6188,6 +6219,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -6787,6 +6821,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} @@ -6812,6 +6849,10 @@ packages: resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} engines: {node: '>=12.17'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6889,6 +6930,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6905,10 +6949,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -10598,6 +10650,10 @@ snapshots: pg-protocol: 1.10.3 pg-types: 2.2.0 + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.10.0 + '@types/react@19.1.17': dependencies: csstype: 3.1.3 @@ -11345,6 +11401,12 @@ snapshots: client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -11503,6 +11565,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decode-uri-component@0.2.2: {} decompress-response@6.0.0: @@ -11547,6 +11611,8 @@ snapshots: detect-node-es@1.1.0: {} + dijkstrajs@1.0.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -14037,6 +14103,8 @@ snapshots: pngjs@3.4.0: {} + pngjs@5.0.0: {} + pngjs@6.0.0: {} pngjs@7.0.0: {} @@ -14153,6 +14221,12 @@ snapshots: qrcode-terminal@0.11.0: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -14648,6 +14722,8 @@ snapshots: transitivePeerDependencies: - supports-color + require-main-filename@2.0.0: {} + requireg@0.2.2: dependencies: nested-error-stacks: 2.0.1 @@ -14797,6 +14873,8 @@ snapshots: server-only@0.0.1: {} + set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -15479,6 +15557,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 @@ -15503,6 +15583,12 @@ snapshots: wordwrapjs@5.1.1: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -15553,6 +15639,8 @@ snapshots: xtend@4.0.2: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -15561,8 +15649,27 @@ snapshots: yaml@2.8.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0e6371f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "compilerOptions": {}, + "extends": "expo/tsconfig.base" +}