@gyeonghokim/bruno-to-openapi 0.0.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.
@@ -0,0 +1,391 @@
1
+ import path from 'node:path';
2
+ import { FileReader } from './file-reader';
3
+ /**
4
+ * Utility functions for parsing Bruno collection structures
5
+ */
6
+ export class BrunoParser {
7
+ /**
8
+ * Parses a Bruno collection from a directory path
9
+ */
10
+ static async parseCollection(collectionPath) {
11
+ // Check if the path exists and is a directory
12
+ const isDir = await FileReader.isDirectory(collectionPath);
13
+ if (!isDir) {
14
+ throw new Error(`Collection path does not exist or is not a directory: ${collectionPath}`);
15
+ }
16
+ // Look for the required collection files
17
+ const brunoJsonPath = path.join(collectionPath, 'bruno.json');
18
+ const collectionBruPath = path.join(collectionPath, 'collection.bru');
19
+ let collection = {
20
+ version: '1',
21
+ uid: BrunoParser.generateUid(), // We'll generate a UID if not provided
22
+ name: path.basename(collectionPath), // Default to directory name
23
+ items: [],
24
+ pathname: collectionPath,
25
+ brunoConfig: undefined,
26
+ };
27
+ // Try to read bruno.json if it exists
28
+ if (await FileReader.fileExists(brunoJsonPath)) {
29
+ try {
30
+ const brunoJsonContent = await FileReader.readFile(brunoJsonPath);
31
+ let brunoJson;
32
+ try {
33
+ brunoJson = JSON.parse(brunoJsonContent);
34
+ }
35
+ catch (parseError) {
36
+ throw new Error(`Invalid JSON in bruno.json: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`);
37
+ }
38
+ // Validate that brunoJson is an object
39
+ if (typeof brunoJson !== 'object' || brunoJson === null) {
40
+ throw new Error('bruno.json must contain a valid JSON object');
41
+ }
42
+ // Type guard to check if it has the required properties
43
+ if (typeof brunoJson.name === 'string' &&
44
+ brunoJson.name) {
45
+ collection = { ...collection, name: brunoJson.name };
46
+ }
47
+ // Validate version
48
+ if (typeof brunoJson.version === 'string' &&
49
+ ['1'].includes(brunoJson.version)) {
50
+ collection = { ...collection, version: '1' };
51
+ }
52
+ // Store bruno config
53
+ if (typeof brunoJson === 'object' && brunoJson !== null) {
54
+ collection = { ...collection, brunoConfig: brunoJson };
55
+ }
56
+ }
57
+ catch (error) {
58
+ if (error instanceof Error && error.message.includes('Invalid JSON')) {
59
+ throw error; // Re-throw specific JSON errors
60
+ }
61
+ throw new Error(`Failed to parse bruno.json: ${error.message}`);
62
+ }
63
+ }
64
+ // Try to read collection.bru if it exists
65
+ if (await FileReader.fileExists(collectionBruPath)) {
66
+ try {
67
+ const collectionBruContent = await FileReader.readFile(collectionBruPath);
68
+ // TODO: Parse collection.bru content properly using bruno-lang if needed
69
+ // For now, we'll just acknowledge its existence
70
+ collection.root = { type: 'collection', name: collection.name };
71
+ }
72
+ catch (error) {
73
+ throw new Error(`Failed to parse collection.bru: ${error.message}`);
74
+ }
75
+ }
76
+ // Parse all .bru files in the collection directory
77
+ const bruFiles = await FileReader.getBruFiles(collectionPath);
78
+ if (bruFiles.length === 0) {
79
+ console.warn(`No .bru files found in collection directory: ${collectionPath}`);
80
+ }
81
+ const collectionItems = await BrunoParser.parseBruFiles(bruFiles, collectionPath);
82
+ collection = { ...collection, items: collectionItems };
83
+ return collection;
84
+ }
85
+ /**
86
+ * Parses individual .bru files into BrunoItem structures
87
+ */
88
+ static async parseBruFiles(bruFilePaths, collectionPath) {
89
+ const items = [];
90
+ for (const bruPath of bruFilePaths) {
91
+ try {
92
+ const content = await FileReader.readFile(bruPath);
93
+ const relativePath = path.relative(collectionPath, bruPath);
94
+ const parsedItem = await BrunoParser.parseBruContent(content, relativePath);
95
+ items.push(parsedItem);
96
+ }
97
+ catch (error) {
98
+ throw new Error(`Failed to parse .bru file ${bruPath}: ${error.message}`);
99
+ }
100
+ }
101
+ return items;
102
+ }
103
+ /**
104
+ * Parses the content of a single .bru file into a BrunoItem
105
+ */
106
+ static async parseBruContent(content, relativePath) {
107
+ // This is a more comprehensive parser for .bru files
108
+ const lines = content.split('\n');
109
+ const item = {
110
+ name: path.basename(relativePath, '.bru'),
111
+ pathname: relativePath,
112
+ type: 'http-request', // Default type
113
+ depth: relativePath.split('/').length - 1 || 0,
114
+ request: {}, // Initialize with empty request
115
+ };
116
+ // Parse the .bru content into sections
117
+ const sections = {};
118
+ let currentSection = null;
119
+ let currentSectionContent = [];
120
+ for (const line of lines) {
121
+ const trimmedLine = line.trim();
122
+ // Check if this is a section header like [sectionName]
123
+ if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
124
+ // Save previous section if exists
125
+ if (currentSection) {
126
+ sections[currentSection] = currentSectionContent;
127
+ }
128
+ // Start new section
129
+ currentSection = trimmedLine.substring(1, trimmedLine.length - 1);
130
+ currentSectionContent = [];
131
+ }
132
+ else {
133
+ // Add line to current section
134
+ currentSectionContent.push(line);
135
+ }
136
+ }
137
+ // Save the last section
138
+ if (currentSection) {
139
+ sections[currentSection] = currentSectionContent;
140
+ }
141
+ // Process each section
142
+ for (const [sectionName, sectionLines] of Object.entries(sections)) {
143
+ switch (sectionName) {
144
+ case 'meta':
145
+ BrunoParser.parseMetaSection(sectionLines, item);
146
+ break;
147
+ case 'request':
148
+ BrunoParser.parseRequestSection(sectionLines, item);
149
+ break;
150
+ case 'headers':
151
+ if (!item.request)
152
+ item.request = {};
153
+ item.request.headers = BrunoParser.parseHeadersSection(sectionLines);
154
+ break;
155
+ case 'params':
156
+ if (!item.request)
157
+ item.request = {};
158
+ item.request.params = BrunoParser.parseParamsSection(sectionLines);
159
+ break;
160
+ case 'body':
161
+ if (!item.request)
162
+ item.request = {};
163
+ item.request.body = BrunoParser.parseBodySection(sectionLines);
164
+ break;
165
+ case 'auth':
166
+ if (!item.request)
167
+ item.request = {};
168
+ item.request.auth = BrunoParser.parseAuthSection(sectionLines);
169
+ break;
170
+ default:
171
+ // Process general properties not in sections
172
+ for (const line of sectionLines) {
173
+ if (line.includes('=')) {
174
+ const parts = line.split('=', 2);
175
+ const key = parts[0];
176
+ const value = parts[1];
177
+ const trimmedKey = key?.trim();
178
+ const trimmedValue = value ? value.trim() : '';
179
+ if (trimmedKey === 'name') {
180
+ item.name = trimmedValue;
181
+ }
182
+ else if (trimmedKey === 'type' && trimmedValue) {
183
+ item.type = trimmedValue;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // If no name was extracted, use the filename
190
+ if (!item.name) {
191
+ item.name = path.basename(relativePath, '.bru');
192
+ }
193
+ return item;
194
+ }
195
+ /**
196
+ * Parses the meta section of a .bru file
197
+ */
198
+ static parseMetaSection(lines, item) {
199
+ for (const line of lines) {
200
+ if (line.includes('=')) {
201
+ const parts = line.split('=', 2);
202
+ const key = parts[0];
203
+ const value = parts[1];
204
+ const trimmedKey = key?.trim();
205
+ const trimmedValue = value ? value.trim() : '';
206
+ if (trimmedKey === 'name') {
207
+ item.name = trimmedValue;
208
+ }
209
+ else if (trimmedKey === 'seq') {
210
+ item.depth = Number.parseInt(trimmedValue, 10) || 0;
211
+ }
212
+ }
213
+ }
214
+ }
215
+ /**
216
+ * Parses the request section of a .bru file
217
+ */
218
+ static parseRequestSection(lines, item) {
219
+ if (!item.request)
220
+ item.request = {};
221
+ for (const line of lines) {
222
+ if (line.includes('=')) {
223
+ const parts = line.split('=', 2);
224
+ const key = parts[0];
225
+ const value = parts[1];
226
+ const trimmedKey = key?.trim();
227
+ const trimmedValue = value ? value.trim() : '';
228
+ if (trimmedKey === 'method') {
229
+ item.request.method = trimmedValue;
230
+ }
231
+ else if (trimmedKey === 'url') {
232
+ item.request.url = trimmedValue;
233
+ }
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Parses the headers section of a .bru file
239
+ */
240
+ static parseHeadersSection(lines) {
241
+ const headers = [];
242
+ for (const line of lines) {
243
+ if (line.includes('=')) {
244
+ const parts = line.split('=', 2);
245
+ const key = parts[0];
246
+ const value = parts[1];
247
+ const trimmedKey = key?.trim();
248
+ const trimmedValue = value ? value.trim() : '';
249
+ if (trimmedKey && trimmedKey !== 'enabled') {
250
+ // Skip the 'enabled' line if it's not part of a header
251
+ headers.push({
252
+ uid: BrunoParser.generateUid(),
253
+ name: trimmedKey,
254
+ value: trimmedValue,
255
+ enabled: true,
256
+ });
257
+ }
258
+ }
259
+ }
260
+ return headers;
261
+ }
262
+ /**
263
+ * Parses the params section of a .bru file
264
+ */
265
+ static parseParamsSection(lines) {
266
+ const params = [];
267
+ for (const line of lines) {
268
+ if (line.includes('=')) {
269
+ const parts = line.split('=', 2);
270
+ const key = parts[0];
271
+ const value = parts[1];
272
+ const trimmedKey = key?.trim();
273
+ const trimmedValue = value ? value.trim() : '';
274
+ if (trimmedKey && trimmedKey !== 'enabled') {
275
+ // Skip the 'enabled' line if it's not part of a param
276
+ params.push({
277
+ uid: BrunoParser.generateUid(),
278
+ name: trimmedKey,
279
+ value: trimmedValue,
280
+ type: 'query', // default type
281
+ enabled: true,
282
+ });
283
+ }
284
+ }
285
+ }
286
+ return params;
287
+ }
288
+ /**
289
+ * Parses the body section of a .bru file
290
+ */
291
+ static parseBodySection(lines) {
292
+ const body = {};
293
+ for (const line of lines) {
294
+ if (line.includes('=')) {
295
+ const parts = line.split('=', 2);
296
+ const key = parts[0];
297
+ const value = parts[1];
298
+ const trimmedKey = key?.trim();
299
+ const trimmedValue = value ? value.trim() : '';
300
+ if (trimmedKey === 'mode') {
301
+ body.mode = trimmedValue;
302
+ }
303
+ else if (trimmedKey === 'json') {
304
+ body.json = trimmedValue;
305
+ }
306
+ else if (trimmedKey === 'xml') {
307
+ body.xml = trimmedValue;
308
+ }
309
+ else if (trimmedKey === 'text') {
310
+ body.text = trimmedValue;
311
+ }
312
+ }
313
+ }
314
+ return body;
315
+ }
316
+ /**
317
+ * Parses the auth section of a .bru file
318
+ */
319
+ static parseAuthSection(lines) {
320
+ const auth = { mode: 'none' };
321
+ for (const line of lines) {
322
+ if (line.includes('=')) {
323
+ const parts = line.split('=', 2);
324
+ const key = parts[0];
325
+ const value = parts[1];
326
+ const trimmedKey = key?.trim();
327
+ const trimmedValue = value ? value.trim() : '';
328
+ if (trimmedKey === 'mode') {
329
+ auth.mode = trimmedValue;
330
+ }
331
+ else if (trimmedKey === 'username') {
332
+ if (auth.mode === 'basic' || auth.mode === 'digest') {
333
+ if (!auth.basic)
334
+ auth.basic = { username: '', password: '' };
335
+ auth.basic.username = trimmedValue;
336
+ }
337
+ else if (auth.mode === 'oauth2') {
338
+ if (!auth.oauth2)
339
+ auth.oauth2 = { grantType: 'password', accessTokenUrl: '' };
340
+ // Note: OAuth2 has more complex structure that needs to be handled properly
341
+ }
342
+ }
343
+ else if (trimmedKey === 'password') {
344
+ if (auth.mode === 'basic' || auth.mode === 'digest') {
345
+ if (!auth.basic)
346
+ auth.basic = { username: '', password: '' };
347
+ auth.basic.password = trimmedValue;
348
+ }
349
+ }
350
+ else if (trimmedKey === 'token') {
351
+ if (auth.mode === 'bearer') {
352
+ if (!auth.bearer)
353
+ auth.bearer = { token: '' };
354
+ auth.bearer.token = trimmedValue;
355
+ }
356
+ }
357
+ }
358
+ }
359
+ return auth;
360
+ }
361
+ /**
362
+ * Validates if a given path contains a valid Bruno collection
363
+ */
364
+ static async isValidCollection(collectionPath) {
365
+ try {
366
+ // A Bruno collection should have either:
367
+ // 1. A bruno.json file, OR
368
+ // 2. At least one .bru file
369
+ const brunoJsonPath = path.join(collectionPath, 'bruno.json');
370
+ const hasBrunoJson = await FileReader.fileExists(brunoJsonPath);
371
+ if (hasBrunoJson) {
372
+ return true;
373
+ }
374
+ // Check for .bru files in the directory
375
+ const bruFiles = await FileReader.getBruFiles(collectionPath);
376
+ return bruFiles.length > 0;
377
+ }
378
+ catch (error) {
379
+ return false;
380
+ }
381
+ }
382
+ /**
383
+ * Generates a unique identifier
384
+ */
385
+ static generateUid() {
386
+ // In a real implementation, you might want to use nanoid or similar
387
+ // For now, we'll create a simple UID based on timestamp and random number
388
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
389
+ }
390
+ }
391
+ //# sourceMappingURL=bruno-parser.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Utility functions for reading files in the Bruno collection
3
+ */
4
+ export declare class FileReader {
5
+ /**
6
+ * Reads a file and returns its content as a string
7
+ */
8
+ static readFile(filePath: string): Promise<string>;
9
+ /**
10
+ * Checks if a file exists
11
+ */
12
+ static fileExists(filePath: string): Promise<boolean>;
13
+ /**
14
+ * Checks if a path is a directory
15
+ */
16
+ static isDirectory(dirPath: string): Promise<boolean>;
17
+ /**
18
+ * Reads all files in a directory recursively
19
+ */
20
+ static readDirRecursively(dirPath: string): Promise<string[]>;
21
+ /**
22
+ * Gets all JSON files in a directory
23
+ */
24
+ static getJsonFiles(dirPath: string): Promise<string[]>;
25
+ /**
26
+ * Gets all .bru files in a directory
27
+ */
28
+ static getBruFiles(dirPath: string): Promise<string[]>;
29
+ /**
30
+ * Safely resolves a path to prevent path traversal
31
+ */
32
+ static safeResolve(basePath: string, relativePath: string): string;
33
+ }
34
+ //# sourceMappingURL=file-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-reader.d.ts","sourceRoot":"","sources":["../../src/utils/file-reader.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,qBAAa,UAAU;IACrB;;OAEG;WACU,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBxD;;OAEG;WACU,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsBnE;;OAEG;WACU,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK7D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK5D;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;CAWnE"}
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /**
4
+ * Utility functions for reading files in the Bruno collection
5
+ */
6
+ export class FileReader {
7
+ /**
8
+ * Reads a file and returns its content as a string
9
+ */
10
+ static async readFile(filePath) {
11
+ try {
12
+ // Validate file path to prevent path traversal attacks
13
+ const resolvedPath = path.resolve(filePath);
14
+ if (!(resolvedPath.startsWith(path.resolve('.')) || resolvedPath.startsWith(process.cwd()))) {
15
+ throw new Error('Invalid file path: path traversal detected');
16
+ }
17
+ return await fs.readFile(filePath, 'utf-8');
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT') {
21
+ throw new Error(`File does not exist: ${filePath}`);
22
+ }
23
+ if (error.code === 'EACCES') {
24
+ throw new Error(`Access denied reading file: ${filePath}`);
25
+ }
26
+ throw new Error(`Failed to read file ${filePath}: ${error.message}`);
27
+ }
28
+ }
29
+ /**
30
+ * Checks if a file exists
31
+ */
32
+ static async fileExists(filePath) {
33
+ try {
34
+ await fs.access(filePath);
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Checks if a path is a directory
43
+ */
44
+ static async isDirectory(dirPath) {
45
+ try {
46
+ const stats = await fs.stat(dirPath);
47
+ return stats.isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /**
54
+ * Reads all files in a directory recursively
55
+ */
56
+ static async readDirRecursively(dirPath) {
57
+ try {
58
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
59
+ const files = [];
60
+ for (const entry of entries) {
61
+ // Prevent symbolic link loops and other path issues
62
+ const fullPath = path.resolve(dirPath, entry.name);
63
+ if (entry.isDirectory()) {
64
+ const nestedFiles = await FileReader.readDirRecursively(fullPath);
65
+ files.push(...nestedFiles);
66
+ }
67
+ else {
68
+ files.push(fullPath);
69
+ }
70
+ }
71
+ return files;
72
+ }
73
+ catch (error) {
74
+ throw new Error(`Failed to read directory ${dirPath}: ${error.message}`);
75
+ }
76
+ }
77
+ /**
78
+ * Gets all JSON files in a directory
79
+ */
80
+ static async getJsonFiles(dirPath) {
81
+ const files = await FileReader.readDirRecursively(dirPath);
82
+ return files.filter(file => path.extname(file) === '.json');
83
+ }
84
+ /**
85
+ * Gets all .bru files in a directory
86
+ */
87
+ static async getBruFiles(dirPath) {
88
+ const files = await FileReader.readDirRecursively(dirPath);
89
+ return files.filter(file => path.extname(file) === '.bru');
90
+ }
91
+ /**
92
+ * Safely resolves a path to prevent path traversal
93
+ */
94
+ static safeResolve(basePath, relativePath) {
95
+ const resolved = path.resolve(basePath, relativePath);
96
+ const normalizedBase = path.resolve(basePath);
97
+ // Ensure the resolved path is within the base path
98
+ if (!resolved.startsWith(normalizedBase)) {
99
+ throw new Error('Path traversal detected');
100
+ }
101
+ return resolved;
102
+ }
103
+ }
104
+ //# sourceMappingURL=file-reader.js.map
@@ -0,0 +1,53 @@
1
+ import type { BrunoCollection } from '../types/bruno';
2
+ import type { ResponsesObject, SchemaObject } from '../types/openapi';
3
+ import type { ConvertResult } from '../types/result';
4
+ /**
5
+ * Utility functions for generating OpenAPI specifications
6
+ */
7
+ export declare class OpenApiGenerator {
8
+ /**
9
+ * Generates an OpenAPI specification from a Bruno collection
10
+ */
11
+ static generateOpenApiSpec(collection: BrunoCollection): ConvertResult;
12
+ /**
13
+ * Processes collection items to build OpenAPI paths
14
+ */
15
+ private static processCollectionItems;
16
+ /**
17
+ * Processes an HTTP request item to add to OpenAPI paths
18
+ */
19
+ private static processHttpRequest;
20
+ /**
21
+ * Creates an OpenAPI operation from a Bruno request
22
+ */
23
+ private static createOperationFromRequest;
24
+ /**
25
+ * Creates OpenAPI parameters from Bruno parameters
26
+ */
27
+ private static createParametersFromBrunoParams;
28
+ /**
29
+ * Creates OpenAPI header parameters from Bruno headers
30
+ */
31
+ private static createParametersFromBrunoHeaders;
32
+ /**
33
+ * Creates security requirements from Bruno auth configuration
34
+ */
35
+ private static createSecurityRequirementsFromAuth;
36
+ /**
37
+ * Creates an OpenAPI request body from Bruno body
38
+ */
39
+ private static createRequestBodyFromBrunoBody;
40
+ /**
41
+ * Infers a schema from JSON content
42
+ */
43
+ private static inferSchemaFromJson;
44
+ /**
45
+ * Infers a schema from a JavaScript value
46
+ */
47
+ static inferSchemaFromValue(value: unknown): SchemaObject;
48
+ /**
49
+ * Creates default responses for an operation
50
+ */
51
+ static createDefaultResponses(): ResponsesObject;
52
+ }
53
+ //# sourceMappingURL=openapi-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-generator.d.ts","sourceRoot":"","sources":["../../src/utils/openapi-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,eAAe,EAKhB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAUV,eAAe,EACf,YAAY,EACb,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,iBAAiB,CAAA;AAEpE;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,eAAe,GAAG,aAAa;IAsDtE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAiBrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAmFjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAgEzC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAsB9C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gCAAgC;IAmB/C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kCAAkC;IAwCjD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,8BAA8B;IA6H7C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAUlC;;OAEG;IACH,MAAM,CAAC,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY;IAiDzD;;OAEG;IACH,MAAM,CAAC,sBAAsB,IAAI,eAAe;CA8EjD"}