luchy 0.2.0 → 1.0.1

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.
@@ -118,7 +118,7 @@ export interface paths {
118
118
  patch?: never;
119
119
  trace?: never;
120
120
  };
121
- "/query": {
121
+ "/ingest/identify": {
122
122
  parameters: {
123
123
  query?: never;
124
124
  header?: never;
@@ -127,6 +127,7 @@ export interface paths {
127
127
  };
128
128
  get?: never;
129
129
  put?: never;
130
+ /** @description Stores person profile traits. Requires the project's secret key (`lsk_…`) as the bearer token; the public ingest key is rejected. */
130
131
  post: {
131
132
  parameters: {
132
133
  query?: never;
@@ -136,17 +137,17 @@ export interface paths {
136
137
  };
137
138
  requestBody?: {
138
139
  content: {
139
- "application/json": components["schemas"]["QueryRequest"];
140
+ "application/json": components["schemas"]["IdentifyInput"];
140
141
  };
141
142
  };
142
143
  responses: {
143
- /** @description Query executed successfully */
144
+ /** @description Profile stored */
144
145
  200: {
145
146
  headers: {
146
147
  [name: string]: unknown;
147
148
  };
148
149
  content: {
149
- "application/json": components["schemas"]["QueryResponse"];
150
+ "application/json": components["schemas"]["IdentifyResponse"];
150
151
  };
151
152
  };
152
153
  /** @description Invalid request body or validation error */
@@ -158,6 +159,131 @@ export interface paths {
158
159
  "application/json": components["schemas"]["ValidationErrorResponse"];
159
160
  };
160
161
  };
162
+ /** @description Missing or invalid secret key */
163
+ 401: {
164
+ headers: {
165
+ [name: string]: unknown;
166
+ };
167
+ content: {
168
+ "application/json": components["schemas"]["ErrorResponse"];
169
+ };
170
+ };
171
+ /** @description Internal server error */
172
+ 500: {
173
+ headers: {
174
+ [name: string]: unknown;
175
+ };
176
+ content: {
177
+ "application/json": components["schemas"]["ServerErrorResponse"];
178
+ };
179
+ };
180
+ };
181
+ };
182
+ delete?: never;
183
+ options?: never;
184
+ head?: never;
185
+ patch?: never;
186
+ trace?: never;
187
+ };
188
+ "/analytics/query": {
189
+ parameters: {
190
+ query?: never;
191
+ header?: never;
192
+ path?: never;
193
+ cookie?: never;
194
+ };
195
+ get?: never;
196
+ put?: never;
197
+ /** @description Metrics for the key's project. */
198
+ post: {
199
+ parameters: {
200
+ query?: never;
201
+ header?: never;
202
+ path?: never;
203
+ cookie?: never;
204
+ };
205
+ requestBody?: {
206
+ content: {
207
+ "application/json": components["schemas"]["AnalyticsQueryRequest"];
208
+ };
209
+ };
210
+ responses: {
211
+ /** @description Query result */
212
+ 200: {
213
+ headers: {
214
+ [name: string]: unknown;
215
+ };
216
+ content: {
217
+ "application/json": components["schemas"]["AnalyticsQueryResponse"];
218
+ };
219
+ };
220
+ /** @description Invalid request */
221
+ 400: {
222
+ headers: {
223
+ [name: string]: unknown;
224
+ };
225
+ content: {
226
+ "application/json": components["schemas"]["ValidationErrorResponse"];
227
+ };
228
+ };
229
+ /** @description Internal server error */
230
+ 500: {
231
+ headers: {
232
+ [name: string]: unknown;
233
+ };
234
+ content: {
235
+ "application/json": components["schemas"]["ServerErrorResponse"];
236
+ };
237
+ };
238
+ };
239
+ };
240
+ delete?: never;
241
+ options?: never;
242
+ head?: never;
243
+ patch?: never;
244
+ trace?: never;
245
+ };
246
+ "/analytics/sql": {
247
+ parameters: {
248
+ query?: never;
249
+ header?: never;
250
+ path?: never;
251
+ cookie?: never;
252
+ };
253
+ get?: never;
254
+ put?: never;
255
+ /** @description Read-only SQL over the whole database — not scoped to the key’s project. Luchy is single-owner, so any valid query key may use it; filter on project_id yourself. */
256
+ post: {
257
+ parameters: {
258
+ query?: never;
259
+ header?: never;
260
+ path?: never;
261
+ cookie?: never;
262
+ };
263
+ requestBody?: {
264
+ content: {
265
+ "application/json": components["schemas"]["SqlRequest"];
266
+ };
267
+ };
268
+ responses: {
269
+ /** @description Rows */
270
+ 200: {
271
+ headers: {
272
+ [name: string]: unknown;
273
+ };
274
+ content: {
275
+ "application/json": components["schemas"]["SqlResponse"];
276
+ };
277
+ };
278
+ /** @description Invalid request */
279
+ 400: {
280
+ headers: {
281
+ [name: string]: unknown;
282
+ };
283
+ content: {
284
+ "application/json": components["schemas"]["ValidationErrorResponse"];
285
+ };
286
+ };
161
287
  /** @description Internal server error */
162
288
  500: {
163
289
  headers: {
@@ -175,6 +301,42 @@ export interface paths {
175
301
  patch?: never;
176
302
  trace?: never;
177
303
  };
304
+ "/analytics/schema": {
305
+ parameters: {
306
+ query?: never;
307
+ header?: never;
308
+ path?: never;
309
+ cookie?: never;
310
+ };
311
+ /** @description Tables, columns and conventions for writing /analytics/sql queries. */
312
+ get: {
313
+ parameters: {
314
+ query?: never;
315
+ header?: never;
316
+ path?: never;
317
+ cookie?: never;
318
+ };
319
+ requestBody?: never;
320
+ responses: {
321
+ /** @description Schema documentation */
322
+ 200: {
323
+ headers: {
324
+ [name: string]: unknown;
325
+ };
326
+ content: {
327
+ "application/json": components["schemas"]["AnalyticsSchemaResponse"];
328
+ };
329
+ };
330
+ };
331
+ };
332
+ put?: never;
333
+ post?: never;
334
+ delete?: never;
335
+ options?: never;
336
+ head?: never;
337
+ patch?: never;
338
+ trace?: never;
339
+ };
178
340
  "/health": {
179
341
  parameters: {
180
342
  query?: never;
@@ -275,6 +437,15 @@ export interface components {
275
437
  * @example https://google.com
276
438
  */
277
439
  referrer?: string;
440
+ /**
441
+ * @description Pageview properties
442
+ * @example {
443
+ * "plan": "pro"
444
+ * }
445
+ */
446
+ payload?: {
447
+ [key: string]: string | number | boolean;
448
+ };
278
449
  /**
279
450
  * @description The user agent string
280
451
  * @example Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
@@ -286,10 +457,20 @@ export interface components {
286
457
  */
287
458
  language?: string;
288
459
  /**
289
- * @description The user country code
460
+ * @description The user country code. Wins over the country Cloudflare infers from the request.
290
461
  * @example US
291
462
  */
292
463
  country?: string;
464
+ /**
465
+ * @description Signed identity token (`base64url(json).base64url(hmac)`). An invalid token is ignored and the event is stored anonymously.
466
+ * @example eyJ1IjoidV8xIn0.3q2-7w
467
+ */
468
+ identity?: string;
469
+ /**
470
+ * @description Client IP, for server-side senders. Only used to derive the daily visitor hash; never stored. Defaults to the connecting IP.
471
+ * @example 203.0.113.7
472
+ */
473
+ ip?: string;
293
474
  };
294
475
  /** @description Event tracking data */
295
476
  EventInput: {
@@ -339,99 +520,206 @@ export interface components {
339
520
  */
340
521
  language?: string;
341
522
  /**
342
- * @description The user country code
523
+ * @description The user country code. Wins over the country Cloudflare infers from the request.
343
524
  * @example US
344
525
  */
345
526
  country?: string;
527
+ /**
528
+ * @description Signed identity token (`base64url(json).base64url(hmac)`). An invalid token is ignored and the event is stored anonymously.
529
+ * @example eyJ1IjoidV8xIn0.3q2-7w
530
+ */
531
+ identity?: string;
532
+ /**
533
+ * @description Client IP, for server-side senders. Only used to derive the daily visitor hash; never stored. Defaults to the connecting IP.
534
+ * @example 203.0.113.7
535
+ */
536
+ ip?: string;
346
537
  };
347
- /**
348
- * Analytics Query Request
349
- * @description Query parameters for analytics data
350
- */
351
- QueryRequest: {
352
- /** @description Time range for the query */
353
- date_range: string | {
354
- /**
355
- * @description Start date (ISO 8601)
356
- * @example 2025-01-01
357
- */
358
- from: string;
359
- /**
360
- * @description End date (ISO 8601)
361
- * @example 2025-01-31
362
- */
363
- to: string;
538
+ /** @description Profile stored */
539
+ IdentifyResponse: {
540
+ /** @example true */
541
+ success: boolean;
542
+ };
543
+ /** @description Error response */
544
+ ErrorResponse: {
545
+ /**
546
+ * @description Error message
547
+ * @example Invalid request body
548
+ */
549
+ error: string;
550
+ /**
551
+ * @description Detailed error information
552
+ * @example Invalid JSON format
553
+ */
554
+ details: string;
555
+ };
556
+ /** @description Person profile traits */
557
+ IdentifyInput: {
558
+ /**
559
+ * @description The stable user id, the same one identity tokens carry as `user`.
560
+ * @example usr_7Hq2mXk9
561
+ */
562
+ user: string;
563
+ /**
564
+ * @description Merged into the stored profile. `null` deletes a trait. The dashboard labels a person by `name`, else `email`, else the id.
565
+ * @example {
566
+ * "name": "María Pérez",
567
+ * "email": "maria@example.com"
568
+ * }
569
+ */
570
+ traits: {
571
+ [key: string]: string | number | boolean | unknown | unknown;
572
+ };
573
+ };
574
+ AnalyticsPeriod: {
575
+ from: string;
576
+ to: string;
577
+ totals: {
578
+ events?: number;
579
+ pageviews?: number;
580
+ sessions?: number;
581
+ visitors?: number;
582
+ users?: number;
583
+ avg_duration?: number;
584
+ bounce_rate?: number;
585
+ };
586
+ rows?: {
587
+ events?: number;
588
+ pageviews?: number;
589
+ sessions?: number;
590
+ visitors?: number;
591
+ users?: number;
592
+ avg_duration?: number;
593
+ bounce_rate?: number;
594
+ key: string | number | unknown;
595
+ }[];
596
+ series?: {
597
+ events?: number;
598
+ pageviews?: number;
599
+ sessions?: number;
600
+ visitors?: number;
601
+ users?: number;
602
+ avg_duration?: number;
603
+ bounce_rate?: number;
604
+ bucket: string;
605
+ }[];
606
+ };
607
+ AnalyticsQueryResponse: {
608
+ current: components["schemas"]["AnalyticsPeriod"];
609
+ previous?: components["schemas"]["AnalyticsPeriod"];
610
+ delta?: {
611
+ events?: {
612
+ abs: number;
613
+ /** @description null when previous is 0 */
614
+ pct: number | null;
615
+ };
616
+ pageviews?: {
617
+ abs: number;
618
+ /** @description null when previous is 0 */
619
+ pct: number | null;
620
+ };
621
+ sessions?: {
622
+ abs: number;
623
+ /** @description null when previous is 0 */
624
+ pct: number | null;
625
+ };
626
+ visitors?: {
627
+ abs: number;
628
+ /** @description null when previous is 0 */
629
+ pct: number | null;
630
+ };
631
+ users?: {
632
+ abs: number;
633
+ /** @description null when previous is 0 */
634
+ pct: number | null;
635
+ };
636
+ avg_duration?: {
637
+ abs: number;
638
+ /** @description null when previous is 0 */
639
+ pct: number | null;
640
+ };
641
+ bounce_rate?: {
642
+ abs: number;
643
+ /** @description null when previous is 0 */
644
+ pct: number | null;
645
+ };
364
646
  };
365
- /** @description Timezone (e.g., America/Santo_Domingo) */
647
+ };
648
+ AnalyticsQueryRequest: {
649
+ /**
650
+ * @description Inclusive start (ISO 8601)
651
+ * @example 2025-01-01T00:00:00Z
652
+ */
653
+ from: string;
654
+ /**
655
+ * @description Exclusive end (ISO 8601)
656
+ * @example 2025-02-01T00:00:00Z
657
+ */
658
+ to: string;
659
+ /**
660
+ * @description IANA timezone for buckets, hour and weekday. Default UTC.
661
+ * @example America/Santo_Domingo
662
+ */
366
663
  tz?: string;
367
664
  /**
368
- * @description Metrics to calculate
665
+ * @description Every metric is computed. visitors = distinct coalesce(user, visitor_hash); avg_duration in seconds; bounce_rate in %.
369
666
  * @example [
370
667
  * "pageviews",
371
668
  * "visitors"
372
669
  * ]
373
670
  */
374
- metrics: ("pageviews" | "visitors" | "sessions" | "visit:bounce_rate" | "visit:avg_duration" | "events")[];
671
+ metrics: ("events" | "pageviews" | "sessions" | "visitors" | "users" | "avg_duration" | "bounce_rate")[];
672
+ /**
673
+ * @description A fixed field or `props.<key>` (a key of the event payload). `hour` and `weekday` (0 = Sunday) are in `tz`.
674
+ * @example tenant
675
+ */
676
+ breakdown?: ("page" | "event" | "type" | "source" | "referrer" | "country" | "device" | "browser" | "os" | "tenant" | "user" | "actor" | "hour" | "weekday") | string;
375
677
  /**
376
- * @description Dimensions to group by
678
+ * @description [field, op, value]; `in` takes a list.
377
679
  * @example [
378
- * "event:page"
680
+ * [
681
+ * "props.role",
682
+ * "eq",
683
+ * "admin"
684
+ * ]
379
685
  * ]
380
686
  */
381
- dimensions?: ("event:page" | "visit:country_name" | "visit:source" | "visit:referrer" | "visit:device" | "visit:browser" | "event:name")[];
382
- /**
383
- * @description Filters to apply
384
- * @example []
385
- */
386
- filters?: (("equals" | "not_equals" | "contains" | "starts_with" | "in" | "not_in" | "between" | "is_not") | string | (string | number | string[] | number[] | string[]))[][] | (("equals" | "not_equals" | "contains" | "starts_with" | "in" | "not_in" | "between" | "is_not") | string | (string | number | string[] | number[] | string[]))[][];
387
- /**
388
- * @description Ordering criteria
389
- * @example []
390
- */
391
- order_by?: (string | ("asc" | "desc"))[][] | (string | ("asc" | "desc"))[][];
392
- /** @description Pagination options */
393
- pagination?: {
394
- /**
395
- * @default 100
396
- * @example 50
397
- */
398
- limit: number;
399
- /**
400
- * @default 0
401
- * @example 0
402
- */
403
- offset: number;
404
- };
405
- /** @description Additional data to include in response */
406
- include?: {
407
- /** @default false */
408
- time_labels: boolean;
409
- /** @default false */
410
- total_rows: boolean;
411
- /**
412
- * @default daily
413
- * @enum {string}
414
- */
415
- granularity: "hourly" | "daily" | "weekly" | "monthly" | "yearly";
687
+ filters?: ((("page" | "event" | "type" | "source" | "referrer" | "country" | "device" | "browser" | "os" | "tenant" | "user" | "actor" | "hour" | "weekday") | string) | ("eq" | "neq" | "in") | (string | number | boolean | unknown | (string | number | boolean | unknown | unknown)[] | unknown))[][];
688
+ /**
689
+ * @description Adds a zero-filled time series
690
+ * @enum {string}
691
+ */
692
+ granularity?: "hour" | "day" | "week" | "month";
693
+ /** @description Also runs the query on another range and returns deltas */
694
+ compare?: "previous_period" | {
695
+ /** @example 2025-01-01T00:00:00Z */
696
+ from: string;
697
+ /** @example 2025-02-01T00:00:00Z */
698
+ to: string;
416
699
  };
700
+ /** @description Max breakdown rows (default 100) */
701
+ limit?: number;
417
702
  };
418
- /**
419
- * Analytics Query Response
420
- * @description Analytics query results
421
- */
422
- QueryResponse: {
423
- /** @description Query results */
424
- results: {
425
- dimensions: string[];
426
- metrics: number[];
427
- time_series?: number[];
703
+ SqlResponse: {
704
+ columns: string[];
705
+ rows: {
706
+ [key: string]: unknown;
428
707
  }[];
429
- /** @description Metadata about the query */
430
- meta: {
431
- time_labels?: string[];
432
- total_rows?: number;
433
- query: components["schemas"]["QueryRequest"];
434
- };
708
+ truncated: boolean;
709
+ };
710
+ SqlRequest: {
711
+ /**
712
+ * @description One SELECT or WITH statement. See GET /analytics/schema.
713
+ * @example SELECT tenant, COUNT(*) AS actions FROM events WHERE project_id = ? GROUP BY tenant
714
+ */
715
+ query: string;
716
+ /** @description Bound to `?` placeholders in order */
717
+ params?: (string | number | boolean | unknown | unknown)[];
718
+ /** @description Max rows (default 1000) */
719
+ limit?: number;
720
+ };
721
+ AnalyticsSchemaResponse: {
722
+ doc: string;
435
723
  };
436
724
  /** @description Health check response */
437
725
  HealthResponse: {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Signed identity tokens.
3
+ *
4
+ * Ingest only trusts who a hit belongs to when the app vouches for it: the
5
+ * server signs `{ user, actor?, tenant? }` with the project's secret key,
6
+ * the token travels with the hit (browser or server), and ingest verifies it.
7
+ * An unsigned or tampered token is dropped and the hit is stored anonymous, so
8
+ * a visitor cannot attribute traffic to someone else by editing the page.
9
+ *
10
+ * WebCrypto only — runs in Workers, Node >= 18, Bun and Deno.
11
+ *
12
+ * @module
13
+ */
14
+ /**
15
+ * Who a hit belongs to. `user` is the account being viewed as, `actor` the
16
+ * person actually acting when they differ (impersonation), `tenant` the app's
17
+ * own workspace/client id.
18
+ */
19
+ export type LuchyIdentity = {
20
+ user: string;
21
+ actor?: string;
22
+ tenant?: string;
23
+ };
24
+ /**
25
+ * Signs an identity for ingest.
26
+ *
27
+ * Format: `base64url(JSON({ u, a?, t? })) + "." + base64url(HMAC-SHA256(secret, firstPart))`.
28
+ * The short keys keep the token small: it is rendered into every page.
29
+ *
30
+ * ```ts
31
+ * const token = await signIdentity({ user: 'u_1', tenant: '5' }, env.LUCHY_SECRET);
32
+ * ```
33
+ */
34
+ export declare function signIdentity(identity: LuchyIdentity, secret: string): Promise<string>;
35
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAgB9E;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,20 @@
1
+ export type { LuchyIdentity } from './identity.js';
2
+ /** A property bag. Nested objects are not supported by the ingest API. */
3
+ export type LuchyProps = Record<string, string | number | boolean>;
1
4
  export type LuchyInitOptions = {
2
5
  endpoint?: string;
3
6
  apiKey: string;
4
7
  trackLocalhost?: boolean;
8
+ /** A signed identity token (see `signIdentity` in `luchy/server`). */
9
+ identity?: string;
10
+ /** Merged into the payload of every event and pageview. */
11
+ props?: LuchyProps;
12
+ /**
13
+ * The script tag the tracker was loaded from. When set, `data-identity` and
14
+ * `data-props` are read from it on every send (so a server re-render after
15
+ * login is picked up), and `identify`/`reset` write to it.
16
+ */
17
+ script?: Element;
5
18
  };
6
19
  export type LuchyTrackerOptions = {
7
20
  autoPageviews?: boolean;
@@ -13,10 +26,19 @@ export type LuchyTracker = {
13
26
  trackPageview(data?: {
14
27
  pathname?: string;
15
28
  referrer?: string;
29
+ payload?: LuchyProps;
16
30
  }): Promise<void>;
17
- trackEvent(name: string, payload?: Record<string, string | number>, options?: {
31
+ trackEvent(name: string, payload?: LuchyProps, options?: {
18
32
  type?: string;
19
33
  }): Promise<void>;
34
+ /**
35
+ * Attributes everything sent from now on to a signed identity token, for
36
+ * SPAs that learn who the user is after load. `props` replace the current
37
+ * ones.
38
+ */
39
+ identify(token: string, props?: LuchyProps): void;
40
+ /** Back to anonymous, e.g. on logout. Drops props too. */
41
+ reset(): void;
20
42
  setOptions(options: LuchyTrackerOptions): void;
21
43
  };
22
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,aAAa,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EACzC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,UAAU,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAChD,CAAC;AAEF;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAyW7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEnE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,aAAa,CAAC,IAAI,CAAC,EAAE;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAClD,0DAA0D;IAC1D,KAAK,IAAI,IAAI,CAAC;IACd,UAAU,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAChD,CAAC;AAgBF;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CA2Z7D"}