najm-auth 2.0.15 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1089,6 +1089,149 @@ declare const credentialSetupSessionsTable: drizzle_orm_sqlite_core.SQLiteTableW
1089
1089
  };
1090
1090
  dialect: "sqlite";
1091
1091
  }>;
1092
+ /**
1093
+ * Durable record that a user still owes a credential-setup purpose. Survives
1094
+ * browser sessions, unlike credential_setup_sessions — the composite key lets
1095
+ * one user owe more than one purpose at a time.
1096
+ */
1097
+ declare const credentialSetupRequirementsTable: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
1098
+ name: "credential_setup_requirements";
1099
+ schema: undefined;
1100
+ columns: {
1101
+ userId: drizzle_orm_sqlite_core.SQLiteColumn<{
1102
+ name: "user_id";
1103
+ tableName: "credential_setup_requirements";
1104
+ dataType: "string";
1105
+ columnType: "SQLiteText";
1106
+ data: string;
1107
+ driverParam: string;
1108
+ notNull: true;
1109
+ hasDefault: false;
1110
+ isPrimaryKey: false;
1111
+ isAutoincrement: false;
1112
+ hasRuntimeDefault: false;
1113
+ enumValues: [string, ...string[]];
1114
+ baseColumn: never;
1115
+ identity: undefined;
1116
+ generated: undefined;
1117
+ }, {}, {
1118
+ length: number;
1119
+ }>;
1120
+ purpose: drizzle_orm_sqlite_core.SQLiteColumn<{
1121
+ name: "purpose";
1122
+ tableName: "credential_setup_requirements";
1123
+ dataType: "string";
1124
+ columnType: "SQLiteText";
1125
+ data: string;
1126
+ driverParam: string;
1127
+ notNull: true;
1128
+ hasDefault: false;
1129
+ isPrimaryKey: false;
1130
+ isAutoincrement: false;
1131
+ hasRuntimeDefault: false;
1132
+ enumValues: [string, ...string[]];
1133
+ baseColumn: never;
1134
+ identity: undefined;
1135
+ generated: undefined;
1136
+ }, {}, {
1137
+ length: number;
1138
+ }>;
1139
+ temporaryCredentialKind: drizzle_orm_sqlite_core.SQLiteColumn<{
1140
+ name: "temporary_credential_kind";
1141
+ tableName: "credential_setup_requirements";
1142
+ dataType: "string";
1143
+ columnType: "SQLiteText";
1144
+ data: string;
1145
+ driverParam: string;
1146
+ notNull: false;
1147
+ hasDefault: false;
1148
+ isPrimaryKey: false;
1149
+ isAutoincrement: false;
1150
+ hasRuntimeDefault: false;
1151
+ enumValues: [string, ...string[]];
1152
+ baseColumn: never;
1153
+ identity: undefined;
1154
+ generated: undefined;
1155
+ }, {}, {
1156
+ length: number;
1157
+ }>;
1158
+ required: drizzle_orm_sqlite_core.SQLiteColumn<{
1159
+ name: "required";
1160
+ tableName: "credential_setup_requirements";
1161
+ dataType: "boolean";
1162
+ columnType: "SQLiteBoolean";
1163
+ data: boolean;
1164
+ driverParam: number;
1165
+ notNull: true;
1166
+ hasDefault: true;
1167
+ isPrimaryKey: false;
1168
+ isAutoincrement: false;
1169
+ hasRuntimeDefault: false;
1170
+ enumValues: undefined;
1171
+ baseColumn: never;
1172
+ identity: undefined;
1173
+ generated: undefined;
1174
+ }, {}, {}>;
1175
+ completedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
1176
+ name: "completed_at";
1177
+ tableName: "credential_setup_requirements";
1178
+ dataType: "string";
1179
+ columnType: "SQLiteText";
1180
+ data: string;
1181
+ driverParam: string;
1182
+ notNull: false;
1183
+ hasDefault: false;
1184
+ isPrimaryKey: false;
1185
+ isAutoincrement: false;
1186
+ hasRuntimeDefault: false;
1187
+ enumValues: [string, ...string[]];
1188
+ baseColumn: never;
1189
+ identity: undefined;
1190
+ generated: undefined;
1191
+ }, {}, {
1192
+ length: number;
1193
+ }>;
1194
+ createdAt: drizzle_orm_sqlite_core.SQLiteColumn<{
1195
+ name: "created_at";
1196
+ tableName: "credential_setup_requirements";
1197
+ dataType: "string";
1198
+ columnType: "SQLiteText";
1199
+ data: string;
1200
+ driverParam: string;
1201
+ notNull: false;
1202
+ hasDefault: true;
1203
+ isPrimaryKey: false;
1204
+ isAutoincrement: false;
1205
+ hasRuntimeDefault: true;
1206
+ enumValues: [string, ...string[]];
1207
+ baseColumn: never;
1208
+ identity: undefined;
1209
+ generated: undefined;
1210
+ }, {}, {
1211
+ length: number;
1212
+ }>;
1213
+ updatedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
1214
+ name: "updated_at";
1215
+ tableName: "credential_setup_requirements";
1216
+ dataType: "string";
1217
+ columnType: "SQLiteText";
1218
+ data: string;
1219
+ driverParam: string;
1220
+ notNull: false;
1221
+ hasDefault: true;
1222
+ isPrimaryKey: false;
1223
+ isAutoincrement: false;
1224
+ hasRuntimeDefault: true;
1225
+ enumValues: [string, ...string[]];
1226
+ baseColumn: never;
1227
+ identity: undefined;
1228
+ generated: undefined;
1229
+ }, {}, {
1230
+ length: number;
1231
+ }>;
1232
+ };
1233
+ dialect: "sqlite";
1234
+ }>;
1092
1235
  /**
1093
1236
  * Junction table for many-to-many relationship between roles and permissions
1094
1237
  */
