format: format with biome
This commit is contained in:
@@ -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);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,5 +10,5 @@ export const config = {
|
||||
authClientId: "koon-family",
|
||||
authClientUserAgent: "CLI",
|
||||
zeroUrl: "http://laptop:4848",
|
||||
apiUrl: "http://laptop:3000"
|
||||
apiUrl: "http://laptop:3000",
|
||||
};
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user