format: format with biome

This commit is contained in:
Max Koon
2025-11-24 22:20:40 -05:00
parent 01edded95a
commit 6fd531d9c3
41 changed files with 1025 additions and 667 deletions

View File

@@ -20,25 +20,25 @@ export const auth = betterAuth({
"money://",
],
advanced: {
crossSubDomainCookies: {
enabled: process.env.NODE_ENV == 'production',
domain: "koon.us",
},
crossSubDomainCookies: {
enabled: process.env.NODE_ENV == "production",
domain: "koon.us",
},
},
plugins: [
expo(),
genericOAuth({
config: [
{
providerId: 'koon-family',
providerId: "koon-family",
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
discoveryUrl: process.env.OAUTH_DISCOVERY_URL!,
scopes: ["profile", "email"],
}
]
},
],
}),
deviceAuthorization(),
bearer(),
]
],
});

View File

@@ -12,7 +12,7 @@ const app = getHono();
app.use(
"/api/*",
cors({
origin: ['https://money.koon.us', `${BASE_URL}:8081`],
origin: ["https://money.koon.us", `${BASE_URL}:8081`],
allowMethods: ["POST", "GET", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,

View File

@@ -1,13 +1,15 @@
import { Configuration, PlaidApi, PlaidEnvironments } from "plaid";
const configuration = new Configuration({
basePath: process.env.PLAID_ENV == 'production' ? PlaidEnvironments.production : PlaidEnvironments.sandbox,
basePath:
process.env.PLAID_ENV == "production"
? PlaidEnvironments.production
: PlaidEnvironments.sandbox,
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
'PLAID-SECRET': process.env.PLAID_SECRET,
}
"PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID,
"PLAID-SECRET": process.env.PLAID_SECRET,
},
},
});
export const plaidClient = new PlaidApi(configuration);

View File

@@ -3,12 +3,9 @@ import { plaidClient } from "./plaid";
// import { LinkSessionFinishedWebhook, WebhookType } from "plaid";
export const webhook = async (c: Context) => {
console.log("Got webhook");
const b = await c.req.text();
console.log("body:", b);
return c.text("Hi");
}
};

View File

