nostr-double-ratchet 0.0.37 → 0.0.48

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.
@@ -58,8 +58,7 @@ export class LocalStorageAdapter implements StorageAdapter {
58
58
  try {
59
59
  const item = localStorage.getItem(this.getFullKey(key))
60
60
  return item ? JSON.parse(item) : undefined
61
- } catch (e) {
62
- console.warn(`Failed to get key ${key} from localStorage:`, e)
61
+ } catch {
63
62
  return undefined
64
63
  }
65
64
  }
@@ -76,8 +75,8 @@ export class LocalStorageAdapter implements StorageAdapter {
76
75
  async del(key: string): Promise<void> {
77
76
  try {
78
77
  localStorage.removeItem(this.getFullKey(key))
79
- } catch (e) {
80
- console.warn(`Failed to delete key ${key} from localStorage:`, e)
78
+ } catch {
79
+ // Ignore deletion failures
81
80
  }
82
81
  }
83
82
 
@@ -93,8 +92,8 @@ export class LocalStorageAdapter implements StorageAdapter {
93
92
  keys.push(key.substring(this.keyPrefix.length))
94
93
  }
95
94
  }
96
- } catch (e) {
97
- console.warn("Failed to list keys from localStorage:", e)
95
+ } catch {
96
+ // Ignore list failures
98
97
  }
99
98
 
100
99
  return keys
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export * from "./Session"
2
2
  export * from "./Invite"
3
+ export * from "./AppKeys"
4
+ export * from "./inviteUtils"
3
5
  export * from "./types"
4
6
  export * from "./utils"
5
7
  export * from "./SessionManager"
