sonamu 0.9.1 → 0.9.3

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.
Files changed (50) hide show
  1. package/dist/api/config.d.ts +9 -2
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +1 -0
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +50 -2
  7. package/dist/auth/audit-log-ingestor.d.ts +9 -0
  8. package/dist/auth/audit-log-ingestor.d.ts.map +1 -0
  9. package/dist/auth/audit-log-ingestor.js +200 -0
  10. package/dist/auth/audit-log-proxy-types.d.ts +23 -0
  11. package/dist/auth/audit-log-proxy-types.d.ts.map +1 -0
  12. package/dist/auth/audit-log-proxy-types.js +1 -0
  13. package/dist/auth/index.d.ts +1 -0
  14. package/dist/auth/index.d.ts.map +1 -1
  15. package/dist/auth/index.js +4 -2
  16. package/dist/auth/plugins/entity-definitions/audit-log.d.ts +12 -0
  17. package/dist/auth/plugins/entity-definitions/audit-log.d.ts.map +1 -0
  18. package/dist/auth/plugins/entity-definitions/audit-log.js +291 -0
  19. package/dist/auth/plugins/entity-definitions/index.d.ts +1 -0
  20. package/dist/auth/plugins/entity-definitions/index.d.ts.map +1 -1
  21. package/dist/auth/plugins/entity-definitions/index.js +5 -3
  22. package/dist/auth/plugins/entity-definitions/types.d.ts +1 -1
  23. package/dist/auth/plugins/entity-definitions/types.d.ts.map +1 -1
  24. package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
  25. package/dist/auth/plugins/wrappers/sso.d.ts +1 -1
  26. package/dist/database/knex.d.ts.map +1 -1
  27. package/dist/database/knex.js +10 -18
  28. package/dist/entity/entity-manager.d.ts +2 -2
  29. package/dist/index.js +3 -2
  30. package/dist/storage/buffered-file.d.ts +1 -1
  31. package/dist/storage/buffered-file.js +2 -2
  32. package/dist/types/types.d.ts +14 -14
  33. package/dist/ui-web/assets/index-Dr8pRJC_.css +1 -0
  34. package/dist/ui-web/assets/{index-C8qhvZbs.js → index-DrTfl0Ts.js} +49 -46
  35. package/dist/ui-web/index.html +2 -2
  36. package/package.json +4 -3
  37. package/src/api/config.ts +9 -2
  38. package/src/api/sonamu.ts +65 -1
  39. package/src/auth/audit-log-ingestor.ts +234 -0
  40. package/src/auth/audit-log-proxy-types.ts +23 -0
  41. package/src/auth/index.ts +1 -0
  42. package/src/auth/plugins/entity-definitions/audit-log.ts +171 -0
  43. package/src/auth/plugins/entity-definitions/index.ts +3 -0
  44. package/src/auth/plugins/entity-definitions/types.ts +2 -1
  45. package/src/database/knex.ts +9 -19
  46. package/src/skills/sonamu/auth.md +17 -0
  47. package/src/skills/sonamu/create-sonamu.md +8 -0
  48. package/src/skills/sonamu/scaffolding.md +13 -0
  49. package/src/storage/buffered-file.ts +1 -1
  50. package/dist/ui-web/assets/index-DqgrO7Za.css +0 -1
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>{{projectName}}: Sonamu UI</title>
8
- <script type="module" crossorigin src="/sonamu-ui/assets/index-C8qhvZbs.js"></script>
9
- <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-DqgrO7Za.css">
8
+ <script type="module" crossorigin src="/sonamu-ui/assets/index-DrTfl0Ts.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-Dr8pRJC_.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "framework",
@@ -85,6 +85,7 @@
85
85
  "@aws-sdk/lib-storage": "^3.971.0",
86
86
  "@aws-sdk/s3-request-presigner": "^3.958.0",
87
87
  "@better-auth/api-key": "~1.6.0",