@@ -2018,6 +2161,144 @@ declare const authSchema: {
2018
2161
  };
2019
2162
  dialect: "sqlite";
2020
2163
  }>;
2164
+ readonly credentialSetupRequirements: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
2165
+ name: "credential_setup_requirements";
2166
+ schema: undefined;
2167
+ columns: {
2168
+ userId: drizzle_orm_sqlite_core.SQLiteColumn<{
2169
+ name: "user_id";
2170
+ tableName: "credential_setup_requirements";
2171
+ dataType: "string";
2172
+ columnType: "SQLiteText";
2173
+ data: string;
2174
+ driverParam: string;
2175
+ notNull: true;
2176
+ hasDefault: false;
2177
+ isPrimaryKey: false;
2178
+ isAutoincrement: false;
2179
+ hasRuntimeDefault: false;
2180
+ enumValues: [string, ...string[]];
2181
+ baseColumn: never;
2182
+ identity: undefined;
2183
+ generated: undefined;
2184
+ }, {}, {
2185
+ length: number;
2186
+ }>;
2187
+ purpose: drizzle_orm_sqlite_core.SQLiteColumn<{
2188
+ name: "purpose";
2189
+ tableName: "credential_setup_requirements";
2190
+ dataType: "string";
2191
+ columnType: "SQLiteText";
2192
+ data: string;
2193
+ driverParam: string;
2194
+ notNull: true;
2195
+ hasDefault: false;
2196
+ isPrimaryKey: false;
2197
+ isAutoincrement: false;
2198
+ hasRuntimeDefault: false;
2199
+ enumValues: [string, ...string[]];
2200
+ baseColumn: never;
2201
+ identity: undefined;
2202
+ generated: undefined;
2203
+ }, {}, {
2204
+ length: number;
2205
+ }>;
2206
+ temporaryCredentialKind: drizzle_orm_sqlite_core.SQLiteColumn<{
2207
+ name: "temporary_credential_kind";
2208
+ tableName: "credential_setup_requirements";
2209
+ dataType: "string";
2210
+ columnType: "SQLiteText";
2211
+ data: string;
2212
+ driverParam: string;
2213
+ notNull: false;
2214
+ hasDefault: false;
2215
+ isPrimaryKey: false;
2216
+ isAutoincrement: false;
2217
+ hasRuntimeDefault: false;
2218
+ enumValues: [string, ...string[]];
2219
+ baseColumn: never;
2220
+ identity: undefined;
2221
+ generated: undefined;
2222
+ }, {}, {
2223
+ length: number;
2224
+ }>;
2225
+ required: drizzle_orm_sqlite_core.SQLiteColumn<{
2226
+ name: "required";
2227
+ tableName: "credential_setup_requirements";
2228
+ dataType: "boolean";
2229
+ columnType: "SQLiteBoolean";
2230
+ data: boolean;
2231
+ driverParam: number;
2232
+ notNull: true;
2233
+ hasDefault: true;
2234
+ isPrimaryKey: false;
2235
+ isAutoincrement: false;
2236
+ hasRuntimeDefault: false;
2237
+ enumValues: undefined;
2238
+ baseColumn: never;
2239
+ identity: undefined;
2240
+ generated: undefined;
2241
+ }, {}, {}>;
2242
+ completedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
2243
+ name: "completed_at";
2244
+ tableName: "credential_setup_requirements";
2245
+ dataType: "string";
2246
+ columnType: "SQLiteText";
2247
+ data: string;
2248
+ driverParam: string;
2249
+ notNull: false;
2250
+ hasDefault: false;
2251
+ isPrimaryKey: false;
2252
+ isAutoincrement: false;
2253
+ hasRuntimeDefault: false;
2254
+ enumValues: [string, ...string[]];
2255
+ baseColumn: never;
2256
+ identity: undefined;
2257
+ generated: undefined;
2258
+ }, {}, {
2259
+ length: number;
2260
+ }>;
2261
+ createdAt: drizzle_orm_sqlite_core.SQLiteColumn<{
2262
+ name: "created_at";
2263
+ tableName: "credential_setup_requirements";
2264
+ dataType: "string";
2265
+ columnType: "SQLiteText";
2266
+ data: string;
2267
+ driverParam: string;
2268
+ notNull: false;
2269
+ hasDefault: true;
2270
+ isPrimaryKey: false;
2271
+ isAutoincrement: false;
2272
+ hasRuntimeDefault: true;
2273
+ enumValues: [string, ...string[]];
2274
+ baseColumn: never;
2275
+ identity: undefined;
2276
+ generated: undefined;
2277
+ }, {}, {
2278
+ length: number;
2279
+ }>;
2280
+ updatedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
2281
+ name: "updated_at";
2282
+ tableName: "credential_setup_requirements";
2283
+ dataType: "string";
2284
+ columnType: "SQLiteText";
2285
+ data: string;
2286
+ driverParam: string;
2287
+ notNull: false;
2288
+ hasDefault: true;
2289
+ isPrimaryKey: false;
2290
+ isAutoincrement: false;
2291
+ hasRuntimeDefault: true;
2292
+ enumValues: [string, ...string[]];
2293
+ baseColumn: never;
2294
+ identity: undefined;
2295
+ generated: undefined;
2296
+ }, {}, {
2297
+ length: number;
2298
+ }>;
2299
+ };
2300
+ dialect: "sqlite";
2301
+ }>;
2021
2302
  readonly roles: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
