semola 0.2.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 +669 -0
  2. package/dist/lib/cache/index.d.ts +21 -0
  3. package/dist/lib/cache/index.d.ts.map +1 -0
  4. package/dist/lib/cache/index.js +45 -0
  5. package/dist/lib/cache/index.js.map +1 -0
  6. package/dist/lib/cache/types.d.ts +5 -0
  7. package/dist/lib/cache/types.d.ts.map +1 -0
  8. package/dist/lib/cache/types.js +2 -0
  9. package/dist/lib/cache/types.js.map +1 -0
  10. package/dist/lib/errors/index.d.ts +9 -0
  11. package/dist/lib/errors/index.d.ts.map +1 -0
  12. package/dist/lib/errors/index.js +25 -0
  13. package/dist/lib/errors/index.js.map +1 -0
  14. package/dist/lib/errors/types.d.ts +2 -0
  15. package/dist/lib/errors/types.d.ts.map +1 -0
  16. package/dist/lib/errors/types.js +2 -0
  17. package/dist/lib/errors/types.js.map +1 -0
  18. package/dist/lib/http/index.d.ts +29 -0
  19. package/dist/lib/http/index.d.ts.map +1 -0
  20. package/dist/lib/http/index.js +641 -0
  21. package/dist/lib/http/index.js.map +1 -0
  22. package/dist/lib/http/types.d.ts +175 -0
  23. package/dist/lib/http/types.d.ts.map +1 -0
  24. package/dist/lib/http/types.js +2 -0
  25. package/dist/lib/http/types.js.map +1 -0
  26. package/dist/lib/i18n/index.d.ts +18 -0
  27. package/dist/lib/i18n/index.d.ts.map +1 -0
  28. package/dist/lib/i18n/index.js +38 -0
  29. package/dist/lib/i18n/index.js.map +1 -0
  30. package/dist/lib/i18n/types.d.ts +15 -0
  31. package/dist/lib/i18n/types.d.ts.map +1 -0
  32. package/dist/lib/i18n/types.js +2 -0
  33. package/dist/lib/i18n/types.js.map +1 -0
  34. package/dist/lib/policy/index.d.ts +9 -0
  35. package/dist/lib/policy/index.d.ts.map +1 -0
  36. package/dist/lib/policy/index.js +51 -0
  37. package/dist/lib/policy/index.js.map +1 -0
  38. package/dist/lib/policy/index.test.d.ts +2 -0
  39. package/dist/lib/policy/index.test.d.ts.map +1 -0
  40. package/dist/lib/policy/index.test.js +328 -0
  41. package/dist/lib/policy/index.test.js.map +1 -0
  42. package/dist/lib/policy/types.d.ts +27 -0
  43. package/dist/lib/policy/types.d.ts.map +1 -0
  44. package/dist/lib/policy/types.js +2 -0
  45. package/dist/lib/policy/types.js.map +1 -0
  46. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,669 @@