88
+ "@better-auth/infra": "~0.1.14",
88
89
  "@better-auth/passkey": "~1.6.0",
89
90
  "@better-auth/sso": "~1.6.0",
90
91
  "@faker-js/faker": "^9.2.0",
@@ -125,11 +126,11 @@
125
126
  "qs": "^6.14.1",
126
127
  "radashi": "^12.2.0",
127
128
  "tsicli": "^1.0.5",
128
- "vite": "8.0.3",
129
+ "vite": "8.0.5",
129
130
  "vitest": "^4.1.2",
130
131
  "@sonamu-kit/hmr-hook": "^0.5.1",
131
- "@sonamu-kit/hmr-runner": "^0.2.0",
132
132
  "@sonamu-kit/tasks": "^0.3.0",
133
+ "@sonamu-kit/hmr-runner": "^0.2.0",
133
134
  "@sonamu-kit/ts-loader": "^2.2.0"
134
135
  },
135
136
  "devDependencies": {
package/src/api/config.ts CHANGED
@@ -198,20 +198,27 @@ export type SonamuServerOptions = {
198
198
  }
199
199
  >;
200
200
  };
201
+ /**
202
+ * AuditLog 활성화 여부
203
+ * true로 설정하면 Better Auth dash() 플러그인을 자동 주입하고
204
+ * /api/audit-log/events/track 엔드포인트에서 이벤트를 수신하여
205
+ * audit_events 테이블에 적재합니다.
206
+ * 사전에 `sonamu auth generate --plugins audit-log`로 엔티티를 생성해야 합니다.
207
+ */
208
+ auditLog?: boolean;
201
209
  };
202
210
 
203
211
  apiConfig: SonamuFastifyConfig;
204
212
 