2022
2303
  name: "roles";
2023
2304
  schema: undefined;
@@ -2375,7 +2656,9 @@ type Token = typeof tokensTable.$inferSelect;
2375
2656
  type NewToken = typeof tokensTable.$inferInsert;
2376
2657
  type CredentialSetupSession = typeof credentialSetupSessionsTable.$inferSelect;
2377
2658
  type NewCredentialSetupSession = typeof credentialSetupSessionsTable.$inferInsert;
2659
+ type CredentialSetupRequirement = typeof credentialSetupRequirementsTable.$inferSelect;
2660
+ type NewCredentialSetupRequirement = typeof credentialSetupRequirementsTable.$inferInsert;
2378
2661
  type RolePermission = typeof rolePermissionsTable.$inferSelect;
2379
2662
  type NewRolePermission = typeof rolePermissionsTable.$inferInsert;
2380
2663
 
2381
- export { type CredentialSetupSession, type NewCredentialSetupSession, type NewOAuthAccount, type NewPermission, type NewRoleEntity, type NewRolePermission, type NewToken, type NewUser, type OAuthAccount, type Permission, type RoleEntity, type RolePermission, type Token, type User, authSchema, baseFields, credentialSetupSessionsTable, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokensTable, usersTable };
2664
+ export { type CredentialSetupRequirement, type CredentialSetupSession, type NewCredentialSetupRequirement, type NewCredentialSetupSession, type NewOAuthAccount, type NewPermission, type NewRoleEntity, type NewRolePermission, type NewToken, type NewUser, type OAuthAccount, type Permission, type RoleEntity, type RolePermission, type Token, type User, authSchema, baseFields, credentialSetupRequirementsTable, credentialSetupSessionsTable, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokensTable, usersTable };
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/schema/sqlite.ts
5
- import { sqliteTable, text, integer, uniqueIndex, index } from "drizzle-orm/sqlite-core";
5
+ import { sqliteTable, text, integer, primaryKey, uniqueIndex, index } from "drizzle-orm/sqlite-core";
6
6
  import { sql } from "drizzle-orm";
