@estiva-app/protocol 0.1.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.
package/dist/live.js ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * One live relay socket, with NIP-42 AUTH and reconnect (PEE-5).
3
+ *
4
+ * A WebSocket that stays open, authenticates as the viewer, and keeps
5
+ * subscriptions alive across disconnects. Nothing is *interpreted* here: this
6
+ * delivers frames, and folding them into state is the consuming app's business.
7
+ *
8
+ * The socket belongs in the **client**, not in a backend: a request-scoped
9
+ * server runtime cannot hold one, and more importantly the viewer's own
10
+ * credential is the only thing that can see the viewer's channels. Buzz gates
11
+ * reads per channel (`is_member_cached`), so a service identity would need
12
+ * membership in every conversation — a product decision, not an implementation
13
+ * detail.
14
+ *
15
+ * **This was built inside Peek** (PEE-5, PEE-6) because Gate 2 had not happened
16
+ * when it was due, and it was Peek-only for two days. SHA-3 is the ticket that
17
+ * owed the move; `subscriptions.ts` beside this file is PEE-6's half.
18
+ *
19
+ * No nostr library. Raw `WebSocket` plus `buildUnsignedRelayAuthEvent` is
20
+ * enough, and the tag layout stays in one place.
21
+ *
22
+ * ## Three things the relay does that shape this file
23
+ *
24
+ * All three were read out of `crates/buzz-*` rather than assumed, because each
25
+ * one fails as a healthy-looking socket that delivers nothing.
26
+ *
27
+ * 1. **A second AUTH on an authenticated connection is refused.** `handle_auth`
28
+ * matches on `AuthState::{Pending, Authenticated, Failed}` and answers
29
+ * anything but `Pending` with `OK … false "auth-required: already
30
+ * authenticated"`. So re-authenticating in place is not possible, and
31
+ * {@link LiveRelay} never tries.
32
+ *
33
+ * 2. **A failed AUTH poisons the connection and leaves it open.** Only a *ban*
34
+ * closes the socket; a verification failure sets `AuthState::Failed` and
35
+ * returns. Every later REQ is answered `CLOSED … "auth-required:
36
+ * authenticate before subscribing"`, forever, on a socket whose readyState
37
+ * is OPEN. Treating an AUTH refusal as fatal-for-this-socket and reconnecting
38
+ * is the only way out.
39
+ *
40
+ * 3. **The `relay` tag is bound to the connection's host**, not the deployment
41
+ * URL — see `relayAuthUrl`.
42
+ *
43
+ * ## Token rotation needs nothing here, and that is worth stating
44
+ *
45
+ * PEE-5 asked for re-authentication when the app renews its access token,
46
+ * preferring it to a reconnect. Neither is needed, and the first is impossible
47
+ * (point 1 above).
48
+ *
49
+ * The relay authenticates a **pubkey**, by verifying a Schnorr signature. It
50
+ * never sees the Estiva ID access token and has no idea one exists. Renewal
51
+ * issues a new token for the *same* keypair, so nothing the relay checked has
52
+ * changed and the connection stays valid. The token is needed only to *sign* a
53
+ * fresh 22242, which happens on the next connect.
54
+ *
55
+ * What genuinely does not propagate is offboarding: a person whose Estiva ID
56
+ * access is revoked keeps an already-authenticated socket until it drops. That
57
+ * is the relay's session model — its own ban gate is the control for it — and
58
+ * re-authenticating on a timer would not have fixed it either, since a banned
59
+ * pubkey is caught at connect.
60
+ *
61
+ * ## Never logged, never persisted
62
+ *
63
+ * The AUTH event and the access token appear in no log line. Buzz refuses to
64
+ * store kind:22242 for this reason, and Convex surfaces function arguments in
65
+ * its dashboard logs — which is the entire reason Peek's credential lives in the
66
+ * browser. {@link LiveRelayOptions.log} receives states and reasons, never
67
+ * events or credentials, and this module must keep it that way.
68
+ */
69
+ import { buildUnsignedRelayAuthEvent, } from './events.js';
70
+ /** How many consecutive AUTH refusals before giving up rather than looping. */
71
+ const MAX_AUTH_FAILURES = 3;
72
+ /** `https://` → `wss://`, `http://` → `ws://`; a `ws`-scheme URL is left alone. */
73
+ export function toWebSocketUrl(url) {
74
+ const trimmed = url.trim().replace(/\/+$/, '');
75
+ if (trimmed.startsWith('wss://') || trimmed.startsWith('ws://'))
76
+ return trimmed;
77
+ if (trimmed.startsWith('https://'))
78
+ return `wss://${trimmed.slice('https://'.length)}`;
79
+ if (trimmed.startsWith('http://'))
80
+ return `ws://${trimmed.slice('http://'.length)}`;
81
+ return `wss://${trimmed}`;
82
+ }
83
+ export function parseFrame(raw) {
84
+ if (typeof raw !== 'string')
85
+ return { type: 'OTHER' };
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(raw);
89
+ }
90
+ catch {
91
+ return { type: 'OTHER' };
92
+ }
93
+ if (!Array.isArray(parsed) || typeof parsed[0] !== 'string')
94
+ return { type: 'OTHER' };
95
+ switch (parsed[0]) {
96
+ case 'AUTH':
97
+ return typeof parsed[1] === 'string'
98
+ ? { type: 'AUTH', challenge: parsed[1] }
99
+ : { type: 'OTHER' };
100
+ case 'OK':
101
+ return typeof parsed[1] === 'string' && typeof parsed[2] === 'boolean'
102
+ ? {
103
+ type: 'OK',
104
+ eventId: parsed[1],
105
+ accepted: parsed[2],
106
+ message: typeof parsed[3] === 'string' ? parsed[3] : '',
107
+ }
108
+ : { type: 'OTHER' };
109
+ case 'EVENT':
110
+ return typeof parsed[1] === 'string' && parsed[2] && typeof parsed[2] === 'object'
111
+ ? { type: 'EVENT', subId: parsed[1], event: parsed[2] }
112
+ : { type: 'OTHER' };
113
+ case 'EOSE':
114
+ return typeof parsed[1] === 'string' ? { type: 'EOSE', subId: parsed[1] } : { type: 'OTHER' };
115
+ case 'CLOSED':
116
+ return typeof parsed[1] === 'string'
117
+ ? {
118
+ type: 'CLOSED',
119
+ subId: parsed[1],
120
+ message: typeof parsed[2] === 'string' ? parsed[2] : '',
121
+ }
122
+ : { type: 'OTHER' };
123
+ case 'NOTICE':
124
+ return typeof parsed[1] === 'string'
125
+ ? { type: 'NOTICE', message: parsed[1] }
126
+ : { type: 'OTHER' };
127
+ default:
128
+ return { type: 'OTHER' };
129
+ }
130
+ }
131
+ export function createLiveRelay(options) {
132
+ const wsUrl = toWebSocketUrl(options.url);
133
+ const now = options.now ?? (() => Date.now());
134
+ const setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
135
+ const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
136
+ const makeSocket = options.socketFactory ?? ((url) => new WebSocket(url));
137
+ const backoff = options.backoff ?? { baseMs: 1_000, maxMs: 30_000, jitter: () => Math.random() };
138
+ const log = options.log ?? (() => { });
139
+ const subscriptions = new Map();
140
+ let socket = null;
141
+ let state = 'connecting';
142
+ let attempt = 0;
143
+ let authFailures = 0;
144
+ let stopped = false;
145
+ let reconnectTimer = null;
146
+ /** The id of the AUTH event in flight, so its `OK` is distinguishable. */
147
+ let pendingAuthEventId = null;
148
+ let nextSubId = 0;
149
+ function setState(next) {
150
+ if (state === next)
151
+ return;
152
+ state = next;
153
+ options.onState?.(next);
154
+ }
155
+ function send(frame) {
156
+ try {
157
+ socket?.send(JSON.stringify(frame));
158
+ }
159
+ catch (error) {
160
+ // A send on a socket the browser has already torn down. The close
161
+ // handler is what recovers; swallowing here keeps that the only path.
162
+ log('send failed', { reason: error.message });
163
+ }
164
+ }
165
+ function openSubscription(sub) {
166
+ send(['REQ', sub.id, ...sub.filters]);
167
+ }
168
+ /**
169
+ * Tear the socket down and schedule another attempt.
170
+ *
171
+ * `fatal` is for authentication refusals, which reconnecting cannot fix past
172
+ * a point — see {@link MAX_AUTH_FAILURES}.
173
+ */
174
+ function scheduleReconnect(reason) {
175
+ if (stopped)
176
+ return;
177
+ if (socket) {
178
+ // Drop the handlers before closing so our own `onclose` does not fire and
179
+ // schedule a second reconnect on top of this one.
180
+ socket.onopen = socket.onmessage = socket.onclose = socket.onerror = null;
181
+ try {
182
+ socket.close();
183
+ }
184
+ catch {
185
+ // Already closing. Nothing to recover.
186
+ }
187
+ socket = null;
188
+ }
189
+ pendingAuthEventId = null;
190
+ if (authFailures >= MAX_AUTH_FAILURES) {
191
+ setState('failed');
192
+ log('giving up after repeated auth refusals', { authFailures, reason });
193
+ return;
194
+ }
195
+ setState('reconnecting');
196
+ const delay = Math.min(backoff.maxMs, backoff.baseMs * 2 ** attempt) * (0.5 + backoff.jitter() / 2);
197
+ attempt += 1;
198
+ log('reconnecting', { reason, delayMs: Math.round(delay), attempt });
199
+ reconnectTimer = setTimer(() => {
200
+ reconnectTimer = null;
201
+ connect();
202
+ }, delay);
203
+ }
204
+ async function answerChallenge(challenge) {
205
+ const credential = options.getCredential();
206
+ if (!credential) {
207
+ // Not signed in. Not an auth *failure* — there is nothing to sign with,
208
+ // and a session may yet appear, so this does not count toward the cap.
209
+ log('no credential; deferring auth');
210
+ scheduleReconnect('no credential');
211
+ return;
212
+ }
213
+ setState('authenticating');
214
+ try {
215
+ const unsigned = buildUnsignedRelayAuthEvent({
216
+ // Left empty deliberately: `/sign` overwrites it with the token's
217
+ // subject, and `expectedPubkey` below is the check that it matched.
218
+ pubkey: '',
219
+ relayUrl: wsUrl,
220
+ challenge,
221
+ nowMs: now(),
222
+ });
223
+ // `expectedPubkey` is not a request — `/sign` signs as the token's
224
+ // subject whatever it is handed, so a mismatch returns HTTP 200 with a
225
+ // valid event authored by somebody else. Passing it is the only thing
226
+ // that catches that, and `bridge.ts` says not to drop it.
227
+ const signed = await options.sign(unsigned, credential.accessToken, credential.pubkey);
228
+ if (stopped || !socket)
229
+ return;
230
+ pendingAuthEventId = signed.id;
231
+ send(['AUTH', signed]);
232
+ }
233
+ catch (error) {
234
+ // Signing failed: an expired token, /sign refusing the kind, the network.
235
+ // Not counted as an auth refusal — the relay never saw anything, and the
236
+ // common cause (a token that just expired) fixes itself on reconnect.
237
+ log('could not sign the auth challenge', { reason: error.message });
238
+ scheduleReconnect('sign failed');
239
+ }
240
+ }
241
+ function onAuthResult(frame) {
242
+ pendingAuthEventId = null;
243
+ if (!frame.accepted) {
244
+ // The connection is now `AuthState::Failed` relay-side and will refuse
245
+ // every REQ while staying open. There is no recovery on this socket.
246
+ authFailures += 1;
247
+ log('relay refused the auth event', { reason: frame.message, authFailures });
248
+ scheduleReconnect('auth refused');
249
+ return;
250
+ }
251
+ authFailures = 0;
252
+ attempt = 0;
253
+ setState('live');
254
+ log('authenticated', { subscriptions: subscriptions.size });
255
+ // Re-issue every live subscription. A reconnect that restores the socket
256
+ // and not the subscriptions is the silent half of this failure: the app
257
+ // looks connected and never hears anything again.
258
+ for (const sub of subscriptions.values())
259
+ openSubscription(sub);
260
+ }
261
+ function onFrame(raw) {
262
+ const frame = parseFrame(raw);
263
+ switch (frame.type) {
264
+ case 'AUTH':
265
+ void answerChallenge(frame.challenge);
266
+ return;
267
+ case 'OK':
268
+ if (pendingAuthEventId && frame.eventId === pendingAuthEventId)
269
+ onAuthResult(frame);
270
+ return;
271
+ case 'EVENT':
272
+ subscriptions.get(frame.subId)?.onEvent(frame.event);
273
+ return;
274
+ case 'EOSE':
275
+ subscriptions.get(frame.subId)?.onEose?.();
276
+ return;
277
+ case 'CLOSED': {
278
+ const sub = subscriptions.get(frame.subId);
279
+ log('subscription closed by the relay', { subId: frame.subId, reason: frame.message });
280
+ sub?.onClosed?.(frame.message);
281
+ // `auth-required` here means the connection is unauthenticated — the
282
+ // poisoned-but-open state. Reconnecting is the only fix, and dropping
283
+ // the subscription would hide it.
284
+ if (frame.message.startsWith('auth-required'))
285
+ scheduleReconnect('req refused');
286
+ return;
287
+ }
288
+ case 'NOTICE':
289
+ log('relay notice', { message: frame.message });
290
+ return;
291
+ default:
292
+ return;
293
+ }
294
+ }
295
+ function connect() {
296
+ if (stopped)
297
+ return;
298
+ setState(attempt === 0 ? 'connecting' : 'reconnecting');
299
+ let created;
300
+ try {
301
+ created = makeSocket(wsUrl);
302
+ }
303
+ catch (error) {
304
+ log('could not open a socket', { reason: error.message });
305
+ scheduleReconnect('open threw');
306
+ return;
307
+ }
308
+ socket = created;
309
+ created.onopen = () => {
310
+ // Nothing to do but wait: Buzz sends `["AUTH", challenge]` immediately on
311
+ // connect, so authentication starts from the message handler.
312
+ log('socket open');
313
+ };
314
+ created.onmessage = (ev) => {
315
+ if (socket !== created)
316
+ return;
317
+ onFrame(ev.data);
318
+ };
319
+ created.onerror = () => {
320
+ // `onclose` always follows, and that is where recovery lives. Logging
321
+ // here only helps distinguish a refused connection from a clean drop.
322
+ log('socket error');
323
+ };
324
+ created.onclose = () => {
325
+ if (socket !== created)
326
+ return;
327
+ socket = null;
328
+ scheduleReconnect('socket closed');
329
+ };
330
+ }
331
+ connect();
332
+ return {
333
+ subscribe(filters, onEvent, subOptions) {
334
+ const id = `${options.subscriptionPrefix ?? 'sub'}-${nextSubId++}`;
335
+ const sub = {
336
+ id,
337
+ filters,
338
+ onEvent,
339
+ onEose: subOptions?.onEose,
340
+ onClosed: subOptions?.onClosed,
341
+ };
342
+ subscriptions.set(id, sub);
343
+ if (state === 'live')
344
+ openSubscription(sub);
345
+ return {
346
+ close() {
347
+ if (!subscriptions.delete(id))
348
+ return;
349
+ if (state === 'live')
350
+ send(['CLOSE', id]);
351
+ },
352
+ };
353
+ },
354
+ reconnect() {
355
+ if (stopped)
356
+ return;
357
+ if (reconnectTimer !== null) {
358
+ clearTimer(reconnectTimer);
359
+ reconnectTimer = null;
360
+ }
361
+ // Reset the backoff: this is not another failed attempt in a series, it
362
+ // is new information that the network changed.
363
+ attempt = 0;
364
+ if (socket) {
365
+ socket.onopen = socket.onmessage = socket.onclose = socket.onerror = null;
366
+ try {
367
+ socket.close();
368
+ }
369
+ catch {
370
+ // Already gone.
371
+ }
372
+ socket = null;
373
+ }
374
+ pendingAuthEventId = null;
375
+ connect();
376
+ },
377
+ state: () => state,
378
+ close() {
379
+ stopped = true;
380
+ if (reconnectTimer !== null) {
381
+ clearTimer(reconnectTimer);
382
+ reconnectTimer = null;
383
+ }
384
+ subscriptions.clear();
385
+ if (socket) {
386
+ socket.onopen = socket.onmessage = socket.onclose = socket.onerror = null;
387
+ try {
388
+ socket.close();
389
+ }
390
+ catch {
391
+ // Already gone.
392
+ }
393
+ socket = null;
394
+ }
395
+ },
396
+ };
397
+ }
398
+ //# sourceMappingURL=live.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ /** Regroup bytes between bit widths, e.g. 8-bit bytes → 5-bit bech32 symbols. */
2
+ export declare function convertBits(data: ArrayLike<number>, from: number, to: number, pad: boolean): number[];
3
+ /**
4
+ * Exported for the bare NIP-19 types (`npub`, `note`) and for tests.
5
+ *
6
+ * The checksum layer is the part worth testing directly: a wrong constant still
7
+ * round-trips against itself, so only an external vector catches it.
8
+ *
9
+ * Strict about case, deliberately — BIP-173 forbids mixing. `decodeNaddr` is the
10
+ * forgiving entry point, because what reaches it was typed or pasted by a person.
11
+ */
12
+ export declare function bech32Encode(hrp: string, data: number[]): string;
13
+ export declare function bech32Decode(encoded: string): {
14
+ hrp: string;
15
+ data: number[];
16
+ };
17
+ /** A decoded `naddr` — everything needed to fetch the event it points at. */
18
+ export interface AddressPointer {
19
+ /** The `d` tag of the addressable event. */
20
+ identifier: string;
21
+ pubkey: string;
22
+ kind: number;
23
+ /** Relay hints, in the order they appeared. May be empty. */
24
+ relays: string[];
25
+ }
26
+ /**
27
+ * Encode an address as `naddr1…`.
28
+ *
29
+ * TLV order is identifier, relays, author, kind — see the header on why that is
30
+ * a choice rather than a rule.
31
+ */
32
+ export declare function encodeNaddr(pointer: AddressPointer): string;
33
+ /**
34
+ * Decode `naddr1…`, with or without a `nostr:` prefix, in any case.
35
+ *
36
+ * Forgiving on purpose: what arrives here was pasted by a person, sometimes out
37
+ * of an email client that capitalised the first letter. `bech32Decode` is the
38
+ * strict primitive underneath — this lowercases first, so a mixed-case string
39
+ * that would be refused there is accepted here.
40
+ */
41
+ export declare function decodeNaddr(encoded: string): AddressPointer;
42
+ /** `<kind>:<pubkey>:<d>` — the form used in `a` tags and relay filters. */
43
+ export declare function pointerToAddress(pointer: AddressPointer): string;
44
+ /**
45
+ * The inverse: `<kind>:<pubkey>:<d>` back to a pointer.
46
+ *
47
+ * An address carries no relay hints — `naddr` has a TLV for them and an `a` tag
48
+ * does not — so the relays come back empty. That is a real loss of information,
49
+ * not an oversight: it is why the two forms are not interchangeable and why the
50
+ * encoder is not simply run in reverse.
51
+ *
52
+ * The `d` identifier may itself contain colons, so only the first two are
53
+ * separators.
54
+ */
55
+ export declare function addressToPointer(address: string): AddressPointer;
56
+ /** `<kind>:<pubkey>:<d>` → `naddr1…`. */
57
+ export declare function addrToNaddr(address: string, relays?: string[]): string;
58
+ /** `naddr1…` → `<kind>:<pubkey>:<d>`. */
59
+ export declare function naddrToAddr(encoded: string): string;
60
+ /**
61
+ * A pointer from *either* form a reference arrives in.
62
+ *
63
+ * A message composed in an app carries `nostr:naddr1…` in its body. The same
64
+ * message read back off the relay carries the same reference as an `a` tag,
65
+ * which is a plain `<kind>:<pubkey>:<d>` address — that is what the NIP says a
66
+ * tag holds.
67
+ *
68
+ * Both name one object. Accepting only the first is what made a reference stop
69
+ * rendering the moment its own message came back from the relay (FEE-2).
70
+ */
71
+ export declare function referenceToPointer(input: string): AddressPointer;
72
+ /**
73
+ * Every `nostr:naddr1…` in a body of text (NIP-27).
74
+ *
75
+ * The character class is bech32's own alphabet, which excludes `1`, `b`, `i`
76
+ * and `o`, so a match ends cleanly at punctuation without a lookahead.
77
+ */
78
+ export declare const NADDR_RE: RegExp;
79
+ export declare function findNaddrs(text: string): string[];
80
+ /**
81
+ * The same text with every `nostr:naddr1…` taken out (PEEK-18).
82
+ *
83
+ * A reference that resolves into a widget should not also sit in the prose as
84
+ * sixty characters of bech32: the widget *is* the reference, rendered. Ship
85
+ * appends one to every thread it starts about an issue, so leaving it in means
86
+ * most cross-app messages open with a wall of noise nobody reads.
87
+ *
88
+ * Whitespace is repaired rather than merely removed. A pointer is usually
89
+ * trailing or on a line of its own, and deleting it in place otherwise leaves a
90
+ * double space mid-sentence or a hole between paragraphs — both of which look
91
+ * like the message itself is broken. Runs of blanks collapse *within* a line
92
+ * only, so indentation and paragraph breaks survive.
93
+ *
94
+ * Display-only. The stored body keeps the pointer, which is what lets the
95
+ * reference still be found, resolved and followed.
96
+ */
97
+ export declare function stripNaddrs(text: string): string;
98
+ //# sourceMappingURL=nip19.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nip19.d.ts","sourceRoot":"","sources":["../src/nip19.ts"],"names":[],"mappings":"AAkFA,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAqBrG;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAGhE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAgB7E;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAWD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CA8B3D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAyC3D;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAEhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAWhE;AAED,yCAAyC;AACzC,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,EAAO,GAAG,MAAM,CAG1E;AAED,yCAAyC;AACzC,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAKhE;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,QAAwD,CAAA;AAE7E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBhD"}