@xformmedia/sdk 0.5.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.
package/README.md ADDED
@@ -0,0 +1,501 @@
1
+ # @xformmedia/sdk
2
+
3
+ Official SDK for the [xform.media](https://xform.media) service.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @xformmedia/sdk
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ The SDK ships two clients:
16
+
17
+ | Client | Key type | Use for |
18
+ |---|---|---|
19
+ | `XformClient` | Source key | Video ingest, upload, and playback operations on a single source |
20
+ | `XformAdminClient` | Organization key | Programmatic source management + all video operations (enterprise / server-to-server) |
21
+
22
+ API keys are created in the xform admin:
23
+ - **Source keys** — Source detail page → API Keys tab
24
+ - **Organization keys** — Org settings → API Keys
25
+
26
+ ---
27
+
28
+ ## `XformClient` — Source-scoped client
29
+
30
+ ```ts
31
+ import { XformClient } from '@xformmedia/sdk'
32
+
33
+ const client = new XformClient({
34
+ sourceId: 'your-source-id',
35
+ organizationId: 'your-org-id',
36
+ apiKey: 'xfm_your_source_key',
37
+ baseUrl: 'https://admin.xform.media',
38
+ })
39
+ ```
40
+
41
+ ### `client.ingest(options)`
42
+
43
+ Trigger transcoding for a video already in your connected S3/R2 bucket. Call this after your own upload completes.
44
+
45
+ ```ts
46
+ const result = await client.ingest({
47
+ key: 'recordings/standup.mp4',
48
+ callbackUrl: 'https://your-app.com/webhooks/xform', // optional
49
+ })
50
+
51
+ console.log(result.videoKey) // "recordings-standup"
52
+ console.log(result.transcodeStatus) // "pending"
53
+ ```
54
+
55
+ ### `client.createUploadUrl(options)`
56
+
57
+ Get a presigned URL to upload a file directly to xform's storage, bypassing your own servers.
58
+
59
+ ```ts
60
+ const { uploadUrl, key } = await client.createUploadUrl({
61
+ filename: 'recording.mp4',
62
+ contentType: 'video/mp4',
63
+ })
64
+
65
+ // Upload directly from browser or server
66
+ await fetch(uploadUrl, {
67
+ method: 'PUT',
68
+ body: fileBlob,
69
+ headers: { 'Content-Type': 'video/mp4' },
70
+ })
71
+
72
+ // Then trigger transcoding
73
+ const result = await client.ingest({ key })
74
+ ```
75
+
76
+ ### `client.events()`
77
+
78
+ Open an SSE connection to receive real-time video status updates. Events are emitted as videos transition through: `pending` → `processing` → `ready` / `failed`.
79
+
80
+ ```ts
81
+ const stream = client.events()
82
+
83
+ stream.on('video.ready', (video) => {
84
+ console.log(`${video.videoKey} is ready (${video.duration}s)`)
85
+ console.log(`Stream: https://${subdomain}.xform.media${video.streamUrl}`)
86
+ console.log(`Download: https://${subdomain}.xform.media${video.downloadUrl}`)
87
+ console.log(`Thumbnail: https://${subdomain}.xform.media${video.thumbnailUrl}`)
88
+ })
89
+
90
+ stream.on('video.failed', (video) => {
91
+ console.error(`${video.videoKey} failed: ${video.errorMessage}`)
92
+ })
93
+
94
+ // Listen to all events
95
+ stream.on('*', (video) => {
96
+ console.log(video.videoKey, video.transcodeStatus)
97
+ })
98
+
99
+ // Close when done
100
+ stream.close()
101
+ ```
102
+
103
+ This is an alternative to polling `client.videos.get()` or providing a `callbackUrl`. The stream uses Server-Sent Events over a persistent HTTP connection — no webhook endpoint needed.
104
+
105
+ **Ingest + wait for ready:**
106
+
107
+ ```ts
108
+ const stream = client.events()
109
+
110
+ stream.on('video.ready', (video) => {
111
+ console.log(`Stream: https://${subdomain}.xform.media${video.streamUrl}`)
112
+ console.log(`Download: https://${subdomain}.xform.media${video.downloadUrl}`)
113
+ stream.close()
114
+ })
115
+
116
+ stream.on('video.failed', (video) => {
117
+ console.error(video.errorMessage)
118
+ stream.close()
119
+ })
120
+
121
+ await client.ingest({ key: 'recordings/standup.mp4' })
122
+ ```
123
+
124
+ ### `client.videos.get(videoKey)`
125
+
126
+ Get the current status and metadata of a video.
127
+
128
+ ```ts
129
+ const video = await client.videos.get('recordings-standup')
130
+
131
+ if (video.transcodeStatus === 'ready') {
132
+ const subdomain = 'acme' // your source subdomain
133
+ console.log(`Stream: https://${subdomain}.xform.media/v/${video.videoKey}.m3u8`)
134
+ console.log(`Download: https://${subdomain}.xform.media/d/${video.videoKey}`)
135
+ }
136
+ ```
137
+
138
+ ### `client.videos.delete(videoId)`
139
+
140
+ Delete a video and all its renditions by database ID. This action is irreversible.
141
+
142
+ ```ts
143
+ const result = await client.ingest({ key: 'recordings/standup.mp4' })
144
+
145
+ // Later...
146
+ await client.videos.delete(result.id)
147
+ ```
148
+
149
+ ### `client.videos.list(options?)`
150
+
151
+ List all videos for this source, newest first.
152
+
153
+ ```ts
154
+ const { data } = await client.videos.list({ limit: 20, skip: 0 })
155
+
156
+ for (const video of data) {
157
+ console.log(video.videoKey, video.transcodeStatus, video.duration)
158
+ }
159
+ ```
160
+
161
+ ### `client.usage()`
162
+
163
+ Get storage and bandwidth usage for this source.
164
+
165
+ ```ts
166
+ const usage = await client.usage()
167
+ console.log(usage.totalVideos)
168
+ console.log(usage.totalDurationSeconds)
169
+ console.log(usage.totalFileSizeBytes)
170
+ ```
171
+
172
+ ### `client.updatePlan(input)`
173
+
174
+ Change the source's plan, billing cycle, or add-ons. All fields are optional — omit any to leave it unchanged.
175
+
176
+ ```ts
177
+ // Upgrade to Pro (prorated immediately)
178
+ await client.updatePlan({ name: 'Pro', billingCycle: 'monthly' })
179
+
180
+ // Switch to annual billing
181
+ await client.updatePlan({ billingCycle: 'annual' })
182
+
183
+ // Enable Video add-on on Starter plan
184
+ await client.updatePlan({ addOns: { video: true } })
185
+
186
+ // Downgrade to Starter with Smart Crop (no credit issued, takes effect next cycle)
187
+ const billing = await client.updatePlan({
188
+ name: 'Starter',
189
+ billingCycle: 'monthly',
190
+ addOns: { smartCrop: true },
191
+ })
192
+ console.log(billing.status) // "active"
193
+ console.log(billing.plan.name) // "Starter"
194
+ console.log(billing.plan.billingCycle) // "monthly"
195
+ ```
196
+
197
+ **Behaviour:**
198
+ - Upgrades are prorated and charged immediately
199
+ - Downgrades take effect at the next billing cycle — no credit is issued
200
+ - Downgrading Pro → Starter automatically removes any attached custom domain
201
+ - Upgrading Starter → Pro automatically removes the Smart Crop add-on (Pro includes it)
202
+
203
+ ---
204
+
205
+ ## `XformAdminClient` — Organization-scoped client
206
+
207
+ For enterprise integrations that need to create and manage sources programmatically. Requires an organization-level API key.
208
+
209
+ ```ts
210
+ import { createXformAdminClient } from '@xformmedia/sdk'
211
+
212
+ const admin = createXformAdminClient({
213
+ organizationId: 'your-org-id',
214
+ apiKey: 'xfm_your_org_key',
215
+ baseUrl: 'https://admin.xform.media',
216
+ })
217
+ ```
218
+
219
+ ### `admin.sources.checkSubdomain(subdomain)`
220
+
221
+ Check if a subdomain is available before creating a source.
222
+
223
+ ```ts
224
+ const { available } = await admin.sources.checkSubdomain('my-app')
225
+ ```
226
+
227
+ ### `admin.sources.create(input)`
228
+
229
+ Create a new source. This provisions a subdomain, Cloudflare DNS record, and Stripe subscription item.
230
+
231
+ ```ts
232
+ const source = await admin.sources.create({
233
+ name: 'My App Media',
234
+ subdomain: 'my-app',
235
+ provider: 'cloudflare-r2',
236
+ credentials: {
237
+ bucket: 'my-bucket',
238
+ endpoint: 'https://accountid.r2.cloudflarestorage.com',
239
+ accessKeyId: 'your-r2-key-id',
240
+ secretAccessKey: 'your-r2-secret',
241
+ },
242
+ plan: {
243
+ name: 'Starter',
244
+ billingCycle: 'monthly',
245
+ },
246
+ })
247
+
248
+ console.log(source.id) // "64a1b2c3..."
249
+ console.log(source.subdomain) // "my-app"
250
+ // Media now served at: https://my-app.xform.media/
251
+ ```
252
+
253
+ Supported providers: `'aws'`, `'cloudflare-r2'`, `'public-web'`
254
+
255
+ ### `admin.sources.list()`
256
+
257
+ ```ts
258
+ const sources = await admin.sources.list()
259
+ ```
260
+
261
+ ### `admin.sources.get(sourceId)`
262
+
263
+ ```ts
264
+ const source = await admin.sources.get('source-id')
265
+ console.log(source.subdomain, source.status)
266
+ ```
267
+
268
+ ### `admin.sources.update(sourceId, input)`
269
+
270
+ Update a source's name or credentials.
271
+
272
+ ```ts
273
+ await admin.sources.update('source-id', {
274
+ name: 'New Name',
275
+ credentials: {
276
+ accessKeyId: 'rotated-key-id',
277
+ secretAccessKey: 'rotated-secret',
278
+ },
279
+ })
280
+ ```
281
+
282
+ ### `admin.source(sourceId)` — per-source operations
283
+
284
+ All video operations, event streaming, and plan management are available via `admin.source(id)`:
285
+
286
+ ```ts
287
+ const src = admin.source(sourceId)
288
+
289
+ // Real-time events
290
+ const stream = src.events()
291
+ stream.on('video.ready', (video) => console.log(video.videoKey, 'ready'))
292
+
293
+ // Plan management
294
+ await src.updatePlan({ name: 'Pro' })
295
+ await src.updatePlan({ addOns: { video: true } })
296
+
297
+ // Video ingest
298
+ const { uploadUrl, key } = await src.createUploadUrl({ filename: 'clip.mp4', contentType: 'video/mp4' })
299
+ await fetch(uploadUrl, { method: 'PUT', body: blob, headers: { 'Content-Type': 'video/mp4' } })
300
+ const job = await src.ingest({ key })
301
+
302
+ // Video status
303
+ const video = await src.videos.get(job.videoKey)
304
+ const { data } = await src.videos.list({ limit: 50 })
305
+
306
+ // Delete a video by ID
307
+ await src.videos.delete(job.id)
308
+
309
+ // Usage
310
+ const usage = await src.usage()
311
+ ```
312
+
313
+ ### `admin.source(sourceId).webhook` — Webhook configuration
314
+
315
+ Configure a webhook to receive HTTP POST notifications when video events occur. All payloads are signed with HMAC-SHA256 for verification.
316
+
317
+ ```ts
318
+ const src = admin.source(sourceId)
319
+
320
+ // Set a webhook (subscribes to all events by default)
321
+ await src.webhook.set({
322
+ url: 'https://your-app.com/webhooks/xform',
323
+ secret: 'whsec_your_signing_secret',
324
+ })
325
+
326
+ // Or subscribe to specific events only
327
+ await src.webhook.set({
328
+ url: 'https://your-app.com/webhooks/xform',
329
+ secret: 'whsec_your_signing_secret',
330
+ events: ['video.ready', 'video.failed'],
331
+ })
332
+
333
+ // Get current config (secret is masked)
334
+ const config = await src.webhook.get()
335
+ // { url: "https://...", secret: "whse••••••••", events: [...] }
336
+
337
+ // Remove webhook
338
+ await src.webhook.delete()
339
+ ```
340
+
341
+ **Webhook events:**
342
+
343
+ | Event | Trigger | Payload `data` fields |
344
+ |---|---|---|
345
+ | `video.ready` | Transcode completed | `streamUrl`, `downloadUrl`, `duration`, `thumbnailUrl`, `width`, `height` |
346
+ | `video.failed` | Transcode failed (after retries) | `errorMessage` |
347
+ | `video.renamed` | Video display name changed | `displayName` |
348
+
349
+ **Payload format:**
350
+
351
+ ```json
352
+ {
353
+ "event": "video.ready",
354
+ "sourceId": "64a1b2c3...",
355
+ "videoKey": "recordings-standup",
356
+ "timestamp": "2026-03-06T12:00:00.000Z",
357
+ "data": {
358
+ "streamUrl": "/v/recordings-standup.m3u8",
359
+ "downloadUrl": "/d/recordings-standup",
360
+ "duration": 42.5,
361
+ "thumbnailUrl": "/v/recordings-standup/thumbnail.jpg",
362
+ "width": 1920,
363
+ "height": 1080
364
+ }
365
+ }
366
+ ```
367
+
368
+ **Verifying signatures:**
369
+
370
+ ```ts
371
+ import { createHmac } from 'crypto'
372
+
373
+ function verifyWebhook(body: string, signature: string, secret: string): boolean {
374
+ const expected = createHmac('sha256', secret).update(body).digest('hex')
375
+ return signature === expected
376
+ }
377
+
378
+ // In your webhook handler:
379
+ app.post('/webhooks/xform', (req, res) => {
380
+ const signature = req.headers['x-webhook-signature']
381
+ const event = req.headers['x-webhook-event']
382
+
383
+ if (!verifyWebhook(JSON.stringify(req.body), signature, 'whsec_your_secret')) {
384
+ return res.status(401).send('Invalid signature')
385
+ }
386
+
387
+ // Process event...
388
+ res.sendStatus(200)
389
+ })
390
+ ```
391
+
392
+ Webhooks are retried up to 3 times with exponential backoff if your endpoint returns a non-2xx status.
393
+
394
+ ### Full end-to-end example
395
+
396
+ ```ts
397
+ // 1. Create source
398
+ const { available } = await admin.sources.checkSubdomain('pixelfilm-user-123')
399
+ if (!available) throw new Error('Subdomain taken')
400
+
401
+ const source = await admin.sources.create({
402
+ name: 'User 123',
403
+ subdomain: 'pixelfilm-user-123',
404
+ provider: 'cloudflare-r2',
405
+ credentials: { bucket, endpoint, accessKeyId, secretAccessKey },
406
+ plan: { name: 'Starter', billingCycle: 'monthly' },
407
+ })
408
+
409
+ // 2. Upload and ingest with real-time status
410
+ const src = admin.source(source.id)
411
+ const stream = src.events()
412
+
413
+ stream.on('video.ready', (video) => {
414
+ console.log(`https://${source.subdomain}.xform.media/v/${video.videoKey}.m3u8`)
415
+ stream.close()
416
+ })
417
+
418
+ stream.on('video.failed', (video) => {
419
+ console.error(video.errorMessage)
420
+ stream.close()
421
+ })
422
+
423
+ const { uploadUrl, key } = await src.createUploadUrl({
424
+ filename: 'recording.mp4',
425
+ contentType: 'video/mp4',
426
+ })
427
+ await fetch(uploadUrl, { method: 'PUT', body: blob, headers: { 'Content-Type': 'video/mp4' } })
428
+ await src.ingest({ key })
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Error Handling
434
+
435
+ All methods throw `XformError` on API failures.
436
+
437
+ ```ts
438
+ import { XformError } from '@xformmedia/sdk'
439
+
440
+ try {
441
+ await admin.sources.create({ ... })
442
+ } catch (err) {
443
+ if (err instanceof XformError) {
444
+ console.error(err.statusCode, err.message)
445
+ console.error(err.body) // full response body
446
+ }
447
+ }
448
+ ```
449
+
450
+ | Status | Meaning |
451
+ |---|---|
452
+ | `400` | Invalid input — missing fields, bad subdomain format, etc. |
453
+ | `401` | Invalid or expired API key |
454
+ | `403` | Key scope mismatch — wrong source, or source key used on org endpoint |
455
+ | `404` | Source or video not found |
456
+ | `409` | Subdomain already taken |
457
+ | `500` | Server error — retry with backoff |
458
+
459
+ ---
460
+
461
+ ## TypeScript
462
+
463
+ All types are exported:
464
+
465
+ ```ts
466
+ import type {
467
+ // Clients
468
+ XformClientOptions,
469
+ XformAdminClientOptions,
470
+ // Sources
471
+ Source,
472
+ SourceBilling,
473
+ SourcePlan,
474
+ SourceCredentials,
475
+ SourceProvider,
476
+ SourceStatus,
477
+ CreateSourceInput,
478
+ UpdateSourceInput,
479
+ UpdatePlanInput,
480
+ BillingCycle,
481
+ BillingStatus,
482
+ PlanName,
483
+ CheckSubdomainResult,
484
+ // Webhooks
485
+ WebhookEventType,
486
+ WebhookConfig,
487
+ SetWebhookInput,
488
+ // Videos
489
+ Video,
490
+ VideoEvent,
491
+ VideoEventType,
492
+ VideoEventStream,
493
+ VideoRendition,
494
+ VideoTranscodeStatus,
495
+ VideoRenditionQuality,
496
+ IngestResult,
497
+ UploadUrlResult,
498
+ ListVideosResult,
499
+ VideoUsageResult,
500
+ } from '@xformmedia/sdk'
501
+ ```
@@ -0,0 +1,115 @@
1
+ import type { XformAdminClientOptions, Source, SourceBilling, VideoEventStream, CreateSourceInput, UpdateSourceInput, UpdatePlanInput, CheckSubdomainResult, Video, IngestResult, UploadUrlResult, ListVideosResult, VideoUsageResult, WebhookConfig, SetWebhookInput } from './types.js';
2
+ export declare class XformAdminClient {
3
+ private readonly organizationId;
4
+ private readonly apiKey;
5
+ private readonly baseUrl;
6
+ constructor(options: XformAdminClientOptions);
7
+ private get headers();
8
+ private request;
9
+ /** Source management operations (requires org-scoped API key) */
10
+ readonly sources: {
11
+ /**
12
+ * List all sources for this organization.
13
+ */
14
+ list: () => Promise<Source[]>;
15
+ /**
16
+ * Get a specific source by ID.
17
+ */
18
+ get: (sourceId: string) => Promise<Source>;
19
+ /**
20
+ * Create a new source. This provisions a subdomain, Cloudflare DNS record,
21
+ * and Stripe subscription item.
22
+ */
23
+ create: (input: CreateSourceInput) => Promise<Source>;
24
+ /**
25
+ * Update an existing source's settings.
26
+ */
27
+ update: (sourceId: string, input: UpdateSourceInput) => Promise<Source>;
28
+ /**
29
+ * Check if a subdomain is available before creating a source.
30
+ */
31
+ checkSubdomain: (subdomain: string) => Promise<CheckSubdomainResult>;
32
+ };
33
+ /**
34
+ * Returns a set of video operations scoped to a specific source.
35
+ * Use this after creating or looking up a source.
36
+ */
37
+ source(sourceId: string): {
38
+ /**
39
+ * Open an SSE connection to receive real-time video status updates for this source.
40
+ */
41
+ events(): VideoEventStream;
42
+ /**
43
+ * Ingest a video from the source's connected S3/R2 bucket.
44
+ * Call this after uploading the file to the bucket.
45
+ */
46
+ ingest(options: {
47
+ key: string;
48
+ callbackUrl?: string;
49
+ }): Promise<IngestResult>;
50
+ /**
51
+ * Create a presigned URL for uploading a file directly to xform's R2 storage.
52
+ * After uploading, call `ingest()` with the returned `key`.
53
+ */
54
+ createUploadUrl(options: {
55
+ filename: string;
56
+ contentType: string;
57
+ }): Promise<UploadUrlResult>;
58
+ /**
59
+ * Get storage and bandwidth usage for this source.
60
+ */
61
+ usage(): Promise<VideoUsageResult>;
62
+ /**
63
+ * Change the source's plan, billing cycle, or add-ons.
64
+ * Omit any field to leave it unchanged.
65
+ *
66
+ * Upgrades are prorated immediately.
67
+ * Downgrades take effect at the next billing cycle (no credit issued).
68
+ * Downgrading from Pro to Starter automatically removes any custom domain.
69
+ */
70
+ updatePlan(input: UpdatePlanInput): Promise<SourceBilling>;
71
+ /** Video operations for this source */
72
+ videos: {
73
+ /**
74
+ * List all videos for this source.
75
+ */
76
+ list(options?: {
77
+ limit?: number;
78
+ skip?: number;
79
+ }): Promise<ListVideosResult>;
80
+ /**
81
+ * Get the status and metadata of a specific video.
82
+ */
83
+ get(videoKey: string): Promise<Video>;
84
+ };
85
+ /** Webhook configuration for this source */
86
+ webhook: {
87
+ /**
88
+ * Get the current webhook configuration (secret is masked).
89
+ * Returns null if no webhook is configured.
90
+ */
91
+ get(): Promise<WebhookConfig | null>;
92
+ /**
93
+ * Set or update the webhook configuration.
94
+ * If `events` is omitted, all event types are subscribed.
95
+ */
96
+ set(input: SetWebhookInput): Promise<WebhookConfig>;
97
+ /**
98
+ * Remove the webhook configuration. No more webhooks will be sent.
99
+ */
100
+ delete(): Promise<void>;
101
+ };
102
+ };
103
+ }
104
+ /**
105
+ * Create an organization-scoped xform client.
106
+ * Use this for programmatic source management (enterprise / server-to-server).
107
+ * Requires an organization-level API key.
108
+ *
109
+ * @example
110
+ * const admin = createXformAdminClient({ organizationId, apiKey, baseUrl })
111
+ * const source = await admin.sources.create({ name: 'My Source', ... })
112
+ * const { uploadUrl, key } = await admin.source(source.id).createUploadUrl({ ... })
113
+ */
114
+ export declare function createXformAdminClient(options: XformAdminClientOptions): XformAdminClient;
115
+ //# sourceMappingURL=admin-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-client.d.ts","sourceRoot":"","sources":["../src/admin-client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,uBAAuB,EACvB,MAAM,EACN,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,KAAK,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,eAAe,EAChB,MAAM,YAAY,CAAA;AAEnB,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;gBAEpB,OAAO,EAAE,uBAAuB;IAM5C,OAAO,KAAK,OAAO,GAMlB;YAEa,OAAO;IAuBrB,iEAAiE;IACjE,QAAQ,CAAC,OAAO;QACd;;WAEG;oBACO,OAAO,CAAC,MAAM,EAAE,CAAC;QAI3B;;WAEG;wBACa,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;QAIxC;;;WAGG;wBACa,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAAC;QAInD;;WAEG;2BACgB,MAAM,SAAS,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAAC;QAIrE;;WAEG;oCACyB,MAAM,KAAG,OAAO,CAAC,oBAAoB,CAAC;MAInE;IAED;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM;QAOnB;;WAEG;kBACO,gBAAgB;QAQ1B;;;WAGG;wBACmB;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,YAAY,CAAC;QASnF;;;WAGG;iCAC4B;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,eAAe,CAAC;QASnG;;WAEG;iBACM,OAAO,CAAC,gBAAgB,CAAC;QAIlC;;;;;;;WAOG;0BACe,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC;QAQ1D,uCAAuC;;YAErC;;eAEG;2BACW;gBAAE,KAAK,CAAC,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,GAAQ,OAAO,CAAC,gBAAgB,CAAC;YAQhF;;eAEG;0BACW,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;;QAQvC,4CAA4C;;YAE1C;;;eAGG;mBACI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;YAOpC;;;eAGG;uBACQ,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC;YAQnD;;eAEG;sBACO,OAAO,CAAC,IAAI,CAAC;;;CAM9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAEzF"}