Compare commits

..

2 Commits

Author SHA1 Message Date
Max Koon
02dd064d99 chore: remove hono 2025-11-26 12:10:28 -05:00
Max Koon
cbc220a968 refactor: api routes 2025-11-26 12:09:12 -05:00
17 changed files with 342 additions and 346 deletions

View File

@@ -9,11 +9,9 @@
"dependencies": {
"@effect/platform": "^0.93.2",
"@effect/platform-node": "^0.101.1",
"@hono/node-server": "^1.19.5",
"@money/shared": "workspace:*",
"better-auth": "^1.3.27",
"effect": "^3.19.4",
"hono": "^4.9.12",
"plaid": "^39.0.0",
"tsx": "^4.20.6"
},

View File

@@ -0,0 +1,23 @@
import { Effect, Layer } from "effect";
import { Authorization } from "./context";
import { auth } from "./config";
import { AuthorizationError, AuthorizationUnknownError } from "./errors";
export const make = () =>
Effect.gen(function* () {
return Authorization.of({
use: (fn) =>
Effect.gen(function* () {
const data = yield* Effect.tryPromise({
try: () => fn(auth),
catch: (error) =>
error instanceof Error
? new AuthorizationError({ message: error.message })
: new AuthorizationUnknownError(),
});
return data;
}),
});
});
export const BetterAuthLive = Layer.scoped(Authorization, make());

View File

@@ -3,7 +3,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { bearer, deviceAuthorization, genericOAuth } from "better-auth/plugins";
import { expo } from "@better-auth/expo";
import { drizzleSchema } from "@money/shared/db";
import { db } from "./db";
import { db } from "../db";
import { BASE_URL, HOST } from "@money/shared";
export const auth = betterAuth({

View File

@@ -0,0 +1,14 @@
import { Context, type Effect } from "effect";
import type { AuthorizationError, AuthorizationUnknownError } from "./errors";
import type { auth } from "./config";
export class Authorization extends Context.Tag("Authorization")<
Authorization,
AuthorizationImpl
>() {}
export interface AuthorizationImpl {
use: <T>(
fn: (client: typeof auth) => Promise<T>,
) => Effect.Effect<T, AuthorizationUnknownError | AuthorizationError, never>;
}

View File

@@ -0,0 +1,8 @@
import { Data } from "effect";
export class AuthorizationUnknownError extends Data.TaggedError(
"AuthClientUnknownError",
) {}
export class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
message: string;
}> {}

View File

@@ -0,0 +1,70 @@
import {
HttpApi,
HttpApiBuilder,
HttpApiEndpoint,
HttpApiGroup,
HttpLayerRouter,
HttpServerRequest,
} from "@effect/platform";
import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
import { toNodeHandler } from "better-auth/node";
import { Effect, Layer } from "effect";
import { AuthorizationError } from "./errors";
import { auth } from "./config";
const authHandler = Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const nodeRequest = NodeHttpServerRequest.toIncomingMessage(request);
const nodeResponse = NodeHttpServerRequest.toServerResponse(request);
nodeResponse.setHeader("Access-Control-Allow-Origin", "http://laptop:8081");
nodeResponse.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
nodeResponse.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, B3, traceparent, Cookie",
);
nodeResponse.setHeader("Access-Control-Max-Age", "600");
nodeResponse.setHeader("Access-Control-Allow-Credentials", "true");
// Handle preflight requests
if (nodeRequest.method === "OPTIONS") {
nodeResponse.statusCode = 200;
nodeResponse.end();
return;
// return nodeResponse;
}
yield* Effect.tryPromise({
try: () => toNodeHandler(auth)(nodeRequest, nodeResponse),
catch: (error) => {
return new AuthorizationError({ message: `${error}` });
},
});
// return nodeResponse;
});
export class AuthContractGroup extends HttpApiGroup.make("auth")
.add(HttpApiEndpoint.get("get", "/*"))
.add(HttpApiEndpoint.post("post", "/*"))
.add(HttpApiEndpoint.options("options", "/*"))
.prefix("/api/auth") {}
export class DomainApi extends HttpApi.make("domain").add(AuthContractGroup) {}
export const Api = HttpApi.make("api").addHttpApi(DomainApi);
const AuthLive = HttpApiBuilder.group(Api, "auth", (handlers) =>
handlers
.handle("get", () => authHandler.pipe(Effect.orDie))
.handle("post", () => authHandler.pipe(Effect.orDie))
.handle("options", () => authHandler.pipe(Effect.orDie)),
);
export const AuthRoute = HttpLayerRouter.addHttpApi(Api).pipe(
Layer.provide(AuthLive),
);