6
- export * from "./StorageAdapter"
8
+ export * from "./AppKeysManager"
9
+ export * from "./StorageAdapter"
@@ -0,0 +1,271 @@
1
+ import { generateSecretKey, getPublicKey, nip44 } from 'nostr-tools'
2
+ import { getConversationKey } from 'nostr-tools/nip44'
3
+ import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
4
+ import { Session } from './Session'
5
+ import { NostrSubscribe, INVITE_RESPONSE_KIND, EncryptFunction, DecryptFunction, KeyPair } from './types'
6
+
7
+ /**
8
+ * Device payload for QR code / text code sharing.
9
+ */
10
+ export interface DevicePayload {
11
+ /** Ephemeral public key (64 hex chars) */
12
+ ephemeralPubkey: string
13
+ /** Shared secret (64 hex chars) */
14
+ sharedSecret: string
15
+ /** Device ID (16 hex chars) */
16
+ deviceId: string
17
+ /** Human-readable device label */
18
+ deviceLabel: string
19
+ /** Identity public key for this device (64 hex chars) */
20
+ identityPubkey: string
21
+ }
22
+
23
+ /**
24
+ * Generates a new ephemeral keypair for invites.
25
+ * @returns A keypair with publicKey (hex string) and privateKey (Uint8Array)
26
+ */
27
+ export function generateEphemeralKeypair(): KeyPair {
28
+ const privateKey = generateSecretKey()
29
+ const publicKey = getPublicKey(privateKey)
30
+ return { publicKey, privateKey }
31
+ }
32
+
33
+ /**
34
+ * Generates a new shared secret for invite handshakes.
35
+ * @returns A 64-character hex string (32 bytes)
36
+ */
37
+ export function generateSharedSecret(): string {
38
+ return bytesToHex(generateSecretKey())
39
+ }
40
+
41
+ /**
42
+ * Generates a unique device ID.
43
+ * @returns A random device ID string
44
+ */
45
+ export function generateDeviceId(): string {
46
+ return bytesToHex(generateSecretKey()).slice(0, 16)
47
+ }
48
+
49
+ export interface EncryptInviteResponseParams {
50
+ /** The invitee's session public key */
51
+ inviteeSessionPublicKey: string
52
+ /** The invitee's identity public key (also serves as device ID) */
53
+ inviteePublicKey: string
54
+ /** The invitee's identity private key (optional if encrypt function provided) */
55
+ inviteePrivateKey?: Uint8Array
56
+ /** The inviter's identity public key */
57
+ inviterPublicKey: string
58
+ /** The inviter's ephemeral public key */
59
+ inviterEphemeralPublicKey: string
60
+ /** The shared secret for the invite */
61
+ sharedSecret: string
62
+ /** The invitee's owner/Nostr identity public key (optional for single-device users) */
63
+ ownerPublicKey?: string
64
+ /** Optional custom encrypt function */
65
+ encrypt?: EncryptFunction
66
+ }
67
+
68
+ export interface EncryptedInviteResponse {
69
+ /** The inner event containing the encrypted payload */
70
+ innerEvent: {
71
+ pubkey: string
72
+ content: string
73
+ created_at: number
74
+ }
75
+ /** The outer envelope event */
76
+ envelope: {
77
+ kind: number
78
+ pubkey: string
79
+ content: string
80
+ created_at: number
81
+ tags: string[][]
82
+ }
83
+ /** The random sender's public key used for the envelope */
84
+ randomSenderPublicKey: string
85
+ /** The random sender's private key used for the envelope */
86
+ randomSenderPrivateKey: Uint8Array
87
+ }
88
+
89
+ const TWO_DAYS = 2 * 24 * 60 * 60
90
+ const now = () => Math.round(Date.now() / 1000)
91
+ const randomNow = () => Math.round(now() - Math.random() * TWO_DAYS)
92
+
93
+ /**
94
+ * Encrypts an invite response with two-layer encryption.
95
+ *
96
+ * Layer 1 (inner): Payload encrypted with DH key, then encrypted with shared secret.
97
+ * Layer 2 (outer): Envelope encrypted with random key -> inviter ephemeral key.
98
+ */
99
+ export async function encryptInviteResponse(params: EncryptInviteResponseParams): Promise<EncryptedInviteResponse> {
100
+ const {
101
+ inviteeSessionPublicKey,
102
+ inviteePublicKey,
103
+ inviteePrivateKey,
104
+ inviterPublicKey,
105
+ inviterEphemeralPublicKey,
106
+ sharedSecret,
107
+ ownerPublicKey,
108
+ encrypt,
109
+ } = params
110
+
111
+ const sharedSecretBytes = hexToBytes(sharedSecret)
112
+
113
+ // Create the encrypt function
114
+ const encryptFn = encrypt ?? (async (plaintext: string, pubkey: string) => {
115
+ if (!inviteePrivateKey) {
116
+ throw new Error('inviteePrivateKey is required when encrypt function is not provided')
117
+ }
118
+ return nip44.encrypt(plaintext, getConversationKey(inviteePrivateKey, pubkey))
119
+ })
120
+
121
+ // Create the payload
122
+ // Note: deviceId is no longer needed - inviteePublicKey (identity) serves as device ID
123
+ const payload = JSON.stringify({
124
+ sessionKey: inviteeSessionPublicKey,
125
+ ...(ownerPublicKey && { ownerPublicKey }),
126
+ })
127
+
128
+ // Encrypt with DH key (invitee -> inviter)
129
+ const dhEncrypted = await encryptFn(payload, inviterPublicKey)
130
+
131
+ // Encrypt with shared secret
132
+ const innerEvent = {
133
+ pubkey: inviteePublicKey,
134
+ content: nip44.encrypt(dhEncrypted, sharedSecretBytes),
135
+ created_at: now(),
136
+ }
137
+
138
+ // Create a random keypair for the envelope sender
139
+ const randomSenderPrivateKey = generateSecretKey()
140
+ const randomSenderPublicKey = getPublicKey(randomSenderPrivateKey)
141
+
142
+ // Encrypt the inner event with the random key -> inviter ephemeral key
143
+ const innerJson = JSON.stringify(innerEvent)
144
+ const envelope = {
145
+ kind: INVITE_RESPONSE_KIND,
146
+ pubkey: randomSenderPublicKey,
147
+ content: nip44.encrypt(innerJson, getConversationKey(randomSenderPrivateKey, inviterEphemeralPublicKey)),
148
+ created_at: randomNow(),
149
+ tags: [['p', inviterEphemeralPublicKey]],
150
+ }
151
+
152
+ return {
153
+ innerEvent,
154
+ envelope,
155
+ randomSenderPublicKey,
156
+ randomSenderPrivateKey,
157
+ }
158
+ }
159
+
160
+ export interface DecryptInviteResponseParams {
161
+ /** The encrypted envelope content */
162
+ envelopeContent: string
163
+ /** The envelope sender's public key */
164
+ envelopeSenderPubkey: string
165
+ /** The inviter's ephemeral private key */
166
+ inviterEphemeralPrivateKey: Uint8Array
167
+ /** The inviter's identity private key (optional if decrypt function provided) */
168
+ inviterPrivateKey?: Uint8Array
169
+ /** The shared secret for the invite */
170
+ sharedSecret: string
171
+ /** Optional custom decrypt function */
172
+ decrypt?: DecryptFunction
173
+ }
174
+
175
+ export interface DecryptedInviteResponse {
176
+ /** The invitee's identity public key (also serves as device ID) */
177
+ inviteeIdentity: string
178
+ /** The invitee's session public key */
179
+ inviteeSessionPublicKey: string
180
+ /** The invitee's owner/Nostr identity public key (optional for backward compat) */
181
+ ownerPublicKey?: string
182
+ }
183
+
184
+ /**
185
+ * Decrypts an invite response.
186
+ */
187
+ export async function decryptInviteResponse(params: DecryptInviteResponseParams): Promise<DecryptedInviteResponse> {
188
+ const {
189
+ envelopeContent,
190
+ envelopeSenderPubkey,
191
+ inviterEphemeralPrivateKey,
192
+ inviterPrivateKey,
193
+ sharedSecret,
194
+ decrypt,
195
+ } = params
196
+
197
+ const sharedSecretBytes = hexToBytes(sharedSecret)
198
+
199
+ // Decrypt the outer envelope
200
+ const decrypted = nip44.decrypt(
201
+ envelopeContent,
202
+ getConversationKey(inviterEphemeralPrivateKey, envelopeSenderPubkey)
203
+ )
204
+ const innerEvent = JSON.parse(decrypted)
205
+
206
+ const inviteeIdentity = innerEvent.pubkey
207
+
208
+ // Decrypt the inner content using shared secret
209
+ const dhEncrypted = nip44.decrypt(innerEvent.content, sharedSecretBytes)
210
+
211
+ // Create the decrypt function
212
+ const decryptFn = decrypt ?? (async (ciphertext: string, pubkey: string) => {
213
+ if (!inviterPrivateKey) {
214
+ throw new Error('inviterPrivateKey is required when decrypt function is not provided')
215
+ }
216
+ return nip44.decrypt(ciphertext, getConversationKey(inviterPrivateKey, pubkey))
217
+ })
218
+
219
+ // Decrypt using DH key
220
+ const decryptedPayload = await decryptFn(dhEncrypted, inviteeIdentity)
221
+
222
+ let inviteeSessionPublicKey: string
223
+ let ownerPublicKey: string | undefined
224
+
225
+ try {
226
+ const parsed = JSON.parse(decryptedPayload)
227
+ inviteeSessionPublicKey = parsed.sessionKey
228
+ ownerPublicKey = parsed.ownerPublicKey
229
+ } catch {
230
+ // Backward compatibility: plain session key
231
+ inviteeSessionPublicKey = decryptedPayload
232
+ }
233
+
234
+ return {
235
+ inviteeIdentity,
236
+ inviteeSessionPublicKey,
237
+ ownerPublicKey,
238
+ }
239
+ }
240
+
241
+ export interface CreateSessionFromAcceptParams {
242
+ /** Nostr subscription function */
243
+ nostrSubscribe: NostrSubscribe
244
+ /** The other party's public key */
245
+ theirPublicKey: string
246
+ /** Our session private key */
247
+ ourSessionPrivateKey: Uint8Array
248
+ /** The shared secret (hex string) */
249
+ sharedSecret: string
250
+ /** Whether we are the sender (initiator) */
251
+ isSender: boolean
252
+ /** Optional session name */
253
+ name?: string
254
+ }
255
+
256
+ /**
257
+ * Creates a Session from invite acceptance parameters.
258
+ */
259
+ export function createSessionFromAccept(params: CreateSessionFromAcceptParams): Session {
260
+ const {
261
+ nostrSubscribe,
262
+ theirPublicKey,
263
+ ourSessionPrivateKey,
264
+ sharedSecret,
265
+ isSender,
266
+ name,
267
+ } = params
268
+
269
+ const sharedSecretBytes = hexToBytes(sharedSecret)
270
+ return Session.init(nostrSubscribe, theirPublicKey, ourSessionPrivateKey, isSender, sharedSecretBytes, name)
271
+ }
package/src/types.ts CHANGED
@@ -68,11 +68,18 @@ export type Unsubscribe = () => void;
68
68
  export type NostrSubscribe = (_filter: Filter, _onEvent: (_e: VerifiedEvent) => void) => Unsubscribe;
