@qulib/core 0.4.1 → 0.4.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.
Files changed (135) hide show
  1. package/README.md +56 -8
  2. package/dist/analyze.d.ts.map +1 -1
  3. package/dist/analyze.js +86 -7
  4. package/dist/cli/auth-login-resolve.d.ts +14 -0
  5. package/dist/cli/auth-login-resolve.d.ts.map +1 -0
  6. package/dist/cli/auth-login-resolve.js +68 -0
  7. package/dist/cli/auth-login-run.d.ts +13 -0
  8. package/dist/cli/auth-login-run.d.ts.map +1 -0
  9. package/dist/cli/auth-login-run.js +152 -0
  10. package/dist/cli/index.js +60 -7
  11. package/dist/harness/state-manager.d.ts +10 -0
  12. package/dist/harness/state-manager.d.ts.map +1 -1
  13. package/dist/harness/state-manager.js +15 -0
  14. package/dist/index.d.ts +8 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -6
  17. package/dist/phases/act.js +3 -3
  18. package/dist/phases/observe.js +5 -5
  19. package/dist/phases/think.js +1 -1
  20. package/dist/schemas/automation-maturity.schema.d.ts +40 -0
  21. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
  22. package/dist/schemas/automation-maturity.schema.js +27 -0
  23. package/dist/schemas/index.d.ts +1 -1
  24. package/dist/schemas/index.d.ts.map +1 -1
  25. package/dist/schemas/index.js +1 -1
  26. package/dist/schemas/repo-analysis.schema.d.ts +22 -0
  27. package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
  28. package/dist/schemas/repo-analysis.schema.js +1 -0
  29. package/dist/telemetry/emit.d.ts +22 -0
  30. package/dist/telemetry/emit.d.ts.map +1 -1
  31. package/dist/telemetry/emit.js +37 -0
  32. package/dist/telemetry/telemetry.interface.d.ts +1 -1
  33. package/dist/telemetry/telemetry.interface.d.ts.map +1 -1
  34. package/dist/tools/apply-auth.d.ts +4 -0
  35. package/dist/tools/apply-auth.d.ts.map +1 -0
  36. package/dist/tools/apply-auth.js +35 -0
  37. package/dist/tools/auth/apply.d.ts +4 -0
  38. package/dist/tools/auth/apply.d.ts.map +1 -0
  39. package/dist/tools/auth/apply.js +35 -0
  40. package/dist/tools/auth/block-gap.d.ts +9 -0
  41. package/dist/tools/auth/block-gap.d.ts.map +1 -0
  42. package/dist/tools/auth/block-gap.js +52 -0
  43. package/dist/tools/auth/custom-providers.d.ts +15 -0
  44. package/dist/tools/auth/custom-providers.d.ts.map +1 -0
  45. package/dist/tools/auth/custom-providers.js +62 -0
  46. package/dist/tools/auth/detect.d.ts +23 -0
  47. package/dist/tools/auth/detect.d.ts.map +1 -0
  48. package/dist/tools/auth/detect.js +526 -0
  49. package/dist/tools/auth/detector.d.ts +23 -0
  50. package/dist/tools/auth/detector.d.ts.map +1 -0
  51. package/dist/tools/auth/detector.js +526 -0
  52. package/dist/tools/auth/explore.d.ts +4 -0
  53. package/dist/tools/auth/explore.d.ts.map +1 -0
  54. package/dist/tools/auth/explore.js +346 -0
  55. package/dist/tools/auth/explorer.d.ts +4 -0
  56. package/dist/tools/auth/explorer.d.ts.map +1 -0
  57. package/dist/tools/auth/explorer.js +346 -0
  58. package/dist/tools/auth/gaps.d.ts +9 -0
  59. package/dist/tools/auth/gaps.d.ts.map +1 -0
  60. package/dist/tools/auth/gaps.js +52 -0
  61. package/dist/tools/auth/oauth-providers.d.ts +7 -0
  62. package/dist/tools/auth/oauth-providers.d.ts.map +1 -0
  63. package/dist/tools/auth/oauth-providers.js +21 -0
  64. package/dist/tools/auth/providers.d.ts +7 -0
  65. package/dist/tools/auth/providers.d.ts.map +1 -0
  66. package/dist/tools/auth/providers.js +21 -0
  67. package/dist/tools/auth/surface-analyzer.d.ts +4 -0
  68. package/dist/tools/auth/surface-analyzer.d.ts.map +1 -0
  69. package/dist/tools/auth/surface-analyzer.js +170 -0
  70. package/dist/tools/auth/surface.d.ts +4 -0
  71. package/dist/tools/auth/surface.d.ts.map +1 -0
  72. package/dist/tools/auth/surface.js +170 -0
  73. package/dist/tools/auth/user-providers.d.ts +15 -0
  74. package/dist/tools/auth/user-providers.d.ts.map +1 -0
  75. package/dist/tools/auth/user-providers.js +62 -0
  76. package/dist/tools/auth-block-gap.d.ts +6 -0
  77. package/dist/tools/auth-block-gap.d.ts.map +1 -1
  78. package/dist/tools/auth-block-gap.js +42 -9
  79. package/dist/tools/auth-detector.d.ts +19 -0
  80. package/dist/tools/auth-detector.d.ts.map +1 -1
  81. package/dist/tools/auth-detector.js +186 -8
  82. package/dist/tools/automation-maturity.d.ts.map +1 -1
  83. package/dist/tools/automation-maturity.js +76 -20
  84. package/dist/tools/explorers/browser.d.ts +3 -0
  85. package/dist/tools/explorers/browser.d.ts.map +1 -0
  86. package/dist/tools/explorers/browser.js +13 -0
  87. package/dist/tools/explorers/cypress-explorer.d.ts +8 -0
  88. package/dist/tools/explorers/cypress-explorer.d.ts.map +1 -0
  89. package/dist/tools/explorers/cypress-explorer.js +5 -0
  90. package/dist/tools/explorers/cypress.d.ts +8 -0
  91. package/dist/tools/explorers/cypress.d.ts.map +1 -0
  92. package/dist/tools/explorers/cypress.js +5 -0
  93. package/dist/tools/explorers/explorer.interface.d.ts +7 -0
  94. package/dist/tools/explorers/explorer.interface.d.ts.map +1 -0
  95. package/dist/tools/explorers/explorer.interface.js +1 -0
  96. package/dist/tools/explorers/factory.d.ts +4 -0
  97. package/dist/tools/explorers/factory.d.ts.map +1 -0
  98. package/dist/tools/explorers/factory.js +12 -0
  99. package/dist/tools/explorers/playwright-explorer.d.ts +8 -0
  100. package/dist/tools/explorers/playwright-explorer.d.ts.map +1 -0
  101. package/dist/tools/explorers/playwright-explorer.js +172 -0
  102. package/dist/tools/explorers/playwright.d.ts +8 -0
  103. package/dist/tools/explorers/playwright.d.ts.map +1 -0
  104. package/dist/tools/explorers/playwright.js +172 -0
  105. package/dist/tools/explorers/types.d.ts +7 -0
  106. package/dist/tools/explorers/types.d.ts.map +1 -0
  107. package/dist/tools/explorers/types.js +1 -0
  108. package/dist/tools/playwright-explorer.js +1 -1
  109. package/dist/tools/repo/detect-framework.d.ts +15 -0
  110. package/dist/tools/repo/detect-framework.d.ts.map +1 -0
  111. package/dist/tools/repo/detect-framework.js +153 -0
  112. package/dist/tools/repo/framework-detector.d.ts +15 -0
  113. package/dist/tools/repo/framework-detector.d.ts.map +1 -0
  114. package/dist/tools/repo/framework-detector.js +153 -0
  115. package/dist/tools/repo/scan.d.ts +19 -0
  116. package/dist/tools/repo/scan.d.ts.map +1 -0
  117. package/dist/tools/repo/scan.js +181 -0
  118. package/dist/tools/repo/scanner.d.ts +19 -0
  119. package/dist/tools/repo/scanner.d.ts.map +1 -0
  120. package/dist/tools/repo/scanner.js +181 -0
  121. package/dist/tools/repo-scanner.d.ts.map +1 -1
  122. package/dist/tools/repo-scanner.js +7 -2
  123. package/dist/tools/scoring/automation-maturity.d.ts +4 -0
  124. package/dist/tools/scoring/automation-maturity.d.ts.map +1 -0
  125. package/dist/tools/scoring/automation-maturity.js +219 -0
  126. package/dist/tools/scoring/gap-engine.d.ts +8 -0
  127. package/dist/tools/scoring/gap-engine.d.ts.map +1 -0
  128. package/dist/tools/scoring/gap-engine.js +138 -0
  129. package/dist/tools/scoring/gaps.d.ts +8 -0
  130. package/dist/tools/scoring/gaps.d.ts.map +1 -0
  131. package/dist/tools/scoring/gaps.js +138 -0
  132. package/dist/tools/scoring/public-surface.d.ts +5 -0
  133. package/dist/tools/scoring/public-surface.d.ts.map +1 -0
  134. package/dist/tools/scoring/public-surface.js +13 -0
  135. package/package.json +3 -3