View File

@@ -1,5 +0,0 @@
import type { AuthData } from "@money/shared/auth";
import { Hono } from "hono";
export const getHono = () =>
new Hono<{ Variables: { auth: AuthData | null } }>();

View File

@@ -1,123 +1,15 @@
import * as Layer from "effect/Layer";
import * as Effect from "effect/Effect";
import * as Context from "effect/Context";
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter";
import * as HttpServerResponse from "@effect/platform/HttpServerResponse";
import * as HttpServerRequest from "@effect/platform/HttpServerRequest";
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
import { createServer } from "http";
import {
HttpApi,
HttpApiBuilder,
HttpApiEndpoint,
HttpApiGroup,
HttpApiSchema,
HttpApiSecurity,
HttpMiddleware,
} from "@effect/platform";
import { Schema, Data, Console } from "effect";
import { auth } from "./auth";
import { AuthSchema } from "@money/shared/auth";
import { toNodeHandler } from "better-auth/node";
import { BASE_URL } from "@money/shared";
class CurrentSession extends Context.Tag("CurrentSession")<
CurrentSession,
{ readonly auth: Schema.Schema.Type<typeof AuthSchema> | null }
>() {}
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
"Unauthorized",
{},
HttpApiSchema.annotations({ status: 401 }),
) {}
const parseAuthorization = (input: string) =>
Effect.gen(function* () {
const m = /^Bearer\s+(.+)$/.exec(input);
if (!m) {
return yield* Effect.fail(
new Unauthorized({ message: "Invalid Authorization header" }),
);
}
return m[1];
});
export class Authorization extends Context.Tag("Authorization")<
Authorization,
AuthorizationImpl
>() {}
export interface AuthorizationImpl {
use: <T>(
fn: (client: typeof auth) => Promise<T>,
) => Effect.Effect<T, AuthorizationUnknownError | AuthorizationError, never>;
}
class AuthorizationUnknownError extends Data.TaggedError(
"AuthClientUnknownError",
) {}
class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
message: string;
}> {}
export const make = () =>
Effect.gen(function* () {
return Authorization.of({
use: (fn) =>
Effect.gen(function* () {
const data = yield* Effect.tryPromise({
try: () => fn(auth),
catch: (error) =>
error instanceof Error
? new AuthorizationError({ message: error.message })
: new AuthorizationUnknownError(),
});
return data;
}),
});
});
export const AuthorizationLayer = Layer.scoped(Authorization, make());
const SessionMiddleware = HttpLayerRouter.middleware<{
provides: CurrentSession;
}>()(
Effect.gen(function* () {
const auth = yield* Authorization;
return (httpEffect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const headers = request.headers;
const session = yield* auth
.use((auth) => auth.api.getSession({ headers }))
.pipe(
Effect.flatMap((s) =>
s == null ? Effect.succeed(null) : Schema.decode(AuthSchema)(s),
),
Effect.tap((s) => Console.debug("Auth result", s)),
);
return yield* Effect.provideService(httpEffect, CurrentSession, {
auth: session,
});
});
}),
);
const HelloRoute = HttpLayerRouter.add(
"GET",
"/hello",
Effect.gen(function* () {
const { auth } = yield* CurrentSession;
return HttpServerResponse.text(`Hello, your name is ${auth?.user.name}`);
}),
).pipe(Layer.provide(SessionMiddleware.layer));
import { CorsMiddleware } from "./middleware/cors";
import { AuthRoute } from "./auth/handler";
import { BetterAuthLive } from "./auth/better-auth";
import { WebhookReceiverRoute } from "./webhook";
import { ZeroMutateRoute, ZeroQueryRoute } from "./zero/handler";
const RootRoute = HttpLayerRouter.add(
"GET",
@@ -127,78 +19,17 @@ const RootRoute = HttpLayerRouter.add(
}),
);
const authHandler = Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const nodeRequest = NodeHttpServerRequest.toIncomingMessage(request);
const nodeResponse = NodeHttpServerRequest.toServerResponse(request);
nodeResponse.setHeader("Access-Control-Allow-Origin", "http://laptop:8081");
nodeResponse.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
const AllRoutes = Layer.mergeAll(
RootRoute,
AuthRoute,
ZeroQueryRoute,
ZeroMutateRoute,
WebhookReceiverRoute,
);
nodeResponse.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, B3, traceparent, Cookie",
);
nodeResponse.setHeader("Access-Control-Max-Age", "600");
nodeResponse.setHeader("Access-Control-Allow-Credentials", "true");
// Handle preflight requests
if (nodeRequest.method === "OPTIONS") {
nodeResponse.statusCode = 200;
nodeResponse.end();
return;
// return nodeResponse;
}
yield* Effect.tryPromise({
try: () => toNodeHandler(auth)(nodeRequest, nodeResponse),
catch: (error) => {
return new AuthorizationError({ message: `${error}` });
},
});
// return nodeResponse;
});
export class AuthContractGroup extends HttpApiGroup.make("auth")
.add(HttpApiEndpoint.get("get", "/*"))
.add(HttpApiEndpoint.post("post", "/*"))
.add(HttpApiEndpoint.options("options", "/*"))
.prefix("/api/auth") {}
export class DomainApi extends HttpApi.make("domain").add(AuthContractGroup) {}
export const Api = HttpApi.make("api").addHttpApi(DomainApi);
const AuthLive = HttpApiBuilder.group(Api, "auth", (handlers) =>
handlers
.handle("get", () => authHandler.pipe(Effect.orDie))
.handle("post", () => authHandler.pipe(Effect.orDie))
.handle("options", () => authHandler.pipe(Effect.orDie)),
);
const CorsMiddleware = HttpLayerRouter.middleware(
HttpMiddleware.cors({
allowedOrigins: ["https://money.koon.us", `${BASE_URL}:8081`],
allowedMethods: ["POST", "GET", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
// {
// global: true,
// },
);
const AuthRoute = HttpLayerRouter.addHttpApi(Api).pipe(Layer.provide(AuthLive));
const AllRoutes = Layer.mergeAll(RootRoute, AuthRoute, HelloRoute);
HttpLayerRouter.serve(AllRoutes).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.provide(AuthorizationLayer),
Layer.provide(BetterAuthLive),
Layer.provide(CorsMiddleware.layer),
Layer.launch,
NodeRuntime.runMain,

View File

@@ -1,58 +0,0 @@
import { serve } from "@hono/node-server";
import { authDataSchema } from "@money/shared/auth";
import { BASE_URL } from "@money/shared";
import { cors } from "hono/cors";
import { auth } from "./auth";
import { getHono } from "./hono";
import { zero } from "./zero";
import { webhook } from "./webhook";
const app = getHono();
app.use(
"/api/*",
cors({
origin: ["https://money.koon.us", `${BASE_URL}:8081`],
allowMethods: ["POST", "GET", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
);
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));
app.use("*", async (c, next) => {
const authHeader = c.req.raw.headers.get("Authorization");
const cookie = authHeader?.split("Bearer ")[1];
const newHeaders = new Headers(c.req.raw.headers);
if (cookie) {
newHeaders.set("Cookie", cookie);
}
const session = await auth.api.getSession({ headers: newHeaders });
if (!session) {
c.set("auth", null);
return next();
}
c.set("auth", authDataSchema.parse(session));
return next();
});
app.route("/api/zero", zero);
app.get("/api", (c) => c.text("OK"));
app.get("/api/webhook_receiver", webhook);
app.get("/", (c) => c.text("OK"));
serve(
{
fetch: app.fetch,
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
},
(info) => {
console.log(`Server is running on ${info.address}:${info.port}`);
},
);

View File

@@ -0,0 +1,11 @@
import { HttpLayerRouter, HttpMiddleware } from "@effect/platform";
import { BASE_URL } from "@money/shared";
export const CorsMiddleware = HttpLayerRouter.middleware(
HttpMiddleware.cors({
allowedOrigins: ["https://money.koon.us", `${BASE_URL}:8081`],
allowedMethods: ["POST", "GET", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
);

View File

@@ -0,0 +1,61 @@
import { AuthSchema } from "@money/shared/auth";
import { Context, Effect, Schema, Console } from "effect";
import { Authorization } from "../auth/context";
import { HttpLayerRouter, HttpServerRequest } from "@effect/platform";
export class CurrentSession extends Context.Tag("CurrentSession")<
CurrentSession,
{ readonly auth: Schema.Schema.Type<typeof AuthSchema> | null }
>() {}
export const SessionMiddleware = HttpLayerRouter.middleware<{
provides: CurrentSession;
}>()(
Effect.gen(function* () {
const auth = yield* Authorization;
return (httpEffect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const headers = { ...request.headers };
const token = yield* HttpServerRequest.schemaHeaders(
Schema.Struct({
authorization: Schema.optional(Schema.String),
}),
).pipe(
Effect.tap(Console.debug),
Effect.flatMap(({ authorization }) =>
authorization != undefined
? parseAuthorization(authorization)
: Effect.succeed(undefined),
),
);
if (token) {
headers["cookie"] = token;
}
const session = yield* auth
.use((auth) => auth.api.getSession({ headers }))
.pipe(
Effect.flatMap((s) =>
s == null ? Effect.succeed(null) : Schema.decode(AuthSchema)(s),
),
// Effect.tap((s) => Console.debug("Auth result", s)),
);
return yield* Effect.provideService(httpEffect, CurrentSession, {
auth: session,
});
});
}),
);
const parseAuthorization = (input: string) =>
Effect.gen(function* () {
const m = /^Bearer\s+(.+)$/.exec(input);
if (!m) {
return yield* Effect.fail(new Error("Invalid token"));
}
return m[1];
});

View File

@@ -1,11 +1,21 @@
import type { Context } from "hono";
import {
HttpLayerRouter,
HttpServerRequest,
HttpServerResponse,
} from "@effect/platform";
import { Effect } from "effect";
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);
export const WebhookReceiverRoute = HttpLayerRouter.add(
"*",
"/api/webhook_receiver",
return c.text("Hi");
};
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const body = yield* request.json;
Effect.log("Got a webhook!", body);
return HttpServerResponse.text("HELLO THERE");
}),
);

View File

@@ -0,0 +1,6 @@
import { Data } from "effect";
export class ZeroUnknownError extends Data.TaggedError("ZeroUnknownError") {}
export class ZeroError extends Data.TaggedError("ZeroError")<{
error: Error;
}> {}

View File

@@ -0,0 +1,106 @@
import {
HttpLayerRouter,
HttpServerRequest,
HttpServerResponse,
Url,
} from "@effect/platform";
import { Console, Effect, Layer } from "effect";
import { CurrentSession, SessionMiddleware } from "../middleware/session";
import { ZeroError, ZeroUnknownError } from "./errors";
import { withValidation, type ReadonlyJSONValue } from "@rocicorp/zero";
import {
handleGetQueriesRequest,
PushProcessor,
ZQLDatabase,
} from "@rocicorp/zero/server";
import { BASE_URL, queries, schema } from "@money/shared";
import type { AuthSchemaType } from "@money/shared/auth";
import { PostgresJSConnection } from "@rocicorp/zero/pg";
import postgres from "postgres";
import { createMutators } from "./mutators";
const processor = new PushProcessor(
new ZQLDatabase(
new PostgresJSConnection(postgres(process.env.ZERO_UPSTREAM_DB! as string)),
schema,
),
);
export const ZeroQueryRoute = HttpLayerRouter.add(
"POST",
"/api/zero/get-queries",
Effect.gen(function* () {
const { auth } = yield* CurrentSession;
const request = yield* HttpServerRequest.HttpServerRequest;
const body = yield* request.json;
const result = yield* Effect.tryPromise({
try: () =>
handleGetQueriesRequest(
(name, args) => ({ query: getQuery(auth, name, args) }),
schema,
body as ReadonlyJSONValue,
),
catch: (error) =>
error instanceof Error
? new ZeroError({ error })
: new ZeroUnknownError(),
}).pipe(
Effect.tapErrorTag("ZeroError", (err) =>
Console.error("Zero Error", err.error),
),
);
return yield* HttpServerResponse.json(result);
}),
).pipe(Layer.provide(SessionMiddleware.layer));
function getQuery(
authData: AuthSchemaType | null,
name: string,
args: readonly ReadonlyJSONValue[],
) {
if (name in validatedQueries) {
const q = validatedQueries[name];
return q(authData, ...args);
}
throw new Error(`Unknown query: ${name}`);
}
export const validatedQueries = Object.fromEntries(
Object.values(queries).map((q) => [q.queryName, withValidation(q)]),
);
export const ZeroMutateRoute = HttpLayerRouter.add(
"POST",
"/api/zero/mutate",
Effect.gen(function* () {
const { auth } = yield* CurrentSession;
const request = yield* HttpServerRequest.HttpServerRequest;
const url = yield* Url.fromString(`${BASE_URL}${request.url}`);
const body = yield* request.json;
const result = yield* Effect.tryPromise({
try: () =>
processor.process(
createMutators(auth),
url.searchParams,
body as ReadonlyJSONValue,
),
catch: (error) =>
error instanceof Error
? new ZeroError({ error })
: new ZeroUnknownError(),
}).pipe(
Effect.tapErrorTag("ZeroError", (err) =>
Console.error("Zero Error", err.error),
),
);
return yield* HttpServerResponse.json(result);
}),
).pipe(Layer.provide(SessionMiddleware.layer));

View File

@@ -1,15 +1,3 @@
import {
type ReadonlyJSONValue,
type Transaction,
withValidation,
} from "@rocicorp/zero";
import {
handleGetQueriesRequest,
PushProcessor,
ZQLDatabase,
} from "@rocicorp/zero/server";
import { PostgresJSConnection } from "@rocicorp/zero/pg";
import postgres from "postgres";
import {
createMutators as createMutatorsShared,
isLoggedIn,
@@ -18,36 +6,27 @@ import {
type Mutators,
type Schema,
} from "@money/shared";
import type { AuthData } from "@money/shared/auth";
import { getHono } from "./hono";
import type { AuthSchemaType } from "@money/shared/auth";
import {
Configuration,
CountryCode,
PlaidApi,
PlaidEnvironments,
Products,
} from "plaid";
import { randomUUID } from "crypto";
import { db } from "./db";
type ReadonlyJSONValue,
type Transaction,
withValidation,
} from "@rocicorp/zero";
import { plaidClient } from "../plaid";
import { CountryCode, Products } from "plaid";
import {
balance,
plaidAccessTokens,
plaidLink,
transaction,
} from "@money/shared/db";
import { db } from "../db";
import { randomUUID } from "crypto";
import { and, eq, inArray, sql, type InferInsertModel } from "drizzle-orm";
import { plaidClient } from "./plaid";
const processor = new PushProcessor(
new ZQLDatabase(
new PostgresJSConnection(postgres(process.env.ZERO_UPSTREAM_DB! as string)),
schema,
),
);
type Tx = Transaction<Schema>;
const createMutators = (authData: AuthData | null) => {
export const createMutators = (authData: AuthSchemaType | null) => {
const mutators = createMutatorsShared(authData);
return {
...mutators,
@@ -221,41 +200,3 @@ const createMutators = (authData: AuthData | null) => {
},
} as const satisfies Mutators;
};
const zero = getHono()
.post("/mutate", async (c) => {
const authData = c.get("auth");
const result = await processor.process(createMutators(authData), c.req.raw);
return c.json(result);
})
.post("/get-queries", async (c) => {
const authData = c.get("auth");
const result = await handleGetQueriesRequest(
(name, args) => ({ query: getQuery(authData, name, args) }),
schema,
c.req.raw,
);
return c.json(result);
});
const validatedQueries = Object.fromEntries(
Object.values(queries).map((q) => [q.queryName, withValidation(q)]),
);
function getQuery(
authData: AuthData | null,
name: string,
args: readonly ReadonlyJSONValue[],
) {
if (name in validatedQueries) {
const q = validatedQueries[name];
return q(authData, ...args);
}
throw new Error(`Unknown query: ${name}`);
}
export { zero };

View File

@@ -14,6 +14,7 @@ import { config } from "./config";
import { authClient } from "@/lib/auth-client";
import type { BetterFetchResponse } from "@better-fetch/fetch";
import { AuthSchema } from "@money/shared/auth";
import { encode } from "node:punycode";
class AuthClientUnknownError extends Data.TaggedError(
"AuthClientUnknownError",
@@ -161,9 +162,10 @@ const requestAuth = Effect.gen(function* () {
if (sessionData == null) return yield* Effect.fail(new AuthClientNoData());
const result = yield* Schema.decodeUnknown(AuthSchema)(sessionData);
const encoded = yield* Schema.encode(AuthSchema)(result);
const fs = yield* FileSystem.FileSystem;
yield* fs.writeFileString(config.authPath, JSON.stringify(result));
yield* fs.writeFileString(config.authPath, JSON.stringify(encoded));
return result;
});

22
pnpm-lock.yaml generated
View File

@@ -16,9 +16,6 @@ importers:
'@effect/platform-node':
specifier: ^0.101.1
version: 0.101.1(@effect/cluster@0.52.10(@effect/platform@0.93.2(effect@3.19.4))(@effect/rpc@0.72.1(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(@effect/sql@0.48.0(@effect/experimental@0.57.1(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(@effect/workflow@0.12.5(@effect/experimental@0.57.1(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(@effect/platform@0.93.2(effect@3.19.4))(@effect/rpc@0.72.1(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(effect@3.19.4))(effect@3.19.4))(@effect/platform@0.93.2(effect@3.19.4))(@effect/rpc@0.72.1(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(@effect/sql@0.48.0(@effect/experimental@0.57.1(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(@effect/platform@0.93.2(effect@3.19.4))(effect@3.19.4))(effect@3.19.4)
'@hono/node-server':
specifier: ^1.19.5
version: 1.19.6(hono@4.10.4)
'@money/shared':
specifier: workspace:*
version: link:../../packages/shared
@@ -28,9 +25,6 @@ importers:
effect:
specifier: ^3.19.4
version: 3.19.4
hono:
specifier: ^4.9.12
version: 4.10.4
plaid:
specifier: ^39.0.0
version: 39.1.0
@@ -1561,12 +1555,6 @@ packages:
'@hexagon/base64@1.1.28':
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
'@hono/node-server@1.19.6':
resolution: {integrity: sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -4894,10 +4882,6 @@ packages:
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
hono@4.10.4:
resolution: {integrity: sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg==}
engines: {node: '>=16.9.0'}
hosted-git-info@7.0.2:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -8796,10 +8780,6 @@ snapshots:
'@hexagon/base64@1.1.28': {}
'@hono/node-server@1.19.6(hono@4.10.4)':
dependencies:
hono: 4.10.4
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@@ -13274,8 +13254,6 @@ snapshots:
dependencies:
react-is: 16.13.1
hono@4.10.4: {}
hosted-git-info@7.0.2:
dependencies:
lru-cache: 10.4.3