better-auth-audit-logs 0.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.
Files changed (46) hide show
  1. package/README.md +366 -0
  2. package/dist/adapters/index.d.ts +2 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/memory.d.ts +9 -0
  5. package/dist/adapters/memory.d.ts.map +1 -0
  6. package/dist/client.cjs +53 -0
  7. package/dist/client.d.ts +11 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +13 -0
  10. package/dist/endpoints/get-log.d.ts +28 -0
  11. package/dist/endpoints/get-log.d.ts.map +1 -0
  12. package/dist/endpoints/index.d.ts +4 -0
  13. package/dist/endpoints/index.d.ts.map +1 -0
  14. package/dist/endpoints/insert-log.d.ts +45 -0
  15. package/dist/endpoints/insert-log.d.ts.map +1 -0
  16. package/dist/endpoints/list-logs.d.ts +41 -0
  17. package/dist/endpoints/list-logs.d.ts.map +1 -0
  18. package/dist/hooks/after.d.ts +7 -0
  19. package/dist/hooks/after.d.ts.map +1 -0
  20. package/dist/hooks/before.d.ts +8 -0
  21. package/dist/hooks/before.d.ts.map +1 -0
  22. package/dist/hooks/index.d.ts +3 -0
  23. package/dist/hooks/index.d.ts.map +1 -0
  24. package/dist/index.cjs +564 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +522 -0
  28. package/dist/internal.d.ts +16 -0
  29. package/dist/internal.d.ts.map +1 -0
  30. package/dist/plugin.d.ts +181 -0
  31. package/dist/plugin.d.ts.map +1 -0
  32. package/dist/schema.d.ts +109 -0
  33. package/dist/schema.d.ts.map +1 -0
  34. package/dist/types.d.ts +88 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/utils/index.d.ts +5 -0
  37. package/dist/utils/index.d.ts.map +1 -0
  38. package/dist/utils/normalize-path.d.ts +2 -0
  39. package/dist/utils/normalize-path.d.ts.map +1 -0
  40. package/dist/utils/request-meta.d.ts +6 -0
  41. package/dist/utils/request-meta.d.ts.map +1 -0
  42. package/dist/utils/sanitize.d.ts +4 -0
  43. package/dist/utils/sanitize.d.ts.map +1 -0
  44. package/dist/utils/severity.d.ts +3 -0
  45. package/dist/utils/severity.d.ts.map +1 -0
  46. package/package.json +43 -0
