renderscreenshot 1.0.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/dist/cjs/cache.js +125 -0
  4. package/dist/cjs/cache.js.map +1 -0
  5. package/dist/cjs/client.js +304 -0
  6. package/dist/cjs/client.js.map +1 -0
  7. package/dist/cjs/errors.js +85 -0
  8. package/dist/cjs/errors.js.map +1 -0
  9. package/dist/cjs/index.js +44 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/options.js +659 -0
  12. package/dist/cjs/options.js.map +1 -0
  13. package/dist/cjs/types.js +3 -0
  14. package/dist/cjs/types.js.map +1 -0
  15. package/dist/cjs/webhooks.js +152 -0
  16. package/dist/cjs/webhooks.js.map +1 -0
  17. package/dist/esm/cache.js +121 -0
  18. package/dist/esm/cache.js.map +1 -0
  19. package/dist/esm/client.js +300 -0
  20. package/dist/esm/client.js.map +1 -0
  21. package/dist/esm/errors.js +81 -0
  22. package/dist/esm/errors.js.map +1 -0
  23. package/dist/esm/index.js +34 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/options.js +655 -0
  26. package/dist/esm/options.js.map +1 -0
  27. package/dist/esm/types.js +2 -0
  28. package/dist/esm/types.js.map +1 -0
  29. package/dist/esm/webhooks.js +147 -0
  30. package/dist/esm/webhooks.js.map +1 -0
  31. package/dist/types/cache.d.ts +96 -0
  32. package/dist/types/cache.d.ts.map +1 -0
  33. package/dist/types/client.d.ts +147 -0
  34. package/dist/types/client.d.ts.map +1 -0
  35. package/dist/types/errors.d.ts +51 -0
  36. package/dist/types/errors.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +35 -0
  38. package/dist/types/index.d.ts.map +1 -0
  39. package/dist/types/options.d.ts +265 -0
  40. package/dist/types/options.d.ts.map +1 -0
  41. package/dist/types/types.d.ts +249 -0
  42. package/dist/types/types.d.ts.map +1 -0
  43. package/dist/types/webhooks.d.ts +60 -0
  44. package/dist/types/webhooks.d.ts.map +1 -0
  45. package/package.json +84 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 RenderScreenshot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # RenderScreenshot Node.js SDK