69
69
  export type EncryptFunction = (_plaintext: string, _pubkey: string) => Promise<string>;
70
70
  export type DecryptFunction = (_ciphertext: string, _pubkey: string) => Promise<string>;
71
+
72
+ /**
73
+ * Identity key for cryptographic operations.
74
+ * Either a raw private key (Uint8Array) or encrypt/decrypt functions for extension login (NIP-07).
75
+ */
76
+ export type IdentityKey = Uint8Array | { encrypt: EncryptFunction; decrypt: DecryptFunction };
77
+
71
78
  export type NostrPublish = (_event: UnsignedEvent) => Promise<VerifiedEvent>;
72
79
 
73
80
  export type Rumor = UnsignedEvent & { id: string }
74
81
 
75
- /**
82
+ /**
76
83
  * Callback function for handling decrypted messages
77
84
  * @param _event - The decrypted message object (Rumor)
78
85
  * @param _outerEvent - The outer Nostr event (VerifiedEvent)
@@ -91,9 +98,10 @@ export const INVITE_EVENT_KIND = 30078;
91
98
 
92
99
  export const INVITE_RESPONSE_KIND = 1059;
93
100
 
101
+ export const APP_KEYS_EVENT_KIND = 30078;
102
+
94
103
  export const CHAT_MESSAGE_KIND = 14;
95
104
 
96
- export const MAX_SKIP = 100;
97
105
 
98
106
  export type NostrEvent = {
99
107
  id: string;
@@ -104,3 +112,30 @@ export type NostrEvent = {
104
112
  content: string;
105
113
  sig: string;
106
114
  }
115
+
116
+ /**
117
+ * Payload for reaction messages sent through NDR.
118
+ * Reactions are regular messages with a JSON payload indicating they're a reaction.
119
+ */
120
+ export interface ReactionPayload {
121
+ type: 'reaction';
122
+ /** ID of the message being reacted to */
123
+ messageId: string;
124
+ /** Emoji or reaction content */
125
+ emoji: string;
126
+ }
127
+
128
+ /**
129
+ * Kind constant for reaction inner events
130
+ */
131
+ export const REACTION_KIND = 7;
132
+
133
+ /**
134
+ * Kind constant for read/delivery receipt inner events
135
+ */
136
+ export const RECEIPT_KIND = 15;
137
+
138
+ /**
139
+ * Kind constant for typing indicator inner events
140
+ */
141
+ export const TYPING_KIND = 25;
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
2
- import { Rumor, SessionState } from "./types";
2
+ import { Rumor, SessionState, ReactionPayload } from "./types";
3
3
  import { Session } from "./Session.ts";
