Skip to content

Commit 0ae08be

Browse files
authored
Merge pull request #134 from atomly/feature/resource-protection-limits
feat: Add comprehensive resource protection to database schemas
2 parents 1aabf1f + 619718f commit 0ae08be

15 files changed

+212
-67
lines changed

packages/api/src/api-router/pages.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export const pagesRouter = {
4141
.insert(Page)
4242
.values({
4343
...input,
44-
children: [],
4544
document_id: document.id,
4645
user_id: ctx.session.user.id,
4746
})

packages/db/src/auth/account.schema.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
1+
import { pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
2+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
23
import { user } from "./user.schema.js";
34

45
export const account = pgTable("account", {
@@ -13,7 +14,7 @@ export const account = pgTable("account", {
1314
idToken: text("id_token"),
1415
accessTokenExpiresAt: timestamp("access_token_expires_at"),
1516
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
16-
scope: text("scope"),
17+
scope: varchar("scope", { length: TEXT_LIMITS.URL }),
1718
password: text("password"),
1819
createdAt: timestamp("created_at").notNull(),
1920
updatedAt: timestamp("updated_at").notNull(),

packages/db/src/auth/organization.schema.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
1+
import { pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
2+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
23
import { user } from "./user.schema.js";
34

45
export const organization = pgTable("organization", {
56
id: text("id").primaryKey(),
6-
name: text("name").notNull(),
7-
slug: text("slug").unique(),
8-
logo: text("logo"),
7+
name: varchar("name", { length: TEXT_LIMITS.NAME }).notNull(),
8+
slug: varchar("slug", { length: TEXT_LIMITS.SLUG }).unique(),
9+
logo: varchar("logo", { length: TEXT_LIMITS.URL }),
910
createdAt: timestamp("created_at").notNull(),
10-
metadata: text("metadata"),
11+
metadata: varchar("metadata", { length: TEXT_LIMITS.URL }),
1112
});
1213

1314
export const member = pgTable("member", {
@@ -18,7 +19,9 @@ export const member = pgTable("member", {
1819
userId: text("user_id")
1920
.notNull()
2021
.references(() => user.id, { onDelete: "cascade" }),
21-
role: text("role").default("member").notNull(),
22+
role: varchar("role", { length: TEXT_LIMITS.STATUS })
23+
.default("member")
24+
.notNull(),
2225
createdAt: timestamp("created_at").notNull(),
2326
});
2427

@@ -27,9 +30,11 @@ export const invitation = pgTable("invitation", {
2730
organizationId: text("organization_id")
2831
.notNull()
2932
.references(() => organization.id, { onDelete: "cascade" }),
30-
email: text("email").notNull(),
31-
role: text("role"),
32-
status: text("status").default("pending").notNull(),
33+
email: varchar("email", { length: TEXT_LIMITS.EMAIL }).notNull(),
34+
role: varchar("role", { length: TEXT_LIMITS.STATUS }),
35+
status: varchar("status", { length: TEXT_LIMITS.STATUS })
36+
.default("pending")
37+
.notNull(),
3338
expiresAt: timestamp("expires_at").notNull(),
3439
inviterId: text("inviter_id")
3540
.notNull()
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
1+
import { pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
2+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
23
import { user } from "./user.schema.js";
34

45
export const session = pgTable("session", {
@@ -7,12 +8,14 @@ export const session = pgTable("session", {
78
token: text("token").notNull().unique(),
89
createdAt: timestamp("created_at").notNull(),
910
updatedAt: timestamp("updated_at").notNull(),
10-
ipAddress: text("ip_address"),
11-
userAgent: text("user_agent"),
11+
ipAddress: varchar("ip_address", { length: TEXT_LIMITS.IP_ADDRESS }),
12+
userAgent: varchar("user_agent", { length: TEXT_LIMITS.USER_AGENT }),
1213
userId: text("user_id")
1314
.notNull()
1415
.references(() => user.id, { onDelete: "cascade" }),
15-
activeOrganizationId: text("active_organization_id"),
16+
activeOrganizationId: varchar("active_organization_id", {
17+
length: TEXT_LIMITS.EMAIL,
18+
}),
1619
});
1720

1821
export type Session = typeof session.$inferSelect;

packages/db/src/auth/user.schema.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
1+
import {
2+
boolean,
3+
pgTable,
4+
text,
5+
timestamp,
6+
varchar,
7+
} from "drizzle-orm/pg-core";
8+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
29

310
export const user = pgTable("user", {
411
id: text("id").primaryKey(),
5-
name: text("name").notNull(),
6-
email: text("email").notNull().unique(),
12+
name: varchar("name", { length: TEXT_LIMITS.NAME }).notNull(),
13+
email: varchar("email", { length: TEXT_LIMITS.EMAIL }).notNull().unique(),
714
emailVerified: boolean("email_verified")
815
.$defaultFn(() => false)
916
.notNull(),
10-
image: text("image"),
17+
image: varchar("image", { length: TEXT_LIMITS.URL }),
1118
createdAt: timestamp("created_at")
1219
.$defaultFn(() => /* @__PURE__ */ new Date())
1320
.notNull(),

packages/db/src/auth/verification.schema.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
1+
import { pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
2+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
23

34
export const verification = pgTable("verification", {
45
id: text("id").primaryKey(),
5-
identifier: text("identifier").notNull(),
6+
identifier: varchar("identifier", {
7+
length: TEXT_LIMITS.VERIFICATION_ID,
8+
}).notNull(),
69
value: text("value").notNull(),
710
expiresAt: timestamp("expires_at").notNull(),
811
createdAt: timestamp("created_at").$defaultFn(

packages/db/src/billing/plan.schema.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ import {
66
pgTable,
77
text,
88
timestamp,
9+
varchar,
910
} from "drizzle-orm/pg-core";
11+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
1012
import { Price } from "./price.schema.js";
1113

1214
export const Plan = pgTable(
1315
"plan",
1416
{
1517
id: text("id").primaryKey(),
16-
name: text("name").notNull().unique(),
17-
displayName: text("display_name").notNull(),
18-
description: text("description"),
18+
name: varchar("name", { length: TEXT_LIMITS.PLAN_NAME }).notNull().unique(),
19+
displayName: varchar("display_name", {
20+
length: TEXT_LIMITS.PLAN_NAME,
21+
}).notNull(),
22+
description: varchar("description", { length: TEXT_LIMITS.DESCRIPTION }),
1923
active: boolean("active").default(true).notNull(),
2024
quota: integer("quota").notNull(),
2125
created_at: timestamp("created_at")

packages/db/src/billing/price.schema.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
text,
88
timestamp,
99
unique,
10+
varchar,
1011
} from "drizzle-orm/pg-core";
12+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
1113
import { Plan } from "./plan.schema.js";
1214

1315
export const Price = pgTable(
@@ -18,16 +20,19 @@ export const Price = pgTable(
1820
.notNull()
1921
.unique()
2022
.references(() => Plan.id),
21-
nickname: text("nickname"),
22-
currency: text("currency").notNull(),
23+
nickname: varchar("nickname", { length: TEXT_LIMITS.PLAN_NAME }),
24+
currency: varchar("currency", { length: TEXT_LIMITS.CURRENCY }).notNull(),
2325
unitAmount: integer("unit_amount").notNull(),
2426
recurring: jsonb("recurring").notNull().$type<{
2527
interval: "day" | "week" | "month" | "year";
2628
intervalCount: number;
2729
}>(),
28-
type: text("type", { enum: ["one_time", "recurring"] }).notNull(),
30+
type: varchar("type", {
31+
length: 20,
32+
enum: ["one_time", "recurring"],
33+
}).notNull(),
2934
active: boolean("active").default(true).notNull(),
30-
lookupKey: text("lookup_key"),
35+
lookupKey: varchar("lookup_key", { length: TEXT_LIMITS.LOOKUP_KEY }),
3136
createdAt: timestamp("created_at")
3237
.$defaultFn(() => /* @__PURE__ */ new Date())
3338
.notNull(),

packages/db/src/billing/subscription.schema.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
pgTable,
77
text,
88
timestamp,
9+
varchar,
910
} from "drizzle-orm/pg-core";
11+
import { TEXT_LIMITS } from "../constants/resource-limits.js";
1012
import { user } from "../schema.js";
1113
import { Plan } from "./plan.schema.js";
1214

@@ -18,10 +20,16 @@ export const Subscription = pgTable(
1820
onDelete: "cascade",
1921
}),
2022
planName: text("plan_name").references(() => Plan.name),
21-
stripeCustomerId: text("stripe_customer_id"),
22-
stripeSubscriptionId: text("stripe_subscription_id"),
23+
stripeCustomerId: varchar("stripe_customer_id", {
24+
length: TEXT_LIMITS.STRIPE_ID,
25+
}),
26+
stripeSubscriptionId: varchar("stripe_subscription_id", {
27+
length: TEXT_LIMITS.STRIPE_ID,
28+
}),
2329
seats: integer("seats"),
24-
status: text("status").default("incomplete"),
30+
status: varchar("status", { length: TEXT_LIMITS.STATUS }).default(
31+
"incomplete",
32+
),
2533
periodStart: timestamp("period_start"),
2634
periodEnd: timestamp("period_end"),
2735
trialStart: timestamp("trial_start"),
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Resource protection limits for database fields
3+
* These constants define maximum sizes to prevent resource exhaustion attacks
4+
*/
5+
6+
/* Text content limits (in characters) */
7+
export const JSONB_LIMITS = {
8+
/** Maximum size for block data (10MB) */
9+
BLOCK_DATA: 10485760,
10+
/** Maximum size for text chunks (1MB) */
11+
CHUNK_TEXT: 1048576,
12+
/** Maximum size for JSONB metadata (1MB) */
13+
EMBEDDING_TASK_METADATA: 1048576,
14+
} as const;
15+
16+
/* String length limits (for varchar fields) */
17+
export const TEXT_LIMITS = {
18+
/** Currency codes */
19+
CURRENCY: 10,
20+
/** Plan descriptions */
21+
DESCRIPTION: 1000,
22+
/** Standard email length */
23+
EMAIL: 255,
24+
/** IP addresses (IPv6 max) */
25+
IP_ADDRESS: 45,
26+
/** Lookup keys */
27+
LOOKUP_KEY: 255,
28+
/** Model identifiers */
29+
MODEL_ID: 255,
30+
/** Model providers */
31+
MODEL_PROVIDER: 100,
32+
/** User names and titles */
33+
NAME: 100,
34+
/** Page titles */
35+
PAGE_TITLE: 500,
36+
/** Plan and price names */
37+
PLAN_NAME: 100,
38+
/** Organization slugs */
39+
SLUG: 50,
40+
/** Status and role fields */
41+
STATUS: 50,
42+
/** Stripe identifiers */
43+
STRIPE_ID: 255,
44+
/** Tokens and passwords */
45+
TOKEN: 4096,
46+
/** URLs and image paths */
47+
URL: 1024,
48+
/** User agents */
49+
USER_AGENT: 1024,
50+
/** Verification identifiers */
51+
VERIFICATION_ID: 255,
52+
} as const;

0 commit comments

Comments
 (0)