205
213
  /**
206
214
  * Storage 드라이버 설정.
207
- * DRIVE_DISK 환경변수로 사용할 드라이버를 선택합니다. (기본값: default 키)
215
+ * saveToDisk(diskName, key) 호출 드라이버를 명시적으로 지정합니다.
208
216
  *
209
217
  * @example
210
218
  * ```typescript
211
219
  * import { drivers } from "sonamu/storage";
212
220
  *
213
221
  * storage: {
214
- * default: process.env.DRIVE_DISK ?? "fs",
215
222
  * drivers: {
216
223
  * fs: drivers.fs({ location: "./uploads", urlBuilder: { ... } }),
217
224
  * s3: drivers.s3({ bucket: "my-bucket", region: "ap-northeast-2", ... }),
package/src/api/sonamu.ts CHANGED
@@ -5,13 +5,15 @@ import { type IncomingMessage, type Server, type ServerResponse } from "http";
5
5
  import os from "os";
6
6
  import path from "path";
7
7
 
8
- import { dispose as logtapeDispose } from "@logtape/logtape";
8
+ import { dispose as logtapeDispose, getLogger } from "@logtape/logtape";
9
9
  import { type Auth, type BetterAuthOptions } from "better-auth";
10
10
  import { type FSWatcher } from "chokidar";
11
11
  import { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
12
12
  import mime, { lookup as mimeLookup } from "mime-types";
13
13
  import { type ZodObject } from "zod";
14
14
 
15
+ import { ingestAuditEvent } from "../auth/audit-log-ingestor";
16
+ import { type AuditLogEvent } from "../auth/audit-log-proxy-types";
15
17
  import { BASE_FIELD_MAPPINGS } from "../auth/better-auth-entities";
16
18
  import { applyCacheHeaders, CachePresets } from "../cache-control/cache-control";
17
19
  import { type CacheControlConfig, type CacheControlRequest } from "../cache-control/types";
@@ -233,6 +235,15 @@ class SonamuClass {
233
235
  // 사용자 설정과 기본값을 merge
234
236
  const mergedFieldMappings = merge(BASE_FIELD_MAPPINGS, authConfig);
235
237
 
238
+ // auth.auditLog: true인 경우 dash() 플러그인 자동 주입
239
+ if (authConfig.auditLog) {
240
+ const { dash } = await import("@better-auth/infra");
241
+ const auditLogBasePath = "/api/audit-log";
242
+ const apiUrl = `${authConfig.baseURL}${auditLogBasePath}`;
243
+ const existingPlugins = mergedFieldMappings.plugins ?? [];
244
+ mergedFieldMappings.plugins = [...existingPlugins, dash({ apiUrl })];
245
+ }
246
+
236
247
  // better-auth 인스턴스 생성
237
248
  const { betterAuth } = await import("better-auth");
238
249
  const { sonamuKnexAdapter } = await import("../auth/knex-adapter");
@@ -314,6 +325,10 @@ class SonamuClass {
314
325
  await this.registerBetterAuth(server, options.auth);
315
326
  }
316
327
 
328
+ if (options.auth?.auditLog) {
329
+ this.registerAuditLogProxy(server);
330
+ }
331
+
317
332
  // API 라우팅 설정
318
333
  await this.withFastify(server, options.apiConfig, {
319
334
  enableSync: initOptions?.enableSync,
@@ -1259,6 +1274,20 @@ class SonamuClass {
1259
1274
  handler: async (request, reply) => {
1260
1275
  const url = new URL(request.url, `http://${request.headers.host}`);
1261
1276
  const headers = convertFastifyHeadersToStandard(request.headers);
1277
+
1278
+ // IP 헤더 fallback: 프록시가 표준 IP 헤더를 주입하지 않는 환경에서도
1279
+ // better-auth/infra의 getClientIpFromRequest()가 IP를 인식할 수 있도록
1280
+ // Fastify가 resolve한 request.ip를 x-real-ip로 주입한다.
1281
+ const IP_HEADERS = [
1282
+ "cf-connecting-ip",
1283
+ "x-forwarded-for",
1284
+ "x-real-ip",
1285
+ "x-vercel-forwarded-for",
1286
+ ];
1287
+ if (request.ip && !IP_HEADERS.some((h) => headers.has(h))) {
1288
+ headers.set("x-real-ip", request.ip);
1289
+ }
1290
+
1262
1291
  const req = new Request(url.toString(), {
1263
1292
  method: request.method,
1264
1293
  headers,
@@ -1276,6 +1305,38 @@ class SonamuClass {
1276
1305
  });
1277
1306
  }
1278
1307
 
1308
+ private registerAuditLogProxy(server: FastifyInstance) {
1309
+ const logger = getLogger(["sonamu", "audit-log"]);
1310
+ const basePath = "/api/audit-log";
1311
+
1312
+ server.route<{ Body: AuditLogEvent }>({
1313
+ method: "POST",
1314
+ url: `${basePath}/events/track`,
1315
+ handler: async (request, reply) => {
1316
+ const event = request.body;
1317
+
1318
+ logger.info(
1319
+ "Audit event received: {eventType} {eventKey} {eventDisplayName} from {ipAddress} ({country})",
1320
+ {
1321
+ eventType: event.eventType,
1322
+ eventKey: event.eventKey,
1323
+ eventDisplayName: event.eventDisplayName,
1324
+ ipAddress: event.ipAddress,
1325
+ country: event.country,
1326
+ },
1327
+ );
1328
+
1329
+ try {
1330
+ await ingestAuditEvent(DB.getDB("w"), event);
1331
+ } catch (err) {
1332
+ logger.error("audit event ingest failed: {error}", { error: err });
1333
+ }
1334
+
1335
+ return reply.status(200).send({ ok: true });
1336
+ },
1337
+ });
1338
+ }
1339
+
1279
1340
  private async printStartupSummary() {
1280
1341
  const chalk = (await import("chalk")).default;
1281
1342
  const env = process.env.NODE_ENV ?? "development";
@@ -1311,6 +1372,9 @@ class SonamuClass {
1311
1372
  const basePath = this.config.server.auth.basePath ?? "/api/auth";
1312
1373
  dim(`Auth: better-auth at ${basePath}/*`);
1313
1374
  }
1375
+ if (this.config.server.auth?.auditLog) {
1376
+ dim(`AuditLog: proxy at /api/audit-log/events/track`);
1377
+ }
1314
1378
  if (this.config.api.timezone) {
1315
1379
  dim(`Timezone: ${this.config.api.timezone}`);
1316
1380
  }
@@ -0,0 +1,234 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { type Knex } from "knex";
4
+
5
+ import { type AuditLogEvent } from "./audit-log-proxy-types";
6
+
7
+ const AUDIT_EVENT_SOURCE = "better_auth";
8
+ const AUDIT_EVENT_SOURCE_VERSION = "better-auth|@better-auth/infra";
9
+
10
+ const SESSION_EVENT_TYPES = new Set<string>([
11
+ "user_signed_in",
12
+ "user_signed_out",
13
+ "user_sign_in_failed",
14
+ "session_created",
15
+ "session_revoked",
16
+ "all_sessions_revoked",
17
+ "user_impersonated",
18
+ "user_impersonated_stopped",
19
+ ]);
20
+
21
+ const ACCOUNT_EVENT_TYPES = new Set<string>([
22
+ "account_linked",
23
+ "account_unlinked",
24
+ "password_changed",
25
+
26
+ // @better-auth/infra@0.1.14 기준 EVENT_TYPES 상수에는 정의되어 있으나 실제 trackEvent() 호출이 없는 미구현 이벤트임
27
+ // 향후 버전에서 emit될 경우를 대비해 account로 임시 등록
28
+ "two_factor_enabled",
29
+ "two_factor_disabled",
30
+ "two_factor_verified",
31
+ ]);
32
+
33
+ const VERIFICATION_EVENT_TYPES = new Set<string>([
34
+ "email_verification_sent",
35
+ "password_reset_requested",
36
+ "password_reset_completed",
37
+ ]);
38
+
39
+ const SECURITY_EVENT_TYPES = new Set<string>([
40
+ "security_blocked",
41
+ "security_allowed",
42
+ "security_challenged",
43
+ "security_stale_account",
44
+ ]);
45
+
46
+ const USER_EVENT_TYPES = new Set<string>([
47
+ "user_created",
48
+ "profile_updated",
49
+ "profile_image_updated",
50
+ "email_verified",
51
+ "email_changed",
52
+ "user_banned",
53
+ "user_unbanned",
54
+ "user_deleted",
55
+ ]);
56
+
57
+ function pickString(source: Record<string, unknown>, key: string): string | null {
58
+ const value = source[key];
59
+ return typeof value === "string" ? value : null;
60
+ }
61
+
62
+ function pickFirstString(source: Record<string, unknown>, keys: readonly string[]): string | null {
63
+ for (const key of keys) {
64
+ const value = pickString(source, key);
65
+ if (value !== null) {
66
+ return value;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function parseOccurredAt(raw: unknown): Date {
73
+ if (raw instanceof Date && !Number.isNaN(raw.getTime())) {
74
+ return raw;
75
+ }
76
+ if (typeof raw === "string") {
77
+ const parsed = new Date(raw);
78
+ if (!Number.isNaN(parsed.getTime())) {
79
+ return parsed;
80
+ }
81
+ }
82
+ return new Date();
83
+ }
84
+
85
+ function classifyCategory(
86
+ eventType: string,
87
+ ): "user" | "session" | "account" | "verification" | "organization" | "security" {
88
+ if (eventType.startsWith("organization_")) {
89
+ return "organization";
90
+ }
91
+ if (SESSION_EVENT_TYPES.has(eventType)) {
92
+ return "session";
93
+ }
94
+ if (ACCOUNT_EVENT_TYPES.has(eventType)) {
95
+ return "account";
96
+ }
97
+ if (VERIFICATION_EVENT_TYPES.has(eventType)) {
98
+ return "verification";
99
+ }
100
+ if (SECURITY_EVENT_TYPES.has(eventType)) {
101
+ return "security";
102
+ }
103
+ if (USER_EVENT_TYPES.has(eventType)) {
104
+ return "user";
105
+ }
106
+ return "user";
107
+ }
108
+
109
+ function computeDedupeKey(parts: {
110
+ source: string;
111
+ event_type: string;
112
+ event_key: string;
113
+ actor_user_id: string | null;
114
+ subject_user_id: string | null;
115
+ organization_id: string | null;
116
+ team_id: string | null;
117
+ session_id: string | null;
118
+ identifier: string | null;
119
+ reason: string | null;
120
+ action: string | null;
121
+ occurred_at: Date;
122
+ }): string {
123
+ const norm = (v: string | null): string => v ?? "";
124
+ const raw = [
125
+ parts.source,
126
+ parts.event_type,
127
+ parts.event_key,
128
+ norm(parts.actor_user_id),
129
+ norm(parts.subject_user_id),
130
+ norm(parts.organization_id),
131
+ norm(parts.team_id),
132
+ norm(parts.session_id),
133
+ norm(parts.identifier),
134
+ norm(parts.reason),
135
+ norm(parts.action),
136
+ parts.occurred_at.toISOString(),
137
+ ].join("|");
138
+ return createHash("sha256").update(raw).digest("hex");
139
+ }
140
+
141
+ /**
142
+ * Better Auth dash() 이벤트를 audit_events 테이블에 적재합니다.
143
+ * ON CONFLICT (dedupe_key) DO NOTHING으로 중복을 silent 무시합니다.
144
+ * auth.auditLog: true 설정 시 sonamu 내부에서 자동으로 호출됩니다.
145
+ */
146
+ export async function ingestAuditEvent(db: Knex, event: AuditLogEvent): Promise<void> {
147
+ const eventData = event.eventData;
148
+ const occurred_at = parseOccurredAt(eventData["occurredAt"]);
149
+
150
+ const actor_user_id = pickString(eventData, "triggeredBy");
151
+ const subject_user_id = pickFirstString(eventData, [
152
+ "userId",
153
+ "userid",
154
+ "acceptedById",
155
+ "rejectedById",
156
+ ]);
157
+ const organization_id = pickString(eventData, "organizationId");
158
+ const team_id = pickFirstString(eventData, ["teamId", "inviteeTeamId"]);
159
+ const session_id = pickString(eventData, "sessionId");
160
+ const provider_id = pickString(eventData, "providerId");
161
+ const login_method = pickString(eventData, "loginMethod");
162
+ const identifier = pickFirstString(eventData, [
163
+ "identifier",
164
+ "userEmail",
165
+ "memberEmail",
166
+ "inviteeEmail",
167
+ "acceptedByEmail",
168
+ "rejectedByEmail",
169
+ ]);
170
+ const visitor_id = pickString(eventData, "visitorId");
171
+ const reason = pickFirstString(eventData, ["reason", "banReason"]);
172
+ const action = pickString(eventData, "action");
173
+ const trigger_context = pickString(eventData, "triggerContext");
174
+ const user_agent = pickString(eventData, "userAgent");
175
+
176
+ const ip_address = event.ipAddress ?? null;
177
+ const city = event.city ?? null;
178
+ const country = event.country ?? null;
179
+ const country_code = event.countryCode ?? null;
180
+
181
+ const category = classifyCategory(event.eventType);
182
+ const dedupe_key = computeDedupeKey({
183
+ source: AUDIT_EVENT_SOURCE,
184
+ event_type: event.eventType,
185
+ event_key: event.eventKey,
186
+ actor_user_id,
187
+ subject_user_id,
188
+ organization_id,
189
+ team_id,
190
+ session_id,
191
+ identifier,
192
+ reason,
193
+ action,
194
+ occurred_at,
195
+ });
196
+
197
+ // ON CONFLICT DO NOTHING: dedupe_key UNIQUE 위반을 silent 무시.
198
+ // try/catch 방식은 PG 커넥션을 aborted 상태로 만드므로 사용하지 않는다.
199
+ const inserted = await db("audit_events")
200
+ .insert({
201
+ source: AUDIT_EVENT_SOURCE,
202
+ source_version: AUDIT_EVENT_SOURCE_VERSION,
203
+ category,
204
+ event_type: event.eventType,
205
+ event_key: event.eventKey,
206
+ dedupe_key,
207
+ actor_user_id,
208
+ subject_user_id,
209
+ organization_id,
210
+ team_id,
211
+ session_id,
212
+ provider_id,
213
+ login_method,
214
+ identifier,
215
+ visitor_id,
216
+ reason,
217
+ action,
218
+ trigger_context,
219
+ ip_address,
220
+ country_code,
221
+ country,
222
+ city,
223
+ user_agent,
224
+ payload_json: eventData,
225
+ occurred_at,
226
+ })
227
+ .onConflict("dedupe_key")
228
+ .ignore()
229
+ .returning("dedupe_key");
230
+
231
+ if (inserted.length === 0) {
232
+ return; // silent dedupe
233
+ }
234
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Better Auth dash() 플러그인이 POST ${apiUrl}/events/track으로 전송하는 이벤트 body 타입
3
+ */
4
+ export type AuditLogEvent = {
5
+ eventType: string;
6
+ eventData: Record<string, unknown>;
7
+ eventKey: string;
8
+ eventDisplayName?: string;
9
+ ipAddress?: string;
10
+ city?: string;
11
+ country?: string;
12
+ countryCode?: string;
13
+ };
14
+
15
+ /**
16
+ * AuditLog 프록시 옵션
17
+ */
18
+ export type AuditLogProxyOptions = {
19
+ /** 프록시 base path (기본값: "/api/audit-log") */
20
+ basePath?: string;
21
+ /** 이벤트 수신 시 콜백 (향후 DB 적재 확장 포인트) */
22
+ onEvent?: (event: AuditLogEvent) => void | Promise<void>;
23
+ };
package/src/auth/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { GenerateBetterAuthEntitiesOptions } from "./auth-generator";
2
2
  export { generateBetterAuthEntities } from "./auth-generator";
3
+ export { ingestAuditEvent } from "./audit-log-ingestor";
3
4
  export { BASE_FIELD_MAPPINGS, betterAuthV1 } from "./better-auth-entities";
4
5
  export { sonamuKnexAdapter } from "./knex-adapter";
5
6
 
@@ -0,0 +1,171 @@
1
+ import { type BetterAuthEntityDef } from "./types";
2
+
3
+ /**
4
+ * better-auth AuditLog 플러그인 엔티티 정의
5
+ *
6
+ * auth.auditLog: true 로 활성화되며 audit_events 테이블을 생성합니다.
7
+ * - Better Auth dash() 플러그인이 전송하는 이벤트를 1건씩 적재합니다.
8
+ * - dedupe_key(sha256 hex)로 중복 적재를 방지합니다.
9
+ *
10
+ * 생성 방법: pnpm sonamu auth generate --plugins audit-log
11
+ */
12
+ export const auditLogEntityDef: BetterAuthEntityDef = {
13
+ id: "audit-log",
14
+ name: "AuditLog",
15
+ entities: [
16
+ {
17
+ id: "AuditEvent",
18
+ table: "audit_events",
19
+ title: "감사이벤트",
20
+ props: [
21
+ { name: "id", type: "integer", desc: "ID" },
22
+ { name: "source", type: "string", length: 32, desc: "이벤트 소스" },
23
+ { name: "source_version", type: "string", length: 96, nullable: true, desc: "소스 버전" },
24
+ { name: "category", type: "enum", id: "AuditEventCategory", desc: "카테고리" },
25
+ { name: "event_type", type: "string", length: 64, desc: "이벤트 타입" },
26
+ { name: "event_key", type: "string", length: 191, desc: "이벤트 키" },
27
+ { name: "dedupe_key", type: "string", length: 64, desc: "중복 제거 키" },
28
+ {
29
+ name: "actor_user_id",
30
+ type: "string",
31
+ length: 191,
32
+ nullable: true,
33
+ desc: "액터 사용자 ID",
34
+ },
35
+ {
36
+ name: "subject_user_id",
37
+ type: "string",
38
+ length: 191,
39
+ nullable: true,
40
+ desc: "대상 사용자 ID",
41
+ },
42
+ {
43
+ name: "organization_id",
44
+ type: "string",
45
+ length: 191,
46
+ nullable: true,
47
+ desc: "조직 ID",
48
+ },
49
+ { name: "team_id", type: "string", length: 191, nullable: true, desc: "팀 ID" },
50
+ { name: "session_id", type: "string", length: 191, nullable: true, desc: "세션 ID" },
51
+ { name: "provider_id", type: "string", length: 64, nullable: true, desc: "프로바이더 ID" },
52
+ { name: "login_method", type: "string", length: 64, nullable: true, desc: "로그인 방식" },
53
+ { name: "identifier", type: "string", length: 255, nullable: true, desc: "식별자" },
54
+ { name: "visitor_id", type: "string", length: 191, nullable: true, desc: "방문자 ID" },
55
+ { name: "reason", type: "string", length: 128, nullable: true, desc: "사유" },
56
+ { name: "action", type: "string", length: 64, nullable: true, desc: "액션" },
57
+ {
58
+ name: "trigger_context",
59
+ type: "string",
60
+ length: 64,
61
+ nullable: true,
62
+ desc: "트리거 컨텍스트",
63
+ },
64
+ { name: "ip_address", type: "string", length: 45, nullable: true, desc: "IP 주소" },
65
+ { name: "country_code", type: "string", length: 8, nullable: true, desc: "국가 코드" },
66
+ { name: "country", type: "string", length: 100, nullable: true, desc: "국가" },
67
+ { name: "city", type: "string", length: 100, nullable: true, desc: "도시" },
68
+ { name: "user_agent", type: "string", nullable: true, desc: "User-Agent" },
69
+ { name: "payload_json", type: "json", id: "AuditEventPayload", desc: "원본 payload" },
70
+ { name: "occurred_at", type: "date", desc: "발생 시각" },
71
+ {
72
+ name: "ingested_at",
73
+ type: "date",
74
+ dbDefault: "CURRENT_TIMESTAMP",
75
+ desc: "적재 시각",
76
+ },
77
+ ],
78
+ indexes: [
79
+ {
80
+ type: "unique",
81
+ name: "audit_events_dedupe_key_unique",
82
+ columns: [{ name: "dedupe_key" }],
83
+ },
84
+ {
85
+ type: "index",
86
+ name: "audit_events_occurred_at_index",
87
+ columns: [{ name: "occurred_at" }],
88
+ },
89
+ {
90
+ type: "index",
91
+ name: "audit_events_event_type_occurred_at_index",
92
+ columns: [{ name: "event_type" }, { name: "occurred_at" }],
93
+ },
94
+ {
95
+ type: "index",
96
+ name: "audit_events_subject_user_id_occurred_at_index",
97
+ columns: [{ name: "subject_user_id" }, { name: "occurred_at" }],
98
+ },
99
+ {
100
+ type: "index",
101
+ name: "audit_events_actor_user_id_occurred_at_index",
102
+ columns: [{ name: "actor_user_id" }, { name: "occurred_at" }],
103
+ },
104
+ {
105
+ type: "index",
106
+ name: "audit_events_organization_id_occurred_at_index",
107
+ columns: [{ name: "organization_id" }, { name: "occurred_at" }],
108
+ },
109
+ {
110
+ type: "index",
111
+ name: "audit_events_team_id_occurred_at_index",
112
+ columns: [{ name: "team_id" }, { name: "occurred_at" }],
113
+ },
114
+ {
115
+ type: "index",
116
+ name: "audit_events_session_id_index",
117
+ columns: [{ name: "session_id" }],
118
+ },
119
+ {
120
+ type: "index",
121
+ name: "audit_events_reason_occurred_at_index",
122
+ columns: [{ name: "reason" }, { name: "occurred_at" }],
123
+ },
124
+ ],
125
+ subsets: {
126
+ A: [
127
+ "id",
128
+ "source",
129
+ "source_version",
130
+ "category",
131
+ "event_type",
132
+ "event_key",
133
+ "dedupe_key",
134
+ "actor_user_id",
135
+ "subject_user_id",
136
+ "organization_id",
137
+ "team_id",
138
+ "session_id",
139
+ "provider_id",
140
+ "login_method",
141
+ "identifier",
142
+ "visitor_id",
143
+ "reason",
144
+ "action",
145
+ "trigger_context",
146
+ "ip_address",
147
+ "country_code",
148
+ "country",
149
+ "city",
150
+ "user_agent",
151
+ "payload_json",
152
+ "occurred_at",
153
+ "ingested_at",
154
+ ],
155
+ },
156
+ enums: {
157
+ AuditEventOrderBy: { "id-desc": "ID최신순" },
158
+ AuditEventSearchField: { id: "ID" },
159
+ AuditEventCategory: {
160
+ user: "사용자",
161
+ session: "세션",
162
+ account: "계정",
163
+ verification: "인증",
164
+ organization: "조직",
165
+ security: "보안",
166
+ },
167
+ },
168
+ },
169
+ ],
170
+ additionalProps: {},
171
+ };
@@ -1,6 +1,7 @@
1
1
  export { adminEntityDef } from "./admin";
2
2
  export { anonymousEntityDef } from "./anonymous";
3
3
  export { apiKeyEntityDef } from "./api-key";
4
+ export { auditLogEntityDef } from "./audit-log";
4
5
  export { jwtEntityDef } from "./jwt";
5
6
  export { organizationEntityDef } from "./organization";
6
7
  export { passkeyEntityDef } from "./passkey";
@@ -13,6 +14,7 @@ export { usernameEntityDef } from "./username";
13
14
  import { adminEntityDef } from "./admin";
14
15
  import { anonymousEntityDef } from "./anonymous";
15
16
  import { apiKeyEntityDef } from "./api-key";
17
+ import { auditLogEntityDef } from "./audit-log";
16
18
  import { jwtEntityDef } from "./jwt";
17
19
  import { organizationEntityDef } from "./organization";
18
20
  import { passkeyEntityDef } from "./passkey";
@@ -37,6 +39,7 @@ export const ENTITY_DEFINITIONS: Record<BetterAuthPluginId, BetterAuthEntityDef>
37
39
  "api-key": apiKeyEntityDef,
38
40
  jwt: jwtEntityDef,
39
41
  anonymous: anonymousEntityDef,
42
+ "audit-log": auditLogEntityDef,
40
43
  };
41
44
 
42
45
  /**
@@ -14,7 +14,8 @@ export type BetterAuthPluginId =
14
14
  | "organization"
15
15
  | "api-key"
16
16
  | "jwt"
17
- | "anonymous";
17
+ | "anonymous"
18
+ | "audit-log";
18
19
 
19
20
  /**
20
21
  * better-auth 엔티티 정의