package/dist/index.js ADDED
@@ -0,0 +1,522 @@
1
+ // src/schema.ts
2
+ import { mergeSchema } from "better-auth/db";
3
+ var baseSchema = {
4
+ auditLog: {
5
+ modelName: "audit_log",
6
+ fields: {
7
+ userId: {
8
+ type: "string",
9
+ required: false,
10
+ references: {
11
+ model: "user",
12
+ field: "id",
13
+ onDelete: "set null"
14
+ },
15
+ index: true
16
+ },
17
+ action: {
18
+ type: "string",
19
+ required: true,
20
+ sortable: true,
21
+ index: true
22
+ },
23
+ status: {
24
+ type: "string",
25
+ required: true,
26
+ sortable: true
27
+ },
28
+ severity: {
29
+ type: "string",
30
+ required: true,
31
+ sortable: true
32
+ },
33
+ ipAddress: {
34
+ type: "string",
35
+ required: false
36
+ },
37
+ userAgent: {
38
+ type: "string",
39
+ required: false,
40
+ returned: false
41
+ },
42
+ metadata: {
43
+ type: "string",
44
+ required: false
45
+ },
46
+ createdAt: {
47
+ type: "date",
48
+ required: true,
49
+ sortable: true,
50
+ index: true,
51
+ defaultValue: () => new Date
52
+ }
53
+ }
54
+ }
55
+ };
56
+ function buildSchema(options) {
57
+ return mergeSchema(baseSchema, options?.schema);
58
+ }
59
+ function getModelName(options) {
60
+ return options?.schema?.auditLog?.modelName ?? "audit_log";
61
+ }
62
+
63
+ // src/hooks/before.ts
64
+ import { createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
65
+
66
+ // src/utils/normalize-path.ts
67
+ function normalizePath(path) {
68
+ return path.replace(/^\//, "").replace(/\//g, ":");
69
+ }
70
+ // src/utils/severity.ts
71
+ var CRITICAL = ["ban-user", "impersonate-user"];
72
+ var HIGH = [
73
+ "delete-user",
74
+ "delete-account",
75
+ "revoke-sessions",
76
+ "revoke-other-sessions"
77
+ ];
78
+ var MEDIUM = [
79
+ "sign-in",
80
+ "sign-out",
81
+ "revoke-session",
82
+ "two-factor",
83
+ "change-password",
84
+ "reset-password"
85
+ ];
86
+ function inferSeverity(action, status) {
87
+ if (CRITICAL.some((p) => action.includes(p)))
88
+ return "critical";
89
+ if (HIGH.some((p) => action.includes(p)))
90
+ return "high";
91
+ if (MEDIUM.some((p) => action.includes(p)))
92
+ return status === "failed" ? "high" : "medium";
93
+ return "low";
94
+ }
95
+ // src/utils/request-meta.ts
96
+ import { getIp } from "better-auth/api";
97
+ function extractRequestMeta(request, headers, options) {
98
+ return {
99
+ ipAddress: request ? getIp(request, options) ?? null : null,
100
+ userAgent: headers?.get("user-agent") ?? null
101
+ };
102
+ }
103
+ // src/utils/sanitize.ts
104
+ var DEFAULT_PII_FIELDS = [
105
+ "password",
106
+ "newPassword",
107
+ "currentPassword",
108
+ "token",
109
+ "secret",
110
+ "apiKey",
111
+ "refreshToken",
112
+ "accessToken",
113
+ "code",
114
+ "backupCode",
115
+ "otp"
116
+ ];
117
+ async function sha256(value) {
118
+ const encoded = new TextEncoder().encode(value);
119
+ const buffer = await crypto.subtle.digest("SHA-256", encoded);
120
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
121
+ }
122
+ async function redactPII(data, config) {
123
+ if (!config.enabled)
124
+ return data;
125
+ const fields = config.fields ?? DEFAULT_PII_FIELDS;
126
+ const strategy = config.strategy ?? "mask";
127
+ const result = { ...data };
128
+ for (const field of fields) {
129
+ if (!(field in result) || result[field] == null)
130
+ continue;
131
+ if (strategy === "remove") {
132
+ delete result[field];
133
+ } else if (strategy === "hash") {
134
+ result[field] = await sha256(String(result[field]));
135
+ } else {
136
+ result[field] = "[REDACTED]";
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+ // src/internal.ts
142
+ async function buildLogEntry(path, status, params) {
143
+ const action = normalizePath(path);
144
+ const severity = params.pathConfig?.severity ?? inferSeverity(action, status);
145
+ const captureOpts = {
146
+ ...params.options.capture,
147
+ ...params.pathConfig?.capture
148
+ };
149
+ const { ipAddress, userAgent } = extractRequestMeta(captureOpts.ipAddress !== false ? params.request : undefined, captureOpts.userAgent !== false ? params.headers : undefined, params.authOptions);
150
+ let metadata = params.metadata ?? {};
151
+ if (params.options.piiRedaction.enabled) {
152
+ metadata = await redactPII(metadata, params.options.piiRedaction);
153
+ }
154
+ return {
155
+ userId: params.userId,
156
+ action,
157
+ status,
158
+ severity,
159
+ ipAddress,
160
+ userAgent,
161
+ metadata,
162
+ createdAt: new Date
163
+ };
164
+ }
165
+ async function buildLogEntryFromAction(action, status, params) {
166
+ const severity = inferSeverity(action, status);
167
+ const { ipAddress, userAgent } = extractRequestMeta(params.options.capture.ipAddress !== false ? params.request : undefined, params.options.capture.userAgent !== false ? params.headers : undefined, params.authOptions);
168
+ let metadata = params.metadata ?? {};
169
+ if (params.options.piiRedaction.enabled) {
170
+ metadata = await redactPII(metadata, params.options.piiRedaction);
171
+ }
172
+ return {
173
+ userId: params.userId,
174
+ action,
175
+ status,
176
+ severity,
177
+ ipAddress,
178
+ userAgent,
179
+ metadata,
180
+ createdAt: new Date
181
+ };
182
+ }
183
+ async function writeEntry(ctx, entry, opts, modelName) {
184
+ let finalEntry = entry;
185
+ if (opts.beforeLog) {
186
+ const modified = await opts.beforeLog(finalEntry);
187
+ if (modified === null)
188
+ return;
189
+ finalEntry = modified;
190
+ }
191
+ const doWrite = async () => {
192
+ let written;
193
+ if (opts.storage) {
194
+ written = { id: crypto.randomUUID(), ...finalEntry };
195
+ await opts.storage.write(written);
196
+ } else {
197
+ const record = await ctx.context.adapter.create({
198
+ model: modelName,
199
+ data: {
200
+ ...finalEntry,
201
+ metadata: JSON.stringify(finalEntry.metadata)
202
+ }
203
+ });
204
+ written = { ...record, metadata: finalEntry.metadata };
205
+ }
206
+ if (opts.afterLog)
207
+ await opts.afterLog(written);
208
+ };
209
+ if (opts.nonBlocking) {
210
+ ctx.context.runInBackground(doWrite().catch((err) => ctx.context.logger?.error("[audit-log] write failed", err)));
211
+ } else {
212
+ await doWrite();
213
+ }
214
+ }
215
+
216
+ // src/hooks/before.ts
217
+ var BEFORE_PATHS = [
218
+ "/sign-out",
219
+ "/delete-user",
220
+ "/revoke-session",
221
+ "/revoke-sessions",
222
+ "/revoke-other-sessions"
223
+ ];
224
+ function createBeforeHooks(opts, modelName) {
225
+ return [
226
+ {
227
+ matcher: (context) => !!context.path && BEFORE_PATHS.some((p) => context.path.startsWith(p)) && opts.shouldCapture(context.path),
228
+ handler: createAuthMiddleware(async (ctx) => {
229
+ try {
230
+ const session = await getSessionFromCtx(ctx);
231
+ const path = ctx.path;
232
+ const pathConfig = opts.getPathConfig(path);
233
+ const entry = await buildLogEntry(path, "success", {
234
+ userId: session?.user?.id ?? null,
235
+ request: ctx.request,
236
+ headers: ctx.headers,
237
+ pathConfig,
238
+ options: opts,
239
+ authOptions: ctx.context.options
240
+ });
241
+ await writeEntry(ctx, entry, opts, modelName);
242
+ } catch (err) {
243
+ ctx.context.logger?.error("[audit-log] before hook failed", err);
244
+ }
245
+ })
246
+ }
247
+ ];
248
+ }
249
+ // src/hooks/after.ts
250
+ import { createAuthMiddleware as createAuthMiddleware2 } from "better-auth/api";
251
+ function createAfterHooks(opts, modelName) {
252
+ return [
253
+ {
254
+ matcher: (context) => !!context.path && !BEFORE_PATHS.some((p) => context.path.startsWith(p)) && opts.shouldCapture(context.path),
255
+ handler: createAuthMiddleware2(async (ctx) => {
256
+ try {
257
+ const path = ctx.path;
258
+ const isError = ctx.context.returned instanceof Error;
259
+ const status = isError ? "failed" : "success";
260
+ const user = ctx.context.newSession?.user ?? ctx.context.session?.user;
261
+ const pathConfig = opts.getPathConfig(path);
262
+ const metadata = {};
263
+ if (opts.capture.requestBody && ctx.body) {
264
+ metadata.requestBody = ctx.body;
265
+ }
266
+ if (isError) {
267
+ const err = ctx.context.returned;
268
+ metadata.error = {
269
+ message: err.message,
270
+ ...err.status !== undefined && { status: err.status },
271
+ ...err.code !== undefined && { code: err.code }
272
+ };
273
+ }
274
+ const entry = await buildLogEntry(path, status, {
275
+ userId: user?.id ?? null,
276
+ request: ctx.request,
277
+ headers: ctx.headers,
278
+ metadata,
279
+ pathConfig,
280
+ options: opts,
281
+ authOptions: ctx.context.options
282
+ });
283
+ await writeEntry(ctx, entry, opts, modelName);
284
+ } catch (err) {
285
+ ctx.context.logger?.error("[audit-log] after hook failed", err);
286
+ }
287
+ })
288
+ }
289
+ ];
290
+ }
291
+ // src/endpoints/list-logs.ts
292
+ import { createAuthEndpoint, sessionMiddleware, APIError } from "better-auth/api";
293
+ import { z } from "zod";
294
+ function createListLogsEndpoint(opts, modelName) {
295
+ return createAuthEndpoint("/audit-log/list", {
296
+ method: "GET",
297
+ use: [sessionMiddleware],
298
+ query: z.object({
299
+ userId: z.string().optional(),
300
+ action: z.string().optional(),
301
+ status: z.enum(["success", "failed"]).optional(),
302
+ from: z.string().optional(),
303
+ to: z.string().optional(),
304
+ limit: z.coerce.number().min(1).max(500).optional().default(50),
305
+ offset: z.coerce.number().min(0).optional().default(0)
306
+ })
307
+ }, async (ctx) => {
308
+ const session = ctx.context.session;
309
+ const targetUserId = ctx.query.userId ?? session.user.id;
310
+ if (targetUserId !== session.user.id) {
311
+ throw new APIError("FORBIDDEN", {
312
+ message: "Cannot query other users' audit logs"
313
+ });
314
+ }
315
+ const fromDate = ctx.query.from ? new Date(ctx.query.from) : undefined;
316
+ const toDate = ctx.query.to ? new Date(ctx.query.to) : undefined;
317
+ if (opts.storage?.read) {
318
+ const readOpts = {
319
+ userId: targetUserId,
320
+ action: ctx.query.action,
321
+ status: ctx.query.status,
322
+ from: fromDate,
323
+ to: toDate,
324
+ limit: ctx.query.limit,
325
+ offset: ctx.query.offset
326
+ };
327
+ const result = await opts.storage.read(readOpts);
328
+ return ctx.json(result);
329
+ }
330
+ const where = [{ field: "userId", value: targetUserId }];
331
+ if (ctx.query.action) {
332
+ where.push({ field: "action", value: ctx.query.action });
333
+ }
334
+ if (ctx.query.status) {
335
+ where.push({ field: "status", value: ctx.query.status });
336
+ }
337
+ if (fromDate) {
338
+ where.push({ field: "createdAt", operator: "gte", value: fromDate });
339
+ }
340
+ if (toDate) {
341
+ where.push({ field: "createdAt", operator: "lte", value: toDate });
342
+ }
343
+ const [entries, total] = await Promise.all([
344
+ ctx.context.adapter.findMany({
345
+ model: modelName,
346
+ where,
347
+ sortBy: { field: "createdAt", direction: "desc" },
348
+ limit: ctx.query.limit,
349
+ offset: ctx.query.offset
350
+ }),
351
+ ctx.context.adapter.count({ model: modelName, where })
352
+ ]);
353
+ const parsed = entries.map((e) => ({
354
+ ...e,
355
+ metadata: typeof e["metadata"] === "string" ? JSON.parse(e["metadata"]) : e["metadata"] ?? {}
356
+ }));
357
+ return ctx.json({ entries: parsed, total });
358
+ });
359
+ }
360
+ // src/endpoints/get-log.ts
361
+ import { createAuthEndpoint as createAuthEndpoint2, sessionMiddleware as sessionMiddleware2, APIError as APIError2 } from "better-auth/api";
362
+ function createGetLogEndpoint(opts, modelName) {
363
+ return createAuthEndpoint2("/audit-log/:id", { method: "GET", use: [sessionMiddleware2] }, async (ctx) => {
364
+ const { id } = ctx.params;
365
+ const session = ctx.context.session;
366
+ if (opts.storage?.readById) {
367
+ const entry2 = await opts.storage.readById(id);
368
+ if (!entry2 || entry2.userId !== session.user.id) {
369
+ throw new APIError2("NOT_FOUND", {
370
+ message: "Audit log entry not found"
371
+ });
372
+ }
373
+ return ctx.json(entry2);
374
+ }
375
+ const record = await ctx.context.adapter.findOne({
376
+ model: modelName,
377
+ where: [
378
+ { field: "id", value: id },
379
+ { field: "userId", value: session.user.id }
380
+ ]
381
+ });
382
+ if (!record) {
383
+ throw new APIError2("NOT_FOUND", {
384
+ message: "Audit log entry not found"
385
+ });
386
+ }
387
+ const entry = {
388
+ ...record,
389
+ metadata: typeof record["metadata"] === "string" ? JSON.parse(record["metadata"]) : record["metadata"] ?? {}
390
+ };
391
+ return ctx.json(entry);
392
+ });
393
+ }
394
+ // src/endpoints/insert-log.ts
395
+ import { createAuthEndpoint as createAuthEndpoint3, sessionMiddleware as sessionMiddleware3 } from "better-auth/api";
396
+ import { z as z2 } from "zod";
397
+ function createInsertLogEndpoint(opts, modelName) {
398
+ return createAuthEndpoint3("/audit-log/insert", {
399
+ method: "POST",
400
+ use: [sessionMiddleware3],
401
+ body: z2.object({
402
+ action: z2.string().min(1),
403
+ status: z2.enum(["success", "failed"]).optional().default("success"),
404
+ severity: z2.enum(["low", "medium", "high", "critical"]).optional(),
405
+ metadata: z2.record(z2.string(), z2.unknown()).optional().default({})
406
+ })
407
+ }, async (ctx) => {
408
+ const session = ctx.context.session;
409
+ const { action, status, severity, metadata } = ctx.body;
410
+ const entry = await buildLogEntryFromAction(action, status, {
411
+ userId: session.user.id,
412
+ request: ctx.request,
413
+ headers: ctx.headers,
414
+ metadata,
415
+ options: opts,
416
+ authOptions: ctx.context.options
417
+ });
418
+ if (severity) {
419
+ entry.severity = severity;
420
+ }
421
+ await writeEntry(ctx, entry, opts, modelName);
422
+ return ctx.json({ success: true });
423
+ });
424
+ }
425
+ // src/plugin.ts
426
+ function resolveOptions(options) {
427
+ const pathsMap = new Map;
428
+ const hasPaths = (options?.paths?.length ?? 0) > 0;
429
+ for (const p of options?.paths ?? []) {
430
+ if (typeof p === "string") {
431
+ pathsMap.set(p, undefined);
432
+ } else {
433
+ pathsMap.set(p.path, p.config);
434
+ }
435
+ }
436
+ return {
437
+ enabled: options?.enabled ?? true,
438
+ nonBlocking: options?.nonBlocking ?? false,
439
+ storage: options?.storage,
440
+ capture: {
441
+ ipAddress: options?.capture?.ipAddress ?? true,
442
+ userAgent: options?.capture?.userAgent ?? true,
443
+ requestBody: options?.capture?.requestBody ?? false
444
+ },
445
+ piiRedaction: {
446
+ enabled: options?.piiRedaction?.enabled ?? false,
447
+ fields: options?.piiRedaction?.fields,
448
+ strategy: options?.piiRedaction?.strategy ?? "mask"
449
+ },
450
+ retention: options?.retention,
451
+ beforeLog: options?.beforeLog,
452
+ afterLog: options?.afterLog,
453
+ shouldCapture: (path) => !hasPaths || pathsMap.has(path),
454
+ getPathConfig: (path) => pathsMap.get(path)
455
+ };
456
+ }
457
+ function auditLog(options) {
458
+ const schema = buildSchema(options);
459
+ const modelName = getModelName(options);
460
+ const resolved = resolveOptions(options);
461
+ const beforeHooks = resolved.enabled ? createBeforeHooks(resolved, modelName) : [];
462
+ const afterHooks = resolved.enabled ? createAfterHooks(resolved, modelName) : [];
463
+ return {
464
+ id: "audit-log",
465
+ schema,
466
+ hooks: {
467
+ before: beforeHooks,
468
+ after: afterHooks
469
+ },
470
+ endpoints: {
471
+ listAuditLogs: createListLogsEndpoint(resolved, modelName),
472
+ getAuditLog: createGetLogEndpoint(resolved, modelName),
473
+ insertAuditLog: createInsertLogEndpoint(resolved, modelName)
474
+ },
475
+ rateLimit: [
476
+ {
477
+ pathMatcher: (path) => path.startsWith("/audit-log/"),
478
+ window: 60,
479
+ max: 60
480
+ }
481
+ ]
482
+ };
483
+ }
484
+ // src/adapters/memory.ts
485
+ class MemoryStorage {
486
+ entries = [];
487
+ async write(entry) {
488
+ this.entries.push(entry);
489
+ }
490
+ async read(opts) {
491
+ let filtered = this.entries.filter((e) => {
492
+ if (opts.userId !== undefined && e.userId !== opts.userId)
493
+ return false;
494
+ if (opts.action !== undefined && e.action !== opts.action)
495
+ return false;
496
+ if (opts.status !== undefined && e.status !== opts.status)
497
+ return false;
498
+ if (opts.from !== undefined && e.createdAt < opts.from)
499
+ return false;
500
+ if (opts.to !== undefined && e.createdAt > opts.to)
501
+ return false;
502
+ return true;
503
+ });
504
+ const total = filtered.length;
505
+ filtered = filtered.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).slice(opts.offset, opts.offset + opts.limit);
506
+ return { entries: filtered, total };
507
+ }
508
+ async readById(id) {
509
+ return this.entries.find((e) => e.id === id) ?? null;
510
+ }
511
+ async deleteOlderThan(date) {
512
+ const before = this.entries.length;
513
+ const retained = this.entries.filter((e) => e.createdAt >= date);
514
+ this.entries.length = 0;
515
+ this.entries.push(...retained);
516
+ return before - this.entries.length;
517
+ }
518
+ }
519
+ export {
520
+ auditLog,
521
+ MemoryStorage
522
+ };
@@ -0,0 +1,16 @@
1
+ import type { GenericEndpointContext } from "@better-auth/core";
2
+ import type { AuditLogEntry, AuditLogStatus, PathConfig, ResolvedOptions } from "./types";
3
+ interface BuildParams {
4
+ userId: string | null;
5
+ request: Request | undefined;
6
+ headers: Headers | undefined;
7
+ metadata?: Record<string, unknown>;
8
+ pathConfig?: PathConfig;
9
+ options: ResolvedOptions;
10
+ authOptions: GenericEndpointContext["context"]["options"];
11
+ }
12
+ export declare function buildLogEntry(path: string, status: AuditLogStatus, params: BuildParams): Promise<Omit<AuditLogEntry, "id">>;
13
+ export declare function buildLogEntryFromAction(action: string, status: AuditLogStatus, params: Omit<BuildParams, "pathConfig">): Promise<Omit<AuditLogEntry, "id">>;
14
+ export declare function writeEntry(ctx: GenericEndpointContext, entry: Omit<AuditLogEntry, "id">, opts: ResolvedOptions, modelName: string): Promise<void>;
15
+ export {};
16
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,UAAU,EACV,eAAe,EAChB,MAAM,SAAS,CAAC;AAQjB,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,EAAE,eAAe,CAAC;IACzB,WAAW,EAAE,sBAAsB,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC;CAC3D;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CA+BpC;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAwBpC;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,sBAAsB,EAC3B,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAChC,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAuCf"}