@soulbatical/tetra-core 0.1.1 → 0.1.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.
Potentially problematic release.
This version of @soulbatical/tetra-core might be problematic. Click here for more details.
- package/dist/core/createApp.d.ts +70 -0
- package/dist/core/createApp.d.ts.map +1 -0
- package/dist/core/createApp.js +121 -0
- package/dist/core/createApp.js.map +1 -0
- package/dist/frontend/components/FeatureForm.d.ts +125 -0
- package/dist/frontend/components/FeatureForm.d.ts.map +1 -0
- package/dist/frontend/components/FeatureForm.js +388 -0
- package/dist/frontend/components/FeatureForm.js.map +1 -0
- package/dist/frontend/index.d.ts +2 -0
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/index.js +1 -0
- package/dist/frontend/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/authMiddleware.d.ts +6 -6
- package/dist/middleware/authMiddleware.d.ts.map +1 -1
- package/dist/middleware/authMiddleware.js.map +1 -1
- package/dist/middleware/securityMiddleware.d.ts +57 -0
- package/dist/middleware/securityMiddleware.d.ts.map +1 -0
- package/dist/middleware/securityMiddleware.js +224 -0
- package/dist/middleware/securityMiddleware.js.map +1 -0
- package/package.json +8 -3
- package/scripts/enforce-api-standards.sh +179 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Bootstrap — createApp()
|
|
3
|
+
*
|
|
4
|
+
* Standardized Express app creation with security middleware,
|
|
5
|
+
* error handling, and graceful shutdown.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createApp } from '@tetra/core';
|
|
10
|
+
*
|
|
11
|
+
* const { app, start, shutdown } = createApp({
|
|
12
|
+
* projectName: 'my-project',
|
|
13
|
+
* security: {
|
|
14
|
+
* allowedOrigins: ['https://myapp.com'],
|
|
15
|
+
* },
|
|
16
|
+
* initAuth: () => initAuth(),
|
|
17
|
+
* setupRoutes: (app) => {
|
|
18
|
+
* app.use('/api/users', authenticateToken, usersRouter);
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* start();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import express, { Express, Request, Response, NextFunction } from 'express';
|
|
26
|
+
import { SecurityConfig } from '../middleware/securityMiddleware.js';
|
|
27
|
+
export interface CreateAppConfig {
|
|
28
|
+
/** Project name for logging */
|
|
29
|
+
projectName: string;
|
|
30
|
+
/** Port (default: process.env.PORT || 3000) */
|
|
31
|
+
port?: number;
|
|
32
|
+
/** Security middleware config */
|
|
33
|
+
security?: SecurityConfig;
|
|
34
|
+
/** Called before routes are set up (for auth init, etc.) */
|
|
35
|
+
initAuth?: () => void;
|
|
36
|
+
/** Mount your routes on the Express app */
|
|
37
|
+
setupRoutes: (app: Express) => void;
|
|
38
|
+
/** Called after server starts listening (for background services) */
|
|
39
|
+
onStarted?: () => Promise<void>;
|
|
40
|
+
/** Called during shutdown (stop background services) */
|
|
41
|
+
onShutdown?: () => Promise<void>;
|
|
42
|
+
/** Trust proxy setting (default: 1 for Railway/Netlify) */
|
|
43
|
+
trustProxy?: number | string | boolean;
|
|
44
|
+
/** Raw body paths (e.g., ['/api/webhooks/stripe'] for Stripe signature verification) */
|
|
45
|
+
rawBodyPaths?: string[];
|
|
46
|
+
/** Body limit (default: '50mb') */
|
|
47
|
+
bodyLimit?: string;
|
|
48
|
+
/** Skip cookie parser (default: false) */
|
|
49
|
+
skipCookieParser?: boolean;
|
|
50
|
+
/** Custom error handler (default: logs + returns 500) */
|
|
51
|
+
errorHandler?: (err: Error, req: Request, res: Response, next: NextFunction) => void;
|
|
52
|
+
}
|
|
53
|
+
export declare function createApp(config: CreateAppConfig): {
|
|
54
|
+
app: import("express-serve-static-core").Express;
|
|
55
|
+
start: () => Promise<void>;
|
|
56
|
+
shutdown: () => Promise<void>;
|
|
57
|
+
security: {
|
|
58
|
+
enforceHTTPS: (req: Request, res: Response, next: NextFunction) => void;
|
|
59
|
+
securityHeaders: (req: import("http").IncomingMessage, res: import("http").ServerResponse, next: (err?: unknown) => void) => void;
|
|
60
|
+
configureCORS: (req: Request, res: Response, next: NextFunction) => express.Response<any, Record<string, any>>;
|
|
61
|
+
generalRateLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
62
|
+
strictRateLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
63
|
+
publicContentRateLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
64
|
+
createRateLimiter: (windowMs?: number, max?: number) => import("express-rate-limit").RateLimitRequestHandler;
|
|
65
|
+
smartRateLimiter: (limiter: any) => (req: Request, res: Response, next: NextFunction) => any;
|
|
66
|
+
limitRequestSize: (req: Request, res: Response, next: NextFunction) => express.Response<any, Record<string, any>>;
|
|
67
|
+
sanitizeInput: (req: Request, res: Response, next: NextFunction) => void;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=createApp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createApp.d.ts","sourceRoot":"","sources":["../../src/core/createApp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5E,OAAO,EAAqB,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAExF,MAAM,WAAW,eAAe;IAC9B,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,2CAA2C;IAC3C,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACvC,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yDAAyD;IACzD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CACtF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe;;;;;;6GA4G8oB,CAAC;;;;;;6CAA20B,YAAY,EAAC,aAAc,EAAC,kBAAmB;;;;EADzjD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Bootstrap — createApp()
|
|
3
|
+
*
|
|
4
|
+
* Standardized Express app creation with security middleware,
|
|
5
|
+
* error handling, and graceful shutdown.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createApp } from '@tetra/core';
|
|
10
|
+
*
|
|
11
|
+
* const { app, start, shutdown } = createApp({
|
|
12
|
+
* projectName: 'my-project',
|
|
13
|
+
* security: {
|
|
14
|
+
* allowedOrigins: ['https://myapp.com'],
|
|
15
|
+
* },
|
|
16
|
+
* initAuth: () => initAuth(),
|
|
17
|
+
* setupRoutes: (app) => {
|
|
18
|
+
* app.use('/api/users', authenticateToken, usersRouter);
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* start();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import express from 'express';
|
|
26
|
+
import cookieParser from 'cookie-parser';
|
|
27
|
+
import { createLogger } from '../utils/logger.js';
|
|
28
|
+
import { configureSecurity } from '../middleware/securityMiddleware.js';
|
|
29
|
+
export function createApp(config) {
|
|
30
|
+
const logger = createLogger(`system:app:${config.projectName}`);
|
|
31
|
+
const app = express();
|
|
32
|
+
const port = config.port || parseInt(process.env.PORT || '3000', 10);
|
|
33
|
+
// Trust proxy
|
|
34
|
+
app.set('trust proxy', config.trustProxy ?? 1);
|
|
35
|
+
// Security middleware
|
|
36
|
+
const security = configureSecurity({
|
|
37
|
+
bodyLimit: config.bodyLimit || '50mb',
|
|
38
|
+
...config.security,
|
|
39
|
+
});
|
|
40
|
+
app.use(security.enforceHTTPS);
|
|
41
|
+
app.use(security.securityHeaders);
|
|
42
|
+
app.use(security.configureCORS);
|
|
43
|
+
app.use(security.limitRequestSize);
|
|
44
|
+
app.use(security.sanitizeInput);
|
|
45
|
+
// Raw body for webhook paths (must be before JSON parser)
|
|
46
|
+
if (config.rawBodyPaths) {
|
|
47
|
+
for (const path of config.rawBodyPaths) {
|
|
48
|
+
app.use(path, express.raw({ type: 'application/json' }));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// JSON + URL-encoded body parser
|
|
52
|
+
const bodyLimit = config.bodyLimit || '50mb';
|
|
53
|
+
app.use(express.json({ limit: bodyLimit }));
|
|
54
|
+
app.use(express.urlencoded({ extended: true, limit: bodyLimit }));
|
|
55
|
+
// Cookie parser
|
|
56
|
+
if (!config.skipCookieParser) {
|
|
57
|
+
app.use(cookieParser());
|
|
58
|
+
}
|
|
59
|
+
// Auth init
|
|
60
|
+
if (config.initAuth) {
|
|
61
|
+
config.initAuth();
|
|
62
|
+
}
|
|
63
|
+
// Health check
|
|
64
|
+
app.get('/api/health', (_req, res) => {
|
|
65
|
+
res.json({ status: 'ok', project: config.projectName });
|
|
66
|
+
});
|
|
67
|
+
// Routes
|
|
68
|
+
config.setupRoutes(app);
|
|
69
|
+
// Error handler
|
|
70
|
+
const errorHandler = config.errorHandler || ((err, _req, res, _next) => {
|
|
71
|
+
logger.error({ error: err }, 'Global error handler triggered');
|
|
72
|
+
res.status(500).json({
|
|
73
|
+
message: 'Internal server error',
|
|
74
|
+
error: process.env.NODE_ENV !== 'production' ? err.message : 'Something went wrong'
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
app.use(errorHandler);
|
|
78
|
+
// Server instance
|
|
79
|
+
let server = null;
|
|
80
|
+
const start = async () => {
|
|
81
|
+
server = app.listen(port, () => {
|
|
82
|
+
logger.info({ port, project: config.projectName, env: process.env.NODE_ENV }, 'Server started');
|
|
83
|
+
});
|
|
84
|
+
server.on('error', (error) => {
|
|
85
|
+
if (error.code === 'EADDRINUSE') {
|
|
86
|
+
logger.fatal({ port }, 'Port already in use');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
// Start background services after server is listening
|
|
92
|
+
if (config.onStarted) {
|
|
93
|
+
setImmediate(async () => {
|
|
94
|
+
try {
|
|
95
|
+
await config.onStarted();
|
|
96
|
+
logger.info('Background services started');
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
logger.error({ error }, 'Error starting background services');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const shutdown = async () => {
|
|
105
|
+
logger.info('Shutting down');
|
|
106
|
+
if (config.onShutdown) {
|
|
107
|
+
await config.onShutdown();
|
|
108
|
+
}
|
|
109
|
+
if (server) {
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
server.close(() => resolve());
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
logger.info('Shutdown complete');
|
|
115
|
+
};
|
|
116
|
+
// Graceful shutdown handlers
|
|
117
|
+
process.on('SIGTERM', async () => { await shutdown(); process.exit(0); });
|
|
118
|
+
process.on('SIGINT', async () => { await shutdown(); process.exit(0); });
|
|
119
|
+
return { app, start, shutdown, security };
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=createApp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createApp.js","sourceRoot":"","sources":["../../src/core/createApp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeatureForm — Config-driven create/edit form component
|
|
3
|
+
*
|
|
4
|
+
* Replaces ALL per-feature form implementations with a single generic component
|
|
5
|
+
* driven by FeatureConfig.formFields[].
|
|
6
|
+
*
|
|
7
|
+
* Before (200+ lines boilerplate PER feature):
|
|
8
|
+
* <Form onSubmit={handleSubmit}>
|
|
9
|
+
* <Input name="title" label="Title" ... />
|
|
10
|
+
* <Select name="status" options={statusOptions} ... />
|
|
11
|
+
* <Textarea name="description" ... />
|
|
12
|
+
* </Form>
|
|
13
|
+
*
|
|
14
|
+
* After (ZERO boilerplate):
|
|
15
|
+
* <FeatureForm config={ordersConfig} mode="create" onSubmit={handleCreate} />
|
|
16
|
+
*
|
|
17
|
+
* Field type → UI mapping:
|
|
18
|
+
* - text/email/password/url/phone → text input
|
|
19
|
+
* - number/currency/percentage → number input (with prefix/suffix)
|
|
20
|
+
* - textarea → textarea
|
|
21
|
+
* - select/combobox → select dropdown
|
|
22
|
+
* - multiselect → multi-checkbox dropdown
|
|
23
|
+
* - boolean/switch/checkbox → toggle / checkbox
|
|
24
|
+
* - date/datetime → date/datetime input
|
|
25
|
+
* - daterange → two date inputs
|
|
26
|
+
* - color → color input
|
|
27
|
+
* - file/image → file input
|
|
28
|
+
* - hidden → hidden input
|
|
29
|
+
* - richtext → textarea (upgrade to rich editor via customComponents)
|
|
30
|
+
* - custom → custom component from registry
|
|
31
|
+
*
|
|
32
|
+
* @module @tetra/core/frontend
|
|
33
|
+
* Created: February 20, 2026
|
|
34
|
+
*/
|
|
35
|
+
import React from 'react';
|
|
36
|
+
import type { FeatureConfig, FormFieldConfig } from '../../shared/types/feature-config.js';
|
|
37
|
+
/** Registry of custom field components */
|
|
38
|
+
export type CustomComponentMap = Record<string, (props: {
|
|
39
|
+
field: FormFieldConfig;
|
|
40
|
+
value: unknown;
|
|
41
|
+
onChange: (value: unknown) => void;
|
|
42
|
+
error?: string;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
}) => React.ReactNode>;
|
|
45
|
+
export interface FeatureFormProps<TItem = Record<string, unknown>> {
|
|
46
|
+
/** Feature configuration with formFields[] */
|
|
47
|
+
config: FeatureConfig<TItem>;
|
|
48
|
+
/** Form mode: determines which fields to show and submit label */
|
|
49
|
+
mode: 'create' | 'edit';
|
|
50
|
+
/** Initial values for edit mode (existing record data) */
|
|
51
|
+
defaultValues?: Partial<TItem>;
|
|
52
|
+
/** Submit handler — receives validated form data */
|
|
53
|
+
onSubmit: (data: Record<string, unknown>) => void | Promise<void>;
|
|
54
|
+
/** Cancel handler */
|
|
55
|
+
onCancel?: () => void;
|
|
56
|
+
/** External validation errors (e.g., from API response) */
|
|
57
|
+
errors?: Record<string, string>;
|
|
58
|
+
/** Zod schema for validation (optional — uses config.required as fallback) */
|
|
59
|
+
schema?: {
|
|
60
|
+
parse: (data: unknown) => unknown;
|
|
61
|
+
safeParse: (data: unknown) => {
|
|
62
|
+
success: boolean;
|
|
63
|
+
error?: {
|
|
64
|
+
issues: Array<{
|
|
65
|
+
path: Array<string | number>;
|
|
66
|
+
message: string;
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/** Custom field components (keyed by customComponent name) */
|
|
72
|
+
customComponents?: CustomComponentMap;
|
|
73
|
+
/** Submit button label (default: 'Create' / 'Save') */
|
|
74
|
+
submitLabel?: string;
|
|
75
|
+
/** Cancel button label (default: 'Cancel') */
|
|
76
|
+
cancelLabel?: string;
|
|
77
|
+
/** Show cancel button (default: true if onCancel provided) */
|
|
78
|
+
showCancel?: boolean;
|
|
79
|
+
/** Form is submitting (shows loading state) */
|
|
80
|
+
submitting?: boolean;
|
|
81
|
+
/** CSS class for form wrapper */
|
|
82
|
+
className?: string;
|
|
83
|
+
/** Disable all fields (e.g., while submitting) */
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
/** Layout: single column or two columns (default: auto based on field.width) */
|
|
86
|
+
layout?: 'single' | 'grid';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* FeatureForm — Config-driven create/edit form
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* import { ordersConfig } from '@/config/features/orders.config';
|
|
94
|
+
* import { FeatureForm } from '@tetra/core/frontend';
|
|
95
|
+
*
|
|
96
|
+
* function CreateOrderDialog() {
|
|
97
|
+
* return (
|
|
98
|
+
* <FeatureForm
|
|
99
|
+
* config={ordersConfig}
|
|
100
|
+
* mode="create"
|
|
101
|
+
* onSubmit={async (data) => {
|
|
102
|
+
* await createOrder(data);
|
|
103
|
+
* }}
|
|
104
|
+
* onCancel={() => setOpen(false)}
|
|
105
|
+
* />
|
|
106
|
+
* );
|
|
107
|
+
* }
|
|
108
|
+
*
|
|
109
|
+
* function EditOrderDialog({ order }) {
|
|
110
|
+
* return (
|
|
111
|
+
* <FeatureForm
|
|
112
|
+
* config={ordersConfig}
|
|
113
|
+
* mode="edit"
|
|
114
|
+
* defaultValues={order}
|
|
115
|
+
* onSubmit={async (data) => {
|
|
116
|
+
* await updateOrder(order.id, data);
|
|
117
|
+
* }}
|
|
118
|
+
* onCancel={() => setOpen(false)}
|
|
119
|
+
* />
|
|
120
|
+
* );
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export declare function FeatureForm<TItem = Record<string, unknown>>({ config, mode, defaultValues, onSubmit, onCancel, errors: externalErrors, schema, customComponents, submitLabel, cancelLabel, showCancel, submitting, className, disabled: formDisabled, layout, }: FeatureFormProps<TItem>): import("react/jsx-runtime").JSX.Element;
|
|
125
|
+
//# sourceMappingURL=FeatureForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureForm.d.ts","sourceRoot":"","sources":["../../../src/frontend/components/FeatureForm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAmB3F,0CAA0C;AAC1C,MAAM,MAAM,kBAAkB,GAAG,MAAM,CACrC,MAAM,EACN,CAAC,KAAK,EAAE;IACN,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,KAAK,KAAK,CAAC,SAAS,CACtB,CAAC;AAEF,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC/D,8CAA8C;IAC9C,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IAE7B,kEAAkE;IAClE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAExB,0DAA0D;IAC1D,aAAa,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAE/B,oDAAoD;IACpD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE,qBAAqB;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC,8EAA8E;IAC9E,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;QAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK;YAAE,OAAO,EAAE,OAAO,CAAC;YAAC,KAAK,CAAC,EAAE;gBAAE,MAAM,EAAE,KAAK,CAAC;oBAAE,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;oBAAC,OAAO,EAAE,MAAM,CAAA;iBAAE,CAAC,CAAA;aAAE,CAAA;SAAE,CAAA;KAAE,CAAC;IAEjL,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IAEtC,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC5B;AAshBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,WAAW,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3D,MAAM,EACN,IAAI,EACJ,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,MAAM,EAAE,cAAc,EACtB,MAAM,EACN,gBAAgB,EAChB,WAAW,EACX,WAAsB,EACtB,UAAU,EACV,UAAkB,EAClB,SAAS,EACT,QAAQ,EAAE,YAAoB,EAC9B,MAAM,GACP,EAAE,gBAAgB,CAAC,KAAK,CAAC,2CAuNzB"}
|