@harborclient/team-hub-api 0.1.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,665 @@
1
+ import { TeamHubClientError } from './TeamHubClientError.js';
2
+ import { collectionRecordSchema, environmentRecordSchema, errorResponseSchema, folderRecordSchema, healthResponseSchema, listCollectionsResponseSchema, listEnvironmentsResponseSchema, listFoldersResponseSchema, listHubLlmModelsResponseSchema, pluginSourcesResponseSchema, listRequestsResponseSchema, hubChatStepResponseSchema, savedRequestRecordSchema, sessionResponseSchema, listAdminUsersResponseSchema, listAdminCollectionsResponseSchema, listAdminEnvironmentsResponseSchema, adminEntityConfigSchema, createAdminUserResponseSchema, createdApiTokenResponseSchema, listAdminTokensResponseSchema, hubUserRecordSchema, reloadConfigResponseSchema } from './schemas.js';
3
+ /**
4
+ * Default request timeout when {@link TeamHubClientConfig.requestTimeoutMs} is omitted.
5
+ */
6
+ export const DEFAULT_TEAM_HUB_REQUEST_TIMEOUT_MS = 30_000;
7
+ /**
8
+ * Executes typed HTTP requests against HarborClient Server.
9
+ */
10
+ export class TeamHubClient {
11
+ baseUrl;
12
+ token;
13
+ requestTimeoutMs;
14
+ /**
15
+ * Creates a client bound to a HarborClient Server instance and bearer token.
16
+ *
17
+ * @param config - Base URL, token, and optional request timeout.
18
+ */
19
+ constructor(config) {
20
+ this.baseUrl = config.baseUrl.replace(/\/+$/, '');
21
+ this.token = config.token;
22
+ this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_TEAM_HUB_REQUEST_TIMEOUT_MS;
23
+ }
24
+ /**
25
+ * Joins the configured base URL with a relative API path.
26
+ *
27
+ * @param path - Path beginning with `/`.
28
+ */
29
+ buildUrl(path) {
30
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
31
+ return `${this.baseUrl}${normalizedPath}`;
32
+ }
33
+ /**
34
+ * Parses a failed response body into a human-readable error message.
35
+ *
36
+ * @param response - Non-success fetch response.
37
+ * @param method - HTTP method used for the request.
38
+ * @param path - Request path relative to the base URL.
39
+ */
40
+ async parseErrorMessage(response) {
41
+ const contentType = response.headers.get('content-type') ?? '';
42
+ if (contentType.includes('application/json')) {
43
+ try {
44
+ const json = await response.json();
45
+ const parsed = errorResponseSchema.safeParse(json);
46
+ if (parsed.success) {
47
+ return parsed.data.error;
48
+ }
49
+ }
50
+ catch {
51
+ // Fall through to status-based message.
52
+ }
53
+ }
54
+ return `Request failed with status ${response.status}`;
55
+ }
56
+ /**
57
+ * Sends an HTTP request to HarborClient Server and validates the response.
58
+ *
59
+ * @param method - HTTP method.
60
+ * @param path - Path relative to the configured base URL.
61
+ * @param options - Optional body, response schema, and auth flag.
62
+ * @returns Parsed response body, or `undefined` for `204 No Content`.
63
+ * @throws {TeamHubClientError} When the request fails or the response is invalid.
64
+ */
65
+ async request(method, path, options = {}) {
66
+ const { body, schema, auth = true } = options;
67
+ const headers = {
68
+ Accept: 'application/json'
69
+ };
70
+ if (auth) {
71
+ headers.Authorization = `Bearer ${this.token}`;
72
+ }
73
+ let requestBody;
74
+ if (body !== undefined) {
75
+ headers['Content-Type'] = 'application/json';
76
+ requestBody = JSON.stringify(body);
77
+ }
78
+ let response;
79
+ try {
80
+ response = await fetch(this.buildUrl(path), {
81
+ method,
82
+ headers,
83
+ body: requestBody,
84
+ signal: AbortSignal.timeout(this.requestTimeoutMs)
85
+ });
86
+ }
87
+ catch (err) {
88
+ const message = err instanceof Error && err.name === 'TimeoutError'
89
+ ? `Request timed out after ${this.requestTimeoutMs} ms`
90
+ : err instanceof Error
91
+ ? err.message
92
+ : 'Unknown network error';
93
+ throw new TeamHubClientError(message, { status: 0, method, path });
94
+ }
95
+ if (response.status === 204) {
96
+ return undefined;
97
+ }
98
+ if (!response.ok) {
99
+ const message = await this.parseErrorMessage(response);
100
+ throw new TeamHubClientError(message, {
101
+ status: response.status,
102
+ method,
103
+ path
104
+ });
105
+ }
106
+ if (!schema) {
107
+ return undefined;
108
+ }
109
+ let json;
110
+ try {
111
+ json = await response.json();
112
+ }
113
+ catch {
114
+ throw new TeamHubClientError('Response body is not valid JSON', {
115
+ status: response.status,
116
+ method,
117
+ path
118
+ });
119
+ }
120
+ const parsed = schema.safeParse(json);
121
+ if (!parsed.success) {
122
+ throw new TeamHubClientError('Response body failed validation', {
123
+ status: response.status,
124
+ method,
125
+ path
126
+ });
127
+ }
128
+ return parsed.data;
129
+ }
130
+ /**
131
+ * Probes server availability via the public health endpoint.
132
+ */
133
+ async checkHealth() {
134
+ const result = await this.request('GET', '/health', {
135
+ auth: false,
136
+ schema: healthResponseSchema
137
+ });
138
+ return result;
139
+ }
140
+ /**
141
+ * Returns the authenticated user, token metadata, and derived API capabilities.
142
+ */
143
+ async getSession() {
144
+ const result = await this.request('GET', '/auth/session', {
145
+ schema: sessionResponseSchema
146
+ });
147
+ return result;
148
+ }
149
+ /**
150
+ * Lists all Team Hub user accounts visible to an admin-role token.
151
+ */
152
+ async listAdminUsers() {
153
+ const result = await this.request('GET', '/admin/users', {
154
+ schema: listAdminUsersResponseSchema
155
+ });
156
+ return result.users;
157
+ }
158
+ /**
159
+ * Creates a Team Hub user account and an initial API bearer token.
160
+ *
161
+ * @param input - User fields for the new account.
162
+ */
163
+ async createAdminUser(input) {
164
+ const result = await this.request('POST', '/admin/users', {
165
+ body: input,
166
+ schema: createAdminUserResponseSchema
167
+ });
168
+ return result;
169
+ }
170
+ /**
171
+ * Updates a Team Hub user account via the management API.
172
+ *
173
+ * @param id - User account identifier.
174
+ * @param input - Partial user fields to apply.
175
+ */
176
+ async updateAdminUser(id, input) {
177
+ const result = await this.request('PUT', `/admin/users/${id}`, {
178
+ body: input,
179
+ schema: hubUserRecordSchema
180
+ });
181
+ return result;
182
+ }
183
+ /**
184
+ * Deletes a Team Hub user account and their API tokens via the management API.
185
+ *
186
+ * @param id - User account identifier.
187
+ */
188
+ async deleteAdminUser(id) {
189
+ await this.request('DELETE', `/admin/users/${id}`);
190
+ }
191
+ /**
192
+ * Lists all API bearer tokens visible to an admin-role token.
193
+ */
194
+ async listAdminTokens() {
195
+ const result = await this.request('GET', '/admin/tokens', {
196
+ schema: listAdminTokensResponseSchema
197
+ });
198
+ return result.tokens;
199
+ }
200
+ /**
201
+ * Creates an additional API bearer token for a user account.
202
+ *
203
+ * @param userId - Owning user account identifier.
204
+ * @param input - Human-readable label for the new token.
205
+ */
206
+ async createAdminUserToken(userId, input) {
207
+ const result = await this.request('POST', `/admin/users/${userId}/tokens`, {
208
+ body: input,
209
+ schema: createdApiTokenResponseSchema
210
+ });
211
+ return result;
212
+ }
213
+ /**
214
+ * Permanently deletes an API bearer token via the management API.
215
+ *
216
+ * @param id - Token record identifier.
217
+ */
218
+ async deleteAdminToken(id) {
219
+ await this.request('DELETE', `/admin/tokens/${id}`);
220
+ }
221
+ /**
222
+ * Lists all collections as id/name metadata for admin user management.
223
+ */
224
+ async listAdminCollections() {
225
+ const result = await this.request('GET', '/admin/collections', {
226
+ schema: listAdminCollectionsResponseSchema
227
+ });
228
+ return result.collections;
229
+ }
230
+ /**
231
+ * Lists all environments as id/name metadata for admin user management.
232
+ */
233
+ async listAdminEnvironments() {
234
+ const result = await this.request('GET', '/admin/environments', {
235
+ schema: listAdminEnvironmentsResponseSchema
236
+ });
237
+ return result.environments;
238
+ }
239
+ /**
240
+ * Lists folders in a collection for operator inspection.
241
+ *
242
+ * @param collectionId - Parent collection UUID.
243
+ */
244
+ async listAdminCollectionFolders(collectionId) {
245
+ const result = await this.request('GET', `/admin/collections/${collectionId}/folders`, {
246
+ schema: listFoldersResponseSchema
247
+ });
248
+ return result.folders;
249
+ }
250
+ /**
251
+ * Lists saved requests in a collection for operator inspection.
252
+ *
253
+ * @param collectionId - Parent collection UUID.
254
+ */
255
+ async listAdminCollectionRequests(collectionId) {
256
+ const result = await this.request('GET', `/admin/collections/${collectionId}/requests`, {
257
+ schema: listRequestsResponseSchema
258
+ });
259
+ return result.requests;
260
+ }
261
+ /**
262
+ * Deletes a collection via the admin management API.
263
+ *
264
+ * @param id - Collection UUID.
265
+ */
266
+ async deleteAdminCollection(id) {
267
+ await this.request('DELETE', `/admin/collections/${id}`);
268
+ }
269
+ /**
270
+ * Deletes an environment via the admin management API.
271
+ *
272
+ * @param id - Environment UUID.
273
+ */
274
+ async deleteAdminEnvironment(id) {
275
+ await this.request('DELETE', `/admin/environments/${id}`);
276
+ }
277
+ /**
278
+ * Deletes a saved request via the admin management API.
279
+ *
280
+ * @param id - Saved request UUID.
281
+ */
282
+ async deleteAdminRequest(id) {
283
+ await this.request('DELETE', `/admin/requests/${id}`);
284
+ }
285
+ /**
286
+ * Updates whether non-admin users may delete a collection.
287
+ *
288
+ * @param id - Collection UUID.
289
+ * @param deletionLocked - When true, user-role tokens cannot delete the collection.
290
+ */
291
+ async updateAdminCollectionDeletionLocked(id, deletionLocked) {
292
+ const result = await this.request('PUT', `/admin/collections/${id}`, {
293
+ body: { deletionLocked },
294
+ schema: adminEntityConfigSchema
295
+ });
296
+ return result;
297
+ }
298
+ /**
299
+ * Updates whether non-admin users may delete an environment.
300
+ *
301
+ * @param id - Environment UUID.
302
+ * @param deletionLocked - When true, user-role tokens cannot delete the environment.
303
+ */
304
+ async updateAdminEnvironmentDeletionLocked(id, deletionLocked) {
305
+ const result = await this.request('PUT', `/admin/environments/${id}`, {
306
+ body: { deletionLocked },
307
+ schema: adminEntityConfigSchema
308
+ });
309
+ return result;
310
+ }
311
+ /**
312
+ * Lists all hub-offered LLM models for admin user management.
313
+ *
314
+ * Returns an empty list when LLM support is not configured on the hub.
315
+ */
316
+ async listAdminLlmModels() {
317
+ try {
318
+ const result = await this.request('GET', '/admin/llm/models', {
319
+ schema: listHubLlmModelsResponseSchema
320
+ });
321
+ return result.models;
322
+ }
323
+ catch (error) {
324
+ if (error instanceof TeamHubClientError && error.status === 503) {
325
+ return [];
326
+ }
327
+ throw error;
328
+ }
329
+ }
330
+ /**
331
+ * Returns whether the Team Hub server has LLM support configured.
332
+ *
333
+ * Uses the admin models route for management tokens and the user route otherwise.
334
+ * A 503 response means LLM is not configured; any other successful response means
335
+ * configured.
336
+ *
337
+ * @param managementApi - When true, probes `GET /admin/llm/models`.
338
+ */
339
+ async probeLlmServiceEnabled(managementApi) {
340
+ const path = managementApi ? '/admin/llm/models' : '/llm/models';
341
+ try {
342
+ await this.request('GET', path, {
343
+ schema: listHubLlmModelsResponseSchema
344
+ });
345
+ return true;
346
+ }
347
+ catch (error) {
348
+ if (error instanceof TeamHubClientError && error.status === 503) {
349
+ return false;
350
+ }
351
+ throw error;
352
+ }
353
+ }
354
+ /**
355
+ * Loads collection, environment, and LLM model options for admin user forms.
356
+ */
357
+ async listAdminResourceOptions() {
358
+ const [collections, environments, models] = await Promise.all([
359
+ this.listAdminCollections(),
360
+ this.listAdminEnvironments(),
361
+ this.listAdminLlmModels()
362
+ ]);
363
+ return { collections, environments, models };
364
+ }
365
+ /**
366
+ * Re-reads server.yaml on the Team Hub and applies reloadable config sections.
367
+ *
368
+ * Returns parsed bodies for both `200` and `400` responses. Only auth and
369
+ * transport failures throw {@link TeamHubClientError}.
370
+ */
371
+ async reloadConfig() {
372
+ const method = 'POST';
373
+ const path = '/admin/config/reload';
374
+ let response;
375
+ try {
376
+ response = await fetch(this.buildUrl(path), {
377
+ method,
378
+ headers: {
379
+ Accept: 'application/json',
380
+ Authorization: `Bearer ${this.token}`
381
+ },
382
+ signal: AbortSignal.timeout(this.requestTimeoutMs)
383
+ });
384
+ }
385
+ catch (err) {
386
+ const message = err instanceof Error && err.name === 'TimeoutError'
387
+ ? `Request timed out after ${this.requestTimeoutMs} ms`
388
+ : err instanceof Error
389
+ ? err.message
390
+ : 'Unknown network error';
391
+ throw new TeamHubClientError(message, { status: 0, method, path });
392
+ }
393
+ if (response.status !== 200 && response.status !== 400) {
394
+ const message = await this.parseErrorMessage(response);
395
+ throw new TeamHubClientError(message, {
396
+ status: response.status,
397
+ method,
398
+ path
399
+ });
400
+ }
401
+ let json;
402
+ try {
403
+ json = await response.json();
404
+ }
405
+ catch {
406
+ throw new TeamHubClientError('Response body is not valid JSON', {
407
+ status: response.status,
408
+ method,
409
+ path
410
+ });
411
+ }
412
+ const parsed = reloadConfigResponseSchema.safeParse(json);
413
+ if (!parsed.success) {
414
+ throw new TeamHubClientError('Response body failed validation', {
415
+ status: response.status,
416
+ method,
417
+ path
418
+ });
419
+ }
420
+ return parsed.data;
421
+ }
422
+ /**
423
+ * Lists all collections visible to the authenticated token.
424
+ *
425
+ * Admin tokens receive the full catalog from `GET /collections`; create, update,
426
+ * and delete remain forbidden on the server.
427
+ */
428
+ async listCollections() {
429
+ const result = await this.request('GET', '/collections', {
430
+ schema: listCollectionsResponseSchema
431
+ });
432
+ return result.collections;
433
+ }
434
+ /**
435
+ * Creates a new top-level collection.
436
+ *
437
+ * @param input - Display name for the collection.
438
+ */
439
+ async createCollection(input) {
440
+ const result = await this.request('POST', '/collections', {
441
+ body: input,
442
+ schema: collectionRecordSchema
443
+ });
444
+ return result;
445
+ }
446
+ /**
447
+ * Updates an existing collection's settings.
448
+ *
449
+ * @param id - Collection UUID.
450
+ * @param input - Updated collection fields.
451
+ */
452
+ async updateCollection(id, input) {
453
+ const result = await this.request('PUT', `/collections/${id}`, {
454
+ body: input,
455
+ schema: collectionRecordSchema
456
+ });
457
+ return result;
458
+ }
459
+ /**
460
+ * Deletes a collection and all nested folders and requests.
461
+ *
462
+ * @param id - Collection UUID.
463
+ */
464
+ async deleteCollection(id) {
465
+ await this.request('DELETE', `/collections/${id}`);
466
+ }
467
+ /**
468
+ * Lists all environments visible to the authenticated token.
469
+ *
470
+ * Admin tokens receive the full catalog from `GET /environments`; create, update,
471
+ * and delete remain forbidden on the server.
472
+ */
473
+ async listEnvironments() {
474
+ const result = await this.request('GET', '/environments', {
475
+ schema: listEnvironmentsResponseSchema
476
+ });
477
+ return result.environments;
478
+ }
479
+ /**
480
+ * Creates a new top-level environment.
481
+ *
482
+ * @param input - Display name for the environment.
483
+ */
484
+ async createEnvironment(input) {
485
+ const result = await this.request('POST', '/environments', {
486
+ body: input,
487
+ schema: environmentRecordSchema
488
+ });
489
+ return result;
490
+ }
491
+ /**
492
+ * Updates an existing environment's name and variables.
493
+ *
494
+ * @param id - Environment UUID.
495
+ * @param input - Updated environment fields.
496
+ */
497
+ async updateEnvironment(id, input) {
498
+ const result = await this.request('PUT', `/environments/${id}`, {
499
+ body: input,
500
+ schema: environmentRecordSchema
501
+ });
502
+ return result;
503
+ }
504
+ /**
505
+ * Deletes an environment by id.
506
+ *
507
+ * @param id - Environment UUID.
508
+ */
509
+ async deleteEnvironment(id) {
510
+ await this.request('DELETE', `/environments/${id}`);
511
+ }
512
+ /**
513
+ * Lists folders in a collection ordered by sort order, then name.
514
+ *
515
+ * @param collectionId - Parent collection UUID.
516
+ */
517
+ async listFolders(collectionId) {
518
+ const result = await this.request('GET', `/collections/${collectionId}/folders`, {
519
+ schema: listFoldersResponseSchema
520
+ });
521
+ return result.folders;
522
+ }
523
+ /**
524
+ * Creates a folder in the given collection.
525
+ *
526
+ * @param collectionId - Parent collection UUID.
527
+ * @param input - Display name for the folder.
528
+ */
529
+ async createFolder(collectionId, input) {
530
+ const result = await this.request('POST', `/collections/${collectionId}/folders`, {
531
+ body: input,
532
+ schema: folderRecordSchema
533
+ });
534
+ return result;
535
+ }
536
+ /**
537
+ * Renames a folder by id.
538
+ *
539
+ * @param id - Folder UUID.
540
+ * @param input - Updated folder name.
541
+ */
542
+ async renameFolder(id, input) {
543
+ const result = await this.request('PATCH', `/folders/${id}`, {
544
+ body: input,
545
+ schema: folderRecordSchema
546
+ });
547
+ return result;
548
+ }
549
+ /**
550
+ * Deletes a folder and all saved requests inside it.
551
+ *
552
+ * @param id - Folder UUID.
553
+ */
554
+ async deleteFolder(id) {
555
+ await this.request('DELETE', `/folders/${id}`);
556
+ }
557
+ /**
558
+ * Reorders folders within a collection.
559
+ *
560
+ * @param collectionId - Parent collection UUID.
561
+ * @param input - Folder ids in the desired order.
562
+ */
563
+ async reorderFolders(collectionId, input) {
564
+ await this.request('PUT', `/collections/${collectionId}/folders/reorder`, {
565
+ body: input
566
+ });
567
+ }
568
+ /**
569
+ * Lists saved requests in a collection.
570
+ *
571
+ * @param collectionId - Parent collection UUID.
572
+ */
573
+ async listRequests(collectionId) {
574
+ const result = await this.request('GET', `/collections/${collectionId}/requests`, {
575
+ schema: listRequestsResponseSchema
576
+ });
577
+ return result.requests;
578
+ }
579
+ /**
580
+ * Creates a new saved request in a collection.
581
+ *
582
+ * @param collectionId - Parent collection UUID.
583
+ * @param input - Saved request fields.
584
+ */
585
+ async createRequest(collectionId, input) {
586
+ const result = await this.request('POST', `/collections/${collectionId}/requests`, {
587
+ body: input,
588
+ schema: savedRequestRecordSchema
589
+ });
590
+ return result;
591
+ }
592
+ /**
593
+ * Updates an existing saved request by id.
594
+ *
595
+ * @param id - Saved request UUID.
596
+ * @param input - Updated request fields including collection id.
597
+ */
598
+ async updateRequest(id, input) {
599
+ const result = await this.request('PUT', `/requests/${id}`, {
600
+ body: input,
601
+ schema: savedRequestRecordSchema
602
+ });
603
+ return result;
604
+ }
605
+ /**
606
+ * Deletes a saved request by id.
607
+ *
608
+ * @param id - Saved request UUID.
609
+ */
610
+ async deleteRequest(id) {
611
+ await this.request('DELETE', `/requests/${id}`);
612
+ }
613
+ /**
614
+ * Reorders saved requests within a folder or the collection root.
615
+ *
616
+ * @param collectionId - Parent collection UUID.
617
+ * @param input - Destination folder and ordered request ids.
618
+ */
619
+ async reorderRequests(collectionId, input) {
620
+ await this.request('PUT', `/collections/${collectionId}/requests/reorder`, {
621
+ body: input
622
+ });
623
+ }
624
+ /**
625
+ * Moves a saved request to another folder or root index.
626
+ *
627
+ * @param id - Saved request UUID.
628
+ * @param input - Destination folder and target index.
629
+ */
630
+ async moveRequest(id, input) {
631
+ await this.request('PUT', `/requests/${id}/move`, {
632
+ body: input
633
+ });
634
+ }
635
+ /**
636
+ * Lists hub-offered LLM models visible to the authenticated token.
637
+ */
638
+ async listLlmModels() {
639
+ const result = await this.request('GET', '/llm/models', {
640
+ schema: listHubLlmModelsResponseSchema
641
+ });
642
+ return result.models;
643
+ }
644
+ /**
645
+ * Returns plugin catalog and trusted-publisher URLs configured on this Team Hub.
646
+ */
647
+ async getPluginSources() {
648
+ const result = await this.request('GET', '/plugins/sources', {
649
+ schema: pluginSourcesResponseSchema
650
+ });
651
+ return result;
652
+ }
653
+ /**
654
+ * Runs one hub-proxied LLM completion step.
655
+ *
656
+ * @param input - Model, messages, tools, and system prompt for the step.
657
+ */
658
+ async completeChatStep(input) {
659
+ const result = await this.request('POST', '/llm/chat/step', {
660
+ body: input,
661
+ schema: hubChatStepResponseSchema
662
+ });
663
+ return result;
664
+ }
665
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Thrown when a HarborClient Server request fails or the response body is invalid.
3
+ */
4
+ export declare class TeamHubClientError extends Error {
5
+ /**
6
+ * HTTP status code from the server, or `0` for network and parse failures.
7
+ */
8
+ readonly status: number;
9
+ /**
10
+ * HTTP method used for the failed request.
11
+ */
12
+ readonly method: string;
13
+ /**
14
+ * Request path relative to the configured base URL.
15
+ */
16
+ readonly path: string;
17
+ /**
18
+ * Creates a team hub client error with request context for logging and UI display.
19
+ *
20
+ * @param message - Human-readable error description.
21
+ * @param options - HTTP status and request metadata.
22
+ */
23
+ constructor(message: string, options: {
24
+ status: number;
25
+ method: string;
26
+ path: string;
27
+ });
28
+ }
29
+ //# sourceMappingURL=TeamHubClientError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TeamHubClientError.d.ts","sourceRoot":"","sources":["../src/TeamHubClientError.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;gBAED,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd;CAQJ"}