@@ -0,0 +1,346 @@
1
+ import { AuthExplorationSchema, } from '../../schemas/config.schema.js';
2
+ import { launchBrowser } from '../explorers/browser.js';
3
+ import { BUILT_IN_OAUTH_PROVIDERS } from './providers.js';
4
+ import { loadUserProviders } from './custom-providers.js';
5
+ const MAGIC_LINK_PATTERNS = [
6
+ /email me a (sign[- ]?in )?link/i,
7
+ /sign in with email/i,
8
+ /passwordless/i,
9
+ /we'll send you a link/i,
10
+ ];
11
+ async function waitNetworkIdleBestEffort(page) {
12
+ try {
13
+ await page.waitForLoadState('networkidle', { timeout: 5000 });
14
+ }
15
+ catch {
16
+ // best-effort
17
+ }
18
+ }
19
+ function textLooksLikeOAuthIdpButton(text) {
20
+ const t = text.trim();
21
+ if (t.length === 0 || t.length > 120) {
22
+ return false;
23
+ }
24
+ return (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t) ||
25
+ /^(github|google|microsoft|apple)$/i.test(t));
26
+ }
27
+ function slugifyLabel(text) {
28
+ const s = text
29
+ .trim()
30
+ .toLowerCase()
31
+ .replace(/\s+/g, '-')
32
+ .replace(/[^a-z0-9-]/g, '')
33
+ .slice(0, 48);
34
+ return s.length > 0 ? s : 'unknown';
35
+ }
36
+ function onLoginishPage(url) {
37
+ return /login|sign[- ]?in|auth|sso|oauth/i.test(new URL(url).pathname + new URL(url).hostname);
38
+ }
39
+ function debugExplore() {
40
+ return process.env.QULIB_DEBUG === '1';
41
+ }
42
+ function isHeuristicUnknownSso(text, loginish) {
43
+ const t = text.trim();
44
+ if (!loginish || t.length < 3 || t.length > 80) {
45
+ return false;
46
+ }
47
+ if (/^(submit|cancel|back|close|next|skip|help|faq)$/i.test(t)) {
48
+ return false;
49
+ }
50
+ if (/\b(sign in with|log in with|continue with)\b/i.test(t)) {
51
+ return true;
52
+ }
53
+ if (/\b(sync|sso|portal|workspace|federation)\b/i.test(t)) {
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+ function storageRequirement() {
59
+ return {
60
+ method: 'storage-state',
61
+ instruction: 'OAuth and most SSO flows cannot be scripted. Run `qulib auth init --base-url <app-url>` on this machine, then pass the saved storage state JSON to `analyze` or MCP `analyze_app` as `auth: { type: "storage-state", path: "..." }`.',
62
+ };
63
+ }
64
+ async function collectVisibleControlTexts(page) {
65
+ return page.evaluate(() => {
66
+ const seen = new Set();
67
+ const out = [];
68
+ const nodes = document.querySelectorAll('button, a[href], [role="button"]');
69
+ for (const el of nodes) {
70
+ const t = (el.textContent ?? '').trim().replace(/\s+/g, ' ');
71
+ if (!t || t.length > 120) {
72
+ continue;
73
+ }
74
+ const style = window.getComputedStyle(el);
75
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
76
+ continue;
77
+ }
78
+ if (!seen.has(t)) {
79
+ seen.add(t);
80
+ out.push(t);
81
+ }
82
+ }
83
+ return out;
84
+ });
85
+ }
86
+ function buildAllProviders() {
87
+ const builtIn = BUILT_IN_OAUTH_PROVIDERS.map((p) => ({ ...p, source: 'built-in' }));
88
+ const user = loadUserProviders().map((p) => ({ ...p, source: 'user-local' }));
89
+ return [...builtIn, ...user];
90
+ }
91
+ function matchProvider(text, p) {
92
+ return p.patterns.some((re) => re.test(text));
93
+ }
94
+ function oauthConfidence(source, loginish) {
95
+ if (source === 'user-local') {
96
+ return 'high';
97
+ }
98
+ if (source === 'built-in' && loginish) {
99
+ return 'high';
100
+ }
101
+ if (source === 'built-in') {
102
+ return 'medium';
103
+ }
104
+ return 'low';
105
+ }
106
+ async function buildFormPaths(page) {
107
+ const passwordCount = await page.locator('input[type="password"]').count();
108
+ if (passwordCount === 0) {
109
+ return [];
110
+ }
111
+ const formType = passwordCount > 1 ? 'form-multi' : 'form-login';
112
+ const fields = await page.evaluate(() => {
113
+ const pwd = document.querySelector('input[type="password"]');
114
+ if (!pwd) {
115
+ return [];
116
+ }
117
+ const form = pwd.closest('form') ?? document.body;
118
+ const out = [];
119
+ const inputs = form.querySelectorAll('input, select, textarea');
120
+ for (const el of inputs) {
121
+ if (!(el instanceof HTMLElement)) {
122
+ continue;
123
+ }
124
+ const tag = el.tagName.toLowerCase();
125
+ if (tag === 'input') {
126
+ const inp = el;
127
+ const t = (inp.type || 'text').toLowerCase();
128
+ if (['hidden', 'submit', 'button', 'image', 'reset'].includes(t)) {
129
+ continue;
130
+ }
131
+ let fieldType = 'text';
132
+ if (t === 'password') {
133
+ fieldType = 'password';
134
+ }
135
+ else if (t === 'email') {
136
+ fieldType = 'email';
137
+ }
138
+ else if (t === 'checkbox') {
139
+ fieldType = 'checkbox';
140
+ }
141
+ const id = inp.id;
142
+ let label = inp.getAttribute('aria-label') ?? inp.placeholder ?? inp.name ?? fieldType;
143
+ if (id) {
144
+ const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
145
+ if (lab?.textContent) {
146
+ label = lab.textContent.trim();
147
+ }
148
+ }
149
+ out.push({
150
+ name: inp.name || inp.id || fieldType,
151
+ label: label.slice(0, 120),
152
+ type: fieldType,
153
+ observedOptions: [],
154
+ });
155
+ }
156
+ else if (tag === 'select') {
157
+ const sel = el;
158
+ const opts = Array.from(sel.options).map((o) => o.text.trim()).filter(Boolean);
159
+ out.push({
160
+ name: sel.name || sel.id || 'select',
161
+ label: (sel.getAttribute('aria-label') ?? sel.name ?? 'select').slice(0, 120),
162
+ type: 'select',
163
+ observedOptions: opts.slice(0, 50),
164
+ });
165
+ }
166
+ }
167
+ return out;
168
+ });
169
+ const requirements = fields.length > 0
170
+ ? { method: 'credentials', fields }
171
+ : {
172
+ method: 'unknown',
173
+ instruction: 'A password field exists but field metadata could not be read. Inspect the page in devtools and configure form-login selectors manually, or use `qulib auth init`.',
174
+ };
175
+ return [
176
+ {
177
+ id: formType === 'form-multi' ? 'form-multi' : 'form-login',
178
+ label: formType === 'form-multi' ? 'Multi-field sign-in form' : 'Username / password form',
179
+ type: formType,
180
+ provider: null,
181
+ source: 'heuristic',
182
+ automatable: requirements.method === 'credentials',
183
+ confidence: requirements.method === 'credentials' ? 'medium' : 'low',
184
+ requirements,
185
+ },
186
+ ];
187
+ }
188
+ export async function exploreAuth(url, timeoutMs = 20000, progress) {
189
+ const browser = await launchBrowser();
190
+ try {
191
+ const context = await browser.newContext();
192
+ const page = await context.newPage();
193
+ progress?.info(`explore_auth URL=${url}`);
194
+ await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
195
+ await waitNetworkIdleBestEffort(page);
196
+ if (debugExplore()) {
197
+ const html = await page.content();
198
+ progress?.debug(`explore_auth HTML byteLength=${Buffer.byteLength(html, 'utf8')}`);
199
+ }
200
+ const loginishAfterFirst = /login|sign[- ]?in|auth/i.test(page.url()) || (await page.locator('input[type="password"]').count()) > 0;
201
+ if (!loginishAfterFirst) {
202
+ const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
203
+ const cnt = await loginLink.count();
204
+ progress?.debug(`explore_auth selector loginLink count=${cnt}`);
205
+ if (cnt > 0) {
206
+ const href = await loginLink.getAttribute('href');
207
+ progress?.debug(`explore_auth selector loginLink href matched=${Boolean(href)}`);
208
+ if (href) {
209
+ const next = new URL(href, url).toString();
210
+ await page.goto(next, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
211
+ await waitNetworkIdleBestEffort(page);
212
+ }
213
+ }
214
+ }
215
+ const finalUrl = page.url();
216
+ const loginish = onLoginishPage(finalUrl) || (await page.locator('input[type="password"]').count()) > 0;
217
+ const allProviders = buildAllProviders();
218
+ const texts = await collectVisibleControlTexts(page);
219
+ const consumed = new Set();
220
+ const authPaths = [];
221
+ const unrecognizedButtons = [];
222
+ for (const rawText of texts) {
223
+ const text = rawText.trim();
224
+ if (!text) {
225
+ continue;
226
+ }
227
+ let providerMatch = null;
228
+ for (const p of allProviders) {
229
+ const hit = matchProvider(text, p);
230
+ if (debugExplore()) {
231
+ progress?.debug(`explore_auth provider try id=${p.id} matched=${hit}`);
232
+ }
233
+ if (!hit) {
234
+ continue;
235
+ }
236
+ if (p.source === 'built-in' && !(textLooksLikeOAuthIdpButton(text) || loginish)) {
237
+ continue;
238
+ }
239
+ providerMatch = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
240
+ break;
241
+ }
242
+ if (providerMatch) {
243
+ const { p, gate } = providerMatch;
244
+ const id = `oauth:${p.id}`;
245
+ if (consumed.has(id)) {
246
+ continue;
247
+ }
248
+ consumed.add(id);
249
+ authPaths.push({
250
+ id,
251
+ label: p.label,
252
+ type: 'oauth',
253
+ provider: p.id,
254
+ source: p.source,
255
+ automatable: false,
256
+ confidence: oauthConfidence(p.source, loginish || gate),
257
+ requirements: storageRequirement(),
258
+ });
259
+ progress?.info(`explore_auth path id=${id} type=oauth provider=${p.id} automatable=false`);
260
+ continue;
261
+ }
262
+ if (isHeuristicUnknownSso(text, loginish)) {
263
+ const slug = slugifyLabel(text);
264
+ const id = `oauth-unknown:${slug}`;
265
+ if (consumed.has(id)) {
266
+ continue;
267
+ }
268
+ consumed.add(id);
269
+ authPaths.push({
270
+ id,
271
+ label: text.slice(0, 100),
272
+ type: 'oauth-unknown',
273
+ provider: null,
274
+ source: 'heuristic',
275
+ automatable: false,
276
+ confidence: 'low',
277
+ requirements: storageRequirement(),
278
+ });
279
+ progress?.info(`explore_auth path id=${id} type=oauth-unknown automatable=false`);
280
+ const safePattern = text.slice(0, 48).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
281
+ unrecognizedButtons.push({
282
+ label: text.slice(0, 100),
283
+ hint: `If this is your org SSO, register it: qulib auth providers add --id "${slug}" --label "${text.replace(/"/g, '\\"').slice(0, 80)}" --pattern "${safePattern}"`,
284
+ });
285
+ }
286
+ }
287
+ const pageText = await page.locator('body').innerText().catch(() => '');
288
+ if (MAGIC_LINK_PATTERNS.some((re) => re.test(pageText))) {
289
+ authPaths.push({
290
+ id: 'magic-link',
291
+ label: 'Magic link / passwordless',
292
+ type: 'magic-link',
293
+ provider: null,
294
+ source: 'heuristic',
295
+ automatable: false,
296
+ confidence: 'medium',
297
+ requirements: {
298
+ method: 'storage-state',
299
+ instruction: 'Magic-link flows need a human in the loop. Use `qulib auth init --base-url <app-url>` and complete email or provider steps in the opened browser, then reuse the saved storage state for scans.',
300
+ },
301
+ });
302
+ progress?.info('explore_auth path id=magic-link type=magic-link automatable=false');
303
+ }
304
+ const formPaths = await buildFormPaths(page);
305
+ for (const fp of formPaths) {
306
+ authPaths.push(fp);
307
+ progress?.info(`explore_auth path id=${fp.id} type=${fp.type} automatable=${fp.automatable}`);
308
+ }
309
+ const authRequired = authPaths.length > 0;
310
+ let authScope = 'none';
311
+ if (authRequired) {
312
+ if (loginish) {
313
+ authScope = 'site-wide';
314
+ }
315
+ else {
316
+ authScope = /login|signin|auth/i.test(new URL(finalUrl).pathname) ? 'site-wide' : 'optional';
317
+ }
318
+ }
319
+ const suggestedParts = [];
320
+ if (authPaths.some((p) => p.type === 'oauth' || p.type === 'oauth-unknown')) {
321
+ suggestedParts.push('For OAuth or unrecognized SSO buttons, collect a Playwright storage state with `qulib auth init` before calling `analyze_app`.');
322
+ }
323
+ if (authPaths.some((p) => p.type === 'form-login' || p.type === 'form-multi')) {
324
+ suggestedParts.push('For password forms, gather username/password and stable selectors (or use storage state if MFA applies).');
325
+ }
326
+ if (authPaths.some((p) => p.type === 'magic-link')) {
327
+ suggestedParts.push('For magic-link, use `qulib auth init` after the user completes email delivery.');
328
+ }
329
+ if (!authRequired) {
330
+ suggestedParts.push('No sign-in surface detected at this URL; you can run `analyze_app` without auth unless gated deeper in the app.');
331
+ }
332
+ const exploration = {
333
+ url: finalUrl,
334
+ authRequired,
335
+ authScope,
336
+ authPaths,
337
+ observedAt: new Date().toISOString(),
338
+ suggestedAgentBehavior: suggestedParts.join(' '),
339
+ unrecognizedButtons,
340
+ };
341
+ return AuthExplorationSchema.parse(exploration);
342
+ }
343
+ finally {
344
+ await browser.close();
345
+ }
346
+ }
@@ -0,0 +1,4 @@
1
+ import { type AuthExploration } from '../../schemas/config.schema.js';
2
+ import type { AnalyzeProgressSink } from '../../harness/progress-log.js';
3
+ export declare function exploreAuth(url: string, timeoutMs?: number, progress?: AnalyzeProgressSink): Promise<AuthExploration>;
4
+ //# sourceMappingURL=explorer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explorer.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/explorer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAiNzE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAgL1B"}