Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"drizzle-typebox": "^0.3.3",
"elysia": "^1.4.15",
"elysia-prometheus": "^1.0.0",
"elysia-rate-limit": "^4.4.2",
Expand Down
5 changes: 3 additions & 2 deletions apps/core/server/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
* Re-exports database connection and schema
*/

export { db, queryClient } from './db'
export * from './schema'
export { db, queryClient } from "./db";
export * from "./schema";
export * from "./typebox-schemas";
11 changes: 5 additions & 6 deletions apps/core/server/db/migrations/0032_fix_schema_drift.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
-- Drop unused admin_whitelist table that exists in DB but not in TypeScript schemas
-- This table was created in migration 0000 but was never needed (single-team app)

-- Drop the foreign key constraint first
ALTER TABLE "admin_whitelist" DROP CONSTRAINT IF EXISTS "admin_whitelist_added_by_users_id_fk";

-- Drop the index
-- Drop the index if it exists (safe even if table doesn't exist)
DROP INDEX IF EXISTS "idx_admin_whitelist_wallet";

Comment on lines +5 to 7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The DROP INDEX statement on line 6 is redundant because DROP TABLE ... CASCADE on line 11 will automatically drop any dependent objects, including indexes. Removing lines 5-7 will make the script more concise and rely on the intended behavior of CASCADE.

-- Drop the table
DROP TABLE IF EXISTS "admin_whitelist";
-- Drop the table with CASCADE to handle any dependent constraints
-- CASCADE will automatically drop the foreign key constraint if the table exists
-- IF EXISTS ensures this is safe even if the table was already dropped
DROP TABLE IF EXISTS "admin_whitelist" CASCADE;
161 changes: 161 additions & 0 deletions apps/core/server/db/typebox-schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* Drizzle-TypeBox Schema Conversion Utilities
*
* Converts Drizzle ORM schemas to TypeBox schemas for Elysia validation.
* This eliminates duplicate type definitions - define once in Drizzle, use everywhere.
*
* Usage:
* import { UserInsertSchema, UserSelectSchema, spread } from './typebox-schemas'
*
* .post('/users', handler, {
* body: UserInsertSchema,
* response: UserSelectSchema
* })
*
* IMPORTANT: To avoid TypeScript infinite type instantiation errors,
* always declare TypeBox schemas as separate variables before using in Elysia.
*
* @see https://elysiajs.com/integrations/drizzle
*/

import { createInsertSchema, createSelectSchema } from "drizzle-typebox";
import type { TObject } from "@sinclair/typebox";
import { t } from "elysia";

// Import Drizzle schemas
import { users, projects, activityLog } from "./schema/users.schema";
import { assets } from "./schema/assets.schema";
import { apiKeys } from "./schema/api-keys.schema";
import { prompts } from "./schema/prompts.schema";

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

/**
* Spread utility - extracts TypeBox properties as plain object
* Use this to pick specific fields from a schema
*
* @example
* body: t.Object({
* ...spread(UserInsertSchema, ['displayName', 'email']),
* customField: t.String()
* })
*/
export function spread<T extends TObject>(
schema: T,
keys: (keyof T["properties"])[],
): Partial<T["properties"]> {
const result: Partial<T["properties"]> = {};
for (const key of keys) {
if (key in schema.properties) {
result[key] = schema.properties[key];
}
}
return result;
}
Comment on lines +45 to +56

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent Failure in spread Utility

The spread function silently ignores keys that do not exist in the schema's properties. This can lead to subtle bugs if a caller misspells a key or requests a non-existent property, as the function will simply omit it from the result without any warning or error.

Recommendation:
Consider throwing an error or logging a warning when a requested key is not found in the schema's properties. For example:

for (const key of keys) {
  if (key in schema.properties) {
    result[key] = schema.properties[key];
  } else {
    throw new Error(`Key '${String(key)}' not found in schema properties.`);
  }
}

This will make debugging easier and ensure that schema construction is intentional and correct.


/**
* Spreads all properties from a schema
*
* @example
* body: t.Object({
* ...spreads(UserInsertSchema)
* })
*/
export function spreads<T extends TObject>(schema: T): T["properties"] {
return schema.properties;
}

// ============================================================================
// USER SCHEMAS
// ============================================================================

/** Schema for inserting a new user */
export const UserInsertSchema = createInsertSchema(users, {
// Add email format validation
email: t.Optional(t.String({ format: "email" })),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: TypeBox email format validation is silently skipped due to missing FormatRegistry.Set("email", ...) call.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

TypeBox's Value.Check silently skips email format validation for schemas like UserProfileUpdateSchema because FormatRegistry.Set("email", ...) is only called in test files and not in the application code. This allows invalid email strings (e.g., "invalid-email", "test@") to pass validation, be accepted by the API, and potentially saved to the database, leading to data integrity issues and silent validation failures.

💡 Suggested Fix

Add FormatRegistry.Set("email", ...) and other necessary format registrations (e.g., "uuid") to typebox-schemas.ts before exporting the schemas.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: apps/core/server/db/typebox-schemas.ts#L77

Potential issue: TypeBox's `Value.Check` silently skips email format validation for
schemas like `UserProfileUpdateSchema` because `FormatRegistry.Set("email", ...)` is
only called in test files and not in the application code. This allows invalid email
strings (e.g., "invalid-email", "test@") to pass validation, be accepted by the API, and
potentially saved to the database, leading to data integrity issues and silent
validation failures.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 3549656

// Ensure wallet addresses are lowercase
walletAddress: t.Optional(t.String({ minLength: 42, maxLength: 42 })),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: wallet address validation assumes Ethereum addresses (42 chars), but comment mentions lowercase while validation only checks length. Should the walletAddress validation include a pattern check for hexadecimal format and enforce lowercase, or are non-Ethereum addresses also supported?

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/core/server/db/typebox-schemas.ts
Line: 79:79

Comment:
**logic:** wallet address validation assumes Ethereum addresses (42 chars), but comment mentions lowercase while validation only checks length. Should the walletAddress validation include a pattern check for hexadecimal format and enforce lowercase, or are non-Ethereum addresses also supported?

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +78 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validation for walletAddress could be more robust. The current implementation only checks for length. To better align with the comment "Ensure wallet addresses are lowercase" and to properly validate the format, consider using a regex pattern. This will enforce the 0x prefix, hexadecimal characters, and lowercase requirement in a single rule.

Suggested change
// Ensure wallet addresses are lowercase
walletAddress: t.Optional(t.String({ minLength: 42, maxLength: 42 })),
// Ensure wallet addresses are lowercase and in the correct format
walletAddress: t.Optional(t.String({ pattern: "^0x[a-f0-9]{40}$" })),

});

/** Schema for selecting/returning a user */
export const UserSelectSchema = createSelectSchema(users);

/** Schema for user profile updates */
export const UserProfileUpdateSchema = t.Object({
displayName: t.String({ minLength: 1, maxLength: 255 }),
email: t.String({ format: "email" }),
discordUsername: t.Optional(t.String({ maxLength: 255 })),
});
Comment on lines +85 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: UserProfileUpdateSchema is manually defined instead of using createInsertSchema with field selection - this creates potential schema drift if the users table changes

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/core/server/db/typebox-schemas.ts
Line: 85:90

Comment:
**style:** UserProfileUpdateSchema is manually defined instead of using createInsertSchema with field selection - this creates potential schema drift if the users table changes

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.


// ============================================================================
// PROJECT SCHEMAS
// ============================================================================

/** Schema for inserting a new project */
export const ProjectInsertSchema = createInsertSchema(projects, {
name: t.String({ minLength: 1, maxLength: 255 }),
});

/** Schema for selecting/returning a project */
export const ProjectSelectSchema = createSelectSchema(projects);

// ============================================================================
// ACTIVITY LOG SCHEMAS
// ============================================================================

/** Schema for inserting activity log entries */
export const ActivityLogInsertSchema = createInsertSchema(activityLog);

/** Schema for selecting activity log entries */
export const ActivityLogSelectSchema = createSelectSchema(activityLog);

// ============================================================================
// ASSET SCHEMAS
// ============================================================================

/** Schema for inserting a new asset */
export const AssetInsertSchema = createInsertSchema(assets, {
name: t.String({ minLength: 1, maxLength: 255 }),
});

/** Schema for selecting/returning an asset */
export const AssetSelectSchema = createSelectSchema(assets);

// ============================================================================
// API KEY SCHEMAS
// ============================================================================

/** Schema for inserting a new API key */
export const ApiKeyInsertSchema = createInsertSchema(apiKeys, {
name: t.String({ minLength: 1, maxLength: 255 }),
});

/** Schema for selecting API keys */
export const ApiKeySelectSchema = createSelectSchema(apiKeys);

// ============================================================================
// PROMPT SCHEMAS
// ============================================================================

/** Schema for inserting prompts */
export const PromptInsertSchema = createInsertSchema(prompts);

/** Schema for selecting prompts */
export const PromptSelectSchema = createSelectSchema(prompts);

// ============================================================================
// TYPE EXPORTS (inferred from TypeBox schemas)
// ============================================================================

import type { Static } from "@sinclair/typebox";

export type UserInsert = Static<typeof UserInsertSchema>;
export type UserSelect = Static<typeof UserSelectSchema>;
export type ProjectInsert = Static<typeof ProjectInsertSchema>;
export type ProjectSelect = Static<typeof ProjectSelectSchema>;
export type AssetInsert = Static<typeof AssetInsertSchema>;
export type AssetSelect = Static<typeof AssetSelectSchema>;
export type ApiKeyInsert = Static<typeof ApiKeyInsertSchema>;
export type ApiKeySelect = Static<typeof ApiKeySelectSchema>;
Comment on lines +154 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's an inconsistency in the exported types. While schemas for ActivityLog and Prompt are defined, their corresponding static types (ActivityLogInsert, ActivityLogSelect, PromptInsert, PromptSelect) are missing from this type exports section. For consistency with the other schemas in this file, these types should also be exported.

Suggested change
export type UserInsert = Static<typeof UserInsertSchema>;
export type UserSelect = Static<typeof UserSelectSchema>;
export type ProjectInsert = Static<typeof ProjectInsertSchema>;
export type ProjectSelect = Static<typeof ProjectSelectSchema>;
export type AssetInsert = Static<typeof AssetInsertSchema>;
export type AssetSelect = Static<typeof AssetSelectSchema>;
export type ApiKeyInsert = Static<typeof ApiKeyInsertSchema>;
export type ApiKeySelect = Static<typeof ApiKeySelectSchema>;
export type UserInsert = Static<typeof UserInsertSchema>;
export type UserSelect = Static<typeof UserSelectSchema>;
export type ProjectInsert = Static<typeof ProjectInsertSchema>;
export type ProjectSelect = Static<typeof ProjectSelectSchema>;
export type AssetInsert = Static<typeof AssetInsertSchema>;
export type AssetSelect = Static<typeof AssetSelectSchema>;
export type ApiKeyInsert = Static<typeof ApiKeyInsertSchema>;
export type ApiKeySelect = Static<typeof ApiKeySelectSchema>;
export type ActivityLogInsert = Static<typeof ActivityLogInsertSchema>;
export type ActivityLogSelect = Static<typeof ActivityLogSelectSchema>;
export type PromptInsert = Static<typeof PromptInsertSchema>;
export type PromptSelect = Static<typeof PromptSelectSchema>;

10 changes: 5 additions & 5 deletions apps/core/server/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { userService } from "../services/UserService";
import { ActivityLogService } from "../services/ActivityLogService";
import { ApiKeyService } from "../services/ApiKeyService";
import type { AuthUser } from "../types/auth";
// Import drizzle-typebox schemas for Elysia validation
import { UserProfileUpdateSchema } from "../db/typebox-schemas";

export const usersRoutes = new Elysia({ prefix: "/api/users" })
// Regular authenticated user routes
Expand Down Expand Up @@ -76,11 +78,9 @@ export const usersRoutes = new Elysia({ prefix: "/api/users" })
return { user: updatedUser };
},
Comment on lines 78 to 79

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Risk: Potential Exposure of Sensitive User Fields

The response { user: updatedUser } may expose sensitive fields from the user object, such as internal IDs, authentication tokens, or other confidential attributes. To mitigate this, ensure that only non-sensitive, client-safe fields are included in the response. For example:

return { user: filterUserForClient(updatedUser) };

Where filterUserForClient is a utility that strips sensitive fields before returning the user object.

{
body: t.Object({
displayName: t.String(),
email: t.String(),
discordUsername: t.Optional(t.String()),
}),
// Using drizzle-typebox schema for validation
// Defined once in Drizzle, used for both DB and API validation
body: UserProfileUpdateSchema,
detail: {
tags: ["Users"],
summary: "Update user profile",
Expand Down
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"drizzle-typebox": "^0.3.3",
"elysia": "^1.4.15",
"elysia-prometheus": "^1.0.0",
"elysia-rate-limit": "^4.4.2",
Expand Down Expand Up @@ -2272,6 +2273,8 @@

"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],

"drizzle-typebox": ["drizzle-typebox@0.3.3", "", { "peerDependencies": { "@sinclair/typebox": ">=0.34.8", "drizzle-orm": ">=0.36.0" } }, "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w=="],

"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],

"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
Expand Down
Loading