1
+ # ts-kit
2
+
3
+ A TypeScript utility kit providing type-safe error handling, caching, internationalization, policy-based authorization, and developer tools.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install ts-kit
9
+ ```
10
+
11
+ ```bash
12
+ bun add ts-kit
13
+ ```
14
+
15
+ ## Features
16
+
17
+ ### Policy
18
+
19
+ A type-safe policy-based authorization system for defining and enforcing access control rules with conditional logic.
20
+
21
+ #### Import
22
+
23
+ ```typescript
24
+ import { Policy } from "ts-kit/policy";
25
+ ```
26
+
27
+ #### API
28
+
29
+ **`new Policy()`**
30
+
31
+ Creates a new policy instance for managing authorization rules.
32
+
33
+ ```typescript
34
+ const policy = new Policy();
35
+ ```
36
+
37
+ **`policy.allow<T>(params: AllowParams<T>)`**
38
+
39
+ Defines a rule that grants permission for an action on an entity, optionally with conditions and a reason.
40
+
41
+ ```typescript
42
+ type Post = {
43
+ id: number;
44
+ title: string;
45
+ authorId: number;
46
+ status: string;
47
+ };
48
+
49
+ // Allow reading all published posts
50
+ policy.allow<Post>({
51
+ action: "read",
52
+ entity: "Post",
53
+ reason: "Public posts are visible to everyone",
54
+ conditions: {
55
+ status: "published"
56
+ }
57
+ });
58
+
59
+ // Allow all read access without conditions
60
+ policy.allow({
61
+ action: "read",
62
+ entity: "Comment",
63
+ reason: "Comments are public"
64
+ });
65
+ ```
66
+
67
+ **`policy.forbid<T>(params: ForbidParams<T>)`**
68
+
69
+ Defines a rule that denies permission for an action on an entity, optionally with conditions and a reason.
70
+
71
+ ```typescript
72
+ // Forbid updating published posts
73
+ policy.forbid<Post>({
74
+ action: "update",
75
+ entity: "Post",
76
+ reason: "Published posts cannot be modified",
77
+ conditions: {
78
+ status: "published"
79
+ }
80
+ });
81
+
82
+ // Forbid deleting admin users
83
+ policy.forbid({
84
+ action: "delete",
85
+ entity: "User",
86
+ reason: "You cannot delete admins"
87
+ });
88
+ ```
89
+
90
+ **`policy.can<T>(action: Action, entity: Entity, object?: T): CanResult`**
91
+
92
+ Checks if an action is permitted on an entity, optionally validating against an object's conditions. Returns a result object with `allowed` (boolean) and optional `reason` (string).
93
+
94
+ ```typescript
95
+ const post: Post = {
96
+ id: 1,
97
+ title: "My Post",
98
+ authorId: 1,
99
+ status: "published"
100
+ };
101
+
102
+ policy.can<Post>("read", "Post", post);
103
+ // { allowed: true, reason: "Public posts are visible to everyone" }
104
+
105
+ policy.can<Post>("update", "Post", post);
106
+ // { allowed: false, reason: "Published posts cannot be modified" }
107
+
108
+ policy.can("delete", "Post");
109
+ // { allowed: false, reason: undefined }
110
+ ```
111
+
112
+ #### Types
113
+
114
+ ```typescript
115
+ type Action = "read" | "create" | "update" | "delete" | (string & {});
116
+ type Entity = string;
117
+ type Conditions<T> = Partial<T>;
118
+
119
+ type CanResult = {
120
+ allowed: boolean;
121
+ reason?: string;
122
+ };
123
+ ```
124
+
125
+ #### Features
126
+
127
+ - **Type-safe conditions**: Conditions are validated against the object type
128
+ - **Flexible actions**: Built-in CRUD actions plus custom string actions
129
+ - **Multiple conditions**: Rules can match multiple object properties
130
+ - **Allow/Forbid semantics**: Explicit permission and denial rules
131
+ - **Human-readable reasons**: Optional explanations for authorization decisions
132
+ - **No match defaults to deny**: Conservative security model
133
+ - **Zero dependencies**: Pure TypeScript implementation
134
+
135
+ #### Usage Example
136
+
137
+ ```typescript
138
+ import { Policy } from "ts-kit/policy";
139
+
140
+ type Post = {
141
+ id: number;
142
+ title: string;
143
+ authorId: number;
144
+ status: string;
145
+ };
146
+
147
+ // Create policy
148
+ const policy = new Policy();
149
+
150
+ // Define rules with reasons
151
+ policy.allow<Post>({
152
+ action: "read",
153
+ entity: "Post",
154
+ reason: "Published posts are publicly accessible",
155
+ conditions: {
156
+ status: "published"
157
+ }
158
+ });
159
+
160
+ policy.allow<Post>({
161
+ action: "update",
162
+ entity: "Post",
163
+ reason: "Draft posts can be edited",
164
+ conditions: {
165
+ status: "draft"
166
+ }
167
+ });
168
+
169
+ policy.forbid<Post>({
170
+ action: "delete",
171
+ entity: "Post",
172
+ reason: "Published posts cannot be deleted",
173
+ conditions: {
174
+ status: "published"
175
+ }
176
+ });
177
+
178
+ // Check permissions
179
+ const publishedPost: Post = {
180
+ id: 1,
181
+ title: "Hello World",
182
+ authorId: 1,
183
+ status: "published"
184
+ };
185
+
186
+ const draftPost: Post = {
187
+ id: 2,
188
+ title: "Work in Progress",
189
+ authorId: 1,
190
+ status: "draft"
191
+ };
192
+
193
+ // Check permissions with reasons
194
+ const readResult = policy.can<Post>("read", "Post", publishedPost);
195
+ console.log(readResult);
196
+ // { allowed: true, reason: "Published posts are publicly accessible" }
197
+
198
+ const updateDraftResult = policy.can<Post>("update", "Post", draftPost);
199
+ console.log(updateDraftResult);
200
+ // { allowed: true, reason: "Draft posts can be edited" }
201
+
202
+ const deleteResult = policy.can<Post>("delete", "Post", publishedPost);
203
+ console.log(deleteResult);
204
+ // { allowed: false, reason: "Published posts cannot be deleted" }
205
+
206
+ // Use in authorization middleware
207
+ function authorize<T>(action: Action, entity: Entity, object?: T) {
208
+ const result = policy.can(action, entity, object);
209
+ if (!result.allowed) {
210
+ throw new Error(result.reason || "Unauthorized");
211
+ }
212
+ }
213
+
214
+ // Protect routes with meaningful error messages
215
+ authorize<Post>("delete", "Post", publishedPost);
216
+ // throws Error: "Published posts cannot be deleted"
217
+
218
+ authorize<Post>("read", "Post", publishedPost);
219
+ // passes
220
+ ```
221
+
222
+ ### Internationalization (i18n)
223
+
224
+ A fully type-safe internationalization utility with compile-time validation of translation keys and parameters.
225
+
226
+ #### Import
227
+
228
+ ```typescript
229
+ import { I18n } from "ts-kit/i18n";
230
+ ```
231
+
232
+ #### API
233
+
234
+ **`new I18n<TLocales, TDefaultLocale>(config)`**
235
+
236
+ Creates a new i18n instance with type-safe locale management.
237
+
238
+ ```typescript
239
+ const i18n = new I18n({
240
+ defaultLocale: "en",
241
+ locales: {
242
+ en: {
243
+ common: {
244
+ hello: "Hello, world",
245
+ sayHi: "Hi, {name:string}",
246
+ age: "I am {age:number} years old",
247
+ active: "Status: {active:boolean}"
248
+ }
249
+ },
250
+ es: {
251
+ common: {
252
+ hello: "Hola, mundo",
253
+ sayHi: "Hola, {name:string}",
254
+ age: "Tengo {age:number} años",
255
+ active: "Estado: {active:boolean}"
256
+ }
257
+ }
258
+ } as const // Required for type inference
259
+ });
260
+ ```
261
+
262
+ **`i18n.translate(key, params?)`**
263
+
264
+ Translates a key with optional parameters. Fully type-safe - invalid keys, missing parameters, or wrong parameter types cause compile-time errors.
265
+
266
+ ```typescript
267
+ // Basic translation
268
+ i18n.translate("common.hello")
269
+ // "Hello, world"
270
+
271
+ // With parameters
272
+ i18n.translate("common.sayHi", { name: "Leonardo" })
273
+ // "Hi, Leonardo"
274
+
275
+ i18n.translate("common.age", { age: 25 })
276
+ // "I am 25 years old"
277
+
278
+ // Type errors (won't compile)
279
+ i18n.translate("invalid.key") // ✗ Invalid key
280
+ i18n.translate("common.sayHi") // ✗ Missing required params
281
+ i18n.translate("common.sayHi", { name: 123 }) // ✗ Wrong param type
282
+ i18n.translate("common.hello", { name: "x" }) // ✗ Unnecessary params
283
+ ```
284
+
285
+ **`i18n.setLocale(locale)`**
286
+
287
+ Switches to a different locale. Type-safe - only valid locale keys are accepted.
288
+
289
+ ```typescript
290
+ i18n.setLocale("es")
291
+ i18n.translate("common.hello")
292
+ // "Hola, mundo"
293
+
294
+ i18n.setLocale("invalid") // ✗ Type error
295
+ ```
296
+
297
+ **`i18n.getLocale()`**
298
+
299
+ Returns the current locale.
300
+
301
+ ```typescript
302
+ const currentLocale = i18n.getLocale()
303
+ // "es"
304
+ ```
305
+
306
+ #### Parameter Syntax
307
+
308
+ Translation strings support typed parameters with the syntax `{paramName:type}`:
309
+
310
+ - `{name:string}` - String parameter
311
+ - `{age:number}` - Number parameter
312
+ - `{active:boolean}` - Boolean parameter
313
+
314
+ The type system extracts these at compile time and enforces them in the `translate()` method.
315
+
316
+ #### Features
317
+
318
+ - **Type-safe keys**: Only valid nested keys accepted (e.g., `"common.hello"`)
319
+ - **Type-safe parameters**: Parameter types validated at compile time
320
+ - **Type-safe locales**: Only defined locale keys can be set
321
+ - **Nested translations**: Support for deeply nested translation objects
322
+ - **Locale fallback**: Falls back to default locale if translation missing
323
+ - **Zero runtime overhead**: No runtime type checking - pure TypeScript validation
324
+ - **Const assertion required**: Use `as const` on locale objects for proper type inference
325
+
326
+ #### Usage Example
327
+
328
+ ```typescript
329
+ import { I18n } from "ts-kit/i18n";
330
+
331
+ const translations = {
332
+ en: {
333
+ auth: {
334
+ welcome: "Welcome back, {name:string}!",
335
+ loginSuccess: "Successfully logged in",
336
+ loginFailed: "Login failed"
337
+ },
338
+ profile: {
339
+ age: "Age: {age:number}",
340
+ verified: "Verified: {status:boolean}"
341
+ }
342
+ },
343
+ es: {
344
+ auth: {
345
+ welcome: "Bienvenido, {name:string}!",
346
+ loginSuccess: "Inicio de sesión exitoso",
347
+ loginFailed: "Inicio de sesión fallido"
348
+ },
349
+ profile: {
350
+ age: "Edad: {age:number}",
351
+ verified: "Verificado: {status:boolean}"
352
+ }
353
+ }
354
+ } as const;
355
+
356
+ const i18n = new I18n({
357
+ defaultLocale: "en",
358
+ locales: translations
359
+ });
360
+
361
+ // Use in your app
362
+ function greetUser(name: string) {
363
+ return i18n.translate("auth.welcome", { name });
364
+ }
365
+
366
+ function showProfile(age: number, verified: boolean) {
367
+ console.log(i18n.translate("profile.age", { age }));
368
+ console.log(i18n.translate("profile.verified", { status: verified }));
369
+ }
370
+
371
+ // Switch language
372
+ i18n.setLocale("es");
373
+ greetUser("Maria"); // "Bienvenido, Maria!"
374
+ ```
375
+
376
+ ### Cache
377
+
378
+ A type-safe Redis cache wrapper with TTL support and result-based error handling. Built on Bun's native Redis client.
379
+
380
+ #### Import
381
+
382
+ ```typescript
383
+ import { Cache } from "ts-kit/cache";
384
+ ```
385
+
386
+ #### API
387
+
388
+ **`new Cache<T>(options: CacheOptions)`**
389
+
390
+ Creates a new cache instance with optional TTL configuration.
391
+
392
+ ```typescript
393
+ type CacheOptions = {
394
+ redis: Bun.RedisClient;
395
+ ttl?: number; // Time-to-live in milliseconds
396
+ };
397
+
398
+ const cache = new Cache<User>({
399
+ redis: redisClient,
400
+ ttl: 60000 // Optional: cache entries expire after 60 seconds
401
+ });
402
+ ```
403
+
404
+ **`cache.get(key: string)`**
405
+
406
+ Retrieves a value from the cache. Returns a result tuple with the parsed value or an error.
407
+
408
+ ```typescript
409
+ const [error, user] = await cache.get("user:123");
410
+
411
+ if (error) {
412
+ switch (error.type) {
413
+ case "NotFoundError":
414
+ console.log("Cache miss");
415
+ break;
416
+ case "CacheError":
417
+ console.error("Cache error:", error.message);
418
+ break;
419
+ }
420
+ } else {
421
+ console.log("Cache hit:", user);
422
+ }
423
+ ```
424
+
425
+ **`cache.set(key: string, value: T)`**
426
+
427
+ Stores a value in the cache with automatic JSON serialization. Applies TTL if configured.
428
+
429
+ ```typescript
430
+ const [error, data] = await cache.set("user:123", { id: 123, name: "John" });
431
+
432
+ if (error) {
433
+ console.error("Failed to cache:", error.message);
434
+ } else {
435
+ console.log("Cached successfully");
436
+ }
437
+ ```
438
+
439
+ **`cache.delete(key: string)`**
440
+
441
+ Removes a key from the cache.
442
+
443
+ ```typescript
444
+ const [error] = await cache.delete("user:123");
445
+
446
+ if (error) {
447
+ console.error("Failed to delete:", error.message);
448
+ }
449
+ ```
450
+
451
+ #### Usage Example
452
+
453
+ ```typescript
454
+ import { Cache } from "ts-kit/cache";
455
+
456
+ type User = {
457
+ id: number;
458
+ name: string;
459
+ email: string;
460
+ };
461
+
462
+ // Create cache instance
463
+ const userCache = new Cache<User>({
464
+ redis: new Bun.RedisClient("redis://localhost:6379"),
465
+ ttl: 300000 // 5 minutes
466
+ });
467
+
468
+ // Get or fetch user
469
+ async function getUser(id: string) {
470
+ // Try cache first
471
+ const [cacheError, cachedUser] = await userCache.get(`user:${id}`);
472
+
473
+ if (!cacheError) {
474
+ return ok(cachedUser);
475
+ }
476
+
477
+ // Cache miss - fetch from database
478
+ const [dbError, user] = await fetchUserFromDB(id);
479
+
480
+ if (dbError) {
481
+ return err("NotFoundError", "User not found");
482
+ }
483
+
484
+ // Store in cache for next time
485
+ await userCache.set(`user:${id}`, user);
486
+
487
+ return ok(user);
488
+ }
489
+ ```
490
+
491
+ **Note on lifecycle management:** The `Cache` class does not manage the Redis client lifecycle. Since you provide the client when creating the cache, you're responsible for closing it when done:
492
+
493
+ ```typescript
494
+ const redis = new Bun.RedisClient("redis://localhost:6379");
495
+ const cache = new Cache({ redis });
496
+
497
+ // Use the cache...
498
+
499
+ // Clean up when done
500
+ await redis.quit();
501
+ ```
502
+
503
+ ### Error Utilities
504
+
505
+ Result-based error handling inspired by functional programming patterns. Avoid throwing exceptions and handle errors explicitly with type-safe tuples.
506
+
507
+ #### Import
508
+
509
+ ```typescript
510
+ import { ok, err, mightThrow, mightThrowSync } from "ts-kit/errors";
511
+ ```
512
+
513
+ #### API
514
+
515
+ **`ok<T>(data: T)`**
516
+
517
+ Creates a successful result tuple.
518
+
519
+ ```typescript
520
+ const result = ok({ userId: 123, name: "John" });
521
+ // [null, { userId: 123, name: "John" }]
522
+
523
+ const [error, data] = result;
524
+
525
+ if (error) {
526
+ // Handle error
527
+ } else {
528
+ console.log(data.userId); // Type-safe access
529
+ }
530
+ ```
531
+
532
+ **`err<T>(type: T, message: string)`**
533
+
534
+ Creates an error result tuple with a typed error object.
535
+
536
+ ```typescript
537
+ const result = err("NotFoundError", "User not found");
538
+ // [{ type: "NotFoundError", message: "User not found" }, null]
539
+
540
+ const [error, data] = result;
541
+
542
+ if (error) {
543
+ console.log(error.type); // "NotFoundError"
544
+ console.log(error.message); // "User not found"
545
+ }
546
+ ```
547
+
548
+ **Common error types:** `NotFoundError`, `UnauthorizedError`, `InternalServerError`, `ValidationError`, or any custom string.
549
+
550
+ **`mightThrow<T>(promise: Promise<T>)`**
551
+
552
+ Wraps async operations that might throw into result tuples.
553
+
554
+ ```typescript
555
+ const [error, data] = await mightThrow(fetch('/api/users'));
556
+
557
+ if (error) {
558
+ console.error("Request failed:", error);
559
+ return;
560
+ }
561
+
562
+ console.log("Success:", data);
563
+ ```
564
+
565
+ **`mightThrowSync<T>(fn: () => T)`**
566
+
567
+ Wraps synchronous operations that might throw into result tuples.
568
+
569
+ ```typescript
570
+ const [error, data] = mightThrowSync(() => JSON.parse(input));
571
+
572
+ if (error) {
573
+ console.error("Parse failed:", error);
574
+ return;
575
+ }
576
+
577
+ console.log("Parsed:", data);
578
+ ```
579
+
580
+ #### Usage Example
581
+
582
+ ```typescript
583
+ import { ok, err, mightThrow } from "ts-kit/errors";
584
+
585
+ async function getUser(id: string) {
586
+ if (!id) {
587
+ return err("ValidationError", "User ID is required");
588
+ }
589
+
590
+ const [fetchError, response] = await mightThrow(
591
+ fetch(`/api/users/${id}`)
592
+ );
593
+
594
+ if (fetchError) {
595
+ return err("InternalServerError", "Failed to fetch user");
596
+ }
597
+
598
+ const [parseError, user] = await mightThrow(response.json());
599
+
600
+ if (parseError) {
601
+ return err("InternalServerError", "Invalid response format");
602
+ }
603
+
604
+ return ok(user);
605
+ }
606
+
607
+ // Usage
608
+ const [error, user] = await getUser("123");
609
+
610
+ if (error) {
611
+ switch (error.type) {
612
+ case "ValidationError":
613
+ console.log("Validation failed:", error.message);
614
+ break;
615
+ case "NotFoundError":
616
+ console.log("User not found");
617
+ break;
618
+ default:
619
+ console.log("Error:", error.message);
620
+ }
621
+ } else {
622
+ console.log("User:", user);
623
+ }
624
+ ```
625
+
626
+ ## Publishing
627
+
628
+ This package uses GitHub Actions to automatically publish to npm. To publish a new version:
629
+
630
+ 1. Update the version in `package.json`:
631
+ ```bash
632
+ bun version <major|minor|patch>
633
+ ```
634
+
635
+ 2. Create a new release on GitHub:
636
+ - Go to the [Releases page](https://github.com/leonardodipace/ts-kit/releases)
637
+ - Click "Create a new release"
638
+ - Create a new tag (e.g., `v0.3.0`)
639
+ - Publish the release
640
+
641
+ The GitHub Action will automatically:
642
+ - Run checks and tests
643
+ - Build the package
644
+ - Publish to npm with provenance
645
+
646
+ Alternatively, you can manually trigger the workflow from the Actions tab and optionally specify a version.
647
+
648
+ **Note:** This package uses npm's Trusted Publishing feature, so no NPM_TOKEN is required. The workflow authenticates using GitHub's OIDC token with the `id-token: write` permission.
649
+
650
+ ## Development
651
+
652
+ ```bash
653
+ # Install dependencies
654
+ bun install
655
+
656
+ # Build package
657
+ bun run build
658
+
659
+ # Build types
660
+ bun run build:types
661
+ ```
662
+
663
+ ## License
664
+
665
+ MIT © Leonardo Dipace
666
+
667
+ ## Repository
668
+
669
+ [https://github.com/leonardodipace/ts-kit](https://github.com/leonardodipace/ts-kit)
@@ -0,0 +1,21 @@
1
+ import type { CacheOptions } from "./types.js";
2
+ export declare class Cache<T> {
3
+ private options;
4
+ constructor(options: CacheOptions);
5
+ get(key: string): Promise<readonly [{
6
+ readonly type: "CacheError";
7
+ readonly message: string;
8
+ }, null] | readonly [{
9
+ readonly type: "NotFoundError";
10
+ readonly message: string;
11
+ }, null] | readonly [null, T]>;
12
+ set(key: string, value: T): Promise<readonly [{
13
+ readonly type: "CacheError";
14
+ readonly message: string;
15
+ }, null] | readonly [null, T]>;
16
+ delete(key: string): Promise<readonly [{
17
+ readonly type: "CacheError";
18
+ readonly message: string;
19
+ }, null] | readonly [null, number | null]>;
20
+ }
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,qBAAa,KAAK,CAAC,CAAC;IAClB,OAAO,CAAC,OAAO,CAAe;gBAElB,OAAO,EAAE,YAAY;IAIpB,GAAG,CAAC,GAAG,EAAE,MAAM;;;;;;;IAoBf,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;;;;IA0BzB,MAAM,CAAC,GAAG,EAAE,MAAM;;;;CAShC"}