@@ -8,8 +8,8 @@ import {
PushProcessor,
ZQLDatabase,
} from "@rocicorp/zero/server";
import { PostgresJSConnection } from '@rocicorp/zero/pg';
import postgres from 'postgres';
import { PostgresJSConnection } from "@rocicorp/zero/pg";
import postgres from "postgres";
import {
createMutators as createMutatorsShared,
isLoggedIn,
@@ -20,11 +20,22 @@ import {
} from "@money/shared";
import type { AuthData } from "@money/shared/auth";
import { getHono } from "./hono";
import { Configuration, CountryCode, PlaidApi, PlaidEnvironments, Products } from "plaid";
import {
Configuration,
CountryCode,
PlaidApi,
PlaidEnvironments,
Products,
} from "plaid";
import { randomUUID } from "crypto";
import { db } from "./db";
import { balance, plaidAccessTokens, plaidLink, transaction } from "@money/shared/db";
import { eq, inArray, sql, type InferInsertModel } from "drizzle-orm";
import {
balance,
plaidAccessTokens,
plaidLink,
transaction,
} from "@money/shared/db";
import { and, eq, inArray, sql, type InferInsertModel } from "drizzle-orm";
import { plaidClient } from "./plaid";
const processor = new PushProcessor(
@@ -53,7 +64,7 @@ const createMutators = (authData: AuthData | null) => {
products: [Products.Transactions],
country_codes: [CountryCode.Us],
webhook: "https://webhooks.koon.us/api/webhook_receiver",
hosted_link: {}
hosted_link: {},
});
const { link_token, hosted_link_url } = r.data;
@@ -70,25 +81,52 @@ const createMutators = (authData: AuthData | null) => {
async get(_, { link_token }) {
isLoggedIn(authData);
const linkResp = await plaidClient.linkTokenGet({
link_token,
});
if (!linkResp) throw Error("No link respo");
console.log(JSON.stringify(linkResp.data, null, 4));
const publicToken = linkResp.data.link_sessions?.at(0)?.results?.item_add_results.at(0)?.public_token;
try {
const token = await db.query.plaidLink.findFirst({
where: and(
eq(plaidLink.token, link_token),
eq(plaidLink.user_id, authData.user.id),
),
});
if (!token) throw Error("Link not found");
if (token.completeAt) return;
if (!publicToken) throw Error("No public token");
const { data } = await plaidClient.itemPublicTokenExchange({
public_token: publicToken,
})
const linkResp = await plaidClient.linkTokenGet({
link_token,
});
if (!linkResp) throw Error("No link respo");
await db.insert(plaidAccessTokens).values({
id: randomUUID(),
userId: authData.user.id,
token: data.access_token,
logoUrl: "",
name: ""
});
console.log(JSON.stringify(linkResp.data, null, 4));
const item_add_result = linkResp.data.link_sessions
?.at(0)
?.results?.item_add_results.at(0);
// We will assume its not done yet.
if (!item_add_result) return;
const { data } = await plaidClient.itemPublicTokenExchange({
public_token: item_add_result.public_token,
});
await db.insert(plaidAccessTokens).values({
id: randomUUID(),
userId: authData.user.id,
token: data.access_token,
logoUrl: "",
name: item_add_result.institution?.name || "Unknown",
});
await db
.update(plaidLink)
.set({
completeAt: new Date(),
})
.where(eq(plaidLink.token, link_token));
} catch (e) {
console.error(e);
throw Error("Plaid error");
}
},
async updateTransactions() {
@@ -108,29 +146,39 @@ const createMutators = (authData: AuthData | null) => {
end_date: new Date().toISOString().split("T")[0],
});
const transactions = data.transactions.map(tx => ({
id: randomUUID(),
user_id: authData.user.id,
plaid_id: tx.transaction_id,
account_id: tx.account_id,
name: tx.name,
amount: tx.amount as any,
datetime: tx.datetime ? new Date(tx.datetime) : new Date(tx.date),
authorized_datetime: tx.authorized_datetime ? new Date(tx.authorized_datetime) : undefined,
json: JSON.stringify(tx),
} satisfies InferInsertModel<typeof transaction>));
const transactions = data.transactions.map(
(tx) =>
({
id: randomUUID(),
user_id: authData.user.id,
plaid_id: tx.transaction_id,
account_id: tx.account_id,
name: tx.name,
amount: tx.amount as any,
datetime: tx.datetime
? new Date(tx.datetime)
: new Date(tx.date),
authorized_datetime: tx.authorized_datetime
? new Date(tx.authorized_datetime)
: undefined,
json: JSON.stringify(tx),
}) satisfies InferInsertModel<typeof transaction>,
);
await db.insert(transaction).values(transactions).onConflictDoNothing({
target: transaction.plaid_id,
});
await db
.insert(transaction)
.values(transactions)
.onConflictDoNothing({
target: transaction.plaid_id,
});
const txReplacingPendingIds = data.transactions
.filter(t => t.pending_transaction_id)
.map(t => t.pending_transaction_id!);
.filter((t) => t.pending_transaction_id)
.map((t) => t.pending_transaction_id!);
await db.delete(transaction)
await db
.delete(transaction)
.where(inArray(transaction.plaid_id, txReplacingPendingIds));
}
},
@@ -146,26 +194,33 @@ const createMutators = (authData: AuthData | null) => {
for (const account of accounts) {
const { data } = await plaidClient.accountsBalanceGet({
access_token: account.token
access_token: account.token,
});
await db.insert(balance).values(data.accounts.map(bal => ({
id: randomUUID(),
user_id: authData.user.id,
plaid_id: bal.account_id,
avaliable: bal.balances.available as any,
current: bal.balances.current as any,
name: bal.name,
tokenId: account.id,
}))).onConflictDoUpdate({
target: balance.plaid_id,
set: { current: sql.raw(`excluded.${balance.current.name}`), avaliable: sql.raw(`excluded.${balance.avaliable.name}`) }
})
await db
.insert(balance)
.values(
data.accounts.map((bal) => ({
id: randomUUID(),
user_id: authData.user.id,
plaid_id: bal.account_id,
avaliable: bal.balances.available as any,
current: bal.balances.current as any,
name: bal.name,
tokenId: account.id,
})),
)
.onConflictDoUpdate({
target: balance.plaid_id,
set: {
current: sql.raw(`excluded.${balance.current.name}`),
avaliable: sql.raw(`excluded.${balance.avaliable.name}`),
},
});
}
},
}
},
} as const satisfies Mutators;
}
};
const zero = getHono()
.post("/mutate", async (c) => {

View File

@@ -5,7 +5,9 @@ import { authClient } from "@/lib/auth-client";
export default function Page() {
const { route: initalRoute } = useLocalSearchParams<{ route: string[] }>();
const [route, setRoute] = useState(initalRoute ? "/" + initalRoute.join("/") : "/");
const [route, setRoute] = useState(
initalRoute ? "/" + initalRoute.join("/") : "/",
);
const { data } = authClient.useSession();

View File

@@ -1,17 +1,23 @@
import { Stack } from 'expo-router';
import 'react-native-reanimated';
import { Stack } from "expo-router";
import "react-native-reanimated";
import { authClient } from '@/lib/auth-client';
import { ZeroProvider } from '@rocicorp/zero/react';
import { useMemo } from 'react';
import { authDataSchema } from '@money/shared/auth';
import { Platform } from 'react-native';
import type { ZeroOptions } from '@rocicorp/zero';
import { schema, type Schema, createMutators, type Mutators, BASE_URL } from '@money/shared';
import { authClient } from "@/lib/auth-client";
import { ZeroProvider } from "@rocicorp/zero/react";
import { useMemo } from "react";
import { authDataSchema } from "@money/shared/auth";
import { Platform } from "react-native";
import type { ZeroOptions } from "@rocicorp/zero";
import {
schema,
type Schema,
createMutators,
type Mutators,
BASE_URL,
} from "@money/shared";
import { expoSQLiteStoreProvider } from "@rocicorp/zero/react-native";
export const unstable_settings = {
anchor: 'index',
anchor: "index",
};
const kvStore = Platform.OS === "web" ? undefined : expoSQLiteStoreProvider();
@@ -25,19 +31,22 @@ export default function RootLayout() {
}, [session]);
const cookie = useMemo(() => {
return Platform.OS == 'web' ? undefined : authClient.getCookie();
return Platform.OS == "web" ? undefined : authClient.getCookie();
}, [session, isPending]);
const zeroProps = useMemo(() => {
return {
storageKey: 'money',
storageKey: "money",
kvStore,
server: process.env.NODE_ENV == 'production' ? 'https://zero.koon.us' : `${BASE_URL}:4848`,
server:
process.env.NODE_ENV == "production"
? "https://zero.koon.us"
: `${BASE_URL}:4848`,
userID: authData?.user.id ?? "anon",
schema,
mutators: createMutators(authData),
auth: cookie,
} as const satisfies ZeroOptions<Schema, Mutators>;
} as const satisfies ZeroOptions<Schema, Mutators>;
}, [authData, cookie]);
return (

View File

@@ -4,7 +4,7 @@ import { useEffect } from "react";
import { Text } from "react-native";
export default function Page() {
const { code } = useLocalSearchParams<{code: string }>();
const { code } = useLocalSearchParams<{ code: string }>();
const { isPending, data } = authClient.useSession();
if (isPending) return <Text>Loading...</Text>;
if (!isPending && !data) return <Text>Please log in</Text>;
@@ -13,11 +13,7 @@ export default function Page() {
authClient.device.approve({
userCode: code,
});
}, []);
return <Text>
Approving: {code}
</Text>
return <Text>Approving: {code}</Text>;
}

View File

@@ -6,7 +6,10 @@ export default function Auth() {
const onLogin = () => {
authClient.signIn.oauth2({
providerId: "koon-family",
callbackURL: process.env.NODE_ENV == 'production' ? 'https://money.koon.us' : `${BASE_URL}:8081`,
callbackURL:
process.env.NODE_ENV == "production"
? "https://money.koon.us"
: `${BASE_URL}:8081`,
});
};
@@ -14,5 +17,5 @@ export default function Auth() {
<View>
<Button onPress={onLogin} title="Login with Koon Family" />
</View>
)
);
}

