diff --git a/README.md b/README.md
index 839aa56..a4e480d 100644
--- a/README.md
+++ b/README.md
@@ -22,14 +22,77 @@
-### Features:
+### Features
-- Get student details using roll number
-- Get simplified view of timetable to embed into your applications
+- Get student group or section using roll number — supports **all 4 years**
+- Get timetables for **1st, 2nd, 3rd, and 4th year** students, fetched live from Xceed
- Get real-time library seat availability
+### Tech Stack
+
+- **Runtime**: [Bun](https://bun.sh)
+- **Framework**: [Hono](https://hono.dev) with `@hono/zod-openapi`
+- **Database**: SQLite via [Drizzle ORM](https://orm.drizzle.team)
+- **Validation**: [Zod](https://zod.dev)
+
+---
+
+## Getting Started
+
+### Prerequisites
+
+- [Bun](https://bun.sh) v1.0+
+
+### Installation
+
+```bash
+bun install
+```
+
+### Database Setup
+
+Before starting the server, seed the database with student data:
+
+```bash
+bun seed.ts
+```
+
+> **Note:** `seed.ts` creates all batch tables (`batch2022`–`batch2026`) and inserts sample records. Replace these with your real data before deploying.
+
+### Running the Server
+
+```bash
+bun server.ts
+```
+
+Server starts on port **6969** by default.
+
+### Running Tests
+
+```bash
+bun test test.spec.ts
+```
+
+### OpenAPI Docs
+
+The full interactive OpenAPI spec is served at:
+
+```
+http://localhost:6969/openapi
+```
+
+---
+
## Routes
+### Base URL
+
+```
+https://api.opensourcenitj.com
+```
+
+---
+
### Health Check
```http
@@ -88,7 +151,7 @@ Returns real-time library seat availability information across all floors.
| totalCapacity | number | Total seating capacity of the library |
| capacity | object[] | Floor-wise seating statistics |
-#### capacity Object
+#### `capacity` Object
| Field | Type | Description |
| ---------------- | -------- | -------------------------------------------- |
@@ -101,30 +164,24 @@ Returns real-time library seat availability information across all floors.
#### Error Response (404)
```json
-{
- "error": "Library information not found"
-}
+{ "error": "Library information not found" }
```
-| Field | Type | Description |
-| ----- | ------ | ------------- |
-| error | string | Error message |
-
---
-### Get Student Group (First Year Only)
+### Get Student Group
```http
GET /students/getGroup
```
-Returns the group and subgroup (e.g., `B6`, `a`) of a first-year student based on their roll number.
+Returns the group/section and subgroup of a student based on their roll number. Supports students from **all years** (batches 2022–2026). The batch is automatically inferred from the first two digits of the roll number.
#### Query Parameters
-| Parameter | Type | Description |
-| ---------- | ------ | -------------------------------- |
-| rollNumber | string | Required. Roll number of student |
+| Parameter | Type | Required | Description |
+| ---------- | ------ | -------- | -------------------- |
+| rollNumber | string | ✅ | Roll number of student (8–10 digits) |
#### Response (200)
@@ -135,14 +192,18 @@ Returns the group and subgroup (e.g., `B6`, `a`) of a first-year student based o
}
```
-| Field | Type | Description |
-| -------- | ------ | -------------------------------- |
-| group | string | Student group (`A1`-`B6`) |
-| subGroup | string | Student subgroup (`a`, `b`, `c`) |
+For higher-year students, `group` will reflect their branch section (e.g. `CSE-A`) and `subGroup` their lab batch identifier (e.g. `1`).
+
+| Field | Type | Description |
+| -------- | --------------- | -------------------------------------------------- |
+| group | string | Student group or section (e.g. `B6`, `CSE-A`) |
+| subGroup | string \| null | Student subgroup or lab batch (e.g. `a`, `1`, `2`) |
#### Error Response (404)
-Group not found for the provided roll number.
+```json
+{ "message": "Group not found for the provided roll number." }
+```
---
@@ -152,13 +213,13 @@ Group not found for the provided roll number.
GET /timetable/year/1
```
-Returns the timetable for first-year students.
+Returns the live timetable for first-year students, fetched directly from Xceed.
#### Query Parameters
-| Parameter | Type | Description |
-| --------- | ------ | --------------------------- |
-| group | string | Required. Group (`A1`–`B6`) |
+| Parameter | Type | Required | Description |
+| --------- | ------ | -------- | ---------------------------- |
+| group | string | ✅ | Common group (`A1`–`B6`) |
#### Response (200)
@@ -181,13 +242,98 @@ Returns the timetable for first-year students.
}
```
-#### timetableData structure
+#### Error Responses
+
+| Status | Description |
+| ------ | -------------------------------- |
+| 400 | Invalid `group` parameter |
+| 502 | Failed to fetch data from Xceed |
+| 500 | Internal server error |
+
+---
+
+### Get Higher Year Timetable
+
+```http
+GET /timetable/year/:year
+```
+
+Returns the live timetable for **2nd, 3rd, or 4th year** students for a specific branch and section, fetched directly from Xceed. The correct semester is automatically resolved based on the current academic session (Odd/Even).
+
+#### Path Parameters
+
+| Parameter | Type | Required | Description |
+| --------- | ------ | -------- | ------------------------ |
+| year | number | ✅ | Academic year: `2`, `3`, or `4` |
+
+#### Query Parameters
+
+| Parameter | Type | Required | Description |
+| --------- | ------ | -------- | ------------------------------------------------------- |
+| branch | string | ✅ | Department code (see table below). Use the **parent department**, not the specific programme name. |
+| group | string | ❌ | Section letter or identifier (e.g. `A`, `B`). Default: `A` |
+
+#### Department Codes
+
+> **Note:** The `branch` parameter maps to the **department** on Xceed, not your specific programme name.
+> Programmes that were introduced under a parent department share its timetable infrastructure:
+>
+> - **DSE** (Data Science & Engineering, from ~2023) → use `CSE`
+> - **AI** (Artificial Intelligence, from 2026) → use `CSE`
+> - **VLSI** specialisation → use `ECE`
+> - **DS** (Data Science) elective streams → use `IT`
+>
+> When in doubt, check which department conducts your core theory classes on Xceed.
+
+| Code | Department on Xceed | Programmes covered |
+| ---- | ------------------- | ------------------- |
+| `CSE` / `CS` | Computer Science and Engineering | B.Tech CSE, DSE (2023+), AI (2026+) |
+| `IT` | Information Technology | B.Tech IT, Data Science streams |
+| `ECE` / `EC` | Electronics and Communication Engineering | B.Tech ECE, VLSI specialisation |
+| `EE` | Electrical Engineering | B.Tech EE |
+| `ME` | Mechanical Engineering | B.Tech ME |
+| `CE` | Civil Engineering | B.Tech CE |
+| `BT` | Biotechnology | B.Tech BT |
+| `CH` / `CHE` | Chemical Engineering | B.Tech CH |
+| `ICE` | Instrumentation and Control Engineering | B.Tech ICE |
+| `IPE` | Industrial and Production Engineering | B.Tech IPE |
+| `TT` | Textile Technology | B.Tech TT |
+| `MNC` | Mathematics & Computing | B.Tech MNC |
+
+#### Example Request
+
+```http
+GET /timetable/year/2?branch=CSE&group=A
+```
+
+#### Response (200)
+
+```json
+{
+ "timetableData": {
+ "Monday": {
+ "period1": [
+ [
+ {
+ "subject": "DSA",
+ "faculty": "Lalatendu Behera",
+ "room": "LT-403"
+ }
+ ]
+ ]
+ }
+ },
+ "notes": []
+}
+```
+
+#### `timetableData` Structure
```ts
Record<
- string,
+ string, // Day (e.g. "Monday")
Record<
- string,
+ string, // Period (e.g. "period1", "lunch")
Array<
Array<{
subject: string;
@@ -199,32 +345,48 @@ Record<
>;
```
-| Field | Type | Description |
-| ------- | ------ | ------------ |
-| subject | string | Subject name |
-| faculty | string | Faculty name |
-| room | string | Room or lab |
+Each period contains an outer array (one entry per concurrent group/lab batch) and an inner array of classes happening simultaneously.
-| Field | Type | Description |
-| ----- | -------- | -------------------------- |
-| notes | string[] | Additional timetable notes |
+#### Error Responses
-#### Error Response (400)
+| Status | Description |
+| ------ | --------------------------------------------------------- |
+| 400 | Invalid `branch` or `group` parameter |
+| 404 | No timetable section found for the given parameters |
+| 502 | Failed to fetch data from Xceed |
+| 500 | Internal server error (including upstream timeout) |
-```json
-{
- "error": "Invalid group parameter"
-}
-```
+---
-| Field | Type | Description |
-| ----- | ------ | ------------- |
-| error | string | Error message |
+## Database Schema
+
+Student data is stored in per-batch SQLite tables (`batch2022` through `batch2026`). Each table has the same schema:
+
+| Column | Type | Description |
+| ----------- | ------- | ---------------------------------- |
+| rollnumber | integer | Primary key — student roll number |
+| name | text | Full name |
+| branchabbr | text | Branch abbreviation (e.g. `CSE`) |
+| branch | text | Full branch name |
+| group | text | Group or section |
+| subgroup | text | Sub-group or lab batch |
+
+The correct batch table is automatically selected based on the first two digits of the roll number (e.g. `24xxxxxx` → `batch2024`).
---
-### Base URL
+## Security
-```
-https://api.opensourcenitj.com
-```
+- Input validated via Zod schemas on all endpoints
+- Roll numbers validated to be 8–10 numeric digits with a valid year prefix
+- Branch abbreviations constrained to a strict allowlist — no raw user input reaches Xceed URLs
+- All Xceed fetches have an 8-second timeout
+- Secure HTTP headers via `hono/secure-headers`
+- CORS restricted to `GET` requests
+- Generic error messages in responses — internal details logged server-side only
+
+---
+
+## Contributing
+
+Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/opensourcenitj/api).
diff --git a/app.ts b/app.ts
index 916ce23..9f1dd59 100644
--- a/app.ts
+++ b/app.ts
@@ -1,12 +1,30 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { Hono } from "hono";
+import { cors } from "hono/cors";
+import { secureHeaders } from "hono/secure-headers";
import { prettyJSON } from "hono/pretty-json";
import { routes } from "./routes/_routes";
export const app = new OpenAPIHono();
+// Security middleware
+app.use(secureHeaders());
+app.use(cors({
+ origin: "*",
+ allowMethods: ["GET"],
+ allowHeaders: ["Content-Type"],
+}));
app.use(prettyJSON({ force: true }));
+// Global error handler — prevents raw stack traces leaking to clients
+app.onError((err, c) => {
+ console.error(err);
+ return c.json({ error: "Internal server error" }, 500);
+});
+
+// Global 404 handler
+app.notFound((c) => c.json({ error: "Route not found" }, 404));
+
for (const [path, route] of Object.entries(routes)) {
app.route(path, route as Hono);
}
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index a4f34ed..d24e9c6 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -1,5 +1,32 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
+export const batch2022Table = sqliteTable("batch2022", {
+ rollnumber: integer("rollnumber").primaryKey(),
+ name: text("name"),
+ branchabbr: text("branchabbr"),
+ branch: text("branch"),
+ group: text("group"),
+ subgroup: text("subgroup"),
+});
+
+export const batch2023Table = sqliteTable("batch2023", {
+ rollnumber: integer("rollnumber").primaryKey(),
+ name: text("name"),
+ branchabbr: text("branchabbr"),
+ branch: text("branch"),
+ group: text("group"),
+ subgroup: text("subgroup"),
+});
+
+export const batch2024Table = sqliteTable("batch2024", {
+ rollnumber: integer("rollnumber").primaryKey(),
+ name: text("name"),
+ branchabbr: text("branchabbr"),
+ branch: text("branch"),
+ group: text("group"),
+ subgroup: text("subgroup"),
+});
+
export const batch2025Table = sqliteTable("batch2025", {
rollnumber: integer("rollnumber").primaryKey(),
name: text("name"),
@@ -8,3 +35,12 @@ export const batch2025Table = sqliteTable("batch2025", {
group: text("group"),
subgroup: text("subgroup"),
});
+
+export const batch2026Table = sqliteTable("batch2026", {
+ rollnumber: integer("rollnumber").primaryKey(),
+ name: text("name"),
+ branchabbr: text("branchabbr"),
+ branch: text("branch"),
+ group: text("group"),
+ subgroup: text("subgroup"),
+});
diff --git a/lib/firstYear.ts b/lib/firstYear.ts
index 1d109e4..590d72c 100644
--- a/lib/firstYear.ts
+++ b/lib/firstYear.ts
@@ -1,4 +1,3 @@
-import { fetch } from "bun";
import { eq } from "drizzle-orm";
import config from "../config.json" assert { type: "json" };
import type { studentInfo } from "../types/db";
@@ -16,14 +15,26 @@ export async function getFirstYearGroups(): Promise<{ [key: string]: semArrDataT
}
const currentSession = currentSessionObj.session;
- const dept = sessionDeptData.uniqueDept[0];
+ const dept = sessionDeptData.uniqueDept.find(d => d === "Basic Sciences");
+ if (!dept) {
+ throw new Error("Basic Sciences department not found in Xceed session data");
+ }
const codeFetch = await fetch(
`${config.url.deptCode}/${encodeURIComponent(`${currentSession}`)}/${encodeURIComponent(`${dept}`)}`
);
+ if (!codeFetch.ok) {
+ throw new Error("Failed to fetch department code from Xceed");
+ }
const code = JSON.parse(await codeFetch.text());
+ if (typeof code !== "string" || code.trim() === "") {
+ throw new Error("Unexpected code format from Xceed API");
+ }
const semFetch = await fetch(`${config.url.sem}${encodeURIComponent(code)}`);
+ if (!semFetch.ok) {
+ throw new Error("Failed to fetch semester data from Xceed");
+ }
const semData = (await semFetch.json()) as semArrDataType;
const usableData = semData.filter(semEntry => semEntry.code === code);
@@ -40,16 +51,38 @@ export async function getFirstYearGroups(): Promise<{ [key: string]: semArrDataT
}
export async function getStudentDetails(rollNumber: string, batch: string): Promise {
- const table = batch === "2025" ? dbSchema.batch2025Table : null;
+ let table;
+ switch (batch) {
+ case "2022":
+ table = dbSchema.batch2022Table;
+ break;
+ case "2023":
+ table = dbSchema.batch2023Table;
+ break;
+ case "2024":
+ table = dbSchema.batch2024Table;
+ break;
+ case "2025":
+ table = dbSchema.batch2025Table;
+ break;
+ case "2026":
+ table = dbSchema.batch2026Table;
+ break;
+ default:
+ return null;
+ }
- if (!table) {
+ if (!/^\d+$/.test(rollNumber)) {
+ return null;
+ }
+ const rollInt = parseInt(rollNumber, 10);
+ if (isNaN(rollInt)) {
return null;
}
-
const data = await db
.select()
.from(table)
- .where(eq(table.rollnumber, parseInt(rollNumber)));
+ .where(eq(table.rollnumber, rollInt));
if (data.length === 0) {
return null;
@@ -69,20 +102,21 @@ export async function getStudentDetails(rollNumber: string, batch: string): Prom
}
export async function getGroup(rollNumber: string): Promise<{ group: string; subGroup: string | null } | null> {
- const groups = ["A1", "A2", "A3", "A4", "A5", "A6", "B1", "B2", "B3", "B4", "B5", "B6"];
- const subGroups = ["a", "b", "c"];
-
- const studentDetails = await getStudentDetails(rollNumber, "2025");
+ const rollStr = rollNumber.trim();
+ // Validate: must be numeric digits only
+ if (!/^\d{8,10}$/.test(rollStr)) return null;
+ const yearDigits = rollStr.substring(0, 2);
+ // Validate: year prefix must be a valid recent batch (e.g. 20–30)
+ const yearNum = parseInt(yearDigits, 10);
+ if (isNaN(yearNum) || yearNum < 20 || yearNum > 35) return null;
+ const batch = "20" + yearDigits;
+
+ const studentDetails = await getStudentDetails(rollStr, batch);
if (studentDetails) {
- const studentGroup = studentDetails.group.toUpperCase();
- const subGroup = studentDetails.subgroup.toLowerCase().slice(-1);
- if (groups.includes(studentGroup)) {
- if (subGroups.includes(subGroup)) {
- return { group: studentGroup, subGroup: subGroup };
- } else {
- return { group: studentGroup, subGroup: subGroup[0] as string };
- }
- }
+ const studentGroup = studentDetails.group ? studentDetails.group.toUpperCase() : "";
+ const subgroupStr = studentDetails.subgroup ? studentDetails.subgroup.trim() : "";
+ const subGroup = subgroupStr ? subgroupStr.toLowerCase().slice(-1) : null;
+ return { group: studentGroup, subGroup: subGroup };
}
return null;
diff --git a/lib/higherYears.ts b/lib/higherYears.ts
new file mode 100644
index 0000000..bab6178
--- /dev/null
+++ b/lib/higherYears.ts
@@ -0,0 +1,166 @@
+import config from "../config.json" assert { type: "json" };
+import type {
+ semArrDataType,
+ semDataType,
+ sessionDeptDataType,
+} from "../types/xceed";
+
+export const allowedBranches = [
+ "CSE",
+ "CS",
+ "IT",
+ "ECE",
+ "EC",
+ "EE",
+ "ME",
+ "CE",
+ "BT",
+ "CH",
+ "CHE",
+ "ICE",
+ "IPE",
+ "TT",
+ "MNC",
+ "MC",
+] as const;
+
+export const branchToDeptMap: Record<
+ (typeof allowedBranches)[number],
+ { deptName: string; semPrefix: string }
+> = {
+ CSE: {
+ deptName: "Computer Science and Engineering",
+ semPrefix: "B.Tech-CSE",
+ },
+ CS: { deptName: "Computer Science and Engineering", semPrefix: "B.Tech-CSE" },
+ IT: { deptName: "Information Technology", semPrefix: "B.Tech-IT" },
+ ECE: {
+ deptName: "Electronics and Communication Engineering",
+ semPrefix: "B.Tech-ECE",
+ },
+ EC: {
+ deptName: "Electronics and Communication Engineering",
+ semPrefix: "B.Tech-ECE",
+ },
+ EE: { deptName: "Electrical Engineering", semPrefix: "B.Tech-EE" },
+ ME: { deptName: "Mechanical Engineering", semPrefix: "B.Tech-ME" },
+ CE: { deptName: "Civil Engineering", semPrefix: "B.Tech-CE" },
+ BT: { deptName: "Biotechnology", semPrefix: "B.Tech-BT" },
+ CH: { deptName: "Chemical Engineering", semPrefix: "B.Tech-CH" },
+ CHE: { deptName: "Chemical Engineering", semPrefix: "B.Tech-CH" },
+ ICE: {
+ deptName: "Instrumentation and Control Engineering",
+ semPrefix: "B.Tech-ICE",
+ },
+ IPE: {
+ deptName: "Industrial and Production Engineering",
+ semPrefix: "B.Tech-IPE",
+ },
+ TT: { deptName: "Textile Technology", semPrefix: "B.Tech-TT" },
+ MNC: { deptName: "Mathematics & Computing", semPrefix: "B.Tech-MCE" },
+ MC: { deptName: "Mathematics & Computing", semPrefix: "B.Tech-MCE" },
+};
+
+export async function getHigherYearGroup(
+ year: number,
+ branch: string,
+ group: string,
+ subSection?: string,
+): Promise {
+ const branchUpper = branch.trim().toUpperCase();
+ const branchInfo = branchToDeptMap[branchUpper];
+ if (!branchInfo) {
+ throw new Error(`Invalid branch abbreviation: ${branch}`);
+ }
+
+ const sessions = await fetch(config.url.sessionAndDept);
+ const sessionDeptData = (await sessions.json()) as sessionDeptDataType;
+
+ const currentSessionObj = sessionDeptData.uniqueSessions.find(
+ (sess) => sess.currentSession,
+ );
+ if (!currentSessionObj || !currentSessionObj.session) {
+ throw new Error(
+ "Current session not found in sessionDeptData.uniqueSessions",
+ );
+ }
+ const currentSession = currentSessionObj.session;
+ const isEven = currentSession.toLowerCase().includes("even");
+
+ // Calculate semester number based on college year and odd/even session
+ const semNumber = (year - 1) * 2 + (isEven ? 2 : 1);
+
+ // Get the department code
+ const codeFetch = await fetch(
+ `${config.url.deptCode}/${encodeURIComponent(`${currentSession}`)}/${encodeURIComponent(`${branchInfo.deptName}`)}`,
+ );
+ if (!codeFetch.ok) {
+ throw new Error("Failed to fetch department code from Xceed");
+ }
+ const code = JSON.parse(await codeFetch.text());
+ if (typeof code !== "string" || code.trim() === "") {
+ throw new Error("Unexpected code format from Xceed API");
+ }
+
+ // Fetch the semesters list for this department code
+ const semFetch = await fetch(`${config.url.sem}${encodeURIComponent(code)}`);
+ if (!semFetch.ok) {
+ throw new Error("Failed to fetch semester data from Xceed");
+ }
+ const semData = (await semFetch.json()) as semArrDataType;
+
+ const grpUpper = group.trim().toUpperCase();
+ const subUpper = subSection ? subSection.trim().toUpperCase() : "";
+
+ // Attempt pattern match with subsection first: e.g. B.Tech-CSE-4A1 or B.Tech-CSE-4A-1
+ const targetPatternWithSub1 = `${branchInfo.semPrefix}-${semNumber}${grpUpper}${subUpper}`;
+ const targetPatternWithSub2 = `${branchInfo.semPrefix}-${semNumber}${grpUpper}-${subUpper}`;
+
+ const targetPattern = `${branchInfo.semPrefix}-${semNumber}${grpUpper}`; // e.g. B.Tech-CSE-4A
+ const fallbackPattern2 = `${branchInfo.semPrefix}-${semNumber}-${grpUpper}`;
+ const fallbackPattern3 = `${branchInfo.semPrefix}-${semNumber}`; // e.g. if single section like B.Tech-CSE-8
+
+ let semEntry: semDataType | undefined;
+
+ if (subUpper) {
+ semEntry = semData.find(
+ (entry) =>
+ entry.code === code &&
+ (entry.sem.toUpperCase() === targetPatternWithSub1.toUpperCase() ||
+ entry.sem.toUpperCase().includes(targetPatternWithSub1.toUpperCase()) ||
+ entry.sem.toUpperCase() === targetPatternWithSub2.toUpperCase() ||
+ entry.sem.toUpperCase().includes(targetPatternWithSub2.toUpperCase())),
+ );
+ }
+
+ if (!semEntry) {
+ semEntry = semData.find(
+ (entry) =>
+ entry.code === code &&
+ (entry.sem.toUpperCase() === targetPattern.toUpperCase() ||
+ entry.sem.toUpperCase().includes(targetPattern.toUpperCase()) ||
+ entry.sem.toUpperCase().includes(fallbackPattern2.toUpperCase())),
+ );
+ }
+
+ if (!semEntry) {
+ semEntry = semData.find(
+ (entry) =>
+ entry.code === code &&
+ entry.sem.toUpperCase() === fallbackPattern3.toUpperCase(),
+ );
+ }
+
+ if (!semEntry) {
+ semEntry = semData.find(
+ (entry) =>
+ entry.code === code &&
+ entry.sem.toUpperCase().includes(branchInfo.semPrefix.toUpperCase()) &&
+ entry.sem.includes(`${semNumber}`) &&
+ entry.sem.toUpperCase().includes(grpUpper) &&
+ (!subUpper || entry.sem.toUpperCase().includes(subUpper)),
+ );
+ }
+
+ return semEntry || null;
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..0885b07
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1800 @@
+{
+ "name": "api",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "api",
+ "version": "0.1.0",
+ "dependencies": {
+ "@hono/zod-openapi": "^1.2.0",
+ "api": ".",
+ "drizzle-orm": "^0.45.1",
+ "hono": "^4.11.4",
+ "zod": "^4.3.5"
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ "drizzle-kit": "^0.31.8"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ }
+ },
+ "node_modules/@asteasolutions/zod-to-openapi": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz",
+ "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "openapi3-ts": "^4.1.2"
+ },
+ "peerDependencies": {
+ "zod": "^4.0.0"
+ }
+ },
+ "node_modules/@drizzle-team/brocli": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
+ "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild-kit/core-utils": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
+ "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
+ "deprecated": "Merged into tsx: https://tsx.hirok.io",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.18.20",
+ "source-map-support": "^0.5.21"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/@esbuild-kit/esm-loader": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
+ "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
+ "deprecated": "Merged into tsx: https://tsx.hirok.io",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@esbuild-kit/core-utils": "^3.3.2",
+ "get-tsconfig": "^4.7.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@hono/zod-openapi": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-1.5.1.tgz",
+ "integrity": "sha512-ZaDdEIkn6PEGjIYHXJeyByg6yhvLzI+UXn968pWtahpwcQ6HMjQYXI3zNffTl0Wl9QZ79nUnGqiN1erEIu23fA==",
+ "license": "MIT",
+ "dependencies": {
+ "@asteasolutions/zod-to-openapi": "^8.5.0",
+ "@hono/zod-validator": "^0.9.0",
+ "openapi3-ts": "^4.5.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "hono": ">=4.10.0",
+ "zod": "^4.0.0"
+ }
+ },
+ "node_modules/@hono/zod-validator": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.9.0.tgz",
+ "integrity": "sha512-n0ZSXmCiHVIp4Y5wlOOyZCeTd/rsawA/qW1cipB8QOYKZ9N8Tk0nZUZCXho9cu374AN4JpDNKioNKBJ/W+LBug==",
+ "license": "MIT",
+ "peerDependencies": {
+ "hono": ">=4.11.2",
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/@types/bun": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz",
+ "integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bun-types": "1.3.14"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/api": {
+ "resolved": "",
+ "link": true
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bun-types": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz",
+ "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/drizzle-kit": {
+ "version": "0.31.10",
+ "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
+ "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@drizzle-team/brocli": "^0.10.2",
+ "@esbuild-kit/esm-loader": "^2.5.5",
+ "esbuild": "^0.25.4",
+ "tsx": "^4.21.0"
+ },
+ "bin": {
+ "drizzle-kit": "bin.cjs"
+ }
+ },
+ "node_modules/drizzle-orm": {
+ "version": "0.45.2",
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
+ "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
+ "license": "Apache-2.0",
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/client-rds-data": {
+ "optional": true
+ },
+ "@cloudflare/workers-types": {
+ "optional": true
+ },
+ "@electric-sql/pglite": {
+ "optional": true
+ },
+ "@libsql/client": {
+ "optional": true
+ },
+ "@libsql/client-wasm": {
+ "optional": true
+ },
+ "@neondatabase/serverless": {
+ "optional": true
+ },
+ "@op-engineering/op-sqlite": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@planetscale/database": {
+ "optional": true
+ },
+ "@prisma/client": {
+ "optional": true
+ },
+ "@tidbcloud/serverless": {
+ "optional": true
+ },
+ "@types/better-sqlite3": {
+ "optional": true
+ },
+ "@types/pg": {
+ "optional": true
+ },
+ "@types/sql.js": {
+ "optional": true
+ },
+ "@upstash/redis": {
+ "optional": true
+ },
+ "@vercel/postgres": {
+ "optional": true
+ },
+ "@xata.io/client": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "bun-types": {
+ "optional": true
+ },
+ "expo-sqlite": {
+ "optional": true
+ },
+ "gel": {
+ "optional": true
+ },
+ "knex": {
+ "optional": true
+ },
+ "kysely": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "postgres": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ },
+ "sql.js": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.31",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
+ "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/openapi3-ts": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.6.0.tgz",
+ "integrity": "sha512-a4sfn6L2sIShhtzJqmjGrARvxAW/3F2BJDdyRVvNF9VhAsZSh5hSyI3a9TNvmzBxXmq66nY5LNT5bQcBxYAZZg==",
+ "license": "MIT",
+ "dependencies": {
+ "yaml": "^2.9.0"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.23.1",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
+ "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/yaml": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/routes/students/getGroup.ts b/routes/students/getGroup.ts
index 1517cdb..25e6c2d 100644
--- a/routes/students/getGroup.ts
+++ b/routes/students/getGroup.ts
@@ -8,8 +8,8 @@ const querySchema = z.object({
});
const responseSchema = z.object({
- group: z.enum(["A1", "A2", "A3", "A4", "A5", "A6", "B1", "B2", "B3", "B4", "B5", "B6"]),
- subGroup: z.enum(["a", "b", "c"]),
+ group: z.string().openapi({ example: "B6" }),
+ subGroup: z.string().nullable().openapi({ example: "a" }),
});
getGroupRoute.openapi(
@@ -17,8 +17,8 @@ getGroupRoute.openapi(
method: "get",
path: "/",
tags: ["Students"],
- summary: "Get Student Group [ FIRST YEAR STUDENTS ONLY ]",
- description: "Returns the group (e.g., B6) of a student based on their roll number.",
+ summary: "Get Student Group",
+ description: "Returns the group (e.g., B6) or section of a student based on their roll number.",
request: {
query: querySchema,
},
@@ -41,20 +41,8 @@ getGroupRoute.openapi(
const studentGroup = await getGroup(rollNumber);
if (studentGroup) {
return c.json({
- group: studentGroup.group as
- | "A1"
- | "A2"
- | "A3"
- | "A4"
- | "A5"
- | "A6"
- | "B1"
- | "B2"
- | "B3"
- | "B4"
- | "B5"
- | "B6",
- subGroup: studentGroup.subGroup as "a" | "b" | "c",
+ group: studentGroup.group,
+ subGroup: studentGroup.subGroup,
});
}
return c.json({ message: "Group not found for the provided roll number." }, 404);
diff --git a/routes/timetable/index.ts b/routes/timetable/index.ts
index 2aac84f..8412f21 100644
--- a/routes/timetable/index.ts
+++ b/routes/timetable/index.ts
@@ -1,6 +1,8 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { year1 } from "./year/1";
+import { higherYears } from "./year/higherYears";
export const timeTableRoute = new OpenAPIHono();
timeTableRoute.route("/year/1", year1);
+timeTableRoute.route("/year", higherYears);
diff --git a/routes/timetable/year/1.ts b/routes/timetable/year/1.ts
index 87daa72..a04ea6c 100644
--- a/routes/timetable/year/1.ts
+++ b/routes/timetable/year/1.ts
@@ -1,5 +1,4 @@
import { OpenAPIHono, z } from "@hono/zod-openapi";
-import { fetch } from "bun";
import config from "../../../config.json" assert { type: "json" };
import { getFirstYearGroups } from "../../../lib/firstYear";
import type { semDataType, timetableDataType } from "../../../types/xceed";
@@ -60,6 +59,22 @@ year1.openapi(
},
},
},
+ 500: {
+ description: "Internal server error",
+ content: {
+ "application/json": {
+ schema: errorSchema,
+ },
+ },
+ },
+ 502: {
+ description: "Failed to fetch timetable data",
+ content: {
+ "application/json": {
+ schema: errorSchema,
+ },
+ },
+ },
},
},
async c => {
@@ -73,11 +88,19 @@ year1.openapi(
}
try {
- const fetchedData = await fetch(
- `${config.url.timeTable}/${encodeURIComponent(groupData.code.toString())}/${encodeURIComponent(
- groupData.sem.toString()
- )}`
- );
+ const controller = new AbortController();
+ const fetchTimeout = setTimeout(() => controller.abort(), 8000);
+ let fetchedData: Response;
+ try {
+ fetchedData = await fetch(
+ `${config.url.timeTable}/${encodeURIComponent(groupData.code.toString())}/${encodeURIComponent(
+ groupData.sem.toString()
+ )}`,
+ { signal: controller.signal }
+ );
+ } finally {
+ clearTimeout(fetchTimeout);
+ }
if (!fetchedData.ok) {
return c.json({ error: "Failed to fetch timetable data" }, 502);
@@ -86,8 +109,11 @@ year1.openapi(
const data = (await fetchedData.json()) as timetableDataType;
return c.json(data, 200);
- } catch (error) {
- return c.json({ error: "An error occurred while fetching timetable data" }, 500);
+ } catch (error: unknown) {
+ const message = error instanceof Error && error.name === "AbortError"
+ ? "Timetable fetch timed out"
+ : "An error occurred while fetching timetable data";
+ return c.json({ error: message }, 500);
}
}
);
diff --git a/routes/timetable/year/higherYears.ts b/routes/timetable/year/higherYears.ts
new file mode 100644
index 0000000..cf5a49e
--- /dev/null
+++ b/routes/timetable/year/higherYears.ts
@@ -0,0 +1,255 @@
+import { OpenAPIHono, z } from "@hono/zod-openapi";
+import config from "../../../config.json" assert { type: "json" };
+import { getHigherYearGroup, allowedBranches } from "../../../lib/higherYears";
+import type { timetableDataType } from "../../../types/xceed";
+
+export const higherYears = new OpenAPIHono();
+
+const pathSchema = z.object({
+ year: z.enum(["2", "3", "4"]),
+});
+
+const querySchema = z.object({
+ branch: z
+ .enum(allowedBranches)
+ .openapi({ example: "CSE" }),
+ group: z
+ .string()
+ .max(3)
+ .regex(/^[A-Za-z0-9]*$/, "Group must be alphanumeric")
+ .default("A")
+ .openapi({ example: "A" }),
+ subSection: z
+ .string()
+ .max(3)
+ .regex(/^[A-Za-z0-9]*$/, "SubSection must be alphanumeric")
+ .optional()
+ .openapi({ example: "1" }),
+});
+
+const responseSchema = z.object({
+ timetableData: z.record(
+ z.string(),
+ z.record(
+ z.string(),
+ z.array(
+ z.array(
+ z.object({
+ subject: z.string(),
+ subjectCode: z.string().nullable(),
+ faculty: z.string(),
+ room: z.string(),
+ day: z.string(),
+ startTime: z.string().nullable(),
+ endTime: z.string().nullable(),
+ duration: z.string().nullable(),
+ branch: z.string(),
+ year: z.string(),
+ section: z.string(),
+ subSection: z.string().nullable(),
+ courseType: z.enum(["Lecture", "Tutorial", "Practical", "Unknown"]),
+ }),
+ ),
+ ),
+ ),
+ ),
+ notes: z.array(z.string()),
+});
+
+const errorSchema = z.object({
+ error: z.string(),
+});
+
+higherYears.openapi(
+ {
+ method: "get",
+ path: "/:year",
+ tags: ["Timetable"],
+ summary: "Get Higher Year Timetable",
+ description:
+ "Returns the timetable for second, third, and fourth year students.",
+ request: {
+ params: pathSchema,
+ query: querySchema,
+ },
+ responses: {
+ 200: {
+ description: "Timetable retrieved successfully",
+ content: {
+ "application/json": {
+ schema: responseSchema,
+ },
+ },
+ },
+ 400: {
+ description: "Invalid parameters",
+ content: {
+ "application/json": {
+ schema: errorSchema,
+ },
+ },
+ },
+ 404: {
+ description: "Timetable not found for the specified parameters",
+ content: {
+ "application/json": {
+ schema: errorSchema,
+ },
+ },
+ },
+ 500: {
+ description: "Internal server error",
+ content: {
+ "application/json": {
+ schema: errorSchema,
+ },
+ },
+ },
+ 502: {
+ description: "Failed to fetch timetable data from Xceed",
+ content: {
+ "application/json": {
+ schema: errorSchema,
+ },
+ },
+ },
+ },
+ },
+ async (c) => {
+ const { year } = c.req.valid("param");
+ const { branch, group, subSection } = c.req.valid("query");
+
+ try {
+ const yearNum = parseInt(year, 10);
+ const groupData = await getHigherYearGroup(yearNum, branch, group, subSection);
+
+ if (!groupData) {
+ return c.json(
+ { error: "Timetable section not found for the specified parameters" },
+ 404,
+ );
+ }
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 8000);
+ let fetchedData: Response;
+ try {
+ fetchedData = await fetch(
+ `${config.url.timeTable}/${encodeURIComponent(groupData.code.toString())}/${encodeURIComponent(
+ groupData.sem.toString(),
+ )}`,
+ { signal: controller.signal },
+ );
+ } finally {
+ clearTimeout(timeout);
+ }
+
+ if (!fetchedData.ok) {
+ return c.json(
+ { error: "Failed to fetch timetable data from Xceed" },
+ 502,
+ );
+ }
+
+ const rawData = (await fetchedData.json()) as timetableDataType;
+
+ // Define approximate NITJ slot times for standard periods
+ const timeSlots: Record = {
+ period1: { start: "08:30 AM", end: "09:30 AM", duration: "1 hour" },
+ period2: { start: "09:30 AM", end: "10:30 AM", duration: "1 hour" },
+ period3: { start: "10:30 AM", end: "11:30 AM", duration: "1 hour" },
+ period4: { start: "11:30 AM", end: "12:30 PM", duration: "1 hour" },
+ period5: { start: "01:30 PM", end: "02:30 PM", duration: "1 hour" },
+ period6: { start: "02:30 PM", end: "03:30 PM", duration: "1 hour" },
+ period7: { start: "03:30 PM", end: "04:30 PM", duration: "1 hour" },
+ period8: { start: "04:30 PM", end: "05:30 PM", duration: "1 hour" },
+ };
+
+ const DAYS_OF_WEEK = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
+ const structuredTimetable: Record>>> = {};
+
+ for (const [day, periods] of Object.entries(rawData.timetableData)) {
+ if (!DAYS_OF_WEEK.includes(day)) {
+ continue;
+ }
+ structuredTimetable[day] = {};
+ for (const [periodName, rawSlots] of Object.entries(periods)) {
+ // Normalize slots to a 2D array defensively
+ let slots: Array> = [];
+ if (Array.isArray(rawSlots)) {
+ slots = rawSlots.map((item) => {
+ if (Array.isArray(item)) {
+ return item;
+ } else if (item && typeof item === "object") {
+ return [item];
+ }
+ return [];
+ }).filter(list => list.length > 0);
+ } else if (rawSlots && typeof rawSlots === "object") {
+ const slotObj = rawSlots as any;
+ if (slotObj.subject || slotObj.faculty || slotObj.room) {
+ slots = [[slotObj]];
+ }
+ }
+
+ structuredTimetable[day][periodName] = slots.map((concurrentSlotList) => {
+ return concurrentSlotList.map((slot) => {
+ if (!slot) return null;
+ const subjectText = (slot.subject || "").trim();
+
+ // Attempt to extract subject code if present in subjectText (e.g. "CSX-201 Data Structures" or "CSX 201")
+ const codeMatch = subjectText.match(/^([A-Z]{2,4}[- ]?\d{3})/i);
+ const subjectCode = codeMatch ? codeMatch[1] : null;
+
+ // Determine Course Type: Lab/Practical, Tutorial, or Lecture
+ let courseType: "Lecture" | "Tutorial" | "Practical" | "Unknown" = "Lecture";
+ const lowerSubj = subjectText.toLowerCase();
+ if (lowerSubj.includes("lab") || lowerSubj.includes("practical") || lowerSubj.includes("project")) {
+ courseType = "Practical";
+ } else if (lowerSubj.includes("tutorial") || lowerSubj.includes("tut")) {
+ courseType = "Tutorial";
+ }
+
+ // Retrieve period timings
+ const timings = timeSlots[periodName.toLowerCase()] || { start: null, end: null, duration: null };
+
+ return {
+ subject: subjectText,
+ subjectCode,
+ faculty: (slot.faculty || "").trim(),
+ room: (slot.room || "").trim(),
+ day,
+ startTime: timings.start,
+ endTime: timings.end,
+ duration: timings.duration,
+ branch,
+ year,
+ section: group,
+ subSection: subSection || null,
+ courseType,
+ };
+ }).filter(Boolean);
+ });
+ }
+ }
+
+ return c.json({
+ timetableData: structuredTimetable,
+ notes: rawData.notes,
+ }, 200);
+ } catch (error: unknown) {
+ console.error("Error in higherYears timetable route handler:", error);
+ if (
+ error instanceof Error &&
+ error.message.startsWith("Invalid branch abbreviation")
+ ) {
+ return c.json({ error: "Invalid branch parameter" }, 400);
+ }
+ const message =
+ error instanceof Error && error.name === "AbortError"
+ ? "Timetable fetch timed out"
+ : "An error occurred while fetching timetable data";
+ return c.json({ error: message }, 500);
+ }
+ },
+);
diff --git a/seed.ts b/seed.ts
new file mode 100644
index 0000000..a72c648
--- /dev/null
+++ b/seed.ts
@@ -0,0 +1,64 @@
+import { Database } from "bun:sqlite";
+import { db } from "./lib/db";
+import * as schema from "./lib/db/schema";
+
+async function main() {
+ console.log("Initializing database.sqlite...");
+ const sqlite = new Database("database.sqlite");
+
+ const ALLOWED_TABLES = ["batch2022", "batch2023", "batch2024", "batch2025", "batch2026"];
+ const createTableQuery = (tableName: string) => {
+ if (!ALLOWED_TABLES.includes(tableName)) throw new Error(`Invalid table name: ${tableName}`);
+ return `
+ CREATE TABLE IF NOT EXISTS ${tableName} (
+ rollnumber INTEGER PRIMARY KEY,
+ name TEXT,
+ branchabbr TEXT,
+ branch TEXT,
+ "group" TEXT,
+ subgroup TEXT
+ );
+ `;
+ };
+
+ sqlite.run(createTableQuery("batch2022"));
+ sqlite.run(createTableQuery("batch2023"));
+ sqlite.run(createTableQuery("batch2024"));
+ sqlite.run(createTableQuery("batch2025"));
+ sqlite.run(createTableQuery("batch2026"));
+ sqlite.close();
+
+ console.log("Seeding test data...");
+
+ // Seed dummy students across different years
+ await db.insert(schema.batch2025Table).values({
+ rollnumber: 25103001,
+ name: "First Year CSE Student",
+ branchabbr: "CS",
+ branch: "Computer Science & Engineering",
+ group: "B6",
+ subgroup: "a",
+ }).onConflictDoNothing();
+
+ await db.insert(schema.batch2024Table).values({
+ rollnumber: 24103002,
+ name: "Second Year CSE Student",
+ branchabbr: "CS",
+ branch: "Computer Science & Engineering",
+ group: "CSE-A",
+ subgroup: "C1",
+ }).onConflictDoNothing();
+
+ await db.insert(schema.batch2023Table).values({
+ rollnumber: 23104003,
+ name: "Third Year ECE Student",
+ branchabbr: "EC",
+ branch: "Electronics & Communication Engineering",
+ group: "ECE-B",
+ subgroup: "D2",
+ }).onConflictDoNothing();
+
+ console.log("Database seeded successfully!");
+}
+
+main().catch(console.error);
diff --git a/test.spec.ts b/test.spec.ts
new file mode 100644
index 0000000..9fba53a
--- /dev/null
+++ b/test.spec.ts
@@ -0,0 +1,254 @@
+import { beforeAll, afterAll, describe, expect, it } from "bun:test";
+import { Database } from "bun:sqlite";
+import { app } from "./app";
+import { db } from "./lib/db";
+import * as schema from "./lib/db/schema";
+
+const originalFetch = globalThis.fetch;
+
+beforeAll(async () => {
+ // Ensure all tables are created in the SQLite database
+ const sqlite = new Database("database.sqlite");
+
+ const createTableQuery = (tableName: string) => `
+ CREATE TABLE IF NOT EXISTS ${tableName} (
+ rollnumber INTEGER PRIMARY KEY,
+ name TEXT,
+ branchabbr TEXT,
+ branch TEXT,
+ "group" TEXT,
+ subgroup TEXT
+ );
+ `;
+
+ sqlite.run(createTableQuery("batch2022"));
+ sqlite.run(createTableQuery("batch2023"));
+ sqlite.run(createTableQuery("batch2024"));
+ sqlite.run(createTableQuery("batch2025"));
+ sqlite.run(createTableQuery("batch2026"));
+ sqlite.close();
+
+ // Seed dummy students across different years
+ await db.insert(schema.batch2025Table).values({
+ rollnumber: 25103001,
+ name: "First Year CSE Student",
+ branchabbr: "CS",
+ branch: "Computer Science & Engineering",
+ group: "B6",
+ subgroup: "a",
+ }).onConflictDoNothing();
+
+ await db.insert(schema.batch2024Table).values({
+ rollnumber: 24103002,
+ name: "Second Year CSE Student",
+ branchabbr: "CS",
+ branch: "Computer Science & Engineering",
+ group: "CSE-A",
+ subgroup: "C1",
+ }).onConflictDoNothing();
+
+ await db.insert(schema.batch2023Table).values({
+ rollnumber: 23104003,
+ name: "Third Year ECE Student",
+ branchabbr: "EC",
+ branch: "Electronics & Communication Engineering",
+ group: "ECE-B",
+ subgroup: "D2",
+ }).onConflictDoNothing();
+
+ // Mock global fetch to isolate tests from network dependency
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
+ const urlStr = url.toString();
+
+ if (urlStr.includes("allsessanddept")) {
+ return new Response(JSON.stringify({
+ uniqueSessions: [{ session: "2026-2027 (Odd)", currentSession: true }],
+ uniqueDept: [
+ "Basic Sciences",
+ "Computer Science and Engineering",
+ "Electronics and Communication Engineering",
+ "Chemical Engineering"
+ ]
+ }));
+ }
+
+ if (urlStr.includes("getcode")) {
+ if (urlStr.includes("Basic%20Sciences")) {
+ return new Response(JSON.stringify("basic-sciences-code"));
+ }
+ if (urlStr.includes("Computer%20Science%20and%20Engineering")) {
+ return new Response(JSON.stringify("cse-code"));
+ }
+ if (urlStr.includes("Electronics%20and%20Communication%20Engineering")) {
+ return new Response(JSON.stringify("ece-code"));
+ }
+ if (urlStr.includes("Chemical%20Engineering")) {
+ return new Response(JSON.stringify("che-code"));
+ }
+ }
+
+ if (urlStr.includes("addsem")) {
+ if (urlStr.includes("code=basic-sciences-code")) {
+ return new Response(JSON.stringify([
+ { _id: "1", sem: "B.Tech-CSE-SectionB6", code: "basic-sciences-code" }
+ ]));
+ }
+ if (urlStr.includes("code=cse-code")) {
+ return new Response(JSON.stringify([
+ { _id: "2", sem: "B.Tech-CSE-3A", code: "cse-code" }
+ ]));
+ }
+ if (urlStr.includes("code=ece-code")) {
+ return new Response(JSON.stringify([
+ { _id: "3", sem: "B.Tech-ECE-5B", code: "ece-code" }
+ ]));
+ }
+ if (urlStr.includes("code=che-code")) {
+ return new Response(JSON.stringify([
+ { _id: "4", sem: "B.Tech-CH-3", code: "che-code" }
+ ]));
+ }
+ }
+
+ if (urlStr.includes("lockclasstt")) {
+ return new Response(JSON.stringify({
+ timetableData: {
+ Monday: {
+ period1: [[{ subject: "Mock Subject", faculty: "Mock Faculty", room: "Mock Room" }]]
+ }
+ },
+ notes: ["Mock notes"]
+ }));
+ }
+
+ throw new Error(`Unexpected network fetch in tests: ${urlStr}`);
+ }) as any;
+});
+
+afterAll(() => {
+ globalThis.fetch = originalFetch;
+});
+
+describe("OpensourceNITJ API Integration Tests", () => {
+ describe("Health Endpoint", () => {
+ it("should return status ok", async () => {
+ const res = await app.request("/health");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as any;
+ expect(body.status).toBe("ok");
+ expect(body.uptime).toBeTypeOf("number");
+ });
+ });
+
+ describe("Student Group Lookup", () => {
+ it("should return correct group for 1st Year (Batch 2025)", async () => {
+ const res = await app.request("/students/getGroup?rollNumber=25103001");
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body).toEqual({
+ group: "B6",
+ subGroup: "a",
+ });
+ });
+
+ it("should return correct group for 2nd Year (Batch 2024)", async () => {
+ const res = await app.request("/students/getGroup?rollNumber=24103002");
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body).toEqual({
+ group: "CSE-A",
+ subGroup: "1",
+ });
+ });
+
+ it("should return correct group for 3rd Year (Batch 2023)", async () => {
+ const res = await app.request("/students/getGroup?rollNumber=23104003");
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body).toEqual({
+ group: "ECE-B",
+ subGroup: "2",
+ });
+ });
+
+ it("should return 404 for unknown roll number", async () => {
+ const res = await app.request("/students/getGroup?rollNumber=99999999");
+ expect(res.status).toBe(404);
+ });
+ });
+
+ describe("Timetable Endpoints", () => {
+ it("should fetch 1st Year Timetable", async () => {
+ const res = await app.request("/timetable/year/1?group=B6");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as any;
+ expect(body).toHaveProperty("timetableData");
+ expect(body.timetableData.Monday.period1[0][0].subject).toBe("Mock Subject");
+ expect(body).toHaveProperty("notes");
+ });
+
+ it("should fetch 2nd Year (CSE) Timetable with structured data and subsection", async () => {
+ const res = await app.request("/timetable/year/2?branch=CSE&group=A&subSection=1");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as any;
+ expect(body).toHaveProperty("timetableData");
+
+ const firstSlot = body.timetableData.Monday.period1[0][0];
+ expect(firstSlot.subject).toBe("Mock Subject");
+ expect(firstSlot).toHaveProperty("subjectCode");
+ expect(firstSlot.startTime).toBe("08:30 AM");
+ expect(firstSlot.endTime).toBe("09:30 AM");
+ expect(firstSlot.duration).toBe("1 hour");
+ expect(firstSlot.branch).toBe("CSE");
+ expect(firstSlot.year).toBe("2");
+ expect(firstSlot.section).toBe("A");
+ expect(firstSlot.subSection).toBe("1");
+ expect(firstSlot.courseType).toBe("Lecture");
+
+ expect(body).toHaveProperty("notes");
+ });
+
+ it("should fetch 3rd Year (ECE) Timetable", async () => {
+ const res = await app.request("/timetable/year/3?branch=ECE&group=B");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as any;
+ expect(body).toHaveProperty("timetableData");
+ expect(body.timetableData.Monday.period1[0][0].subject).toBe("Mock Subject");
+ expect(body).toHaveProperty("notes");
+ });
+
+ it("should fetch 2nd Year (CHE) Timetable (single-section branch fallback)", async () => {
+ const res = await app.request("/timetable/year/2?branch=CHE");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as any;
+ expect(body).toHaveProperty("timetableData");
+ expect(body.timetableData.Monday.period1[0][0].subject).toBe("Mock Subject");
+ expect(body).toHaveProperty("notes");
+ });
+
+ it("should return 400 or 404 for non-existent branch or section", async () => {
+ const res = await app.request("/timetable/year/2?branch=XYZ&group=Z");
+ expect([400, 404]).toContain(res.status);
+ });
+ });
+
+ describe("OpenAPI Documentation", () => {
+ it("should expose branch enums correctly", async () => {
+ const res = await app.request("/openapi");
+ expect(res.status).toBe(200);
+ const openapi = (await res.json()) as any;
+
+ // Check that the query parameter schema for branch has an enum listing all branches
+ const branchQueryParam = openapi.paths["/timetable/year/:year"].get.parameters.find(
+ (p: any) => p.name === "branch" && p.in === "query"
+ );
+
+ expect(branchQueryParam).toBeDefined();
+ expect(branchQueryParam.schema.enum).toEqual([
+ "CSE", "CS", "IT", "ECE", "EC", "EE", "ME", "CE",
+ "BT", "CH", "CHE", "ICE", "IPE", "TT", "MNC", "MC"
+ ]);
+ });
+ });
+});
+