@nexus_js/server 0.6.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.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/actions.d.ts +158 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +396 -0
- package/dist/actions.js.map +1 -0
- package/dist/context.d.ts +41 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +68 -0
- package/dist/context.js.map +1 -0
- package/dist/csrf.d.ts +56 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +153 -0
- package/dist/csrf.js.map +1 -0
- package/dist/dev-assets.d.ts +31 -0
- package/dist/dev-assets.d.ts.map +1 -0
- package/dist/dev-assets.js +198 -0
- package/dist/dev-assets.js.map +1 -0
- package/dist/error-boundary.d.ts +87 -0
- package/dist/error-boundary.d.ts.map +1 -0
- package/dist/error-boundary.js +181 -0
- package/dist/error-boundary.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +277 -0
- package/dist/index.js.map +1 -0
- package/dist/load-module.d.ts +26 -0
- package/dist/load-module.d.ts.map +1 -0
- package/dist/load-module.js +288 -0
- package/dist/load-module.js.map +1 -0
- package/dist/logger.d.ts +63 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +158 -0
- package/dist/logger.js.map +1 -0
- package/dist/navigate.d.ts +21 -0
- package/dist/navigate.d.ts.map +1 -0
- package/dist/navigate.js +45 -0
- package/dist/navigate.js.map +1 -0
- package/dist/rate-limit.d.ts +71 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +136 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/renderer.d.ts +92 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +386 -0
- package/dist/renderer.js.map +1 -0
- package/dist/streaming.d.ts +98 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +216 -0
- package/dist/streaming.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Per-Action Rate Limiter — Sliding Window Algorithm.
|
|
3
|
+
*
|
|
4
|
+
* Unlike middleware-level rate limiting (which throttles the entire API),
|
|
5
|
+
* Nexus allows granular limits per Server Action. You define the policy
|
|
6
|
+
* alongside the action handler — not in a separate config file.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* export const capture = createAction({
|
|
12
|
+
* rateLimit: { window: '1m', max: 3, keyFn: (req) => req.userId },
|
|
13
|
+
* handler: async (data) => { ... }
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Algorithm: Sliding window log — stores timestamps of each request.
|
|
18
|
+
* Timestamps older than `window` are evicted before checking the limit.
|
|
19
|
+
* This is more accurate than the fixed window approach (no edge-of-window bursts).
|
|
20
|
+
*
|
|
21
|
+
* Memory: O(max * unique_keys). For typical SaaS usage with 10K users and
|
|
22
|
+
* max=10, this is ~100K timestamp entries — well within Node.js limits.
|
|
23
|
+
* For high-scale, replace the Map with Redis ZSET.
|
|
24
|
+
*/
|
|
25
|
+
export interface RateLimitConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Time window for the limit.
|
|
28
|
+
* Formats: '30s', '1m', '5m', '15m', '1h', '6h', '24h'
|
|
29
|
+
*/
|
|
30
|
+
window: string;
|
|
31
|
+
/** Maximum requests allowed within the window. */
|
|
32
|
+
max: number;
|
|
33
|
+
/**
|
|
34
|
+
* Function to derive the rate limit key from the request.
|
|
35
|
+
* Default: IP address extracted from common proxy headers.
|
|
36
|
+
* Override for user-specific limits: (req) => req.headers.get('x-user-id') ?? 'anon'
|
|
37
|
+
*/
|
|
38
|
+
keyFn?: (request: Request) => string;
|
|
39
|
+
}
|
|
40
|
+
export interface RateLimitResult {
|
|
41
|
+
allowed: boolean;
|
|
42
|
+
remaining: number;
|
|
43
|
+
limit: number;
|
|
44
|
+
resetAt: number;
|
|
45
|
+
retryAfter?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface RateLimiter {
|
|
48
|
+
check(request: Request): RateLimitResult;
|
|
49
|
+
/** Reset the limit for a specific key (useful in tests or after user verification). */
|
|
50
|
+
reset(key: string): void;
|
|
51
|
+
/** Returns a headers object to add to the response for RFC 6585 compliance. */
|
|
52
|
+
headers(result: RateLimitResult): Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
export declare function parseWindow(window: string): number;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a reusable sliding-window rate limiter.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const captureLimit = createRateLimiter({ window: '1m', max: 3 });
|
|
60
|
+
* // In your action handler:
|
|
61
|
+
* const result = captureLimit.check(request);
|
|
62
|
+
* if (!result.allowed) throw new RateLimitError(result);
|
|
63
|
+
*/
|
|
64
|
+
export declare function createRateLimiter(config: RateLimitConfig): RateLimiter;
|
|
65
|
+
export declare class RateLimitError extends Error {
|
|
66
|
+
readonly result: RateLimitResult;
|
|
67
|
+
constructor(result: RateLimitResult);
|
|
68
|
+
}
|
|
69
|
+
export declare function registerLimiter(name: string, limiter: RateLimiter): void;
|
|
70
|
+
export declare function getLimiter(name: string): RateLimiter | undefined;
|
|
71
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAI,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAM,MAAM,CAAC;IAClB,OAAO,EAAI,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAAC;IACzC,uFAAuF;IACvF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,+EAA+E;IAC/E,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1D;AAMD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWlD;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAmEtE;AAID,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;gBAErB,MAAM,EAAE,eAAe;CASpC;AAOD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAExE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEhE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Per-Action Rate Limiter — Sliding Window Algorithm.
|
|
3
|
+
*
|
|
4
|
+
* Unlike middleware-level rate limiting (which throttles the entire API),
|
|
5
|
+
* Nexus allows granular limits per Server Action. You define the policy
|
|
6
|
+
* alongside the action handler — not in a separate config file.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* export const capture = createAction({
|
|
12
|
+
* rateLimit: { window: '1m', max: 3, keyFn: (req) => req.userId },
|
|
13
|
+
* handler: async (data) => { ... }
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Algorithm: Sliding window log — stores timestamps of each request.
|
|
18
|
+
* Timestamps older than `window` are evicted before checking the limit.
|
|
19
|
+
* This is more accurate than the fixed window approach (no edge-of-window bursts).
|
|
20
|
+
*
|
|
21
|
+
* Memory: O(max * unique_keys). For typical SaaS usage with 10K users and
|
|
22
|
+
* max=10, this is ~100K timestamp entries — well within Node.js limits.
|
|
23
|
+
* For high-scale, replace the Map with Redis ZSET.
|
|
24
|
+
*/
|
|
25
|
+
// ── Window parser ─────────────────────────────────────────────────────────────
|
|
26
|
+
const WINDOW_RE = /^(\d+)(s|m|h|d)$/;
|
|
27
|
+
export function parseWindow(window) {
|
|
28
|
+
const m = WINDOW_RE.exec(window);
|
|
29
|
+
if (!m)
|
|
30
|
+
throw new Error(`[Nexus Rate Limit] Invalid window format: "${window}". Use '30s', '1m', '5m', '1h', '24h'.`);
|
|
31
|
+
const n = parseInt(m[1] ?? '1', 10);
|
|
32
|
+
switch (m[2]) {
|
|
33
|
+
case 's': return n * 1_000;
|
|
34
|
+
case 'm': return n * 60 * 1_000;
|
|
35
|
+
case 'h': return n * 60 * 60 * 1_000;
|
|
36
|
+
case 'd': return n * 24 * 60 * 60 * 1_000;
|
|
37
|
+
default: return 60_000;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ── IP extraction ─────────────────────────────────────────────────────────────
|
|
41
|
+
function extractIP(request) {
|
|
42
|
+
return (request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
|
|
43
|
+
request.headers.get('x-real-ip') ??
|
|
44
|
+
request.headers.get('cf-connecting-ip') ?? // Cloudflare
|
|
45
|
+
request.headers.get('x-client-ip') ??
|
|
46
|
+
'unknown');
|
|
47
|
+
}
|
|
48
|
+
// ── Limiter factory ───────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Creates a reusable sliding-window rate limiter.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const captureLimit = createRateLimiter({ window: '1m', max: 3 });
|
|
54
|
+
* // In your action handler:
|
|
55
|
+
* const result = captureLimit.check(request);
|
|
56
|
+
* if (!result.allowed) throw new RateLimitError(result);
|
|
57
|
+
*/
|
|
58
|
+
export function createRateLimiter(config) {
|
|
59
|
+
const windowMs = parseWindow(config.window);
|
|
60
|
+
const keyFn = config.keyFn ?? extractIP;
|
|
61
|
+
// key → sorted array of hit timestamps
|
|
62
|
+
const hits = new Map();
|
|
63
|
+
function check(request) {
|
|
64
|
+
const key = keyFn(request);
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const cutoff = now - windowMs;
|
|
67
|
+
// Evict expired timestamps (sliding window)
|
|
68
|
+
const timestamps = (hits.get(key) ?? []).filter((t) => t > cutoff);
|
|
69
|
+
const resetAt = timestamps.length > 0 ? (timestamps[0] + windowMs) : (now + windowMs);
|
|
70
|
+
const remaining = Math.max(0, config.max - timestamps.length);
|
|
71
|
+
const allowed = timestamps.length < config.max;
|
|
72
|
+
if (allowed) {
|
|
73
|
+
timestamps.push(now);
|
|
74
|
+
hits.set(key, timestamps);
|
|
75
|
+
}
|
|
76
|
+
const result = {
|
|
77
|
+
allowed,
|
|
78
|
+
remaining: allowed ? remaining - 1 : 0,
|
|
79
|
+
limit: config.max,
|
|
80
|
+
resetAt,
|
|
81
|
+
};
|
|
82
|
+
if (!allowed) {
|
|
83
|
+
result.retryAfter = Math.ceil((resetAt - now) / 1_000);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
function reset(key) {
|
|
88
|
+
hits.delete(key);
|
|
89
|
+
}
|
|
90
|
+
function headers(result) {
|
|
91
|
+
const h = {
|
|
92
|
+
'x-ratelimit-limit': String(result.limit),
|
|
93
|
+
'x-ratelimit-remaining': String(result.remaining),
|
|
94
|
+
'x-ratelimit-reset': String(Math.ceil(result.resetAt / 1_000)), // Unix epoch seconds
|
|
95
|
+
};
|
|
96
|
+
if (!result.allowed && result.retryAfter !== undefined) {
|
|
97
|
+
h['retry-after'] = String(result.retryAfter);
|
|
98
|
+
}
|
|
99
|
+
return h;
|
|
100
|
+
}
|
|
101
|
+
// Periodic GC: clean up keys with no recent hits to prevent unbounded growth
|
|
102
|
+
setInterval(() => {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
for (const [key, timestamps] of hits) {
|
|
105
|
+
const recent = timestamps.filter((t) => t > now - windowMs);
|
|
106
|
+
if (recent.length === 0) {
|
|
107
|
+
hits.delete(key);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
hits.set(key, recent);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}, windowMs).unref?.(); // .unref() prevents the timer from keeping the process alive
|
|
114
|
+
return { check, reset, headers };
|
|
115
|
+
}
|
|
116
|
+
// ── Error class ───────────────────────────────────────────────────────────────
|
|
117
|
+
export class RateLimitError extends Error {
|
|
118
|
+
result;
|
|
119
|
+
constructor(result) {
|
|
120
|
+
super(`Rate limit exceeded. ` +
|
|
121
|
+
`Retry in ${result.retryAfter ?? '?'}s. ` +
|
|
122
|
+
`Limit: ${result.limit} requests per window.`);
|
|
123
|
+
this.name = 'RateLimitError';
|
|
124
|
+
this.result = result;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ── Global registry ───────────────────────────────────────────────────────────
|
|
128
|
+
// Named limiters can be looked up by the server to attach headers to responses.
|
|
129
|
+
const registry = new Map();
|
|
130
|
+
export function registerLimiter(name, limiter) {
|
|
131
|
+
registry.set(name, limiter);
|
|
132
|
+
}
|
|
133
|
+
export function getLimiter(name) {
|
|
134
|
+
return registry.get(name);
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus SSR Renderer — assembles server-rendered HTML with island markers.
|
|
3
|
+
* Implements PPR (Partial Pre-Rendering): static shell + dynamic holes.
|
|
4
|
+
*
|
|
5
|
+
* Edge-Cache Header Strategy:
|
|
6
|
+
* Nexus knows the TTL of every cache() call made during rendering.
|
|
7
|
+
* The renderer collects these TTLs and emits the most conservative
|
|
8
|
+
* Cache-Control header, ensuring CDNs never serve stale content.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* 1. No cache() calls → Cache-Control: no-store (dynamic, session data)
|
|
12
|
+
* 2. All static (TTL=∞) → Cache-Control: public, max-age=31536000, immutable
|
|
13
|
+
* 3. Mixed TTLs → Cache-Control: s-maxage=<min(ttls)>, stale-while-revalidate=<swr>
|
|
14
|
+
* 4. Has Suspense boundary → Cache-Control: no-store (streaming, can't cache)
|
|
15
|
+
* 5. Has user session → Cache-Control: private, no-store (never CDN-cache)
|
|
16
|
+
*/
|
|
17
|
+
import type { MatchedRoute } from '@nexus_js/router';
|
|
18
|
+
import type { NexusContext } from './context.js';
|
|
19
|
+
export interface RenderOptions {
|
|
20
|
+
dev: boolean;
|
|
21
|
+
/** Required for loading `.nx` routes (dev compile + prod build path). */
|
|
22
|
+
appRoot: string;
|
|
23
|
+
assets: AssetManifest;
|
|
24
|
+
}
|
|
25
|
+
export interface AssetManifest {
|
|
26
|
+
runtime: string;
|
|
27
|
+
styles: string[];
|
|
28
|
+
islands: Map<string, string>;
|
|
29
|
+
}
|
|
30
|
+
export interface RenderResult {
|
|
31
|
+
html: string;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
status: number;
|
|
34
|
+
/** Resolved cache TTL for this page (seconds). 0 = no-store. */
|
|
35
|
+
cacheTtl: number;
|
|
36
|
+
/** Number of island markers found in the rendered HTML */
|
|
37
|
+
islandCount: number;
|
|
38
|
+
}
|
|
39
|
+
/** A single entry in the server-to-browser log bridge */
|
|
40
|
+
export interface ServerBridgeLog {
|
|
41
|
+
type: 'render' | 'cache' | 'fetch' | 'action' | 'island-count';
|
|
42
|
+
path?: string;
|
|
43
|
+
duration?: number;
|
|
44
|
+
cacheStrategy?: string;
|
|
45
|
+
cacheHit?: boolean;
|
|
46
|
+
islandCount?: number;
|
|
47
|
+
url?: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Build-time info injected into the dev bridge for the "performance score" */
|
|
51
|
+
export interface BuildInfo {
|
|
52
|
+
/** Estimated JS bytes for this route */
|
|
53
|
+
totalJs?: number;
|
|
54
|
+
/** Estimated JS bytes if using React instead */
|
|
55
|
+
reactJs?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Called by @nexus_js/runtime cache() to register a TTL for this render. */
|
|
58
|
+
export declare function registerCacheTTL(ttl: number): void;
|
|
59
|
+
/** Called when the renderer detects a user session cookie. */
|
|
60
|
+
export declare function markSessionRequest(): void;
|
|
61
|
+
/** Called when the renderer detects a Suspense/streaming boundary. */
|
|
62
|
+
export declare function markStreamingResponse(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Computes the Cache-Control header value from the collected TTL context.
|
|
65
|
+
* This is the "smart cache" — the renderer's output is correct by construction.
|
|
66
|
+
*/
|
|
67
|
+
export declare function computeCacheControl(ctx: NexusContext): {
|
|
68
|
+
header: string;
|
|
69
|
+
ttl: number;
|
|
70
|
+
strategy: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Renders a matched route to a full HTML response.
|
|
74
|
+
* Wraps the page with its layout chain and injects island hydration scripts.
|
|
75
|
+
*/
|
|
76
|
+
export declare function renderRoute(matched: MatchedRoute, ctx: NexusContext, opts: RenderOptions): Promise<RenderResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Serializes island props for client-side hydration.
|
|
79
|
+
* Uses base64 to safely embed arbitrary JSON in HTML attributes.
|
|
80
|
+
*/
|
|
81
|
+
export declare function serializeIslandProps(props: Record<string, unknown>): string;
|
|
82
|
+
/**
|
|
83
|
+
* Wraps a component's HTML output with island metadata for client hydration.
|
|
84
|
+
*/
|
|
85
|
+
export declare function wrapIsland(html: string, opts: {
|
|
86
|
+
id: string;
|
|
87
|
+
componentPath: string;
|
|
88
|
+
strategy: string;
|
|
89
|
+
props?: Record<string, unknown>;
|
|
90
|
+
mediaQuery?: string;
|
|
91
|
+
}): string;
|
|
92
|
+
//# sourceMappingURL=renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,OAAO,CAAC;IACb,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAC;IAC/D,IAAI,CAAC,EAAW,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAO,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAO,OAAO,CAAC;IACxB,WAAW,CAAC,EAAI,MAAM,CAAC;IACvB,GAAG,CAAC,EAAY,MAAM,CAAC;IACvB,KAAK,CAAC,EAAU,MAAM,CAAC;CACxB;AAED,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAYD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAElD;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAQD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAgDA;AAID;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,YAAY,CAAC,CA2FvB;AAgMD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE3E;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IACJ,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,MAAM,CASR"}
|