7
7
  import { nanoid } from "nanoid";
8
8
  var baseFields = /* @__PURE__ */ __name((idLength = 5) => ({
@@ -76,6 +76,17 @@ var credentialSetupSessionsTable = sqliteTable("credential_setup_sessions", {
76
76
  userPurposeIdx: index("credential_setup_sessions_user_purpose_idx").on(table.userId, table.purpose),
77
77
  expiresAtIdx: index("credential_setup_sessions_expires_at_idx").on(table.expiresAt)
78
78
  }));
79
+ var credentialSetupRequirementsTable = sqliteTable("credential_setup_requirements", {
80
+ userId: text("user_id").references(() => usersTable.id, { onDelete: "cascade" }).notNull(),
81
+ purpose: text("purpose").notNull(),
82
+ temporaryCredentialKind: text("temporary_credential_kind"),
83
+ required: integer("required", { mode: "boolean" }).default(true).notNull(),
84
+ completedAt: text("completed_at"),
85
+ createdAt: text("created_at").$defaultFn(() => (/* @__PURE__ */ new Date()).toISOString()),
86
+ updatedAt: text("updated_at").$defaultFn(() => (/* @__PURE__ */ new Date()).toISOString()).$onUpdate(() => sql`(datetime('now'))`)
87
+ }, (table) => ({
88
+ pk: primaryKey({ columns: [table.userId, table.purpose] })
89
+ }));
79
90
  var rolePermissionsTable = sqliteTable("role_permissions", {
80
91
  ...baseFields(10),
81
92
  roleId: text("role_id").notNull().references(() => rolesTable.id, { onDelete: "cascade" }),
@@ -88,6 +99,7 @@ var authSchema = {
88
99
  oauthAccounts: oauthAccountsTable,
89
100
  tokens: tokensTable,
90
101
  credentialSetupSessions: credentialSetupSessionsTable,
102
+ credentialSetupRequirements: credentialSetupRequirementsTable,
91
103
  roles: rolesTable,
92
104
  permissions: permissionsTable,
93
105
  rolePermissions: rolePermissionsTable
@@ -95,6 +107,7 @@ var authSchema = {
95
107
  export {
96
108
  authSchema,
97
109
  baseFields,
110
+ credentialSetupRequirementsTable,
98
111
  credentialSetupSessionsTable,
99
112
  oauthAccountsTable,
100
113
  permissionsTable,
@@ -0,0 +1,18 @@
1
+ type SessionRecoveryFailureReason = 'invalid-cookie-name' | 'invalid-refresh-cookie' | 'invalid-endpoint' | 'fetch-error' | 'http-status' | 'missing-set-cookie' | 'session-cookie-parse' | 'session-cookie-hmac' | 'session-cookie-payload';
2
+ interface SessionRecoveryErrorDetails {
3
+ name: string;
4
+ message: string;
5
+ code?: string;
6
+ cause?: {
7
+ name: string;
8
+ message: string;
9
+ code?: string;
10
+ };
11
+ }
12
+ interface SessionRecoveryFailure {
13
+ reason: SessionRecoveryFailureReason;
14
+ httpStatus?: number;
15
+ error?: SessionRecoveryErrorDetails;
16
+ }
17
+
18
+ export type { SessionRecoveryFailure as S, SessionRecoveryErrorDetails as a, SessionRecoveryFailureReason as b };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * User data from the auth server
3
+ */
4
+ interface AuthUser {
5
+ id: string;
6
+ email: string;
7
+ name?: string;
8
+ role?: string | null;
9
+ permissions?: string[];
10
+ [key: string]: unknown;
11
+ }
12
+ /**
13
+ * Full auth state snapshot
14
+ */
15
+ interface AuthState {
16
+ user: AuthUser | null;
17
+ accessToken: string | null;
18
+ isAuthenticated: boolean;
19
+ isLoading: boolean;
20
+ roles: string[];
21
+ permissions: string[];
22
+ }
23
+ /**
24
+ * Decoded JWT payload (client-side, no verification)
25
+ */
26
+ interface DecodedToken {
27
+ userId: string;
28
+ jti?: string;
29
+ sessionVersion?: number;
30
+ roles?: string[];
31
+ permissions?: string[];
32
+ exp?: number;
33
+ iat?: number;
34
+ [key: string]: unknown;
35
+ }
36
+ /**
37
+ * Server response envelope
38
+ */
39
+ interface ServerResponse<T = unknown> {
40
+ data: T;
41
+ message?: string;
42
+ status?: string;
43
+ }
44
+ /**
45
+ * Token pair from login/refresh (internal — refresh token is httpOnly cookie)
46
+ */
47
+ interface TokenPair {
48
+ accessToken: string;
49
+ refreshToken?: string;
50
+ accessTokenExpiresAt?: number;
51
+ refreshTokenExpiresAt?: number;
52
+ }
53
+ /**
54
+ * Login credentials. `identifier` accepts an email address or a phone number;
55
+ * `email` stays supported for existing callers.
56
+ */
57
+ interface LoginCredentials {
58
+ identifier?: string;
59
+ email?: string;
60
+ password: string;
61
+ /** Persist the auth cookies past the browser closing. */
62
+ rememberMe?: boolean;
63
+ [key: string]: unknown;
64
+ }
65
+ /** The account must replace its credential before it gets a session. */
66
+ interface CredentialSetupPending {
67
+ nextStep: 'credential_setup';
68
+ setupRequired: true;
69
+ purpose: string;
70
+ expiresAt: string;
71
+ }
72
+ interface AuthenticatedLogin {
73
+ nextStep: 'authenticated';
74
+ user: AuthUser;
75
+ }
76
+ /**
77
+ * Login answer. Branch on `nextStep`: `credential_setup` carries no tokens and
78
+ * leaves the client unauthenticated.
79
+ */
80
+ type LoginResult = AuthenticatedLogin | CredentialSetupPending;
81
+ /**
82
+ * Retry configuration
83
+ */
84
+ interface RetryConfig {
85
+ maxRetries?: number;
86
+ backoff?: 'exponential' | 'linear';
87
+ baseDelay?: number;
88
+ }
89
+ /**
90
+ * Auth client configuration
91
+ */
92
+ interface AuthClientConfig {
93
+ /** API base URL (e.g., '/api' or 'https://api.example.com') */
94
+ baseURL: string;
95
+ /** Auth endpoints prefix (default: '/auth') */
96
+ authPrefix?: string;
97
+ /** Proactive refresh at this fraction of token lifetime (default: 0.8) */
98
+ refreshThreshold?: number;
99
+ /** Enable multi-tab sync via BroadcastChannel (default: true) */
100
+ tabSync?: boolean;
101
+ /** BroadcastChannel name (default: 'najm-auth') */
102
+ channelName?: string;
103
+ /** Network retry configuration */
104
+ retry?: RetryConfig;
105
+ /** Request timeout in milliseconds (default: 30000) */
106
+ timeout?: number;
107
+ }
108
+ type OAuthProvider = 'google';
109
+ interface OAuthLoginOptions {
110
+ /** Same-origin frontend path after OAuth completes. */
111
+ returnTo?: string;
112
+ }
113
+ /**
114
+ * Auth event types
115
+ */
116
+ interface AuthEventMap {
117
+ login: AuthUser;
118
+ logout: null;
119
+ /** Emitted when server-side logout invalidation fails (state was already cleared) */
120
+ logoutError: unknown;
121
+ tokenRefresh: null;
122
+ sessionExpired: null;
123
+ stateChange: AuthState;
124
+ userUpdated: AuthUser;
125
+ }
126
+ type AuthEvent = keyof AuthEventMap;
127
+ type AuthEventHandler<K extends AuthEvent = AuthEvent> = (data: AuthEventMap[K]) => void;
128
+ /**
129
+ * Tab sync message types
130
+ */
131
+ type TabSyncMessage = {
132
+ type: 'logout';
133
+ } | {
134
+ type: 'sync';
135
+ state: SyncPayload;
136
+ };
137
+ interface SyncPayload {
138
+ accessToken: string | null;
139
+ user: AuthUser | null;
140
+ roles: string[];
141
+ permissions: string[];
142
+ isAuthenticated: boolean;
143
+ }
144
+ /**
145
+ * FetchClient request options
146
+ */
147
+ interface RequestOptions {
148
+ body?: unknown;
149
+ headers?: Record<string, string>;
150
+ signal?: AbortSignal;
151
+ timeout?: number;
152
+ /** Skip auth header attachment (for public endpoints like /login, /register) */
153
+ skipAuth?: boolean;
154
+ /** @internal Prevents 401-refresh loop after a single retry */
155
+ _retried?: boolean;
156
+ }
157
+ /**
158
+ * Auth error thrown by the client
159
+ */
160
+ declare class AuthError extends Error {
161
+ status: number;
162
+ body?: unknown;
163
+ constructor(status: number, message: string, body?: unknown);
164
+ }
165
+
166
+ export { type AuthUser as A, type CredentialSetupPending as C, type DecodedToken as D, type LoginCredentials as L, type OAuthProvider as O, type RetryConfig as R, type SyncPayload as S, type TabSyncMessage as T, type RequestOptions as a, type AuthClientConfig as b, type LoginResult as c, type OAuthLoginOptions as d, type AuthState as e, type AuthEvent as f, type AuthEventHandler as g, AuthError as h, type AuthEventMap as i, type AuthenticatedLogin as j, type ServerResponse as k, type TokenPair as l };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "2.0.15",
3
+ "version": "3.1.0",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [
@@ -34,6 +34,18 @@
34
34
  "import": "./dist/client/server/index.js",
35
35
  "default": "./dist/client/server/index.js"
36
36
  },
37
+ "./client/server/react": {
38
+ "types": "./dist/client/server/react.d.ts",
39
+ "react-server": "./dist/client/server/react.js",
40
+ "browser": "./dist/client/server/reactClientGuard.js",
41
+ "import": "./dist/client/server/react.js",
42
+ "default": "./dist/client/server/react.js"
43
+ },
44
+ "./identity/ma": {
45
+ "types": "./dist/identity/ma.d.ts",
46
+ "import": "./dist/identity/ma.js",
47
+ "default": "./dist/identity/ma.js"
48
+ },
37
49
  "./pg": {
38
50
  "types": "./dist/schema/pg.d.ts",
39
51
  "import": "./dist/schema/pg.js",
@@ -47,7 +59,8 @@
47
59
  },
48
60
  "scripts": {
49
61
  "build": "tsup",
50
- "test": "bun test",
62
+ "test": "bun test && bun run test:rsc",
63
+ "test:rsc": "bun test --conditions react-server test/react-server-auth.test.ts",
51
64
  "test:next16": "bun run build && bun integration/next16-proxy/run.ts",
52
65
  "test:watch": "bun test --watch",
53
66
  "start": "bun --watch src/main.ts",