2
+
3
+ Official Node.js/TypeScript SDK for [RenderScreenshot](https://renderscreenshot.com) - A developer-friendly screenshot API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install renderscreenshot
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { Client, TakeOptions } from 'renderscreenshot';
15
+
16
+ // Initialize the client
17
+ const client = new Client('rs_live_xxxxx');
18
+
19
+ // Take a screenshot
20
+ const image = await client.take(
21
+ TakeOptions.url('https://example.com').preset('og_card')
22
+ );
23
+
24
+ // Save to file
25
+ import { writeFile } from 'fs/promises';
26
+ await writeFile('screenshot.png', image);
27
+ ```
28
+
29
+ ## Features
30
+
31
+ - **Full TypeScript support** - Complete type definitions for all options
32
+ - **Fluent builder pattern** - Chain methods for clean, readable code
33
+ - **All screenshot options** - Viewport, format, blocking, PDF, and more
34
+ - **Batch processing** - Capture multiple screenshots in one request
35
+ - **Cache management** - Retrieve, delete, and purge cached screenshots
36
+ - **Webhook verification** - Secure webhook signature verification
37
+ - **Signed URLs** - Generate secure URLs for public embedding
38
+
39
+ ## Usage
40
+
41
+ ### Basic Screenshot
42
+
43
+ ```typescript
44
+ import { Client, TakeOptions } from 'renderscreenshot';
45
+
46
+ const client = new Client('rs_live_xxxxx');
47
+
48
+ // Take a PNG screenshot
49
+ const image = await client.take(
50
+ TakeOptions.url('https://example.com')
51
+ .width(1200)
52
+ .height(630)
53
+ .format('png')
54
+ );
55
+ ```
56
+
57
+ ### Using Presets
58
+
59
+ ```typescript
60
+ // OG Card preset (1200x630)
61
+ const ogCard = await client.take(
62
+ TakeOptions.url('https://example.com').preset('og_card')
63
+ );
64
+
65
+ // Twitter Card preset
66
+ const twitterCard = await client.take(
67
+ TakeOptions.url('https://example.com').preset('twitter_card')
68
+ );
69
+
70
+ // Full page screenshot
71
+ const fullPage = await client.take(
72
+ TakeOptions.url('https://example.com').preset('full_page')
73
+ );
74
+ ```
75
+
76
+ ### Device Emulation
77
+
78
+ ```typescript
79
+ // iPhone 14 Pro
80
+ const mobile = await client.take(
81
+ TakeOptions.url('https://example.com').device('iphone_14_pro')
82
+ );
83
+
84
+ // iPad Pro
85
+ const tablet = await client.take(
86
+ TakeOptions.url('https://example.com').device('ipad_pro_12')
87
+ );
88
+ ```
89
+
90
+ ### Content Blocking
91
+
92
+ ```typescript
93
+ const image = await client.take(
94
+ TakeOptions.url('https://example.com')
95
+ .blockAds()
96
+ .blockTrackers()
97
+ .blockCookieBanners()
98
+ .blockChatWidgets()
99
+ );
100
+ ```
101
+
102
+ ### Dark Mode & Browser Emulation
103
+
104
+ ```typescript
105
+ const image = await client.take(
106
+ TakeOptions.url('https://example.com')
107
+ .darkMode()
108
+ .timezone('America/New_York')
109
+ .locale('en-US')
110
+ );
111
+ ```
112
+
113
+ ### PDF Generation
114
+
115
+ ```typescript
116
+ const pdf = await client.take(
117
+ TakeOptions.url('https://example.com/report')
118
+ .format('pdf')
119
+ .pdfPaperSize('a4')
120
+ .pdfLandscape()
121
+ .pdfPrintBackground()
122
+ .pdfMargin('1in')
123
+ );
124
+ ```
125
+
126
+ ### JSON Response
127
+
128
+ ```typescript
129
+ const response = await client.takeJson(
130
+ TakeOptions.url('https://example.com').preset('og_card')
131
+ );
132
+
133
+ console.log(response.url); // CDN URL
134
+ console.log(response.width); // 1200
135
+ console.log(response.height); // 630
136
+ console.log(response.cached); // true/false
137
+ ```
138
+
139
+ ### Signed URLs for Embedding
140
+
141
+ Generate secure URLs for public `<img>` tags without exposing your API key:
142
+
143
+ ```typescript
144
+ const signedUrl = client.generateUrl(
145
+ TakeOptions.url('https://example.com').preset('og_card'),
146
+ new Date(Date.now() + 24 * 60 * 60 * 1000) // Expires in 24 hours
147
+ );
148
+
149
+ // Use in HTML: <img src="${signedUrl}" />
150
+ ```
151
+
152
+ ### Batch Processing
153
+
154
+ ```typescript
155
+ // Simple batch with same options
156
+ const results = await client.batch(
157
+ ['https://example1.com', 'https://example2.com', 'https://example3.com'],
158
+ TakeOptions.url('').preset('og_card')
159
+ );
160
+
161
+ // Advanced batch with per-URL options
162
+ const results = await client.batch([
163
+ { url: 'https://example1.com', options: { width: 1200 } },
164
+ { url: 'https://example2.com', options: { preset: 'full_page' } },
165
+ { url: 'https://example3.com', options: { darkMode: true } },
166
+ ]);
167
+
168
+ // Check batch status
169
+ const status = await client.getBatch('batch_abc123');
170
+ ```
171
+
172
+ ### Cache Management
173
+
174
+ ```typescript
175
+ // Get cached screenshot
176
+ const cached = await client.cache.get('cache_xyz789');
177
+
178
+ // Delete single entry
179
+ await client.cache.delete('cache_xyz789');
180
+
181
+ // Bulk purge by keys
182
+ await client.cache.purge(['cache_abc', 'cache_def']);
183
+
184
+ // Purge by URL pattern
185
+ await client.cache.purgeUrl('https://mysite.com/blog/*');
186
+
187
+ // Purge entries older than a date
188
+ await client.cache.purgeBefore(new Date('2024-01-01'));
189
+ ```
190
+
191
+ ### Webhook Verification
192
+
193
+ ```typescript
194
+ import { verifyWebhook, parseWebhook } from 'renderscreenshot';
195
+
196
+ // In your webhook handler
197
+ const signature = req.headers['x-webhook-signature'];
198
+ const timestamp = req.headers['x-webhook-timestamp'];
199
+ const payload = JSON.stringify(req.body);
200
+
201
+ if (verifyWebhook(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) {
202
+ const event = parseWebhook(req.body);
203
+
204
+ if (event.type === 'screenshot.completed') {
205
+ console.log('Screenshot ready:', event.data.response?.url);
206
+ }
207
+ } else {
208
+ res.status(401).send('Invalid signature');
209
+ }
210
+ ```
211
+
212
+ ## All Screenshot Options
213
+
214
+ ### Target
215
+
216
+ | Method | Description |
217
+ |--------|-------------|
218
+ | `url(string)` | URL to capture |
219
+ | `html(string)` | HTML content to render |
220
+
221
+ ### Viewport
222
+
223
+ | Method | Description |
224
+ |--------|-------------|
225
+ | `width(number)` | Viewport width in pixels |
226
+ | `height(number)` | Viewport height in pixels |
227
+ | `scale(number)` | Device scale factor (1-3) |
228
+ | `mobile(boolean)` | Enable mobile emulation |
229
+
230
+ ### Capture
231
+
232
+ | Method | Description |
233
+ |--------|-------------|
234
+ | `fullPage(boolean)` | Capture full scrollable page |
235
+ | `element(string)` | CSS selector for element capture |
236
+ | `format(string)` | `png`, `jpeg`, `webp`, `pdf` |
237
+ | `quality(number)` | JPEG/WebP quality (1-100) |
238
+
239
+ ### Wait Conditions
240
+
241
+ | Method | Description |
242
+ |--------|-------------|
243
+ | `waitFor(string)` | `load`, `networkidle`, `domcontentloaded` |
244
+ | `delay(number)` | Additional delay in milliseconds |
245
+ | `waitForSelector(string)` | Wait for CSS selector to appear |
246
+ | `waitForTimeout(number)` | Maximum wait time in ms |
247
+
248
+ ### Presets & Devices
249
+
250
+ | Method | Description |
251
+ |--------|-------------|
252
+ | `preset(string)` | `og_card`, `twitter_card`, `full_page`, etc. |
253
+ | `device(string)` | `iphone_14_pro`, `pixel_7`, `ipad_pro_12`, etc. |
254
+
255
+ ### Content Blocking
256
+
257
+ | Method | Description |
258
+ |--------|-------------|
259
+ | `blockAds(boolean)` | Block ad network domains |
260
+ | `blockTrackers(boolean)` | Block analytics/tracking |
261
+ | `blockCookieBanners(boolean)` | Auto-dismiss cookie popups |
262
+ | `blockChatWidgets(boolean)` | Block chat widgets |
263
+ | `blockUrls(string[])` | Block URLs matching patterns |
264
+ | `blockResources(string[])` | Block resource types |
265
+
266
+ ### Browser Emulation
267
+
268
+ | Method | Description |
269
+ |--------|-------------|
270
+ | `darkMode(boolean)` | Enable prefers-color-scheme: dark |
271
+ | `reducedMotion(boolean)` | Enable prefers-reduced-motion |
272
+ | `mediaType(string)` | `screen` or `print` |
273
+ | `userAgent(string)` | Custom user agent |
274
+ | `timezone(string)` | IANA timezone |
275
+ | `locale(string)` | BCP 47 locale |
276
+ | `geolocation(lat, lng, accuracy)` | Spoof geolocation |
277
+
278
+ ### Network
279
+
280
+ | Method | Description |
281
+ |--------|-------------|
282
+ | `headers(object)` | Custom HTTP headers |
283
+ | `cookies(array)` | Cookies to set |
284
+ | `authBasic(user, pass)` | HTTP Basic authentication |
285
+ | `authBearer(token)` | Bearer token authentication |
286
+ | `bypassCsp(boolean)` | Bypass Content Security Policy |
287
+
288
+ ### Cache
289
+
290
+ | Method | Description |
291
+ |--------|-------------|
292
+ | `cacheTtl(number)` | Cache TTL in seconds |
293
+ | `cacheRefresh(boolean)` | Force cache refresh |
294
+
295
+ ### PDF Options
296
+
297
+ | Method | Description |
298
+ |--------|-------------|
299
+ | `pdfPaperSize(string)` | `a4`, `letter`, `legal`, etc. |
300
+ | `pdfWidth(string)` | Custom width (CSS units) |
301
+ | `pdfHeight(string)` | Custom height (CSS units) |
302
+ | `pdfLandscape(boolean)` | Landscape orientation |
303
+ | `pdfMargin(string)` | Uniform margin |
304
+ | `pdfScale(number)` | Scale factor (0.1-2.0) |
305
+ | `pdfPrintBackground(boolean)` | Include backgrounds |
306
+ | `pdfPageRanges(string)` | Page ranges |
307
+ | `pdfHeader(string)` | Header HTML template |
308
+ | `pdfFooter(string)` | Footer HTML template |
309
+
310
+ ### Storage (BYOS)
311
+
312
+ | Method | Description |
313
+ |--------|-------------|
314
+ | `storageEnabled(boolean)` | Upload to custom storage |
315
+ | `storagePath(string)` | Path template |
316
+ | `storageAcl(string)` | `public-read` or `private` |
317
+
318
+ ## Error Handling
319
+
320
+ ```typescript
321
+ import { RenderScreenshotError } from 'renderscreenshot';
322
+
323
+ try {
324
+ const image = await client.take(TakeOptions.url('https://example.com'));
325
+ } catch (error) {
326
+ if (error instanceof RenderScreenshotError) {
327
+ console.error('HTTP Status:', error.httpStatus);
328
+ console.error('Error Code:', error.code);
329
+ console.error('Message:', error.message);
330
+ console.error('Retryable:', error.retryable);
331
+
332
+ if (error.retryAfter) {
333
+ console.log(`Retry after ${error.retryAfter} seconds`);
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### Error Codes
340
+
341
+ | Code | HTTP | Retryable | Description |
342
+ |------|------|-----------|-------------|
343
+ | `invalid_request` | 400 | No | Malformed request |
344
+ | `invalid_url` | 400 | No | Invalid URL provided |
345
+ | `unauthorized` | 401 | No | Invalid API key |
346
+ | `forbidden` | 403 | No | Access denied |
347
+ | `not_found` | 404 | No | Resource not found |
348
+ | `rate_limited` | 429 | Yes | Rate limit exceeded |
349
+ | `timeout` | 408 | Yes | Screenshot timed out |
350
+ | `render_failed` | 500 | Yes | Browser rendering failed |
351
+ | `internal_error` | 500 | Yes | Internal server error |
352
+
353
+ ## Requirements
354
+
355
+ - Node.js 18.0.0 or higher
356
+ - Native `fetch` support (built into Node.js 18+)
357
+
358
+ ## License
359
+
360
+ MIT
361
+
362
+ ## Links
363
+
364
+ - [Documentation](https://renderscreenshot.com/docs)
365
+ - [API Reference](https://renderscreenshot.com/docs/endpoints/post-screenshot)
366
+ - [GitHub Issues](https://github.com/Render-Screenshot/rs-node/issues)
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CacheManager = void 0;
4
+ /**
5
+ * Cache management methods for RenderScreenshot
6
+ */
7
+ class CacheManager {
8
+ client;
9
+ constructor(client) {
10
+ this.client = client;
11
+ }
12
+ /**
13
+ * Get a cached screenshot by its cache key
14
+ *
15
+ * @param key - The cache key returned in X-Cache-Key header
16
+ * @returns Buffer containing the screenshot, or null if not found
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const image = await client.cache.get('cache_xyz789');
21
+ * if (image) {
22
+ * await fs.writeFile('cached.png', image);
23
+ * }
24
+ * ```
25
+ */
26
+ async get(key) {
27
+ try {
28
+ return await this.client.request('GET', `/cache/${key}`, {
29
+ responseType: 'buffer',
30
+ });
31
+ }
32
+ catch (error) {
33
+ // Return null for 404 errors
34
+ if (error !== null &&
35
+ typeof error === 'object' &&
36
+ 'httpStatus' in error &&
37
+ error.httpStatus === 404) {
38
+ return null;
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ /**
44
+ * Delete a single cache entry
45
+ *
46
+ * @param key - The cache key to delete
47
+ * @returns true if deleted, false if not found
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const deleted = await client.cache.delete('cache_xyz789');
52
+ * ```
53
+ */
54
+ async delete(key) {
55
+ const response = await this.client.request('DELETE', `/cache/${key}`);
56
+ return response.deleted;
57
+ }
58
+ /**
59
+ * Bulk purge cache entries by keys
60
+ *
61
+ * @param keys - Array of cache keys to purge
62
+ * @returns Purge result with count and keys
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const result = await client.cache.purge(['cache_abc', 'cache_def']);
67
+ * console.log(`Purged ${result.purged} entries`);
68
+ * ```
69
+ */
70
+ async purge(keys) {
71
+ return this.client.request('POST', '/cache/purge', {
72
+ body: { keys },
73
+ });
74
+ }
75
+ /**
76
+ * Purge cache entries matching a URL pattern
77
+ *
78
+ * @param pattern - Glob pattern to match source URLs (e.g., "https://mysite.com/*")
79
+ * @returns Purge result with count
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const result = await client.cache.purgeUrl('https://mysite.com/blog/*');
84
+ * ```
85
+ */
86
+ async purgeUrl(pattern) {
87
+ return this.client.request('POST', '/cache/purge', {
88
+ body: { url: pattern },
89
+ });
90
+ }
91
+ /**
92
+ * Purge cache entries created before a specific date
93
+ *
94
+ * @param date - Purge entries created before this date
95
+ * @returns Purge result with count
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const result = await client.cache.purgeBefore(new Date('2024-01-01'));
100
+ * ```
101
+ */
102
+ async purgeBefore(date) {
103
+ return this.client.request('POST', '/cache/purge', {
104
+ body: { before: date.toISOString() },
105
+ });
106
+ }
107
+ /**
108
+ * Purge cache entries matching a storage path pattern
109
+ *
110
+ * @param pattern - Glob pattern for storage paths (e.g., "screenshots/2024/01/*")
111
+ * @returns Purge result with count
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const result = await client.cache.purgePattern('screenshots/2024/*');
116
+ * ```
117
+ */
118
+ async purgePattern(pattern) {
119
+ return this.client.request('POST', '/cache/purge', {
120
+ body: { pattern },
121
+ });
122
+ }
123
+ }
124
+ exports.CacheManager = CacheManager;
125
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":";;;AAiBA;;GAEG;AACH,MAAa,YAAY;IACN,MAAM,CAAkB;IAEzC,YAAY,MAAuB;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAS,KAAK,EAAE,UAAU,GAAG,EAAE,EAAE;gBAC/D,YAAY,EAAE,QAAQ;aACvB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,6BAA6B;YAC7B,IACE,KAAK,KAAK,IAAI;gBACd,OAAO,KAAK,KAAK,QAAQ;gBACzB,YAAY,IAAI,KAAK;gBACrB,KAAK,CAAC,UAAU,KAAK,GAAG,EACxB,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QAItB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAiB,QAAQ,EAAE,UAAU,GAAG,EAAE,CAAC,CAAC;QACtF,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,KAAK,CAAC,IAAc;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAc,MAAM,EAAE,cAAc,EAAE;YAC9D,IAAI,EAAE,EAAE,IAAI,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,QAAQ,CAAC,OAAe;QAC5B,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAc,MAAM,EAAE,cAAc,EAAE;YAC9D,IAAI,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,WAAW,CAAC,IAAU;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAc,MAAM,EAAE,cAAc,EAAE;YAC9D,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE;SACrC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,YAAY,CAAC,OAAe;QAChC,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAc,MAAM,EAAE,cAAc,EAAE;YAC9D,IAAI,EAAE,EAAE,OAAO,EAAE;SAClB,CAAC,CAAC;IACL,CAAC;CACF;AA/HD,oCA+HC"}