1use std::borrow::Cow;
7use std::sync::Arc;
8
9use aes_gcm::{Aes256Gcm, KeyInit as AesKeyInit, Nonce as AesNonce, aead::Aead as AesAead};
10use base64::Engine;
11use base64::engine::general_purpose::STANDARD as BASE64;
12use chacha20poly1305::{ChaCha20Poly1305, Nonce};
13use getrandom::getrandom;
14use serde::de::Deserializer;
15use serde::ser::SerializeMap;
16use serde::{Deserialize, Serialize};
17use serde_json::{Value, json};
18use thiserror::Error;
19
20const AES256_NONCE_LEN: usize = 12;
21const CHACHA20_NONCE_LEN: usize = 12;
22const MAX_FRAME_BYTES: usize = 10 * 1024 * 1024;
23const MAX_PAYLOAD_BYTES: usize = MAX_FRAME_BYTES - 6;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[repr(u8)]
28pub enum MessageType {
29 Api = 0x00,
31 Event = 0x01,
33}
34
35impl TryFrom<u8> for MessageType {
36 type Error = ProtocolError;
37
38 fn try_from(value: u8) -> Result<Self, Self::Error> {
39 match value {
40 0x00 => Ok(Self::Api),
41 0x01 => Ok(Self::Event),
42 _ => Err(ProtocolError::UnknownMessageType(value)),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[repr(u8)]
50pub enum EncryptionKind {
51 None = 0x00,
53 ChaCha20 = 0x01,
55 Aes256 = 0x02,
57}
58
59impl TryFrom<u8> for EncryptionKind {
60 type Error = ProtocolError;
61
62 fn try_from(value: u8) -> Result<Self, Self::Error> {
63 match value {
64 0x00 => Ok(Self::None),
65 0x01 => Ok(Self::ChaCha20),
66 0x02 => Ok(Self::Aes256),
67 _ => Err(ProtocolError::UnknownEncryption(value)),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct FileAttachment {
75 pub id: String,
77 pub name: String,
79 pub content_type: String,
81 pub encoding: String,
83 pub data: String,
85 pub size: usize,
87}
88
89impl FileAttachment {
90 pub fn inline_text(
92 id: impl Into<String>,
93 name: impl Into<String>,
94 content_type: impl Into<String>,
95 text: impl AsRef<str>,
96 ) -> Self {
97 Self::inline_bytes(id, name, content_type, text.as_ref().as_bytes().to_vec())
98 }
99
100 pub fn inline_bytes(
102 id: impl Into<String>,
103 name: impl Into<String>,
104 content_type: impl Into<String>,
105 bytes: Vec<u8>,
106 ) -> Self {
107 let size = bytes.len();
108 Self {
109 id: id.into(),
110 name: name.into(),
111 content_type: content_type.into(),
112 encoding: "base64".to_string(),
113 data: BASE64.encode(bytes),
114 size,
115 }
116 }
117
118 pub fn decode_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
120 BASE64
121 .decode(self.data.as_bytes())
122 .map_err(|source| ProtocolError::InvalidAttachmentEncoding(source.to_string()))
123 }
124
125 pub fn param_ref(id: impl Into<String>) -> Value {
127 json!({ "$file": id.into() })
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ErrorPayload {
134 pub code: String,
135 pub message: String,
136 pub status: u16,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub details: Option<Value>,
139}
140
141pub const K_API_REQUEST: u8 = 0;
145pub const K_EVENT_EMIT: u8 = 1;
146pub const K_API_RESPONSE: u8 = 2;
147pub const K_EVENT_ACK: u8 = 3;
148
149#[derive(Debug, Clone)]
161pub enum PacketBody {
162 ApiRequest {
164 request_id: u64,
165 route: String,
166 params: Value,
167 attachments: Vec<FileAttachment>,
168 metadata: Value,
169 },
170 ApiResponse {
172 request_id: u64,
173 ok: bool,
174 status: u16,
175 data: Value,
176 error: Option<ErrorPayload>,
177 metadata: Value,
178 },
179 EventEmit {
181 event_id: u64,
182 name: String,
183 data: Value,
184 attachments: Vec<FileAttachment>,
185 metadata: Value,
186 expect_ack: bool,
187 storage_id: Option<u64>,
190 },
191 EventAck {
193 event_id: u64,
194 ok: bool,
195 receipt: Value,
196 error: Option<ErrorPayload>,
197 },
198}
199
200impl Serialize for PacketBody {
203 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204 where
205 S: serde::Serializer,
206 {
207 match self {
208 Self::ApiRequest {
209 request_id,
210 route,
211 params,
212 attachments,
213 metadata,
214 } => {
215 let mut s = serializer.serialize_map(Some(6))?;
216 s.serialize_entry("k", &K_API_REQUEST)?;
217 s.serialize_entry("i", request_id)?;
218 s.serialize_entry("r", route)?;
219 s.serialize_entry("p", params)?;
220 s.serialize_entry("a", attachments)?;
221 s.serialize_entry("m", metadata)?;
222 s.end()
223 }
224 Self::ApiResponse {
225 request_id,
226 ok,
227 status,
228 data,
229 error,
230 metadata,
231 } => {
232 let field_count = 5 + usize::from(error.is_some()) + 1;
233 let mut s = serializer.serialize_map(Some(field_count))?;
234 s.serialize_entry("k", &K_API_RESPONSE)?;
235 s.serialize_entry("i", request_id)?;
236 s.serialize_entry("o", ok)?;
237 s.serialize_entry("s", status)?;
238 s.serialize_entry("d", data)?;
239 if let Some(err) = error {
240 s.serialize_entry("er", err)?;
241 }
242 s.serialize_entry("m", metadata)?;
243 s.end()
244 }
245 Self::EventEmit {
246 event_id,
247 name,
248 data,
249 attachments,
250 metadata,
251 expect_ack,
252 storage_id,
253 } => {
254 let field_count = 7 + usize::from(storage_id.is_some());
255 let mut s = serializer.serialize_map(Some(field_count))?;
256 s.serialize_entry("k", &K_EVENT_EMIT)?;
257 s.serialize_entry("i", event_id)?;
258 s.serialize_entry("n", name)?;
259 s.serialize_entry("d", data)?;
260 s.serialize_entry("a", attachments)?;
261 s.serialize_entry("m", metadata)?;
262 s.serialize_entry("e", expect_ack)?;
263 if let Some(si) = storage_id {
264 s.serialize_entry("si", si)?;
265 }
266 s.end()
267 }
268 Self::EventAck {
269 event_id,
270 ok,
271 receipt,
272 error,
273 } => {
274 let field_count = 3 + usize::from(error.is_some()) + 1;
275 let mut s = serializer.serialize_map(Some(field_count))?;
276 s.serialize_entry("k", &K_EVENT_ACK)?;
277 s.serialize_entry("i", event_id)?;
278 s.serialize_entry("o", ok)?;
279 s.serialize_entry("rc", receipt)?;
280 if let Some(err) = error {
281 s.serialize_entry("er", err)?;
282 }
283 s.end()
284 }
285 }
286 }
287}
288
289#[derive(Deserialize)]
293struct ApiRequestFields {
294 #[serde(rename = "i")]
295 request_id: u64,
296 #[serde(rename = "r")]
297 route: String,
298 #[serde(rename = "p", default)]
299 params: Value,
300 #[serde(rename = "a", default)]
301 attachments: Vec<FileAttachment>,
302 #[serde(rename = "m", default)]
303 metadata: Value,
304}
305
306#[derive(Deserialize)]
308struct ApiResponseFields {
309 #[serde(rename = "i")]
310 request_id: u64,
311 #[serde(rename = "o", default)]
312 ok: bool,
313 #[serde(rename = "s", default)]
314 status: u16,
315 #[serde(rename = "d", default)]
316 data: Value,
317 #[serde(rename = "er", default)]
318 error: Option<ErrorPayload>,
319 #[serde(rename = "m", default)]
320 metadata: Value,
321}
322
323#[derive(Deserialize)]
325struct EventEmitFields {
326 #[serde(rename = "i")]
327 event_id: u64,
328 #[serde(rename = "n")]
329 name: String,
330 #[serde(rename = "d", default)]
331 data: Value,
332 #[serde(rename = "a", default)]
333 attachments: Vec<FileAttachment>,
334 #[serde(rename = "m", default)]
335 metadata: Value,
336 #[serde(rename = "e", default)]
337 expect_ack: bool,
338 #[serde(rename = "si", default)]
339 storage_id: Option<u64>,
340}
341
342#[derive(Deserialize)]
344struct EventAckFields {
345 #[serde(rename = "i")]
346 event_id: u64,
347 #[serde(rename = "o", default)]
348 ok: bool,
349 #[serde(rename = "rc", default)]
350 receipt: Value,
351 #[serde(rename = "er", default)]
352 error: Option<ErrorPayload>,
353}
354
355impl<'de> Deserialize<'de> for PacketBody {
356 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
357 where
358 D: Deserializer<'de>,
359 {
360 let value = Value::deserialize(deserializer)?;
363 let k = value.get("k").and_then(|v| v.as_u64()).ok_or_else(|| {
364 serde::de::Error::custom("missing or non-numeric 'k' field in packet body")
365 })?;
366 let k = u8::try_from(k).map_err(|_| {
367 serde::de::Error::custom(format!("'k' value {k} is outside the valid range"))
368 })?;
369
370 match k {
371 K_API_REQUEST => {
372 let f = serde_json::from_value::<ApiRequestFields>(value)
373 .map_err(serde::de::Error::custom)?;
374 Ok(Self::ApiRequest {
375 request_id: f.request_id,
376 route: f.route,
377 params: f.params,
378 attachments: f.attachments,
379 metadata: f.metadata,
380 })
381 }
382 K_EVENT_EMIT => {
383 let f = serde_json::from_value::<EventEmitFields>(value)
384 .map_err(serde::de::Error::custom)?;
385 Ok(Self::EventEmit {
386 event_id: f.event_id,
387 name: f.name,
388 data: f.data,
389 attachments: f.attachments,
390 metadata: f.metadata,
391 expect_ack: f.expect_ack,
392 storage_id: f.storage_id,
393 })
394 }
395 K_API_RESPONSE => {
396 let f = serde_json::from_value::<ApiResponseFields>(value)
397 .map_err(serde::de::Error::custom)?;
398 Ok(Self::ApiResponse {
399 request_id: f.request_id,
400 ok: f.ok,
401 status: f.status,
402 data: f.data,
403 error: f.error,
404 metadata: f.metadata,
405 })
406 }
407 K_EVENT_ACK => {
408 let f = serde_json::from_value::<EventAckFields>(value)
409 .map_err(serde::de::Error::custom)?;
410 Ok(Self::EventAck {
411 event_id: f.event_id,
412 ok: f.ok,
413 receipt: f.receipt,
414 error: f.error,
415 })
416 }
417 _ => Err(serde::de::Error::custom(format!("unknown 'k' value: {k}"))),
418 }
419 }
420}
421
422impl PacketBody {
423 pub fn message_type(&self) -> MessageType {
424 match self {
425 Self::ApiRequest { .. } | Self::ApiResponse { .. } => MessageType::Api,
426 Self::EventEmit { .. } | Self::EventAck { .. } => MessageType::Event,
427 }
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct PacketEnvelope {
434 pub message_type: MessageType,
436 pub encryption: EncryptionKind,
438 pub body: PacketBody,
440}
441
442impl PacketEnvelope {
443 pub fn new(body: PacketBody) -> Self {
445 Self {
446 message_type: body.message_type(),
447 encryption: EncryptionKind::None,
448 body,
449 }
450 }
451
452 pub fn with_encryption(body: PacketBody, encryption: EncryptionKind) -> Self {
454 Self {
455 message_type: body.message_type(),
456 encryption,
457 body,
458 }
459 }
460}
461
462#[derive(Clone, Default)]
468pub struct FrameCodec {
469 aes256_cipher: Option<Arc<Aes256Gcm>>,
470 chacha20_cipher: Option<Arc<ChaCha20Poly1305>>,
471}
472
473impl std::fmt::Debug for FrameCodec {
474 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475 f.debug_struct("FrameCodec")
476 .field("aes256", &self.aes256_cipher.is_some())
477 .field("chacha20", &self.chacha20_cipher.is_some())
478 .finish()
479 }
480}
481
482impl FrameCodec {
483 pub fn plaintext() -> Self {
485 Self::default()
486 }
487
488 pub fn with_chacha20_key(self, key: [u8; 32]) -> Self {
493 let cipher = ChaCha20Poly1305::new_from_slice(&key)
494 .expect("a 32-byte key is always valid for ChaCha20-Poly1305");
495 Self {
496 chacha20_cipher: Some(Arc::new(cipher)),
497 ..self
498 }
499 }
500
501 pub fn with_aes256_key(self, key: [u8; 32]) -> Self {
506 let cipher =
507 Aes256Gcm::new_from_slice(&key).expect("a 32-byte key is always valid for AES-256-GCM");
508 Self {
509 aes256_cipher: Some(Arc::new(cipher)),
510 ..self
511 }
512 }
513
514 pub fn encode(&self, packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
516 let payload = serde_json::to_vec(&packet.body)?;
517
518 if payload.len() > MAX_PAYLOAD_BYTES {
521 return Err(ProtocolError::PayloadTooLarge {
522 actual: payload.len(),
523 max: MAX_PAYLOAD_BYTES,
524 });
525 }
526
527 let payload = match packet.encryption {
528 EncryptionKind::None => payload,
529 EncryptionKind::ChaCha20 => self.encrypt_chacha20(&payload)?,
530 EncryptionKind::Aes256 => self.encrypt_aes256(&payload)?,
531 };
532
533 if payload.len() > MAX_PAYLOAD_BYTES {
536 return Err(ProtocolError::PayloadTooLarge {
537 actual: payload.len(),
538 max: MAX_PAYLOAD_BYTES,
539 });
540 }
541
542 let frame_len = 2 + payload.len();
543 let mut frame = Vec::with_capacity(4 + frame_len);
544 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
545 frame.push(packet.message_type as u8);
546 frame.push(packet.encryption as u8);
547 frame.extend_from_slice(&payload);
548 Ok(frame)
549 }
550
551 pub fn decode(&self, frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
553 if frame.len() < 6 {
554 return Err(ProtocolError::FrameTooShort);
555 }
556
557 let declared = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
558 let actual = frame.len() - 4;
559 if declared != actual {
560 return Err(ProtocolError::FrameLengthMismatch { declared, actual });
561 }
562
563 let payload_len = actual - 2;
564 if payload_len > MAX_PAYLOAD_BYTES {
565 return Err(ProtocolError::PayloadTooLarge {
566 actual: payload_len,
567 max: MAX_PAYLOAD_BYTES,
568 });
569 }
570
571 let message_type = MessageType::try_from(frame[4])?;
572 let encryption = EncryptionKind::try_from(frame[5])?;
573 let payload: Cow<'_, [u8]> = match encryption {
575 EncryptionKind::None => Cow::Borrowed(&frame[6..]),
576 EncryptionKind::ChaCha20 => Cow::Owned(self.decrypt_chacha20(&frame[6..])?),
577 EncryptionKind::Aes256 => Cow::Owned(self.decrypt_aes256(&frame[6..])?),
578 };
579
580 let body: PacketBody = serde_json::from_slice(&payload)?;
581 if body.message_type() != message_type {
582 return Err(ProtocolError::MessageTypeMismatch);
583 }
584
585 Ok(PacketEnvelope {
586 message_type,
587 encryption,
588 body,
589 })
590 }
591
592 fn encrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
593 let cipher = self
594 .chacha20_cipher
595 .as_ref()
596 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
597 let mut nonce_bytes = [0_u8; CHACHA20_NONCE_LEN];
598 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
599 let ciphertext = cipher
600 .encrypt(Nonce::from_slice(&nonce_bytes), payload)
601 .map_err(|_| ProtocolError::EncryptionFailed("chacha20"))?;
602
603 let mut encoded = Vec::with_capacity(CHACHA20_NONCE_LEN + ciphertext.len());
604 encoded.extend_from_slice(&nonce_bytes);
605 encoded.extend_from_slice(&ciphertext);
606 Ok(encoded)
607 }
608
609 fn decrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
610 if payload.len() < CHACHA20_NONCE_LEN {
611 return Err(ProtocolError::EncryptedPayloadTooShort {
612 algorithm: "chacha20",
613 expected_min: CHACHA20_NONCE_LEN,
614 actual: payload.len(),
615 });
616 }
617
618 let cipher = self
619 .chacha20_cipher
620 .as_ref()
621 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
622 let (nonce_bytes, ciphertext) = payload.split_at(CHACHA20_NONCE_LEN);
623 cipher
624 .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
625 .map_err(|_| ProtocolError::DecryptionFailed("chacha20"))
626 }
627
628 fn encrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
629 let cipher = self
630 .aes256_cipher
631 .as_ref()
632 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
633 let mut nonce_bytes = [0_u8; AES256_NONCE_LEN];
634 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
635 let ciphertext = cipher
636 .encrypt(AesNonce::from_slice(&nonce_bytes), payload)
637 .map_err(|_| ProtocolError::EncryptionFailed("aes256"))?;
638
639 let mut encoded = Vec::with_capacity(AES256_NONCE_LEN + ciphertext.len());
640 encoded.extend_from_slice(&nonce_bytes);
641 encoded.extend_from_slice(&ciphertext);
642 Ok(encoded)
643 }
644
645 fn decrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
646 if payload.len() < AES256_NONCE_LEN {
647 return Err(ProtocolError::EncryptedPayloadTooShort {
648 algorithm: "aes256",
649 expected_min: AES256_NONCE_LEN,
650 actual: payload.len(),
651 });
652 }
653
654 let cipher = self
655 .aes256_cipher
656 .as_ref()
657 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
658 let (nonce_bytes, ciphertext) = payload.split_at(AES256_NONCE_LEN);
659 cipher
660 .decrypt(AesNonce::from_slice(nonce_bytes), ciphertext)
661 .map_err(|_| ProtocolError::DecryptionFailed("aes256"))
662 }
663}
664
665pub fn encode_frame(packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
667 FrameCodec::plaintext().encode(packet)
668}
669
670pub fn decode_frame(frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
672 FrameCodec::plaintext().decode(frame)
673}
674
675#[derive(Debug, Error)]
677pub enum ProtocolError {
678 #[error("frame too short")]
679 FrameTooShort,
680 #[error("frame length mismatch: declared={declared}, actual={actual}")]
681 FrameLengthMismatch { declared: usize, actual: usize },
682 #[error("payload too large: actual={actual}, max={max}")]
683 PayloadTooLarge { actual: usize, max: usize },
684 #[error("unknown message type: {0:#x}")]
685 UnknownMessageType(u8),
686 #[error("unknown encryption kind: {0:#x}")]
687 UnknownEncryption(u8),
688 #[error("unsupported encryption kind: {0:#x}")]
689 UnsupportedEncryption(u8),
690 #[error("missing encryption key for {0}")]
691 MissingEncryptionKey(&'static str),
692 #[error("invalid encryption key for {0}")]
693 InvalidEncryptionKey(&'static str),
694 #[error("secure random generation failed: {0}")]
695 Random(String),
696 #[error(
697 "encrypted payload too short for {algorithm}: expected at least {expected_min}, actual={actual}"
698 )]
699 EncryptedPayloadTooShort {
700 algorithm: &'static str,
701 expected_min: usize,
702 actual: usize,
703 },
704 #[error("encryption failed for {0}")]
705 EncryptionFailed(&'static str),
706 #[error("decryption failed for {0}")]
707 DecryptionFailed(&'static str),
708 #[error("message type does not match packet body")]
709 MessageTypeMismatch,
710 #[error("invalid attachment encoding: {0}")]
711 InvalidAttachmentEncoding(String),
712 #[error("json error: {0}")]
713 Json(#[from] serde_json::Error),
714}
715
716#[cfg(test)]
717mod tests {
718 use super::{
719 EncryptionKind, FrameCodec, MAX_PAYLOAD_BYTES, MessageType, PacketBody, PacketEnvelope,
720 ProtocolError, decode_frame, encode_frame,
721 };
722 use serde_json::json;
723
724 const TEST_KEY: [u8; 32] = [0x11; 32];
725
726 #[test]
727 fn plaintext_helpers_still_work() {
728 let packet = PacketEnvelope::new(PacketBody::EventAck {
729 event_id: 1,
730 ok: true,
731 receipt: json!({ "ok": true }),
732 error: None,
733 });
734
735 let encoded = encode_frame(&packet).expect("encode plaintext");
736 let decoded = decode_frame(&encoded).expect("decode plaintext");
737 assert!(matches!(decoded.encryption, EncryptionKind::None));
738 }
739
740 #[test]
741 fn aes256_roundtrip_works() {
742 let codec = FrameCodec::plaintext().with_aes256_key(TEST_KEY);
743 let packet = PacketEnvelope::with_encryption(
744 PacketBody::ApiResponse {
745 request_id: 1,
746 ok: true,
747 status: 200,
748 data: json!({ "message": "encrypted" }),
749 error: None,
750 metadata: json!({}),
751 },
752 EncryptionKind::Aes256,
753 );
754
755 let encoded = codec.encode(&packet).expect("encode aes256");
756 let decoded = codec.decode(&encoded).expect("decode aes256");
757 assert!(matches!(decoded.encryption, EncryptionKind::Aes256));
758 }
759
760 #[test]
761 fn encode_rejects_payloads_over_limit() {
762 let codec = FrameCodec::plaintext();
763 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
764 request_id: 999,
765 ok: true,
766 status: 200,
767 data: json!({ "blob": "a".repeat(10 * 1024 * 1024) }),
768 error: None,
769 metadata: json!({}),
770 });
771
772 let error = codec
773 .encode(&packet)
774 .expect_err("oversized payload should fail");
775 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
776 }
777
778 #[test]
779 fn decode_rejects_payloads_over_limit() {
780 let payload = vec![0_u8; MAX_PAYLOAD_BYTES + 1];
781 let frame_len = 2 + payload.len();
782 let mut frame = Vec::with_capacity(4 + frame_len);
783 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
784 frame.push(MessageType::Api as u8);
785 frame.push(EncryptionKind::None as u8);
786 frame.extend_from_slice(&payload);
787
788 let error = FrameCodec::plaintext()
789 .decode(&frame)
790 .expect_err("oversized payload should fail");
791 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
792 }
793}