4
4
  import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf';
5
5
  import { sha256 } from '@noble/hashes/sha256';
@@ -136,12 +136,12 @@ export function deepCopyState(s: SessionState): SessionState {
136
136
  receivingChainMessageNumber: s.receivingChainMessageNumber,
137
137
  previousSendingChainMessageCount: s.previousSendingChainMessageCount,
138
138
  skippedKeys: Object.fromEntries(
139
- Object.entries(s.skippedKeys).map(([author, entry]: any) => [
139
+ Object.entries(s.skippedKeys).map(([author, entry]) => [
140
140
  author,
141
141
  {
142
- headerKeys: entry.headerKeys.map((hk: Uint8Array) => new Uint8Array(hk)),
142
+ headerKeys: entry.headerKeys.map((hk) => new Uint8Array(hk)),
143
143
  messageKeys: Object.fromEntries(
144
- Object.entries(entry.messageKeys).map(([n, mk]: any) => [n, new Uint8Array(mk)])
144
+ Object.entries(entry.messageKeys).map(([n, mk]) => [n, new Uint8Array(mk)])
145
145
  ),
146
146
  },
147
147
  ])
@@ -188,10 +188,6 @@ export function kdf(input1: Uint8Array, input2: Uint8Array = new Uint8Array(32),
188
188
  return outputs;
189
189
  }
190
190
 
191
- export function skippedMessageIndexKey(_nostrSender: string, _number: number): string {
192
- return `${_nostrSender}:${_number}`;
193
- }
194
-
195
191
  export function getMillisecondTimestamp(event: Rumor) {
196
192
  const msTag = event.tags?.find((tag: string[]) => tag[0] === "ms");
197
193
  if (msTag) {
@@ -199,3 +195,44 @@ export function getMillisecondTimestamp(event: Rumor) {
199
195
  }
200
196
  return event.created_at * 1000;
201
197
  }
198
+
199
+ /**
200
+ * Check if a message content is a reaction payload.
201
+ * @param content The message content to check
202
+ * @returns The parsed ReactionPayload if valid, null otherwise
203
+ */
204
+ export function parseReaction(content: string): ReactionPayload | null {
205
+ try {
206
+ const parsed = JSON.parse(content);
207
+ if (parsed.type === 'reaction' && typeof parsed.messageId === 'string' && typeof parsed.emoji === 'string') {
208
+ return parsed as ReactionPayload;
209
+ }
210
+ } catch {
211
+ // Not JSON or invalid structure
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * Check if a message content is a reaction.
218
+ * @param content The message content to check
219
+ * @returns true if the content is a reaction payload
220
+ */
221
+ export function isReaction(content: string): boolean {
222
+ return parseReaction(content) !== null;
223
+ }
224
+
225
+ /**
226
+ * Create a reaction payload JSON string.
227
+ * @param messageId The ID of the message being reacted to
228
+ * @param emoji The emoji or reaction content
229
+ * @returns JSON string of the reaction payload
230
+ */
231
+ export function createReactionPayload(messageId: string, emoji: string): string {
232
+ const payload: ReactionPayload = {
233
+ type: 'reaction',
234
+ messageId,
235
+ emoji
236
+ };
237
+ return JSON.stringify(payload);
238
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Martti Malmi
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.
@@ -1,117 +0,0 @@
1
- import { Session } from './Session';
2
- import { NostrSubscribe } from './types';
3
- interface DeviceRecord {
4
- deviceId: string;
5
- publicKey: string;
6
- activeSession?: Session;
7
- inactiveSessions: Session[];
8
- isStale: boolean;
9
- staleTimestamp?: number;
10
- lastActivity?: number;
11
- }
12
- /**
13
- * Manages sessions for a single user across multiple devices
14
- * Structure: UserRecord → DeviceRecord → Sessions
15
- */
16
- export declare class UserRecord {
17
- readonly userId: string;
18
- private readonly nostrSubscribe;
19
- private deviceRecords;
20
- private isStale;
21
- private staleTimestamp?;
22
- constructor(userId: string, nostrSubscribe: NostrSubscribe);
23
- /**
24
- * Creates or updates a device record for this user
25
- */
26
- upsertDevice(deviceId: string, publicKey: string): DeviceRecord;
27
- /**
28
- * Gets a device record by deviceId
29
- */
30
- getDevice(deviceId: string): DeviceRecord | undefined;
31
- /**
32
- * Gets all device records for this user
33
- */
34
- getAllDevices(): DeviceRecord[];
35
- /**
36
- * Gets all active (non-stale) device records
37
- */
38
- getActiveDevices(): DeviceRecord[];
39
- /**
40
- * Removes a device record and closes all its sessions
41
- */
42
- removeDevice(deviceId: string): boolean;
43
- /**
44
- * Adds or updates a session for a specific device
45
- */
46
- upsertSession(deviceId: string, session: Session, publicKey?: string): void;
47
- /**
48
- * Gets the active session for a specific device
49
- */
50
- getActiveSession(deviceId: string): Session | undefined;
51
- /**
52
- * Gets all sessions (active + inactive) for a specific device
53
- */
54
- getDeviceSessions(deviceId: string): Session[];
55
- /**
56
- * Gets all active sessions across all devices for this user
57
- */
58
- getActiveSessions(): Session[];
59
- /**
60
- * Gets all sessions (active + inactive) across all devices
61
- */
62
- getAllSessions(): Session[];
63
- /**
64
- * Gets sessions that are ready to send messages
65
- */
66
- getSendableSessions(): Session[];
67
- /**
68
- * Marks a specific device as stale
69
- */
70
- markDeviceStale(deviceId: string): void;
71
- /**
72
- * Marks the entire user record as stale
73
- */
74
- markUserStale(): void;
75
- /**
76
- * Removes stale devices and sessions older than maxLatency
77
- */
78
- pruneStaleRecords(maxLatency: number): void;
79
- /**
80
- * Gets the most recently active device
81
- */
82
- getMostActiveDevice(): DeviceRecord | undefined;
83
- /**
84
- * Gets the preferred session (from most active device)
85
- */
86
- getPreferredSession(): Session | undefined;
87
- /**
88
- * Checks if this user has any active sessions
89
- */
90
- hasActiveSessions(): boolean;
91
- /**
92
- * Gets total count of devices
93
- */
94
- getDeviceCount(): number;
95
- /**
96
- * Gets total count of active sessions
97
- */
98
- getActiveSessionCount(): number;
99
- /**
100
- * Cleanup when destroying the user record
101
- */
102
- close(): void;
103
- /**
104
- * @deprecated Use upsertDevice instead
105
- */
106
- conditionalUpdate(deviceId: string, publicKey: string): void;
107
- /**
108
- * @deprecated Use upsertSession instead
109
- */
110
- insertSession(deviceId: string, session: Session): void;
111
- /**
112
- * Creates a new session for a device
113
- */
114
- createSession(deviceId: string, sharedSecret: Uint8Array, ourCurrentPrivateKey: Uint8Array, isInitiator: boolean, name?: string): Session;
115
- }
116
- export {};
117
- //# sourceMappingURL=UserRecord.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"UserRecord.d.ts","sourceRoot":"","sources":["../src/UserRecord.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,EAAE,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,qBAAa,UAAU;aAMH,MAAM,EAAE,MAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc;IANjC,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAS;gBAGd,MAAM,EAAE,MAAM,EACb,cAAc,EAAE,cAAc;IAQjD;;OAEG;IACI,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY;IAyBtE;;OAEG;IACI,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI5D;;OAEG;IACI,aAAa,IAAI,YAAY,EAAE;IAItC;;OAEG;IACI,gBAAgB,IAAI,YAAY,EAAE;IAKzC;;OAEG;IACI,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAe9C;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAclF;;OAEG;IACI,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAK9D;;OAEG;IACI,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE;IAYrD;;OAEG;IACI,iBAAiB,IAAI,OAAO,EAAE;IAsBrC;;OAEG;IACI,cAAc,IAAI,OAAO,EAAE;IAalC;;OAEG;IACI,mBAAmB,IAAI,OAAO,EAAE;IAUvC;;OAEG;IACI,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQ9C;;OAEG;IACI,aAAa,IAAI,IAAI;IAK5B;;OAEG;IACI,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAsBlD;;OAEG;IACI,mBAAmB,IAAI,YAAY,GAAG,SAAS;IAWtD;;OAEG;IACI,mBAAmB,IAAI,OAAO,GAAG,SAAS;IAKjD;;OAEG;IACI,iBAAiB,IAAI,OAAO;IAInC;;OAEG;IACI,cAAc,IAAI,MAAM;IAI/B;;OAEG;IACI,qBAAqB,IAAI,MAAM;IAItC;;OAEG;IACI,KAAK,IAAI,IAAI;IAYpB;;OAEG;IACI,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAInE;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9D;;OAEG;IACI,aAAa,CAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,UAAU,EACxB,oBAAoB,EAAE,UAAU,EAChC,WAAW,EAAE,OAAO,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO;CAkBX"}