View File

@@ -1,8 +1,14 @@
import { authClient } from '@/lib/auth-client';
import { RefreshControl, ScrollView, StatusBar, Text, View } from 'react-native';
import { authClient } from "@/lib/auth-client";
import {
RefreshControl,
ScrollView,
StatusBar,
Text,
View,
} from "react-native";
import { useQuery, useZero } from "@rocicorp/zero/react";
import { queries, type Mutators, type Schema } from '@money/shared';
import { useState } from 'react';
import { queries, type Mutators, type Schema } from "@money/shared";
import { useState } from "react";
export default function HomeScreen() {
const { data: session } = authClient.useSession();
@@ -20,16 +26,43 @@ export default function HomeScreen() {
return (
<>
<StatusBar barStyle="dark-content" />
<ScrollView contentContainerStyle={{ paddingTop: StatusBar.currentHeight, flexGrow: 1 }} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />} style={{ paddingHorizontal: 10 }}>
{balances.map(balance => <Balance key={balance.id} balance={balance} />)}
<ScrollView
contentContainerStyle={{
paddingTop: StatusBar.currentHeight,
flexGrow: 1,
}}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
style={{ paddingHorizontal: 10 }}
>
{balances.map((balance) => (
<Balance key={balance.id} balance={balance} />
))}
</ScrollView>
</>
);
}
function Balance({ balance }: { balance: { name: string, current: number, avaliable: number } }) {
return <View style={{ backgroundColor: "#eee", borderColor: "#ddd", borderWidth: 1, marginBottom: 10, borderRadius: 10 }}>
<Text style={{ fontSize: 15, textAlign: "center" }}>{balance.name}</Text>
<Text style={{ fontSize: 30, textAlign: "center" }}>{balance.current}</Text>
</View>
function Balance({
balance,
}: {
balance: { name: string; current: number; avaliable: number };
}) {
return (
<View
style={{
backgroundColor: "#eee",
borderColor: "#ddd",
borderWidth: 1,
marginBottom: 10,
borderRadius: 10,
}}
>
<Text style={{ fontSize: 15, textAlign: "center" }}>{balance.name}</Text>
<Text style={{ fontSize: 30, textAlign: "center" }}>
{balance.current}
</Text>
</View>
);
}

View File

@@ -1,10 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
module.exports = defineConfig([
expoConfig,
{
ignores: ['dist/*'],
ignores: ["dist/*"],
},
]);

View File

@@ -1,11 +1,17 @@
import { createAuthClient } from "better-auth/react";
import { deviceAuthorizationClient, 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";
export const authClient = createAuthClient({
baseURL: process.env.NODE_ENV == 'production' ? 'https://money-api.koon.us' : `${BASE_URL}:3000`,
baseURL:
process.env.NODE_ENV == "production"
? "https://money-api.koon.us"
: `${BASE_URL}:3000`,
plugins: [
expoClient({
scheme: "money",
@@ -14,5 +20,5 @@ export const authClient = createAuthClient({
}),
genericOAuthClient(),
deviceAuthorizationClient(),
]
],
});

View File

@@ -1,10 +1,10 @@
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname)
const config = getDefaultConfig(__dirname);
// Add wasm asset support
config.resolver.assetExts.push("wasm");
config.resolver.unstable_enablePackageExports = true;
config.resolver.unstable_enablePackageExports = true;
module.exports = config;

View File

@@ -5,9 +5,12 @@ import path from "path";
const aliasPlugin = {
name: "alias-react-native",
setup(build) {
build.onResolve({ filter: /^react-native$/ }, args => {
build.onResolve({ filter: /^react-native$/ }, (args) => {
return {
path: path.resolve(__dirname, "../../packages/react-native-opentui/index.tsx"),
path: path.resolve(
__dirname,
"../../packages/react-native-opentui/index.tsx",
),
};
});
},
@@ -16,9 +19,9 @@ const aliasPlugin = {
// Build configuration
await esbuild.build({
entryPoints: ["src/index.tsx"], // your app entry
bundle: true, // inline all dependencies (ui included)
platform: "node", // Node/Bun target
format: "esm", // keep ESM for top-level await
bundle: true, // inline all dependencies (ui included)
platform: "node", // Node/Bun target
format: "esm", // keep ESM for top-level await
outfile: "dist/index.js",
sourcemap: true,
plugins: [aliasPlugin],

View File

@@ -4,9 +4,5 @@ import { deviceAuthorizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: config.apiUrl,
plugins: [
deviceAuthorizationClient(),
]
plugins: [deviceAuthorizationClient()],
});

View File

@@ -1,39 +1,70 @@
import { Context, Data, Effect, Layer, Schema, Console, Schedule, Ref, Duration } from "effect";
import {
Context,
Data,
Effect,
Layer,
Schema,
Console,
Schedule,
Ref,
Duration,
} from "effect";
import { FileSystem } from "@effect/platform";
import { config } from "./config";
import { AuthState } from "./schema";
import { authClient } from "@/lib/auth-client";
import type { BetterFetchResponse } from "@better-fetch/fetch";
class AuthClientUnknownError extends Data.TaggedError("AuthClientUnknownError") {};
class AuthClientExpiredToken extends Data.TaggedError("AuthClientExpiredToken") {};
class AuthClientNoData extends Data.TaggedError("AuthClientNoData") {};
class AuthClientFetchError extends Data.TaggedError("AuthClientFetchError")<{ message: string, }> {};
class AuthClientUnknownError extends Data.TaggedError(
"AuthClientUnknownError",
) {}
class AuthClientExpiredToken extends Data.TaggedError(
"AuthClientExpiredToken",
) {}
class AuthClientNoData extends Data.TaggedError("AuthClientNoData") {}
class AuthClientFetchError extends Data.TaggedError("AuthClientFetchError")<{
message: string;
}> {}
class AuthClientError<T> extends Data.TaggedError("AuthClientError")<{
error: T,
}> {};
error: T;
}> {}
type ErrorType<E> = { [key in keyof ((E extends Record<string, any> ? E : {
message?: string;
}) & {
type ErrorType<E> = {
[key in keyof ((E extends Record<string, any>
? E
: {
message?: string;
}) & {
status: number;
statusText: string;
})]: ((E extends Record<string, any> ? E : {
message?: string;
}) & {
})]: ((E extends Record<string, any>
? E
: {
message?: string;
}) & {
status: number;
statusText: string;
})[key]; };
})[key];
};
export class AuthClient extends Context.Tag("AuthClient")<AuthClient, AuthClientImpl>() {};
export class AuthClient extends Context.Tag("AuthClient")<
AuthClient,
AuthClientImpl
>() {}
export interface AuthClientImpl {
use: <T, E>(
fn: (client: typeof authClient) => Promise<BetterFetchResponse<T, E>>,
) => Effect.Effect<T, AuthClientError<ErrorType<E>> | AuthClientFetchError | AuthClientUnknownError | AuthClientNoData, never>
) => Effect.Effect<
T,
| AuthClientError<ErrorType<E>>
| AuthClientFetchError
| AuthClientUnknownError
| AuthClientNoData,
never
>;
}
export const make = () =>
Effect.gen(function* () {
return AuthClient.of({
@@ -41,11 +72,13 @@ export const make = () =>
Effect.gen(function* () {
const { data, error } = yield* Effect.tryPromise({
try: () => fn(authClient),
catch: (error) => error instanceof Error
? new AuthClientFetchError({ message: error.message })
: new AuthClientUnknownError()
catch: (error) =>
error instanceof Error
? new AuthClientFetchError({ message: error.message })
: new AuthClientUnknownError(),
});
if (error != null) return yield* Effect.fail(new AuthClientError({ error }));
if (error != null)
return yield* Effect.fail(new AuthClientError({ error }));
if (data == null) return yield* Effect.fail(new AuthClientNoData());
return data;
}),
@@ -54,76 +87,80 @@ export const make = () =>
export const AuthClientLayer = Layer.scoped(AuthClient, make());
const pollToken = ({ device_code }: { device_code: string }) => Effect.gen(function* () {
const auth = yield* AuthClient;
const intervalRef = yield* Ref.make(5);
const pollToken = ({ device_code }: { device_code: string }) =>
Effect.gen(function* () {
const auth = yield* AuthClient;
const intervalRef = yield* Ref.make(5);
const tokenEffect = auth.use(client => {
Console.debug("Fetching");
const tokenEffect = auth.use((client) => {
Console.debug("Fetching");
return client.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code,
client_id: config.authClientId,
fetchOptions: { headers: { "user-agent": config.authClientUserAgent } },
})
}
);
return client.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code,
client_id: config.authClientId,
fetchOptions: { headers: { "user-agent": config.authClientUserAgent } },
});
});
return yield* tokenEffect
.pipe(
Effect.tapError(error =>
return yield* tokenEffect.pipe(
Effect.tapError((error) =>
error._tag == "AuthClientError" && error.error.error == "slow_down"
? Ref.update(intervalRef, current => {
Console.debug("updating delay to ", current + 5);
return current + 5
})
: Effect.void
? Ref.update(intervalRef, (current) => {
Console.debug("updating delay to ", current + 5);
return current + 5;
})
: Effect.void,
),
Effect.retry({
schedule: Schedule.addDelayEffect(
Schedule.recurWhile<Effect.Effect.Error<typeof tokenEffect>>(error =>
error._tag == "AuthClientError" &&
(error.error.error == "authorization_pending" || error.error.error == "slow_down")
Schedule.recurWhile<Effect.Effect.Error<typeof tokenEffect>>(
(error) =>
error._tag == "AuthClientError" &&
(error.error.error == "authorization_pending" ||
error.error.error == "slow_down"),
),
() => Ref.get(intervalRef).pipe(Effect.map(Duration.seconds))
)
})
() => Ref.get(intervalRef).pipe(Effect.map(Duration.seconds)),
),
}),
);
});
});
const getFromFromDisk = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const content = yield* fs.readFileString(config.authPath);
const auth = yield* Schema.decode(Schema.parseJson(AuthState))(content);
if (auth.session.expiresAt < new Date()) yield* Effect.fail(new AuthClientExpiredToken());
if (auth.session.expiresAt < new Date())
yield* Effect.fail(new AuthClientExpiredToken());
return auth;
});
const requestAuth = Effect.gen(function* () {
const auth = yield* AuthClient;
const { device_code, user_code } = yield* auth.use(client => client.device.code({
client_id: config.authClientId,
scope: "openid profile email",
}));
const { device_code, user_code } = yield* auth.use((client) =>
client.device.code({
client_id: config.authClientId,
scope: "openid profile email",
}),
);
console.log(`Please use the code: ${user_code}`);
const { access_token } = yield* pollToken({ device_code });
const sessionData = yield* auth.use(client => client.getSession({
fetchOptions: {
auth: {
type: "Bearer",
token: access_token,
}
}
}));
const sessionData = yield* auth.use((client) =>
client.getSession({
fetchOptions: {
auth: {
type: "Bearer",
token: access_token,
},
},
}),
);
if (sessionData == null) return yield* Effect.fail(new AuthClientNoData());
const result = yield* Schema.decodeUnknown(AuthState)(sessionData)
const result = yield* Schema.decodeUnknown(AuthState)(sessionData);
const fs = yield* FileSystem.FileSystem;
yield* fs.writeFileString(config.authPath, JSON.stringify(result));
@@ -134,33 +171,51 @@ const requestAuth = Effect.gen(function* () {
export const getAuth = Effect.gen(function* () {
return yield* getFromFromDisk.pipe(
Effect.catchAll(() => requestAuth),
Effect.catchTag("AuthClientFetchError", (err) => Effect.gen(function* () {
yield* Console.error("Authentication failed: " + err.message);
process.exit(1);
})),
Effect.catchTag("AuthClientNoData", () => Effect.gen(function* () {
yield* Console.error("Authentication failed: No error and no data was given by the auth server.");
process.exit(1);
})),
Effect.catchTag("ParseError", (err) => Effect.gen(function* () {
yield* Console.error("Authentication failed: Auth data failed: " + err.toString());
process.exit(1);
})),
Effect.catchTag("BadArgument", () => Effect.gen(function* () {
yield* Console.error("Authentication failed: Bad argument");
process.exit(1);
})),
Effect.catchTag("SystemError", () => Effect.gen(function* () {
yield* Console.error("Authentication failed: System error");
process.exit(1);
})),
Effect.catchTag("AuthClientError", ({ error }) => Effect.gen(function* () {
yield* Console.error("Authentication error: " + error.statusText);
process.exit(1);
})),
Effect.catchTag("AuthClientUnknownError", () => Effect.gen(function* () {
yield* Console.error("Unknown authentication error");
process.exit(1);
})),
Effect.catchTag("AuthClientFetchError", (err) =>
Effect.gen(function* () {
yield* Console.error("Authentication failed: " + err.message);
process.exit(1);
}),
),
Effect.catchTag("AuthClientNoData", () =>
Effect.gen(function* () {
yield* Console.error(
"Authentication failed: No error and no data was given by the auth server.",
);
process.exit(1);
}),
),
Effect.catchTag("ParseError", (err) =>
Effect.gen(function* () {
yield* Console.error(
"Authentication failed: Auth data failed: " + err.toString(),
);
process.exit(1);
}),
),
Effect.catchTag("BadArgument", () =>
Effect.gen(function* () {
yield* Console.error("Authentication failed: Bad argument");
process.exit(1);
}),
),
Effect.catchTag("SystemError", () =>
Effect.gen(function* () {
yield* Console.error("Authentication failed: System error");
process.exit(1);
}),
),
Effect.catchTag("AuthClientError", ({ error }) =>
Effect.gen(function* () {
yield* Console.error("Authentication error: " + error.statusText);
process.exit(1);
}),
),
Effect.catchTag("AuthClientUnknownError", () =>
Effect.gen(function* () {
yield* Console.error("Unknown authentication error");
process.exit(1);
}),
),
);
});

View File

@@ -10,5 +10,5 @@ export const config = {
authClientId: "koon-family",
authClientUserAgent: "CLI",
zeroUrl: "http://laptop:4848",
apiUrl: "http://laptop:3000"
apiUrl: "http://laptop:3000",
};

View File

@@ -2,7 +2,7 @@ import { createCliRenderer } from "@opentui/core";
import { createRoot, useKeyboard } from "@opentui/react";
import { App, type Route } from "@money/ui";
import { ZeroProvider } from "@rocicorp/zero/react";
import { schema } from '@money/shared';
import { schema } from "@money/shared";
import { useState } from "react";
import { AuthClientLayer, getAuth } from "./auth";
import { Effect } from "effect";
@@ -14,28 +14,30 @@ import { config } from "./config";
function Main({ auth }: { auth: AuthData }) {
const [route, setRoute] = useState<Route>("/");
useKeyboard(key => {
useKeyboard((key) => {
if (key.name == "c" && key.ctrl) process.exit(0);
});
return (
<ZeroProvider {...{ userID: auth.user.id, auth: auth.session.token, server: config.zeroUrl, schema, kvStore }}>
<App
auth={auth}
route={route}
setRoute={setRoute}
/>
<ZeroProvider
{...{
userID: auth.user.id,
auth: auth.session.token,
server: config.zeroUrl,
schema,
kvStore,
}}
>
<App auth={auth} route={route} setRoute={setRoute} />
</ZeroProvider>
);
}
const auth = await Effect.runPromise(
getAuth.pipe(
Effect.provide(BunContext.layer),
Effect.provide(AuthClientLayer),
)
),
);
const renderer = await createCliRenderer({ exitOnCtrlC: false });
createRoot(renderer).render(<Main auth={auth} />);

View File

@@ -1,6 +1,9 @@
import { Schema } from "effect";
const DateFromDateOrString = Schema.Union(Schema.DateFromString, Schema.DateFromSelf);
const DateFromDateOrString = Schema.Union(
Schema.DateFromString,
Schema.DateFromSelf,
);
const SessionSchema = Schema.Struct({
expiresAt: DateFromDateOrString,
@@ -23,11 +26,9 @@ const UserSchema = Schema.Struct({
id: Schema.String,
});
export const AuthState = Schema.Struct({
session: SessionSchema,
user: UserSchema,
});
export type AuthData = typeof AuthState.Type;

View File

@@ -23,7 +23,7 @@ async function loadFile(name: string): Promise<Map<string, ReadonlyJSONValue>> {
const buf = await fs.readFile(filePath, "utf8");
const obj = JSON.parse(buf) as Record<string, ReadonlyJSONValue>;
const frozen = Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, deepFreeze(v)])
Object.entries(obj).map(([k, v]) => [k, deepFreeze(v)]),
);
return new Map(Object.entries(frozen));
} catch (err: any) {
@@ -73,7 +73,9 @@ export const kvStore: StoreProvider = {
closed: txClosed,
async has(key: string) {
if (txClosed) throw new Error("transaction closed");
return staging.has(key) ? staging.get(key) !== undefined : data.has(key);
return staging.has(key)
? staging.get(key) !== undefined
: data.has(key);
},
async get(key: string) {
if (txClosed) throw new Error("transaction closed");

View File

@@ -22,5 +22,3 @@ export function QR(value: string): string {
}
return out;
}