From 28c23a0553b0ccf9fc43860b485fdfc38d0c4444 Mon Sep 17 00:00:00 2001 From: lone-cloud Date: Sun, 18 Jan 2026 02:00:18 -0800 Subject: [PATCH] combine servers, fixing shit --- .env.example | 14 + .gitignore | 7 +- README.md | 64 +- .../sup/SignalNotificationListener.kt | 106 +- assets/SUP Architecture.excalidraw | 2901 +++++++++++++++++ assets/SUP Architecture.webp | Bin 0 -> 62114 bytes bun.lock | 42 - docker-compose.dev.yml | 24 +- docker-compose.yml | 24 +- package.json | 6 +- proton-bridge/.env.example | 10 - proton-bridge/.gitignore | 4 - proton-bridge/Dockerfile | 16 - proton-bridge/README.md | 82 - proton-bridge/package.json | 18 - proton-bridge/src/index.ts | 136 - scripts/install-signal-cli.ts | 18 +- server/bun.lock | 41 + server/constants/config.ts | 17 + server/constants/paths.ts | 9 + server/constants/server.ts | 11 +- server/index.ts | 140 +- server/modules/protonmail.ts | 126 + server/{ => modules}/signal.ts | 44 +- server/modules/store.ts | 50 + server/{ => modules}/unifiedpush.ts | 10 +- server/package.json | 9 +- server/routes/health.ts | 2 +- server/routes/link.ts | 18 +- server/routes/notify.ts | 25 +- server/routes/unifiedpush.ts | 12 +- server/store.ts | 73 - server/{types/signal.ts => types.d.ts} | 0 server/utils/auth.ts | 19 + 34 files changed, 3465 insertions(+), 613 deletions(-) create mode 100644 assets/SUP Architecture.excalidraw create mode 100644 assets/SUP Architecture.webp delete mode 100644 proton-bridge/.env.example delete mode 100644 proton-bridge/.gitignore delete mode 100644 proton-bridge/Dockerfile delete mode 100644 proton-bridge/README.md delete mode 100644 proton-bridge/package.json delete mode 100644 proton-bridge/src/index.ts create mode 100644 server/bun.lock create mode 100644 server/constants/config.ts create mode 100644 server/constants/paths.ts create mode 100644 server/modules/protonmail.ts rename server/{ => modules}/signal.ts (79%) create mode 100644 server/modules/store.ts rename server/{ => modules}/unifiedpush.ts (83%) delete mode 100644 server/store.ts rename server/{types/signal.ts => types.d.ts} (100%) create mode 100644 server/utils/auth.ts diff --git a/.env.example b/.env.example index 9377d1f..99cce91 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,17 @@ # Optional: Enable verbose signal-cli logging # Default: false # VERBOSE=true + +# Optional: ProtonMail integration - receive email notifications via Signal +# Get credentials by running: docker compose run --rm protonmail-bridge init +# Then use 'login' and 'info' commands to get IMAP username/password +# BRIDGE_IMAP_USERNAME=your-email@proton.me +# BRIDGE_IMAP_PASSWORD=bridge-generated-password +# PROTON_BRIDGE_HOST=protonmail-bridge +# PROTON_BRIDGE_PORT=143 +# SUP_TOPIC=Proton Mail +# Optional: Enable Android app integration for ProtonMail notifications +# When enabled, sends special format that SUP Android app intercepts to show custom notifications +# that open ProtonMail app on click. When disabled, ProtonMail notifications appear as plain Signal messages. +# Default: false +# ENABLE_PROTON_ANDROID=true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58bed65..3b33244 100644 --- a/.gitignore +++ b/.gitignore @@ -2,16 +2,11 @@ node_modules/ signal-cli .env sup-server -sup-proton-bridge android/.gradle +android/.kotlin android/local.properties android/build/ android/*/build/ -android/*.iml -android/captures/ -android/.externalNativeBuild -android/.cxx android/*.keystore android/*.jks -android/.kotlin diff --git a/README.md b/README.md index 5f9d0e6..86416d3 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,6 @@ Privacy-preserving push notifications using Signal as transport. SUP is a UnifiedPush distributor that routes push notifications through Signal, allowing you to receive app notifications without exposing unique network fingerprints to your ISP or network observers. All notification traffic appears as regular Signal messages. -## Architecture - -SUP consists of three services that **MUST run together on the same machine**: - -- **sup-server** (Bun/TypeScript): Receives webhooks, sends Signal messages via signal-cli -- **protonmail-bridge** (Official Proton): Decrypts ProtonMail emails, runs local IMAP server -- **sup-proton-bridge** (Custom): Monitors IMAP, forwards to sup-server - -All services communicate over a private Docker network with no external exposure except Signal protocol. **Separating these services across multiple machines would expose plaintext IMAP traffic and compromise security.** - -**Android App** (Kotlin): Monitors Signal notifications, extracts UnifiedPush payloads, delivers to apps - ## Why? Traditional push notification systems (ntfy, FCM) require persistent WebSocket connections or polling to specific servers, creating unique network fingerprints. SUP blends your notification traffic with regular Signal usage for better privacy. @@ -26,24 +14,6 @@ Traditional push notification systems (ntfy, FCM) require persistent WebSocket c **⚠️ DOCKER COMPOSE REQUIRED**: The services must be deployed together using `docker compose`. Running individual Dockerfiles separately is not supported and will compromise security. -### Prerequisites - -#### Installing Docker on Arch Linux - -```bash -# Install Docker and Compose plugin -sudo pacman -S docker docker-buildx - -# Start Docker service -sudo systemctl start docker -sudo systemctl enable docker - -# Add your user to docker group (logout/login required) -sudo usermod -aG docker $USER -``` - -After adding yourself to the docker group, **logout and login** for it to take effect. - ### Quick Start with Docker Compose **Without ProtonMail** (just UnifiedPush): @@ -110,6 +80,17 @@ To receive ProtonMail notifications via Signal: Your phone will now receive Signal notifications when ProtonMail receives new emails. +#### ProtonMail Android App Integration (Optional) + +If you have the ProtonMail Android app installed, you can enable integration so that clicking on email notifications opens the ProtonMail app directly: + +```bash +# Add this to your .env file +ENABLE_PROTON_ANDROID=true +``` + +When enabled, the SUP Android app will intercept email notifications and show them as custom notifications that launch the ProtonMail app on click. When disabled, email notifications appear as regular Signal messages. + ### Development For local development, install Bun and signal-cli: @@ -137,16 +118,6 @@ bun install bun --filter sup-server dev ``` -## How It Works - -1. Android app registers with server via `/up/{app_id}` -2. Server creates a Signal group for the app -3. Server returns UnifiedPush endpoint URL -4. App shares endpoint with notification provider -5. Provider sends notifications to endpoint -6. Server forwards to Signal group -7. Android app monitors Signal, extracts payloads, wakes apps - ## Android App Download the latest APK from [GitHub Releases](https://github.com/lone-cloud/sup/releases). @@ -158,3 +129,16 @@ Download the latest APK from [GitHub Releases](https://github.com/lone-cloud/sup ```text 0D:3C:99:15:0E:12:1A:DE:0D:AE:05:CB:16:46:5E:65:31:56:DC:D6:98:87:59:4E:79:B1:0D:AE:1E:56:F2:E8 ``` + +## Architecture + +![SUP Architecture](assets/SUP%20Architecture.webp) + +SUP consists of two services that **MUST run together on the same machine**: + +- **sup-server** (Bun): Receives webhooks, sends Signal messages via signal-cli. Optional: monitors ProtonMail IMAP +- **protonmail-bridge** (Official Proton, optional): Decrypts ProtonMail emails, runs local IMAP server + +All services communicate over a private Docker network with no external exposure except Signal protocol. **Separating these services across multiple machines would expose plaintext IMAP traffic and compromise security.** + +**Android App** (Kotlin): Monitors Signal notifications, extracts UnifiedPush payloads, delivers to apps diff --git a/android/app/src/main/java/com/lonecloud/sup/SignalNotificationListener.kt b/android/app/src/main/java/com/lonecloud/sup/SignalNotificationListener.kt index 1aa5797..47c2b1d 100644 --- a/android/app/src/main/java/com/lonecloud/sup/SignalNotificationListener.kt +++ b/android/app/src/main/java/com/lonecloud/sup/SignalNotificationListener.kt @@ -28,11 +28,20 @@ class SignalNotificationListener : NotificationListenerService() { companion object { private const val CHANNEL_ID = "sup_notifications" private const val CHANNEL_NAME = "SUP Notifications" + private const val CHANNEL_ID_PROTON = "sup_email" + private const val CHANNEL_NAME_PROTON = "Email Notifications" + private const val SUP_ENDPOINT_PREFIX = "[SUP:" + private const val LAUNCH_PREFIX = "[LAUNCH:" + + private val SIGNAL_PACKAGES = setOf( + "org.thoughtcrime.securesms", // Signal + "im.molly.app" // Molly + ) } override fun onCreate() { super.onCreate() - createNotificationChannel() + createNotificationChannels() } override fun onDestroy() { @@ -41,7 +50,7 @@ class SignalNotificationListener : NotificationListenerService() { } override fun onNotificationPosted(sbn: StatusBarNotification?) { - if (sbn?.packageName != "org.thoughtcrime.securesms") return + if (sbn?.packageName !in SIGNAL_PACKAGES) return val notification = sbn.notification val extras = notification.extras @@ -51,14 +60,21 @@ class SignalNotificationListener : NotificationListenerService() { Log.d(TAG, "Signal notification: title=$title, text=$text") - if (text.startsWith("[UP:")) { - parseAndDeliverUnifiedPush(text) + when { + text.startsWith(SUP_ENDPOINT_PREFIX) -> { + parseAndDeliverUnifiedPush(text) + cancelNotification(sbn.key) + } + text.startsWith(LAUNCH_PREFIX) -> { + parseAndShowLaunchNotification(text) + cancelNotification(sbn.key) + } } } private fun parseAndDeliverUnifiedPush(message: String) { try { - val endpointMatch = Regex("""\[UP:([^\]]+)\]""").find(message) + val endpointMatch = Regex("""\[SUP:([^\]]+)\]""").find(message) val endpointId = endpointMatch?.groupValues?.get(1) ?: run { Log.w(TAG, "No endpoint ID found in message") return @@ -74,9 +90,9 @@ class SignalNotificationListener : NotificationListenerService() { val payload = message.substringAfter("]").trim() val intent = Intent("org.unifiedpush.android.connector.MESSAGE").apply { - putExtra("token", subscription.upConnectorToken) // UnifiedPush connector token + putExtra("token", subscription.upConnectorToken) putExtra("message", payload) - `package` = subscription.upAppId // Target app package + `package` = subscription.upAppId } sendBroadcast(intent) @@ -86,7 +102,70 @@ class SignalNotificationListener : NotificationListenerService() { } } - private fun createNotificationChannel() { + private fun parseAndShowLaunchNotification(message: String) { + try { + val packageMatch = Regex("""\[LAUNCH:([^\]]+)\]""").find(message) + val packageName = packageMatch?.groupValues?.get(1) ?: run { + Log.w(TAG, "No package name found in LAUNCH message") + return + } + + val content = message.substringAfter("]").trim() + + // Parse title and body (format: **Title**\nBody) + val titleMatch = Regex("""\*\*([^*]+)\*\*""").find(content) + val title = titleMatch?.groupValues?.get(1) ?: "Email" + val body = content.substringAfter("**", "").substringAfter("**", "").trim() + + // Check if target app is installed + val isInstalled = try { + packageManager.getPackageInfo(packageName, 0) + true + } catch (e: Exception) { + false + } + + val clickIntent = if (isInstalled) { + packageManager.getLaunchIntentForPackage(packageName)?.let { intent -> + PendingIntent.getActivity( + this, + Random.nextInt(), + intent.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + }, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } + } else { + null + } + + val notification = NotificationCompat.Builder(this, CHANNEL_ID_PROTON) + .setContentTitle(title) + .setContentText(body) + .setSmallIcon(android.R.drawable.ic_dialog_email) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .apply { + if (clickIntent != null) { + setContentIntent(clickIntent) + } + } + .build() + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(Random.nextInt(), notification) + + Log.d(TAG, "Showed app launch notification") + } catch (e: Exception) { + Log.e(TAG, "Failed to parse/show app launch notification", e) + } + } + + private fun createNotificationChannels() { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel( CHANNEL_ID, CHANNEL_NAME, @@ -94,8 +173,15 @@ class SignalNotificationListener : NotificationListenerService() { ).apply { description = "Notifications from SUP topics" } - - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) + + val protonChannel = NotificationChannel( + CHANNEL_ID_PROTON, + CHANNEL_NAME_PROTON, + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = "Email notifications" + } + notificationManager.createNotificationChannel(protonChannel) } } diff --git a/assets/SUP Architecture.excalidraw b/assets/SUP Architecture.excalidraw new file mode 100644 index 0000000..592c734 --- /dev/null +++ b/assets/SUP Architecture.excalidraw @@ -0,0 +1,2901 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "ubnB-MCtd3Y84dUGFa-b7", + "type": "rectangle", + "x": 311.93098958333337, + "y": 15.150065104166629, + "width": 166.41015625, + "height": 133.2421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": { + "type": 3 + }, + "seed": 1606195181, + "version": 351, + "versionNonce": 299790117, + "isDeleted": false, + "boundElements": [ + { + "id": "hauitp4TDHIQsHGW_tjMw", + "type": "arrow" + }, + { + "id": "UJHrk_gTqHqqsQkalk_mI", + "type": "arrow" + }, + { + "id": "fLaTXseSmD8kBh3NLfiPu", + "type": "arrow" + }, + { + "id": "RNCrlFh5EZBwnYr-7bA_j", + "type": "arrow" + } + ], + "updated": 1768722096436, + "link": null, + "locked": false + }, + { + "id": "7fGvBO1wPB7KmcLdQS0oT", + "type": "rectangle", + "x": -156.96024576822913, + "y": 25.20804850260413, + "width": 167.36328125, + "height": 133.2421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aN", + "roundness": { + "type": 3 + }, + "seed": 611898349, + "version": 361, + "versionNonce": 887010157, + "isDeleted": false, + "boundElements": [ + { + "id": "wN-1aiqTy8EelOURWsY4E", + "type": "arrow" + }, + { + "id": "jqv8Vv58NPpsDZoaXIZY8", + "type": "arrow" + } + ], + "updated": 1768687676554, + "link": null, + "locked": false + }, + { + "id": "OLh35JqPLzCNQfQUwFZVV", + "type": "text", + "x": -128.99674479166663, + "y": 77.70084635416663, + "width": 123.85987854003906, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 1914263683, + "version": 128, + "versionNonce": 156587309, + "isDeleted": false, + "boundElements": [], + "updated": 1768687034227, + "link": null, + "locked": false, + "text": "Signal Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Signal Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vgtgZtFmt1xq7s1sOgLPv", + "type": "rectangle", + "x": 5.425130208333371, + "y": 431.17740885416663, + "width": 119.4609375, + "height": 63.421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aS", + "roundness": { + "type": 3 + }, + "seed": 1282447693, + "version": 231, + "versionNonce": 1564305325, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "roYNM1cuh0Q_XzEwhEmVq" + }, + { + "id": "0gzKxrXTCJjBSJaqf17am", + "type": "arrow" + }, + { + "id": "wN-1aiqTy8EelOURWsY4E", + "type": "arrow" + }, + { + "id": "N5sdBqQFXldSJfsfurWOu", + "type": "arrow" + } + ], + "updated": 1768687539056, + "link": null, + "locked": false + }, + { + "id": "roYNM1cuh0Q_XzEwhEmVq", + "type": "text", + "x": 16.175641377766965, + "y": 450.38834635416663, + "width": 97.95991516113281, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aT", + "roundness": null, + "seed": 1388624675, + "version": 151, + "versionNonce": 1022696813, + "isDeleted": false, + "boundElements": [], + "updated": 1768687534238, + "link": null, + "locked": false, + "text": "Signal App", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "vgtgZtFmt1xq7s1sOgLPv", + "originalText": "Signal App", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "aQv859Sl4QY8VMBkh5Hua", + "type": "rectangle", + "x": 315.66866048177087, + "y": 454.81547037760413, + "width": 119.4609375, + "height": 63.421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aU", + "roundness": { + "type": 3 + }, + "seed": 933205187, + "version": 452, + "versionNonce": 506220845, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "PUam-rImZGd-Np-LpoItI" + }, + { + "id": "0gzKxrXTCJjBSJaqf17am", + "type": "arrow" + }, + { + "id": "SZ-W5B5Qivm_fESslnaE8", + "type": "arrow" + }, + { + "id": "1tl7FcPw2hEhQ5uUtix5H", + "type": "arrow" + }, + { + "id": "UJHrk_gTqHqqsQkalk_mI", + "type": "arrow" + } + ], + "updated": 1768687700904, + "link": null, + "locked": false + }, + { + "id": "PUam-rImZGd-Np-LpoItI", + "type": "text", + "x": 333.399159749349, + "y": 474.02640787760413, + "width": 83.99993896484375, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aV", + "roundness": null, + "seed": 284558435, + "version": 370, + "versionNonce": 1569319661, + "isDeleted": false, + "boundElements": [], + "updated": 1768686051157, + "link": null, + "locked": false, + "text": "SUP App", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "aQv859Sl4QY8VMBkh5Hua", + "originalText": "SUP App", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "EGjvtmqpnGt-kHu4jX-_N", + "type": "rectangle", + "x": 112.31709798177087, + "y": 652.3232828776041, + "width": 169.15625, + "height": 85, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aW", + "roundness": { + "type": 3 + }, + "seed": 818701389, + "version": 488, + "versionNonce": 916260909, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "sROVzuXD2ITY-seaUmP7V" + }, + { + "id": "SZ-W5B5Qivm_fESslnaE8", + "type": "arrow" + }, + { + "id": "1tl7FcPw2hEhQ5uUtix5H", + "type": "arrow" + }, + { + "id": "1kbtuffFzuV7NZUisS0pC", + "type": "arrow" + }, + { + "id": "0bZUK-yuL2IKAY1u64HIW", + "type": "arrow" + } + ], + "updated": 1768688069554, + "link": null, + "locked": false + }, + { + "id": "sROVzuXD2ITY-seaUmP7V", + "type": "text", + "x": 147.2552617390951, + "y": 682.3232828776041, + "width": 99.27992248535156, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aX", + "roundness": null, + "seed": 1739870893, + "version": 427, + "versionNonce": 1854810029, + "isDeleted": false, + "boundElements": [], + "updated": 1768687755257, + "link": null, + "locked": false, + "text": "UP App(s)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EGjvtmqpnGt-kHu4jX-_N", + "originalText": "UP App(s)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "wN-1aiqTy8EelOURWsY4E", + "type": "arrow", + "x": 65.05559895833338, + "y": 425.1774088541667, + "width": 138.4342041015625, + "height": 260.7271728515625, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ab", + "roundness": null, + "seed": 1580099555, + "version": 317, + "versionNonce": 480831437, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "PPR4JysV7Oxn5UJ2TDQSX" + } + ], + "updated": 1768687534238, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -130.3635864257813 + ], + [ + -138.4342041015625, + -130.3635864257813 + ], + [ + -138.4342041015625, + -260.7271728515625 + ] + ], + "startBinding": { + "elementId": "vgtgZtFmt1xq7s1sOgLPv", + "mode": "orbit", + "fixedPoint": [ + 0.4991629062847427, + -0.09460458240945956 + ] + }, + "endBinding": { + "elementId": "7fGvBO1wPB7KmcLdQS0oT", + "mode": "orbit", + "fixedPoint": [ + 0.49940249737425607, + 1.0450307827616538 + ] + }, + "startArrowhead": "arrow", + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "PPR4JysV7Oxn5UJ2TDQSX", + "type": "text", + "x": -65.51204325358069, + "y": 278.1585489908854, + "width": 214.33975219726562, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "abV", + "roundness": null, + "seed": 1448567395, + "version": 46, + "versionNonce": 330571043, + "isDeleted": false, + "boundElements": [], + "updated": 1768685762804, + "link": null, + "locked": false, + "text": "WebSocket connection\non chat.signal.com", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "wN-1aiqTy8EelOURWsY4E", + "originalText": "WebSocket connection on chat.signal.com", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "AzmYnwCPxrlLyyfS_ADA5", + "type": "text", + "x": 369.63606770833337, + "y": 664.7633463541666, + "width": 90.63992309570312, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ad", + "roundness": null, + "seed": 970858797, + "version": 291, + "versionNonce": 304693293, + "isDeleted": false, + "boundElements": [], + "updated": 1768687586029, + "link": null, + "locked": false, + "text": "Wake App", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Wake App", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "IHq2qxEqUSCP4Tt7IOSph", + "type": "text", + "x": 337.91731770833337, + "y": 49.52115885416663, + "width": 109.89990234375, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ag", + "roundness": null, + "seed": 87614285, + "version": 70, + "versionNonce": 1300605187, + "isDeleted": false, + "boundElements": [], + "updated": 1768687383820, + "link": null, + "locked": false, + "text": "SUP Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SUP Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "_-S1sAdrh_QGBnrax2n6g", + "type": "text", + "x": 176.51106770833337, + "y": 414.05631510416663, + "width": 130.9921875, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ah", + "roundness": null, + "seed": 2141271629, + "version": 529, + "versionNonce": 193123501, + "isDeleted": false, + "boundElements": [], + "updated": 1768687566554, + "link": null, + "locked": false, + "text": "Listen for\nspecial Signal\nnotifications", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Listen for special Signal notifications", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "8vqVTPTKDvaMBGPI326WS", + "type": "text", + "x": 249.94075520833337, + "y": 624.5211588541666, + "width": 125.33987426757812, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aj", + "roundness": null, + "seed": 1732566605, + "version": 152, + "versionNonce": 854383267, + "isDeleted": false, + "boundElements": [], + "updated": 1768687504362, + "link": null, + "locked": false, + "text": "Register App", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Register App", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "SZ-W5B5Qivm_fESslnaE8", + "type": "arrow", + "x": 287.12483103531673, + "y": 668.0611451149011, + "width": 88.17429819645412, + "height": 143.823799737297, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "al", + "roundness": null, + "seed": 413211939, + "version": 92, + "versionNonce": 1601333293, + "isDeleted": false, + "boundElements": [], + "updated": 1768687501829, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 88.17429819645412, + 0 + ], + [ + 88.17429819645412, + -143.823799737297 + ] + ], + "startBinding": { + "elementId": "EGjvtmqpnGt-kHu4jX-_N", + "mode": "orbit", + "fixedPoint": [ + 1.0334098388534025, + 0.18515132043878835 + ] + }, + "endBinding": { + "elementId": "aQv859Sl4QY8VMBkh5Hua", + "mode": "orbit", + "fixedPoint": [ + 0.49916290628474247, + 1.0946045824094606 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "1tl7FcPw2hEhQ5uUtix5H", + "type": "arrow", + "x": 422.76085883499104, + "y": 524.062424145458, + "width": 135.28751085322017, + "height": 182.12279720870868, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "am", + "roundness": null, + "seed": 1727846093, + "version": 226, + "versionNonce": 2125388429, + "isDeleted": false, + "boundElements": [], + "updated": 1768687501830, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 182.12279720870868 + ], + [ + -135.28751085322017, + 182.12279720870868 + ] + ], + "startBinding": { + "elementId": "aQv859Sl4QY8VMBkh5Hua", + "mode": "orbit", + "fixedPoint": [ + 0.8964620619457316, + 1.0918465240558375 + ] + }, + "endBinding": { + "elementId": "EGjvtmqpnGt-kHu4jX-_N", + "mode": "orbit", + "fixedPoint": [ + 1.0354701644189914, + 0.6336698644301471 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "hcGGzvqT_ihnanzGYaqOv", + "type": "rectangle", + "x": 255.8606770833336, + "y": -30.20735677083337, + "width": 596.9561941964287, + "height": 223.84570312499994, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "an", + "roundness": { + "type": 3 + }, + "seed": 1456200845, + "version": 696, + "versionNonce": 1670648965, + "isDeleted": false, + "boundElements": [ + { + "id": "Gf2m8hClymOsReVNX3j47", + "type": "arrow" + } + ], + "updated": 1768722074032, + "link": null, + "locked": false + }, + { + "id": "xYkxnXL4g5KS5YsJIUrlM", + "type": "text", + "x": 443.77362351190493, + "y": -60.84993489583337, + "width": 250.01974487304688, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ao", + "roundness": null, + "seed": 1877972781, + "version": 898, + "versionNonce": 1537407115, + "isDeleted": false, + "boundElements": [], + "updated": 1768722082615, + "link": null, + "locked": false, + "text": "User's Self-Hosted Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User's Self-Hosted Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "JFj4AIU6qh1LAm8QVQqRG", + "type": "rectangle", + "x": -42.549479166666515, + "y": 402.95865885416663, + "width": 544.8046875, + "height": 355.791015625, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ap", + "roundness": { + "type": 3 + }, + "seed": 1667903597, + "version": 117, + "versionNonce": 1371748973, + "isDeleted": false, + "boundElements": [ + { + "id": "t44vlc42q_1WvhRZtOcdt", + "type": "arrow" + } + ], + "updated": 1768686713708, + "link": null, + "locked": false + }, + { + "id": "0gzKxrXTCJjBSJaqf17am", + "type": "arrow", + "x": 309.66866048177087, + "y": 486.42640787760416, + "width": 107.35835305398024, + "height": 87.46406693193961, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "apV", + "roundness": null, + "seed": 515912323, + "version": 254, + "versionNonce": 549720387, + "isDeleted": false, + "boundElements": [], + "updated": 1768687524851, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -53.148550851004416, + 0 + ], + [ + -53.148550851004416, + 87.46406693193961 + ], + [ + -107.35835305398024, + 87.46406693193961 + ] + ], + "startBinding": { + "elementId": "aQv859Sl4QY8VMBkh5Hua", + "mode": "orbit", + "fixedPoint": [ + -0.05022562291544046, + 0.49842325695984285 + ] + }, + "endBinding": { + "elementId": "WDFxDiYj5BqTwEMg7LiQy", + "mode": "orbit", + "fixedPoint": [ + 1.0313835986172695, + 0.12619421841686257 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "8Nmzpi54TtXde91RroPni", + "type": "text", + "x": 124.14459737141942, + "y": 369.1117045084635, + "width": 223.8397979736328, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aq", + "roundness": null, + "seed": 489832781, + "version": 395, + "versionNonce": 202661229, + "isDeleted": false, + "boundElements": [], + "updated": 1768687467746, + "link": null, + "locked": false, + "text": "User's Phone (Android)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User's Phone (Android)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "8cvm889GbAY8ittddtEJB", + "type": "rectangle", + "x": 612.4082539876304, + "y": 24.463267008463504, + "width": 166.41015625, + "height": 133.2421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ar", + "roundness": { + "type": 3 + }, + "seed": 1821620749, + "version": 425, + "versionNonce": 990282635, + "isDeleted": true, + "boundElements": [ + { + "id": "Lbesiv2QZE4qMpdejIPzY", + "type": "arrow" + }, + { + "id": "RNCrlFh5EZBwnYr-7bA_j", + "type": "arrow" + } + ], + "updated": 1768722055737, + "link": null, + "locked": false + }, + { + "id": "VFsafA6rMCSAeOHHPzZuL", + "type": "text", + "x": 630.0674293154764, + "y": 52.289853050595184, + "width": 139.55943298339844, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "at", + "roundness": null, + "seed": 210388483, + "version": 182, + "versionNonce": 738978149, + "isDeleted": true, + "boundElements": [ + { + "id": "RNCrlFh5EZBwnYr-7bA_j", + "type": "arrow" + } + ], + "updated": 1768722056624, + "link": null, + "locked": false, + "text": "ProtonMail\nBridge\n(Optional)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ProtonMail\nBridge\n(Optional)", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "Lbesiv2QZE4qMpdejIPzY", + "type": "arrow", + "x": 623.8659261067706, + "y": 56.516159369287976, + "width": 139.52478027343722, + "height": 1.795223729407006, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b00", + "roundness": null, + "seed": 669405859, + "version": 147, + "versionNonce": 1201807467, + "isDeleted": true, + "boundElements": [ + { + "type": "text", + "id": "A2at5s2ss9zqWtu77JXsW" + } + ], + "updated": 1768722096436, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -69.76239013671864, + 0 + ], + [ + -69.76239013671864, + -1.795223729407006 + ], + [ + -139.52478027343722, + -1.795223729407006 + ] + ], + "startBinding": { + "elementId": "l68p6MrICi0j1MxyTO7FJ", + "mode": "orbit", + "fixedPoint": [ + -0.03605549165512547, + 0.33691893735853506 + ] + }, + "endBinding": { + "elementId": "ubnB-MCtd3Y84dUGFa-b7", + "mode": "orbit", + "fixedPoint": [ + 1.036055491655126, + 0.2969845457972111 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "A2at5s2ss9zqWtu77JXsW", + "type": "text", + "x": 515.2700388574564, + "y": 21.91319036172733, + "width": 61.11991882324219, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b01", + "roundness": null, + "seed": 1606941325, + "version": 9, + "versionNonce": 324972485, + "isDeleted": true, + "boundElements": [], + "updated": 1768722096436, + "link": null, + "locked": false, + "text": "Notify", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Lbesiv2QZE4qMpdejIPzY", + "originalText": "Notify", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "pDL0AmXVDpmeLiMPNhgXB", + "type": "ellipse", + "x": 693.824683779762, + "y": 392.25804501488096, + "width": 155.6640625, + "height": 128.03125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b02", + "roundness": { + "type": 2 + }, + "seed": 23153837, + "version": 839, + "versionNonce": 939040899, + "isDeleted": false, + "boundElements": [ + { + "id": "sMHy2iCqiPDKAwwq-dRtF", + "type": "text" + }, + { + "id": "hauitp4TDHIQsHGW_tjMw", + "type": "arrow" + }, + { + "id": "Gf2m8hClymOsReVNX3j47", + "type": "arrow" + }, + { + "id": "t44vlc42q_1WvhRZtOcdt", + "type": "arrow" + } + ], + "updated": 1768687455615, + "link": null, + "locked": false + }, + { + "id": "sMHy2iCqiPDKAwwq-dRtF", + "type": "text", + "x": 749.4011796068443, + "y": 444.0077874754859, + "width": 44.43995666503906, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b03", + "roundness": null, + "seed": 2011005347, + "version": 783, + "versionNonce": 28809251, + "isDeleted": false, + "boundElements": [], + "updated": 1768687455615, + "link": null, + "locked": false, + "text": "User", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pDL0AmXVDpmeLiMPNhgXB", + "originalText": "User", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "l68p6MrICi0j1MxyTO7FJ", + "type": "rectangle", + "x": 629.8659261067706, + "y": 11.624343145461296, + "width": 166.41015625, + "height": 133.2421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b06", + "roundness": { + "type": 3 + }, + "seed": 1014887149, + "version": 903, + "versionNonce": 2057912203, + "isDeleted": false, + "boundElements": [ + { + "id": "RNCrlFh5EZBwnYr-7bA_j", + "type": "arrow" + }, + { + "id": "Gf2m8hClymOsReVNX3j47", + "type": "arrow" + }, + { + "id": "arkYz5xpoKLpnfgkI-VOo", + "type": "arrow" + } + ], + "updated": 1768722217839, + "link": null, + "locked": false + }, + { + "id": "hauitp4TDHIQsHGW_tjMw", + "type": "arrow", + "x": 694.3950678409489, + "y": 429.10179501488096, + "width": 213.48697424257762, + "height": 293.8686533429834, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b07", + "roundness": null, + "seed": 1731189997, + "version": 199, + "versionNonce": 334928835, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "vxGx-nqDvOFOiAk4_cs44" + } + ], + "updated": 1768687455615, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -108.31215303440115, + 0 + ], + [ + -108.31215303440115, + -293.8686533429834 + ], + [ + -213.48697424257762, + -293.8686533429834 + ] + ], + "startBinding": { + "elementId": "pDL0AmXVDpmeLiMPNhgXB", + "mode": "orbit", + "fixedPoint": [ + 0.0036641987368588335, + 0.2877715401513302 + ] + }, + "endBinding": { + "elementId": "ubnB-MCtd3Y84dUGFa-b7", + "mode": "orbit", + "fixedPoint": [ + 1.0154254272869112, + 0.901239155712082 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "vxGx-nqDvOFOiAk4_cs44", + "type": "text", + "x": 494.8886246454149, + "y": 227.83543709338926, + "width": 166.91983032226562, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b08", + "roundness": null, + "seed": 1384345283, + "version": 46, + "versionNonce": 1216243885, + "isDeleted": false, + "boundElements": [], + "updated": 1768687050920, + "link": null, + "locked": false, + "text": "Link Signal device\n(one time setup)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hauitp4TDHIQsHGW_tjMw", + "originalText": "Link Signal device\n(one time setup)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "RNCrlFh5EZBwnYr-7bA_j", + "type": "arrow", + "x": 623.8659261067706, + "y": 78.1454368954613, + "width": 139.52478027343722, + "height": 3.525721958705347, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b09", + "roundness": null, + "seed": 1756548205, + "version": 486, + "versionNonce": 1694311243, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "dltbZAYBQ5avRfqMY2QVu" + } + ], + "updated": 1768722102290, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -69.76239013671864, + 0 + ], + [ + -69.76239013671864, + 3.525721958705347 + ], + [ + -139.52478027343722, + 3.525721958705347 + ] + ], + "startBinding": { + "elementId": "l68p6MrICi0j1MxyTO7FJ", + "mode": "orbit", + "fixedPoint": [ + -0.03605549165512547, + 0.4992494869539725 + ] + }, + "endBinding": { + "elementId": "ubnB-MCtd3Y84dUGFa-b7", + "mode": "orbit", + "fixedPoint": [ + 1.036055491655126, + 0.4992494869539726 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "dltbZAYBQ5avRfqMY2QVu", + "type": "text", + "x": 595.2638367425828, + "y": -29.353516351609017, + "width": 53.69996643066406, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b09V", + "roundness": null, + "seed": 1817962659, + "version": 6, + "versionNonce": 867947107, + "isDeleted": false, + "boundElements": [], + "updated": 1768686850795, + "link": null, + "locked": false, + "text": "IMAP", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "RNCrlFh5EZBwnYr-7bA_j", + "originalText": "IMAP", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Gf2m8hClymOsReVNX3j47", + "type": "arrow", + "x": 804.199683779762, + "y": 391.748789638498, + "width": 145.54899204799153, + "height": 240.88225899303671, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0B", + "roundness": null, + "seed": 778790627, + "version": 1214, + "versionNonce": 1528227589, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "dRN0F_NBkLKndkjCpaYdF" + } + ], + "updated": 1768722066265, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -125.98293212361705 + ], + [ + 54.3203125, + -125.98293212361705 + ], + [ + 54.3203125, + -133.25636962361705 + ], + [ + -91.22867954799153, + -133.25636962361705 + ], + [ + -91.22867954799153, + -240.88225899303671 + ] + ], + "startBinding": { + "elementId": "pDL0AmXVDpmeLiMPNhgXB", + "mode": "orbit", + "fixedPoint": [ + 0.7090589711417817, + -0.0039775865375285555 + ] + }, + "endBinding": { + "elementId": "l68p6MrICi0j1MxyTO7FJ", + "mode": "orbit", + "fixedPoint": [ + 0.49939907513908044, + 1.0450307827616534 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": [ + { + "index": 2, + "start": [ + 0, + -125.98293212361705 + ], + "end": [ + 54.3203125, + -125.98293212361705 + ] + }, + { + "index": 3, + "start": [ + 54.3203125, + -125.98293212361705 + ], + "end": [ + 54.3203125, + -133.25636962361705 + ] + }, + { + "index": 4, + "start": [ + 54.3203125, + -133.25636962361705 + ], + "end": [ + -91.22867954799153, + -133.25636962361705 + ] + } + ], + "startIsSpecial": false, + "endIsSpecial": false + }, + { + "id": "dRN0F_NBkLKndkjCpaYdF", + "type": "text", + "x": 778.3100734892347, + "y": 224.62913876488096, + "width": 160.4198455810547, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0C", + "roundness": null, + "seed": 1005555021, + "version": 88, + "versionNonce": 898665445, + "isDeleted": false, + "boundElements": [], + "updated": 1768722064816, + "link": null, + "locked": false, + "text": "Auth ProtonMail\naccount\n(one time setup)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Gf2m8hClymOsReVNX3j47", + "originalText": "Auth ProtonMail account\n(one time setup)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t44vlc42q_1WvhRZtOcdt", + "type": "arrow", + "x": 699.7999249622724, + "y": 492.33617001488096, + "width": 191.5447166289389, + "height": 0.35937499999994316, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0D", + "roundness": null, + "seed": 1473653773, + "version": 95, + "versionNonce": 983484163, + "isDeleted": false, + "boundElements": [], + "updated": 1768687455616, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -191.5447166289389, + 0.35937499999994316 + ] + ], + "startBinding": { + "elementId": "pDL0AmXVDpmeLiMPNhgXB", + "mode": "orbit", + "fixedPoint": [ + 0.03838548915238757, + 0.7816695142787405 + ] + }, + "endBinding": { + "elementId": "JFj4AIU6qh1LAm8QVQqRG", + "mode": "orbit", + "fixedPoint": [ + 1.011013121101312, + 0.2522179656590767 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "LuiOfX2WrGgTFGCh1s_oo", + "type": "text", + "x": 664.5501302083335, + "y": 36.45224144345241, + "width": 103.99989318847656, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0F", + "roundness": null, + "seed": 788874861, + "version": 221, + "versionNonce": 788868075, + "isDeleted": false, + "boundElements": [], + "updated": 1768722062632, + "link": null, + "locked": false, + "text": "ProtonMail\nBridge\n(Optional)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ProtonMail\nBridge\n(Optional)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "G6GfUeIwo3YJpOfYBMe8P", + "type": "text", + "x": 85.91843377976204, + "y": 55.63304501488096, + "width": 143.7398681640625, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0G", + "roundness": null, + "seed": 1208577891, + "version": 477, + "versionNonce": 1172217027, + "isDeleted": false, + "boundElements": [], + "updated": 1768688207743, + "link": null, + "locked": false, + "text": "Create groups\nSend messages", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Create groups\nSend messages", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "WeHPdAoCwAGXZu5ut9jaX", + "type": "rectangle", + "x": 334.86374627976204, + "y": 97.17992001488096, + "width": 108.8515625, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0H", + "roundness": { + "type": 3 + }, + "seed": 1407995459, + "version": 103, + "versionNonce": 523854669, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "-ooE7317g-_nL1NRo7VtK" + }, + { + "id": "jqv8Vv58NPpsDZoaXIZY8", + "type": "arrow" + } + ], + "updated": 1768687674726, + "link": null, + "locked": false + }, + { + "id": "-ooE7317g-_nL1NRo7VtK", + "type": "text", + "x": 348.75957452683235, + "y": 102.17992001488096, + "width": 81.05990600585938, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0I", + "roundness": null, + "seed": 572049933, + "version": 25, + "versionNonce": 708753027, + "isDeleted": false, + "boundElements": [], + "updated": 1768687389129, + "link": null, + "locked": false, + "text": "signal-cli", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "WeHPdAoCwAGXZu5ut9jaX", + "originalText": "signal-cli", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "WDFxDiYj5BqTwEMg7LiQy", + "type": "rectangle", + "x": 40.00437127976204, + "y": 565.468982514881, + "width": 157.3671875, + "height": 66.734375, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0J", + "roundness": { + "type": 3 + }, + "seed": 1228929997, + "version": 71, + "versionNonce": 1094396333, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "VIQdYMKqO2h3K6UEyC2Sw" + }, + { + "id": "0gzKxrXTCJjBSJaqf17am", + "type": "arrow" + }, + { + "id": "N5sdBqQFXldSJfsfurWOu", + "type": "arrow" + }, + { + "id": "1kbtuffFzuV7NZUisS0pC", + "type": "arrow" + } + ], + "updated": 1768687576117, + "link": null, + "locked": false + }, + { + "id": "VIQdYMKqO2h3K6UEyC2Sw", + "type": "text", + "x": 55.52805292038704, + "y": 586.336170014881, + "width": 126.31982421875, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0K", + "roundness": null, + "seed": 580192173, + "version": 15, + "versionNonce": 236781389, + "isDeleted": false, + "boundElements": [], + "updated": 1768687518806, + "link": null, + "locked": false, + "text": "Notifications", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "WDFxDiYj5BqTwEMg7LiQy", + "originalText": "Notifications", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "N5sdBqQFXldSJfsfurWOu", + "type": "arrow", + "x": 65.05559895833339, + "y": 500.5992838541667, + "width": 23.80033482142865, + "height": 58.86969866071428, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0L", + "roundness": null, + "seed": 780212365, + "version": 33, + "versionNonce": 1910016749, + "isDeleted": false, + "boundElements": [], + "updated": 1768687540566, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 29.434849330357167 + ], + [ + 23.80033482142865, + 29.434849330357167 + ], + [ + 23.80033482142865, + 58.86969866071428 + ] + ], + "startBinding": { + "elementId": "vgtgZtFmt1xq7s1sOgLPv", + "mode": "orbit", + "fixedPoint": [ + 0.4991629062847428, + 1.0946045824094615 + ] + }, + "endBinding": { + "elementId": "WDFxDiYj5BqTwEMg7LiQy", + "mode": "orbit", + "fixedPoint": [ + 0.3104304224792732, + -0.0899086864902833 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "1kbtuffFzuV7NZUisS0pC", + "type": "arrow", + "x": 106.31709798177086, + "y": 702.1408575148811, + "width": 37.65647670200882, + "height": 63.937500000000114, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0M", + "roundness": null, + "seed": 162352493, + "version": 54, + "versionNonce": 1881283405, + "isDeleted": false, + "boundElements": [], + "updated": 1768687576117, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -37.65647670200882, + 0 + ], + [ + -37.65647670200882, + -63.937500000000114 + ] + ], + "startBinding": { + "elementId": "EGjvtmqpnGt-kHu4jX-_N", + "mode": "orbit", + "fixedPoint": [ + -0.0354701644189914, + 0.5860891133797288 + ] + }, + "endBinding": { + "elementId": "WDFxDiYj5BqTwEMg7LiQy", + "mode": "orbit", + "fixedPoint": [ + 0.18209799930496948, + 1.0899086864902834 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "oyiilIVX2hFAWiP_sVYzN", + "type": "text", + "x": 29.35593377976204, + "y": 701.484607514881, + "width": 61.11991882324219, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0N", + "roundness": null, + "seed": 1845113859, + "version": 33, + "versionNonce": 1501856579, + "isDeleted": false, + "boundElements": [], + "updated": 1768687611263, + "link": null, + "locked": false, + "text": "Notify", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Notify", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "gANEANXhotXtR7hSKytBm", + "type": "text", + "x": 3.56687127976204, + "y": 521.195545014881, + "width": 61.11991882324219, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0O", + "roundness": null, + "seed": 1551421059, + "version": 81, + "versionNonce": 139753219, + "isDeleted": false, + "boundElements": [], + "updated": 1768687654763, + "link": null, + "locked": false, + "text": "Notify", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Notify", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "jqv8Vv58NPpsDZoaXIZY8", + "type": "arrow", + "x": 328.86374627976204, + "y": 114.57992001488095, + "width": 312.46071079799117, + "height": 0.14687500000000853, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0P", + "roundness": null, + "seed": 586510381, + "version": 30, + "versionNonce": 1977867533, + "isDeleted": false, + "boundElements": [], + "updated": 1768687676554, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -312.46071079799117, + 0.14687500000000853 + ] + ], + "startBinding": { + "elementId": "WeHPdAoCwAGXZu5ut9jaX", + "mode": "orbit", + "fixedPoint": [ + -0.055120935907557594, + 0.4971428571428569 + ] + }, + "endBinding": { + "elementId": "7fGvBO1wPB7KmcLdQS0oT", + "mode": "orbit", + "fixedPoint": [ + 1.0358501575446377, + 0.6718498712149771 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "UJHrk_gTqHqqsQkalk_mI", + "type": "arrow", + "x": 394.46530877976204, + "y": 448.8154703776042, + "width": 0.5707589285713652, + "height": 294.42321777343756, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Q", + "roundness": null, + "seed": 1453298189, + "version": 36, + "versionNonce": 1131521997, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "d2BEnfphKopTp_4VNx3B3" + } + ], + "updated": 1768687716695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.5707589285713652, + -294.42321777343756 + ] + ], + "startBinding": { + "elementId": "aQv859Sl4QY8VMBkh5Hua", + "mode": "orbit", + "fixedPoint": [ + 0.6596017907359145, + -0.09460458240945956 + ] + }, + "endBinding": { + "elementId": "ubnB-MCtd3Y84dUGFa-b7", + "mode": "orbit", + "fixedPoint": [ + 0.49939907513908144, + 1.0450307827616534 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "d2BEnfphKopTp_4VNx3B3", + "type": "text", + "x": 313.80075996035634, + "y": 289.1038614908854, + "width": 161.8998565673828, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0R", + "roundness": null, + "seed": 126578339, + "version": 17, + "versionNonce": 383597635, + "isDeleted": false, + "boundElements": [], + "updated": 1768687714758, + "link": null, + "locked": false, + "text": "Register UP App", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "UJHrk_gTqHqqsQkalk_mI", + "originalText": "Register UP App", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "xHpheprd4bn3oXNHEbr7X", + "type": "rectangle", + "x": -153.92983282180046, + "y": -171.09998721168154, + "width": 167.36328125, + "height": 133.2421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0T", + "roundness": { + "type": 3 + }, + "seed": 1237856813, + "version": 639, + "versionNonce": 1591806275, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "RJjVLF9X7yvV96NihmPfW" + }, + { + "id": "fLaTXseSmD8kBh3NLfiPu", + "type": "arrow" + } + ], + "updated": 1768688125223, + "link": null, + "locked": false + }, + { + "id": "RJjVLF9X7yvV96NihmPfW", + "type": "text", + "x": -140.4781345185778, + "y": -116.97889346168154, + "width": 140.4598846435547, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0U", + "roundness": null, + "seed": 1229385923, + "version": 15, + "versionNonce": 668886819, + "isDeleted": false, + "boundElements": [], + "updated": 1768687794071, + "link": null, + "locked": false, + "text": "UP App Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "xHpheprd4bn3oXNHEbr7X", + "originalText": "UP App Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "fLaTXseSmD8kBh3NLfiPu", + "type": "arrow", + "x": 19.43344842819954, + "y": -78.92164248511904, + "width": 375.60261928013387, + "height": 88.07170758928567, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0V", + "roundness": null, + "seed": 1658736973, + "version": 61, + "versionNonce": 1619233869, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "jya9PHO9hewd8XEkJzn89" + } + ], + "updated": 1768687813655, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 375.60261928013387, + 0 + ], + [ + 375.60261928013387, + 88.07170758928567 + ] + ], + "startBinding": { + "elementId": "xHpheprd4bn3oXNHEbr7X", + "mode": "orbit", + "fixedPoint": [ + 1.0358501575446377, + 0.691810502785107 + ] + }, + "endBinding": { + "elementId": "ubnB-MCtd3Y84dUGFa-b7", + "mode": "orbit", + "fixedPoint": [ + 0.49939907513908144, + -0.045030782761653475 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "jya9PHO9hewd8XEkJzn89", + "type": "text", + "x": 364.4761082967123, + "y": -91.42164248511904, + "width": 61.11991882324219, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0W", + "roundness": null, + "seed": 7729859, + "version": 8, + "versionNonce": 1007992771, + "isDeleted": false, + "boundElements": [], + "updated": 1768687812551, + "link": null, + "locked": false, + "text": "Notify", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fLaTXseSmD8kBh3NLfiPu", + "originalText": "Notify", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "0bZUK-yuL2IKAY1u64HIW", + "type": "arrow", + "x": 185.66062127976204, + "y": 743.3232828776041, + "width": 385.5904541015625, + "height": 889.0166259765625, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0X", + "roundness": null, + "seed": 607487459, + "version": 574, + "versionNonce": 721425667, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "4OfkQ2bqjpN8Pw0m0quT3" + } + ], + "updated": 1768688218158, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 66.43476213727683 + ], + [ + -385.5904541015625, + 66.43476213727683 + ], + [ + -385.5904541015625, + -822.5818638392857 + ], + [ + -345.5904541015625, + -822.5818638392857 + ] + ], + "startBinding": { + "elementId": "EGjvtmqpnGt-kHu4jX-_N", + "mode": "orbit", + "fixedPoint": [ + 0.43358447174131115, + 1.0705882352941176 + ] + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": [ + { + "index": 2, + "start": [ + 0, + 66.43476213727683 + ], + "end": [ + -385.5904541015625, + 66.43476213727683 + ] + } + ], + "startIsSpecial": false, + "endIsSpecial": false + }, + { + "id": "4OfkQ2bqjpN8Pw0m0quT3", + "type": "text", + "x": -300.0497363862536, + "y": 797.258045014881, + "width": 200.23980712890625, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Y", + "roundness": null, + "seed": 1439201059, + "version": 25, + "versionNonce": 400771053, + "isDeleted": false, + "boundElements": [], + "updated": 1768688217033, + "link": null, + "locked": false, + "text": "Register SUP Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "0bZUK-yuL2IKAY1u64HIW", + "originalText": "Register SUP Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "KsN6VJCu_Js44TB79mnZV", + "type": "text", + "x": 719.617652529762, + "y": 551.1148158482142, + "width": 392.6328125, + "height": 275, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Z", + "roundness": null, + "seed": 302706755, + "version": 328, + "versionNonce": 1529226149, + "isDeleted": false, + "boundElements": [], + "updated": 1768724761730, + "link": null, + "locked": false, + "text": "signal-cli:\nhttps://github.com/AsamK/signal-cli\n\nprotonmail bridge:\nhttps://github.com/ProtonMail/proton-\nbridge\n\nprotonmail bridge docker image:\nhttps://github.com/shenxn/protonmail-\nbridge-docker\n", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "signal-cli: https://github.com/AsamK/signal-cli\n\nprotonmail bridge: https://github.com/ProtonMail/proton-bridge\n\nprotonmail bridge docker image: https://github.com/shenxn/protonmail-bridge-docker\n", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "b_1xFx1msT5nd4KKXb3l3", + "type": "rectangle", + "x": 985.221383231027, + "y": 11.740588960193406, + "width": 167.36328125, + "height": 133.2421875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0a", + "roundness": { + "type": 3 + }, + "seed": 663201227, + "version": 953, + "versionNonce": 1136475467, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "I1pGk3B9Ec_xrXMDZgUV0" + }, + { + "id": "arkYz5xpoKLpnfgkI-VOo", + "type": "arrow" + } + ], + "updated": 1768722214705, + "link": null, + "locked": false + }, + { + "id": "I1pGk3B9Ec_xrXMDZgUV0", + "type": "text", + "x": 1016.9030772617887, + "y": 53.361682710193406, + "width": 103.99989318847656, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0b", + "roundness": null, + "seed": 583447659, + "version": 346, + "versionNonce": 581744683, + "isDeleted": false, + "boundElements": [], + "updated": 1768722210839, + "link": null, + "locked": false, + "text": "ProtonMail\nServer", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "b_1xFx1msT5nd4KKXb3l3", + "originalText": "ProtonMail Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "arkYz5xpoKLpnfgkI-VOo", + "type": "arrow", + "x": 979.221383231027, + "y": 78.26168271019341, + "width": 176.9453008742562, + "height": 0.11624581473211038, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0c", + "roundness": null, + "seed": 1417079147, + "version": 49, + "versionNonce": 35663397, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "EGNOTS-XqhS2k84RwsI7Q" + } + ], + "updated": 1768724672820, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -176.9453008742562, + -0.11624581473211038 + ] + ], + "startBinding": { + "elementId": "b_1xFx1msT5nd4KKXb3l3", + "mode": "orbit", + "fixedPoint": [ + -0.03585015754463765, + 0.4992494869539725 + ] + }, + "endBinding": { + "elementId": "l68p6MrICi0j1MxyTO7FJ", + "mode": "orbit", + "fixedPoint": [ + 1.0360554916551261, + 0.4992494869539725 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "EGNOTS-XqhS2k84RwsI7Q", + "type": "text", + "x": 868.9287559872582, + "y": 65.70355980282736, + "width": 43.63995361328125, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0cV", + "roundness": null, + "seed": 1245023659, + "version": 6, + "versionNonce": 1252528491, + "isDeleted": false, + "boundElements": null, + "updated": 1768724671332, + "link": null, + "locked": false, + "text": "Sync", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "arkYz5xpoKLpnfgkI-VOo", + "originalText": "Sync", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1PI-IqUd4I8EvRfkkzFlI", + "type": "text", + "x": 886.7487327938989, + "y": 65.70355980282736, + "width": 8, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0d", + "roundness": null, + "seed": 1872296133, + "version": 3, + "versionNonce": 878177861, + "isDeleted": true, + "boundElements": null, + "updated": 1768722227703, + "link": null, + "locked": false, + "text": "", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "arkYz5xpoKLpnfgkI-VOo", + "originalText": "", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} + }, + "files": {} +} \ No newline at end of file diff --git a/assets/SUP Architecture.webp b/assets/SUP Architecture.webp new file mode 100644 index 0000000000000000000000000000000000000000..8c01cc062ed25b817cbc17682cc4b145799507de GIT binary patch literal 62114 zcmZ^~19W6t+pZnkwr$(CZQD-AcE`5S9ouHdPCB-2{^`B<`+ocX&N-vTs8LmGjjFlk zd~n_OT%{x_CYEsn0H7`^q@bq2LGb5U-W^Bi}FcMw{8Z8}tYK8^3D5RX%a=#C!OQ&FxR4&t~75 zSN*5vUw*b9!yo+b?)RU+`6E9cKLboBmn!Ir9ESornC-ZQms|M~gwfzj>!_2Js*2V0*bKM8-= zdb7hv+qsUSYkHb-uPpb1UHhTcIx2l^^GGS4d?pJmej?If+SKt9kvzZ^Ky zm<-|#9lQg=o`Uy-CjlCP{+%TF|2*s~2^w!r1@VLqIq;SMO|+Cam_b2 zm!;usu>$E&7Ky5p7iqpM3hLi;`@b*n|2nw1HY2@hVru4HGQFr+@{lPS*0O5raDHSG z9}_Y+Z^SKQlJcE$x7vR$;nliP&tbDvW*Eo(|2>cS&6suH(5yaVZj1}JDxm2k=c=f% z_us}XNLGyn6+?m;9QwAGPTg%pHZt5Jd7f33W{JsQOp;H32(u}8%-?A^W}?%8GhL!M zWQ%x5P(D-g%OUx(%I)oJ-RpaQ)7aefY<=5E2c14&kn@3W4Ek$I|Iay8ehJ(Z|7L2s ziv{t72FdJ5Khi$=-#Nx+XSIO{FDeic^{L!bMDEK7!^KQtI zyIb*=0KSqOO^Pf0_0<0wz#rZ`mL?eT4G94(PVTznQ1uNMTHko`7L-3i3k6qQyU?oQx~^J&aiW< zs5)zl04gv5wALV~q?z3)3-|a+%aY?=T3T+l_I5WX^1i^BH-B;jZQI2@IQ8b(?ijMd zy{DUeK7_w-w|X~ZpU%b3vja`92M;{JWdn7~q>ckDd|o23ce>@j$yXkH|248-U^;)t zyYR9yH$c4Y1K&5Wi@;N_o0o|giX+%(5(09eZPI*)Rj-1d?OW6TN?e23LnT3z{|V*+ zh~z!F$gTb{dU(e};hMig0gI8jExuf(np*~eU-!qN+DkC==~wm>gB(oU2#uYH6)Smv zr5+Ob2?CYgfR(u`u~NBSfP%nV3M+!s=-;9Jy_2*JZ3{kCpQL+`qAG8I-gGFRMk7qw z%nGER$?GOMdjV2P$(}?*f+q4U?f1T4Bqrxn@%cGLE_rl?vGzBg+mH? zcAq2P?BKJ=kh*$5>d2}tx6;D8LT8VO;%lqJo^<<#fq0KZzGoc2ne^unwdZR=$Jiyb z13M6O%B_itD9%QHtVZ=;^@A|lD~Wv;wFoSaZ7XFkgky1(os8C$zm$LIZ7Ic9iC#T>F6S9R%EQ=hub|Xc=3yN)MYShSZ5tf4j9b*2Zpzq?+XL%ov<0WA~ z0CF&mp_K$B7igQU`i&fR02`(u^oV9k`_az5e4+W1@Ryj_Z;Lu8qoF2?MNdMRZx~x2sDOu`g{dJl z7j8QFU*&c{l#|YuF`s5+!V*WO;DL}bK~Mx$5c54wyr+3G$?a)E4Ma{!bcQyts3$3D z6$ed~MN56h0C}QtW&*fYvB;Y-KV!x2k1|WEBauikQ1l z*rclnTr)qKYpnb);kZyOY>vwta1E5tKwKtU4ckq)1#m~*qTM&);6ycl0z);b)HR=} zK4?h=Y7&cH2#NjZ)~Jt3Me6LNe7E9Oru(giu|PRfKt}(Z9xcBatu(-2AVEg54kAl# z(A{9QR;G}ZRktF^$8+JwO z9q7WgpcJU!PDJ%VB?*_SL*3ya%nCN&5(rLQuB_T1!=q$%$`=lOAl)Y<0@zzl78!=y zQ^4o!@&ba^204g}jvDM!FY}-pyWIVeO=O|HQA%wtu(r^CFTZTTFl4N1KCpklLAJ|H z|HSgw4-0_e9RPZGilKo979#BC6AGzHta-=KgRe)PZQ@)N^DM-U(lqM0ni3g+O`KN!;7-<7&x=Bp= zjhR0!5M?~D$Md#zGo?>!6jJv?kp-(Rft=%ZIYv=q79C}HL>;KJwD$stEUyp($pq9? ziuvTfOuDiUYwjpGAZP~ev03xgEDMBdxgRp@N}{$WLPXSWsKZ9<8Rjy{d=IN{Q4G&h zpqhg@n_^W^x;qF|&;th4IS(Hrqo^{>u%HgTM;HnVdt`F!-8JUS72>WYIV;gSO+*HWnMXvkm$WXniVRFWNyUDs%UYB|#`b<|xVj!Ie7p!6 zX|gACj!`&j&piwHTvJ4R-oW^>`>piK;rPP*1>zEe4GxgrEHg_#a&amJu%uzVm?mVd zY;M%ULMU_`6Akd7v^R|fF^!4sk?h+Xf11nkwC)fa9kSIxwHAu=9&=7RfOQ$#bL^o4 zWwk!){1^GCEAD`$#xBDAp`bQG&z_gCjU(otf)ACOE#Bh+3O)Rm&tD`Cf=eKx;wp)sH{4BP>SQBcM|Eyk+Ga z?s1fvpNB%NB$`p~-bA#}%i&NN$>cEXR4(8F&0co zIUP3!V|txr?WjW~>4U9fxer!=Q47O~4<8&5Ft4jQ0jJ) zT4hS{4i`ui_f{B-gy;~RIgQbn!>?f$8aZPj*YIOR`@?9S4)Kar@^83Pvdr+ax4QUF zC^q)ZTxO$SVi)W$=FphM;e+3sjG}zkpXwBBs}v-`+3}TlA6C9t9Sp%wr^?%^N!F2X z8B76rEo1!`T-xB8qSo5CAykcg6g&LW^4RCB1NK+T%)IVXXaQq~u06F)?YTWh;zz}vl>bifSyL8`^fb@IInOn zADrCTkg{HOi@~qU_=kVcUd=*JIb=>M`Dn5F6$m1i#+ef4Mxy)`{ERMeQlSsbQQ!J`RfO`;fF z!`_^i-&Faj{cH(hL_)p)Aa?&2yJct~?^Fp>;lJRQ9b{ofUhQik2*n2-Oc;gHnb`#i zbc|*`4LcfiIq(wXU{F;Qm%{`&=%=lRCo4mVcQB*?^u^JRZ$sP>jW4Kbr3mR7k<(t<#ifW-4^PhGOE@AIqgXe>zA%GPis`iBRGCRKAF0Scw5+SlVfi=0>4-CpwnU=4k3whIToi|{@K+x% z6&pNMQOxivMwzGz{KKB*W#xh(86o47{ZOEd-%Zc1#Z1~6+?ezTDtyFya2U+Y%Fzyv zoSx`39~e|Nf{er>wCQ+^+P9Ynmkb?|gNOgoB4K&J2?=^u91C`i=C*Qd09)eO0mTw@k!xbueM2!djbuWTncyN?vU9 z$tvUmn^e-=?$cPyAK9xlrgLL8vR(Oc_*&aXOPQ4d^M&fQx)#bh|JKJg#LEUDeSDz( zwv)X;MH>GEWt~3pD8`qZPU(9_Op5=L2Fre+1^-dTe@cUqL-61Eu}q-!AH~hTmeImQ zYf6@0`f&J1ne)6HZ~aS;rZm+qq1OkB*3OCTee4{Iv6oO>cU zaENVvF9Kt^F0q}1eap$orTn;CzUqpUYz5&TNuM2drS-2eqlrciA+-J10bl9Evgy2y z@#cu+9?>OpEh4Plxc7_<6@Y+QI`qPM%#0sOh$?Te91D?HoW(^r(%4+R6kxg#2SjU* zs&})`b^fPZj|datx+Bb>001W~JB)P5IaytwI=etMY+rr?-r*dk;w1U%J&m%D34>I8 z4P_KQF@^zM_`irWNQlRJF8?;ck;JT zr5uFhd>bfw$buYLK>**^H0;pj=iE23fp#OSucFw#!L+0ejzn+f$hGZGEU$g-GtEDh znMSOCz}^d&hfOsc{g)SUF_qjKZ{=?}X`~f`-A>rw`S_L|6n-)cA@RMf;~xFGR_{?6 zhh$bDRmT+CZ{y^;g?%l4|76|gS^az4g!SlC6O9%_y0)xvjOZDygu5>#|C1i&zf2YW z?EtjFK8`{Z0=RU!t?T8bc$ZWi9HMp3iHsu(fc_RG4m4iiD}pDnHLUe3M4uDmaF;dq zZso9@fX}KYqih=amMz9yfaVpq6#GnZCtKs}kzW}exE{OT()tyl&g4`O=Xw0C7oK+i zQ?fM`gbMuv95RKWsIWNAZEUp5!0Ge@r{}|1#kiyrIJmMcu}6!er$yKj>b8PD`S$*JOMqw+pc%p_uQudl_cl7mcp-xwzraWX!RP)b;QyHhub~0Kr+ETX zT>ezx__jk;P$@B0Rd9ZC zo6MRX8%R3{gtFp))HHqw##k|kH^_z5*mbWR{;nyDb&(j?lV%wFpak{i=O*=l+?}~& z$>XHr{$&M3YHO@xutP0iso{2`7^=w63IBvn+q!*!rU7WKqjp(QVMs z+%r6EgcNFLR>Djx2Rh>@u-+Ql-q^;=k^u@p{!b6QdU^x)B?FxZm=Xs7F_IS{&^|U6 z`kbZ1te?D~2zc`}|8mL#%#ap6iEq#}$@o(8S0Du{+6tG-Yxu-m_?KW5>Qm~;^0o8jVrO5@)H}$~`ItlqF z1$vdel`Zh}EcDXPguj-&Bp5#rCt&V6LW6OIGW^?}vu-qp95{osC_-C6qdPt;y{{yO z{8MticZ3~Vav%i9NI>uinz?12h!uT%l_|zU_g7~>1*reI$BruAAN&`4{*6;;>llBQ;qqJK+~Yi`JYK0S>dzbAL;@fc&Ji3R7+Flw zLFLQ6OIA5eHo&ipvfo6sQJ>Z2=ehBx_-0uJaoFJVm`rQ_r@Altnvnk&tNz%Gflt$4 z_5GVr*)JO0SJVls?n1s=#4|0uevqH7r68MG(|@`GXNStAkOJ+)_BSJ)D_?vfD)88Q zgtWg+OvnKqPtO0EnO`-3t_T78OIB#}dC*TH20vz8e6Lnr-=i)jPcz({Sz0ZtuG&;P9ppR@M3BF7!&Hl$XM|f6Ci(j-dC&n-yBH4I3 z2p!UXzxl`7_hLzdBuxBhXN1V95&hn`UGnrF*aYkIpBa~~5<#$;GT~|dc!b|mcDxon zNstq->1d!)71^AEcEhec)N8;3*4um!vpI8%*a^M!CbC7 z6W=3=Az}Y41mcQgyB|^SdKb~+A6MGG9!HG6U!57_xP*JD+#SVPe>Kj~y-%0=kOGvi z033}MR$BrGC$Ju>#YW8XARHrN7DEgw#4>*+$&lyjC?vc=w~eKd=^%PDxS;1mA%+bOMo zp}!H)oI3K2@VE!NO#3;Bd1bGx_dH@`F$q0tXlWf%)a+ISzH7^`(H|7y<#%i9&VD~F z+|`%x7L|jDFn_HZgHg#QJgk|w+_g{tDOt3p6uR0u*0VOT?4*Qgl|n-QX>AerpIC{z=? z@$u=1azHqTD6?O2qMzliA^+eM`(qnruA$2i8yC%Ls0iD{G3u1ecQAGuJ6GZG!$qp- zmVZEl-p|{SDBUdv-eXfF&J%O{fD}Tq?+x7fwMgbG$u#bSl?CyE22bZ#M;dtz2S-f|LJ zf1vOTi)Hr-a^oxJ>dh+^jbf~Qn!0m@$Dx$WCufTdfo;B=> zf>--_#DA0C+L#7gG*g`ynDKB$Z8FIK!-KH~Eaoa2obEFE zuf2*>Cwsm*p7^J+j4Kqw?&o7zbNV2$K}Y?7@stS_w0?jLQ)h+e$t=4com#>@@)jrs zr*x2$Hjy)sKZ>NYXK_X;yOB{?H|Nt`r%ayz##sbg9$nlhwH2MkL4ROJjvqgPh?Xnq zd;A&Mwl*KfQjj}HC?@`LHjooizA8g=f(Xo>2D*nEm+Jf8uY6%mzfDI+k_7$SO%oL> zs$^TkR#VZOFyuqAZ{!?TlXYfw+6?eNIrx%&ksU4D_NM zME5Xw;h~)AxCe&6ob=txa#^UYf1|d6Mw$rB&o2OWq*REs48>x93lJX(&Nb+y$SgU> z6N4%*?&AJ6b8f~A5LfOJRB{*3El9!eg7&;dH~Y8nhW7>`aPJ_pHPo>#^!?ijkpzt! z*lEtvI^~IfELD`YU9}*EWMh)AE1rQwmH|deWe&TRZwqTr4+a(YI6zmw82qz~rIE9J z@we>}^BZS@{Rz*4p+h8J240@Cyfyq0qMfdHn}6In$s?IbUGJg2|7b>+tQS{utD|&t z&g#jvzABd?C?pT+o%{`ZRgCk}Fz_sesD5g}pq3^b^GToHnV&~FBSje8Sc0@|Ai%J@ z;~OVr5hV3rM$6iZG1XUV@`Hy)2~VZNT_Jfia}C{OvKZ-Kf4tV@;tS z|4N1+x@rMM^@fX*(e~s6p-g}>5ksj2hr!ebTWf(eqU*5tHz-R7GMI&b?BCyA%{;^-%B#Gw!vTW zc&W2M#5DeJcd}fE%^+{KdTGL%3#5Ti-@wav^PmM-^aB=`bMrTAhVPpL+5{TbGtTn)DEwsE~XYd zc#)bUc~5JRJh%m-$n-DM2o7~&-v2WJ)HaCuu34iV(;;jPzCBuIPXag(MP^}+=$G8c zeoY*_#}fZ3eHm&0_UpqJVV!T1AfuCrp8H}k?heOa2B{ffXn~A>OWGkMGoI0yYFAytzTk@ z>y^Pz=Xw?N<&wy0!8DM1L7Dx=6PFBBTAXu5x|1OZvfSN#q-Ja{UF~*!`2lC}Wb6&1 zG9IIcq<|7r2UF;SR<)+M{*gs?2($OVXg7f90IgsAm-VnI=LiY^z^~%E3u1usL7t~6 zh1siwC99MONvstCuLoieE=ah`b{KPs@1mo#)TDglyS_?7nhfr-`mGA0|8n2{cw6n- zgAO*)qkHpKm7AP@b+@k)-{SX?92$$HME)?91qlNcCg^c0TpwlV-sohGe8oqmmgzT) zcN}Zf^7Pvz49`xEiMH1s`6UCJz{M{pX4gNdwiKdDpKOjFDPIXOT}Bj{zXHnq|A%P* z=T2}latqr&(BC$bSf1_hA3Ny(wE=4=0Pyi?<=ZS-I(Ez4iAfwZXou;Z;mwWBJ)b|A z@WS#QoY8AyOB6+dJv9tb7E^OIa5wJzAlL3tNj8@J6PG%T?toYt92F-Q_o4+$Q`f-- z>(G%`UTmcgvkR_{c7n8ZyGl0E^)8N{19fzdIR^b^9Nl2SpGC)M+Lz+_(Y={8n4KAG z?E5dM3`sOGuD*xUk7y$YGVX0k@*p-SQM0&tpbt|dwD}2T9Xghe52-6Jk_C(2<~-&e ziP~tZptey30EX|en(!iKRp1w->?T}~d|4K5p&g1vlNF}dI)D$$1>2Qz`}nS4%=iTi zp7D49W#$7%)#X}w$62bDcqO=aS%H4+dXYa=G*z-fF95I?uVIaTSMK*F0Yat0{vy(U zq^s3Pw8(tU^7q}YieCzl006KsF`$=;iQ~Y_1mf&1w`!$aR?}%n_CQq*G`OwkM8g>^ z&9y4j@Am+=IOr>Ad;f9nG_kb@2q>szSTWD-R;W*8e90*Q0LM$>z$_n{(@sE)qNAK6 zR#!k0rtUTaN1X1&F9JxSLOnPE% znlbxqUoEq`MnCH11bU+uHZP%Y_qekHuGyFankzGBkQ?-1z1o>_5Ea^@*toYM_cR`o zQmV!{C8w^H50G%S!Nb0~85N$jfu%r_w zyV-o>my#nSgmYwmi0<0mr0RfG?7EH*bVdaaYdfU*u<$KDV*HcmZBbrzL*Jg{b`hrU zFvstfiW?V!L^83x<8-Ze6;FXFaN@6(hsm`GUIZ3#G#DkgNsxmFh>b$S!H zHxq-$pzi_lW!YJe8-T=~S<~loEOb+u$ z|DptY0%TjA*oCM4XSc^IHcQGv=W#|o4)<>Fwvc23!|Eric!NyeYQ+Z8DCIR{fz$e1# zhv6bNz;gUl;k)X|!Qe%Iy(WuQnF8L=OZng}j4K%|OYM?mOjy8>leVy#&yKxd-NU7n z>H@^KGTYPYQmY#pwk2Y!RU`08<9Z~54;;NFriEAUj#5!9;iH~)6?j*1y48$q7$uY` zAbjp`KO{`o<+oADwqxYL%o2BB}VLJCHqq;0ZPTqmFzj1n*uE`hLydjqkagQ2sAj)qmN-XXm7+KcA6Rzi!7Z; zU)_*6$Rbet;!C!ht9zteLO?r%az!426hK#{*ukHMs=9mG$kw}Lr)dxMqKwrSxK!gna&*U3=;o&+Pnk}=9q zmmt%&j&X%NxzG=#XK_Wf%`ve~qQ{egE?C=YbbD7K=gno}br*IS9cadjes874IT>04 z!H?yNgO|nEjAA(|2~+#gEC4t>4|(mw84za8$D}z}9pa_LJwVd{Wr1~^`r|S{6XZ5H zw2>mB(lgtCOqV1yHn+w;mmIO8CyfcI3ST)gVh8vW_O=(vx+Z4Kcg^tn=tP&ZFp{(e zY+`&IPD+VK3Y;hlx@~$re@akOb-^>L2HTy$OO&|rdyyhv#t<*;@@dLta5`O}t)^2L zF@P*BE2C6wCPm!VlP2k!CvF(@DwMb-Ax4wkv~Z3p>Z+&yrivCbNX)@=62auTbtAn^ z>DA^K^xg#I@wF3KsF5bV=40`vR$lt@a*ygK(zBhE-a3f-??@niNh(e3rJ z&SaG%r~N1MR*AKEm@!)M&+2jU2tZdEcIeD5lnPM5bPLfarsN}cKgG|$9(TG<#32AZ z=r@Uf8=|Mn+JK6k?&%%fyf?+Kxxl#Pl=YAVd3oMou5awxLA!Igr&oq1bCp{9pzFDs z_?=;p7O<&LODr{6j-W+Ee9N$Jv{=Hl10-i`GmE^3dN132xIYtX6ZBv?L0g;0x*=Rc zg9z8bOt)Erp~m&@Yd?XWnK7vi*#nRqVYCa<@xZk(-xdsb-KKDE)8Mk*a(SbO9~nGP zWIodeVI(-ik-AnJvvwy$I=o^Hnsm6~QctRVkfo}Z;u?JKXGkQ|X7 z^+5fQcyo>;=jYxm2w{7dd^+L^40KDW=0e)As<#HJ{`$(x$T@#%G&Vb#ffY;tAyRCET)- z4}L#tKS>6Mg;urUjRY12$DgOJ^yOPF0u(?bjO^_uqZN0#LkiUWFmS>2Z~UB%l<-So z0DIv=8!ft@fo3bwo^)Hn8Dm4R$m@F&ZJsx*baz(@_=!u)exf);UnmZIm9a)KS`PV4*GQVg6jqOjHl^ z6&>O-CT`tu>rzkAZC4-fdERzwp6EFWN!gxt`dFxv+yP$Gx{R)yDI58sf>7g?$bx(g zToa0k{Id)pvE~wlKIJmqlgJ59W9rOLuYFeF&%NPmd)AP7v@*U6!5^}8Wti!n+~j+a zmf(5>77E!66EbocA@6P!)~7l=4~*gb*75NnWBw267`)bi=v0UtN+R>s_yEN zCY>TF_P|tm!GTTS#!98U!HDqq+>+tozN2?Taw_}}R%Mg#i;_{`rA!$x=;Ff!$O)IN z>lVKnlGTG4-~IcM{o;B@5u_y7XfMjJ4Tc8INHy6@E%;Nza~?}@c>-C_9xC^5v{zWE zb<4Ld}m~*mf=W??xx<;5ccO!ViLj znyE*^gP*yTV7Fkn86E;{u8YH>YObnNL^@Rm4=H}{>TZcY*)8!?l1+fkQZ6AQI^r!3 z(7m#7+=2{3NgK3Gn*vVcHKVM7D&vc?YTZ8o@rR<} zN!CI+Zd@-r9f)TX*P8EgNZ&TABb{KU>?QCd%J1440SyDvYG&N2KR>$Kh3^?o!PRb3 zf4sxekUM+LMq%IX%6nL{423U{8H>;@g2hNqVFGov^L&xRn$o*YlP5I_hLvJwGOWs# zeA6Rg`4;BkFBzobgk7xujXFw5s~ho`FsD%L%vJA^SN>siRzWLIjLvs)6hbl(!Rxgh zVroClW}Bh5*j?G2ET3#v6kn#q)F-lR>Wz#DlcrP|LWUCoItY0qGik|VTU>pc>$eP^ z3cP6^LeJ^=A#=vz)OJpV59}024dz&Gj;U6G73w7gy|D_lTu-M_8;)kM2y{MuoCE?W>FACFdJgYnepK8e&>Mu{e-?*AZtcmV*5z!z*tAlQ1HF&&!8{0u*;*n%q!rr(hSPUc`c0;DnaHjpqxHmj5L$7 z&3n~T1~)K?ul=12u?blg0w(MzSZWY`b0(rl4WPLmX(6S|4wM5|c#wdLK0qub0ht?s zH6hSPD;p+5U>yECG~{N5XSTPFX7XzW`@8#hXR&l-Q2Y9@6LKZdQQId5j((%N!Leb17!xV8J_ z4^L_;+w^9xVIz2F5zZqmFsL4qz@3E5S3Y)qG4|~eEW+ZfKIrv62z}7FbEU(r$w~sp z`8>%O2x-&ODjAe)jRNmO#d_tn4ri$-i8ehe0qPQ6Zwa$jQP!6)xT5M3gP|=$rXi%O z-zn4F3)=$l>=~alu$%p=WWVEeeyF|SLN^LwUXUyzK;!j`xN;O49aKcsI?|}aHzAfb zvReJ*+fe1Z-B~`&XuuKZy5YVp8gMg%=)K;5B(<}Oe~1JO%`zhT9%rkPCXsAZ&xjYY z{Y2SNzRjAw;g0)Q(D{$zx4U z@E#GcvD z5G%<{p2zkjEP5ke&(Pj(yc1OkMPA5zT_UB?I|MHSTQ7f8?yg*p0#%K820-6sXEmO)gA%4Mz8`!3WW@=u zknbWfdnq^?vA$K!wH-~9luxxi6N>P_mDcx;HHt~isRV8hHRjBM2-UC)IwnZvr->ba z9{p()w_08TY-|A0s%M^B#YVFH-Qcm#c4q{Jg z>H=bfz7hxx<4rH47wSR$g3(Kg(n=o+EnT>}`zX?6Ag=`1&2g=9wk7NI6APca2R!#3 zmth>yyPU^C7TrLGXdYX{M$!K`2TIh{o|vA_x{<(^BbUdqEEch(vxD(G;{eb0aOTN6Ccm|x zGoUzaZzqU-WA8AFX@sE)6gYb;DonHUq%${>w;oLkDbJsqv;g&_SrV;}M>tG1>pv|V zYtQviHB;n{o1Ocvu4H*TN?esC{ic&=BI}LGZUo>IiSjVJlWOP@Og$q{p$nEk0l44} zmj`%H`uFL2=%Ohn?#=d#B1`8XI2V2xFek{+)W~c3?u3|OUnH55FYL}TtJO|^>^71W z-qdV@G64tUMf~xO+P0KNVimrdJl1+a7Rzp~azCiU#)-7#9$;v8x<8n#l*4c~`wf#q zkiBCrsS#^ON|dF%zus-c5Lgz(v4!1Fy913|>6e8G@J!~p3Nvsa00C0PxM#24^GW>D z2NZ30vQClYZ9eBNw?CdHX-0+)Kfyet?J>r;1RId6c9gN(*Ua4`f!|Vw#?BLnbAyVD z9ykNlgPAR~+Cia{;q8J((Cvz{JBH5qWOfRyn0TmM(r1E&)r-($wBkmeW@vLWZ}+CL z8B?@cYE?JsJh(4kysm4M<0=$A8Kq-yN8IU0y0yS#4l9Hc`(t$`HTkK(=E&W9VUk@g zK>dMgaLRUR;6#C1uNY4$@$S-d-h-+j1|f0{&=3v!^g)KP3wNU_g=K~6Ct#4gPXh>X zXyR+<5ZbQJwfs3yveH!r^*Z}$ZGxqM5tdA9VH$aa1=f?`z4IFk5ph%?i|5nRT4D`8M+dtZFtQY1R?3La|Zneay-jEO$?Cp_!SS zQ?u}d9pacU;zfd9XFdQd*OI0KV%RnaDtGatt6H$9`R*JBH%I_BKbs&ahS4;dYG{9Z z>aQ9~l%lN1S&2d?#Bh{@^gWVh?CVm$0D@mn1hcXN1{IwgRkr5{E2u zCP~F>C|l!qjNPYj-~a;^5$d~mNittZ;o~1tGYSxLzAzNrJQl(-D`?>@o4ulBY!fLO z;hZbm(6t$WkNV}|#fNa2XRMP$(oq3_c6eAgngi60UXHah;S4*3v;me98DRv8sOrnjTrTLi)r|X>Ce8jSQEBYt#32B4heF86C0*!3+pUh+*^^0w?Gh8Rf>pU}?T75HwbF-LpcxeS=wz71eBde~{8{8Dw! zjiRr^=eB;? zwVAEa`AniBk`Hg%JBPrQ`~wD1+0DCjZ^*H4${@z;r-hFa^v4SzdD*SJl};P)PG|`? z6VAyLu%X#>6sny&e$E+Y5%OXFnbpl%Y8Vhej)U$auohu#(QzgA2S^CX;R;Qj2puQ3 zVh0-U0km=6mCFxiyF;hksA}1}tlS80K@r+ea6<{< z_2IPfoZ0d!mtrVSl8uJyZ`H_VODlN5;@*|PMomx@s>KOmMCyr7CRazOU2Z9G)cL|y z+z1Xm)F`YNeHrH(%Hzq1=9;@h>`8zK+F^OROpZM+s6HH37hPgqJTN-y^e=Pn32QGt z`?((;#rzCV6e0KfavgNX)o0lW=T12zMm|+)UK)UqAJ_blt0+O5Cth)Th3Tgum(0m8 zZZn8zrliX;-3!JD@=323NyItITOXRL(OKXkj-QVwI{J5Fl=!RzZ+Q7EQG^7FFC*HC zPIY_*jIwi0Av0A)$B=w+*m=2w)zlM`Q1qwJD+1@9tQifhYqj<~jatIqRmgU)K zh2q>6j3=ABZkTaZ2)Z`I7D*0Wku%jzRzZLRnE;qudPP)XQ_*fs?HhqAP5$UsT%Bsc zC)J=_D6nuJcUSAqTTqXPGX~l}5e;O69uxGO9!A0MM)6>B zFaz$abY`%%XG0M0b$2li>rZxZju7sshci7f6@_AnjPzY%0vOhZk2+X#3&x;^`U=QW zRh|ggoMnC zHKL-iY+0~b2ICyS+}l>YuiaQLSeqmUC-;Ryt4Wp{9vq8;pBvMELG9I2&*9V1&_#{-Q0s|NB6Ij)(0{FXgo{!ld z@@vIGVJ=^7InK%?CkZzuj}am?Ou~WJf^OgJU1ccIcEdumNwg+mkV1 zZnRKJCVdDJPrJPcAB-jGRdjp-zp!sajBpR(t1+G)4 zq%(w7i8r_=x`Kyhw}a$cb7_5DM@lCraX&0o3IL-|%qg~{mmghaj5vuq+t0)fc({Cb z9-qN#DIa0G05hR>SQxZhzQri8pM4DXE5h4dM+DZK<_YzyVfT`KwVT_~Cr*82IU7uiEJdAn=ZPJ5$n(39C2DuS(N0l>MtT(s?#CLr0 z2R=$b#0r7sca~C7m^pkfB5{gf4V+X;b@x}>SP>Rt=B%r)zj!pYCRU1=AiGp8BmAx% z+eR8%R}pV#nb@-9bnC}geGdqX#c;=;w}CiE_A_O-m2DYaxs^m3po*3l$f?It)y{3E z=u_@Ky8g9=mNmK8)4!-fkKv#>hyG%xyVaO5OCh$j_y*khP<>v#E4yDrOz#3)(#h1M z9n7NsE|Qq#-Mv^Ov@|we)sir*Mf9lw6S2#sBa@!(Z5VsJ;vv2ep6>W-p}TRvf4$DT zuugyG%Q|%Io%Ef~9#I9_i+vMD)<;_~?mK+eDKCij?+ z^fL?i0POC8?QfaOOtYtGDRlQYrfLzH|L@>-DXR21MfUpimWQBL_ z+~gU4JbumV?>)U=#;plQ7GnI;MGF6z>H*ka8sp9*c}$FFI2!-ALr6GhOI=Yrg4n_R zR-6n!@y{EA74`(sn1YJd?3uehD3N@iT#TAjEZ;5W)d9B0peJ~)&EaBdy2jJ!fwF&D zuqmmg!K4;{;$12+BUtN6-nu%hQ8zOXQ@)w_uV3cO|eTXrIas>T#MD(_o^I!4`!}r5! zCxXs^YCWP>?Cm5L^v$TF4qJ>n_U`zfa1}x0J}&W1Tq`X&^BU^Q##DETv=D2FB&bAv zsI2oaZ|6udelcEao%(ma@)Xw%!=f~Ix`>)pJ&h|bRU?oO?WQ(neM-KT?R)n#fmLqr zc)C1M0egefrzRJWP{26_$mr*|u6(c)u4Jz-RovrHqSCZko^eY=dHduxdxf>VgrO3l zm*M50k$LWiarzdi?JWbxO43}J00>7Dhd<=Y8(#dv?z_}>p3ln^a!K=t<6#ClRp@p8 zwwz-|BCVj<1oSyTJN%zXsuL3?5uHbq1aP3q8lXxpg?I{TnaHB+e<9epr#{ixao00ZIvCn+>b9zVf!zI*fP)W;n8@JO5-PFpxS(n|Q;1#}=jDkqjFxRbZ zQAS$wZpzM@TibbL;x_Oe#Pq6L54SHe@p4Yq=t$LNGndMj`3vya?C2m+{s~$h07k;t z3N4EsvZiWVB_-o0-S4%7Y*Vw`Dth+I*}+#O^Tfxa3UI#ya11X&6+~0j z!jD=8g(8X^>ShQh$LF6nBkW7B~|Pr=j!=KERL$$PSnLm1$ukNneHewDi`VHxtm zkfZ=6x_gJg1o&!ZxK2o2m)bC~q1%_^dIubB)=Ut7_&4L9cJR5BJ_Ys_?`zydMaFG~ zfbNsR9sw1i;t_o8&LK;MA&NNy=6;`YQCI)|&wgEp=5hy#xCGB}58@VaAdzpP@PD-h z(hA^a_$dg3(YsX9O@vjq97b4^@PWnJQKR)4DXa5z+)|=PVe}RJfa_5vQBRrXGgBtg z02kT&I-PUz%ik_FWanjLbPB%1fi04Hp^18FtLxvrijfSo0`6c=2Or0~KkkG78#UU= zN{ge~C#@=bmB(?p5S6;0a1E$YIg+){up$1q7YxookBf?qI^PPq9#GUy+2Jyuy$P=bEA#zdv&Be5di z%f_?@rWWZsi2|e2*3YQ8L%2*fP4l?j3G5jWfKrP%gX3DqYXcGJP&l~+A<(G_24C>4 z&kV#Q9v#l*`0;)U{KTc4Vd=K7Rs+FUzHn2@aG@u8T}Td_-mApoas1WFa%8f~D+S_D zHu#22xY5Ir^rHiUJI+%ptj8W6=9Zy5x;$cmkq$XafzKhaM>&j74o|0y3cpR%6a5jM zRu0@f;o?skNxpb&-J}maWfh1qiLO9HgN?-DM36KtA-`Dl_xB$NvWeUbNT}(5K5|w| zK|r&}6HpA@QZRQxUsnOSbHi-7@Ho@+$f%u30zr=pFWUMi);O}AH}BgLZ1&0fL&OEf zeWApg*}!p3SY0rooIv!V=FKyGsbw8@gZ@LHLmlJhe9=a&#G4)jOVuK?*f-+0pToaD zx>X`SJ5D$&@!Knw2V=59diAuS&q@A$SdIhWOA8tFI#P%51S zP`u_J1}GfF(g{mD;WgJlJdn6IW{IzV_HaT0yEKM47RR7^StoV9H?SR~Fu{;gq9aK` z6|RPD>BDP+-|<$QRg_4R3at9!vn#)K8&(-xJBz0(x4B+_I1_s7`P_ zhdE#hyQ${bq57D=)qbjq(3ueW4zq^YO4_GdGA&P7V~#V=WE;sYfD;oIQUl6?y4Z2L z+hI^og#z-kWlRS_jXcPY?9jxV2|?j8rr*lDTR%xBUVlhJ0I}7$b@>IFSXzA$iQaY> z;wCY}N!OYCUQ+ML+E^ERc6(UiL@EJv+&GI8h_!ayuFe$Az{tO)U$0>TuZ%`roa-I} zgwDSOPvW-5IP_g_*SZ5#^%8)NeYBp_tJFv8t<56`?M&nIj-4sVjnZo0V?tQRkSM~y zu$Ck~-9j&+u2L2_#g=wIzp!`n=rriv0O{j}DF|*sfFInmdpGW{_S#}y8^X8HzpW`#_+a09 z%OXQ?c=Nci^@0B!S!BmN0J|3sN}UoC;pTIKpGjVJ?sdkNL)&M_8U z05W>`w92uTuQ#>Hp7(c$iGV(f;OV?+4QESGGE`E%Wt(;-DsILE6+`qTd-TR_#^?=w zCkfR%Z4!MXMZy4$b*L?loFK=8_K)%SZo=pv1UK^f;~iL1Lfny~)}6{pygKfYG`emA zwt1Oz1fO&Num23vM?4gRNSo3VE}NJ8v}bhm-k*9Z*4JfyB)Bz}EE zV9`9B-;qT79(tlF)+dhHi!B$OrmP=lT3NG4Y>5%XtxYMxhtX z*KSj)8}#+*-n2fqpvaIs;dMZ-JP7yW_S+IwAf{fMOs1wuSQBtSicvx`y|Ge*Xiqx0`lmz&OcmxS- zh3En-4d;r+5xZm;;^wTH8HtpC?7}$s-X$GKuU{BbY;c1lnK-5wJ?AVTL+Wwz_t8i4 zTAjo!lDIMR%{}nt*$n3!w)kpi}6?xl`_D|$SDOL5oSs!>n?ZL7oM)!l{XGkBye zEKVt*^`PP_P0UGvNZ|2XhF#{VW4u1o$iNKXFeKj>cZFyd0K%S~yt1A2@>R6|w^{+g{n`lfd8InPhm_ul^q!{FvBdum?*zXypPU>9KjH6G z)%cW4l9vI?r3xGyzYcFQdpNo`(s#SP1*vc=An0|W^HcO`$bBh=F~aK^sJQSO90daJ zJ!!sE$~rh*`wKE9(*I}(qnf(?`F|;!sH}=%HzN$4sJV+TcmuJ=3gE>Vpj+r$=^?a< zWGL^TXHpom82xV*$Hj}09_eaA&37b(<-uao6lDkbKrW_GP$3+pDuo6%3YZ=?X^>h(;V=hB0B6{(xO&?VS93s=PO zB)7>}=4Ee=xvrA4RGP#B)ONruMgrgw2&w5B`Jz8Pe33NzYiObI4C&wc4C?It#%H}p9OzhD@EA$9Yx$Q2-Wz7yo zPj?C7GrBRB((+@Cf$RBqF+HX-R)Ew@XO5mWOMb>7}CgCdNP<1qkhgmuE( zC)DgtG8oJK?x2BYaD|gJNl~_KGKG)h1{;c7E%)!^4ftO1Vw$9$EI~Ln@I=z^dVT;NWiFrAPJkUn^vpf#ogd*q_DeJD#RlKJ>pv9E4aE+_ z{cWoUZjV52jmKj_Ctu+*i?k$j^AMV)go%e6=k#Z8Ehn165lvB6@hFdvW4>yIgDIrc z^Vekt@Y6?qVZv`;HMM-u8^Q~fB3pLpuAfzl5Ntv@1iO{mVRI=^#Ds;U65mj8)enht zJ629F%Ll-3LRJM9TF4^`?0k7y4g|n3C!|JF`!q10aPug2fmONySRpPTEifxdxE8&O zF-jrydfs4P*K=*d1&jjHKsVyvIs6$<)C$MPd~g0PRr`bZB%K~7ksG?)6z>2+$XQ1^ zM&o^pKv_Cm0ID|Su3|k^j!EPsL?*F-jl1eTK*HH2(O6+nPq40XZ6FW?3*>1PJsX0D zbC0WRd2)a2Dcg`+zh)l8ZWjchR#~CiIE1vJu0r5jB-^U8A8J1Z@Ps$NL1o~uM8$d% zf`QAjC`YQdF=jK1>PPE?cmLHV@X@ONPMb414?%e z&J4{m6gIFo{Rcy?=}7~MpPw(&qjujnSpuBS1hSP+?-Ys3j*D~;_>fBtg^ZLY`UOGd zF^H%^x>2F*2shgR>yhLcr?|PadKxgOP_3bLUja<;MVW_~v+krYIEl7gu+gz7XIeaz zOW_t&XeXt-MN>Y+-Mu4nzA&qmM^k!hqtf92Kk=IWpMKXZVc^cta^9g76GW-QID1Pq^3z!#{Q-3F*_rY62!yp$sMI$3C)VTFgK)yrVG1G>b5C;EGh>8Lnw zL{s!f>PJ7R9#yW=jx*cm9^+_ZckFk5!E*(m5Jd7qn??{XF;@V^H)+k&6Rp6PsG^5;$U?A{{T^28mNe~yx6w!r?p#Uu;chLH}Va+|L3`eF;72_AmtY%!Hms+UdVC!d}q64 zPyYnQ|7YdtVck~_YK7J&kRbZ94Kpw9Hb{p-02c zj$SBzYfc_HjDt)v*OMhfMpq3j2QjYWiy_V0Sg0*TAMEC%#SObl&7^c_ZChcd?1UMf z0T6deh}luwP~Q363VnRoS)LaVuHyP29}1IdWUmxC@4d2?8p22lrxZU`sXO>qDW-)D z!E3@3eAOJgOsd#t*>YAslSmX!Q$;cz#E6+@uMNptXJMq10BJHqZb!L}t-Xeo3~ye~ zL0U|lbdgi{p11Le5c!OOi&b^@d#Ks;EMa>4EHan2MBocPZ+%fHRZ|=P1^KfL16z%m z;ac^6osi7y-Bs?V`Xx0Al0~*2fTfn-S_t`vt7!nc9|>rwY$l1G_q!BGj6hgq#br-I z^X)4{U$d@1>HK_tn=4zk@tX-Df< z*bd+fP^((U-;&Dixra{()|QqfTn{JLrcA~8+lCKZ*Va91@1-ZIj{=xLcC10)eF?g(yfo)3);eX2l{y%{m9h6ZJ|HZH#W^+@djzQ7hz#sBG< z`|rX!;y>1H9CQ^W__IHtUykGJHGQQ^iR3L(Gl|iQ;^3NA;JJTXZGg!?f!#^p`n%yc z>YMO|fj8)MCXA~aRzr;5Q!AJqhM~QW5eJ^ZZip8mkfX0+a4h|B?-@Pw?to_tAF6;J z$fd2vwdXDj(ooqSU}3SX;sHMxw~}Ex{E?3jSMvNN&n*^)gLG#@AELj+(h)~e#nkYw zzQ9AxmvgZ2gQnDbnsSOa?Q?xgr>AmsP2w>e^s>)=IK6tIuNtV<154{ph=%6XVEcH6 z2!p2d($YaSc)N5_zNurRODJ{)0q7P&Ny{iAXM%pX9^kDoq5^X}Fb~!bw#Jgtc-Yto z_@wlw;U8h$7&*4pGMl>Epj?-BsqRV%pBpDiNGG9A<~GZIRkt?e(3>I>8pNUcGtv)Y zAu?~}D3Mk%HSePf_|JyT=J3`gW&yOF2L!Q|;_=!xUAvuCcw=M_TnZe*I@Wp7^bwK^ zHcLur8x>Kb9%IavdP1UpJAk*hZ+pnDl5Cqw7oXl0pj(MLuw0TOqv}`F@t_se8iMB!$Hf%9LC{1W58?;e&#l~USQK>XI3lu2N&T_ zi+WhWMhtfWd+a5&Fgs)Hn`xU;-?T*ga<9!X7+o~BL@zXx(nsC0s|$mW*&SP?woQ* zh&yPzuXrooQOm8j7Y(ztUY@|Bt5x#rWpv{@IG)}Do(XWz9-N?!8ZV|0Ag&-a%E1H8 zL-`N!xXoSD$fIuJ?fR%xSk51L)QXK5g@iyDX4@$Q7ThIyqPEw-NvJFm5-M4)A*FC- zHBljdZ#%$G!eMe%XZbcgf$n+jMaD)5P#8=HCt1Y+DvG28`oN-vVdgujhnk}_dk&i) zHZi6iNwFo4TtQpe5`*$KLh|U}m9OzHT-ZpFaJulhT>$hwmy0whT2`2*J>yBZQal(h zRpXorqPiFF5uZMeff<2S{ehAb;-L7Q@}cJkO@aU`;+BLuo81srXUp5h62sOEfeyiX zH&lT=pE%kpm<0YYio6Hb|Jcf&&l+3p?OEPJ7&%EZ4Ufssp%OP_hr_AQN9J4wS;%}S zE-QWs1h=>uvtUD`a$+fu#$PhBp3DWMN8mm)^?r?7vj>BqwfKzDsVSSzuSNjxGLdq{ zEzmXYvnpm?{nZOPUsUKDYE$@ zE;x}uQbyt@?IKiuxf2lQfsq>RU^f6{Sqv>$3C|i+Xy%NWts^sc*a%n7=ko4c=%k|_ zF%RsM@Mci6rJ9*|eKwO-6+f0QvNNh_5rwmxfVj z?Zmty{nzg!waaVZ2Fuqkb|t8@%slc(_Sv*|)ujvA~* zm06YyG})=GLwAG`1KRSnRJfHI1OKv{in>0mv;vMM`0`Y0GNqLv?;1(k+X#^;EE)&j z;30}6Wov3cQp-26%b_jx$Db4>XD%Y6370Vr59_Co7i#JFG?JLoqSnp!&M^14eIW^> zxZd_pGL3hCQ?ewl@=XFe9mMpgwTHj{{TaU#d5c0coF9AnWFe{$iA^0DYEV%dVO&uR z1rT}5d*;zz%{{Y( zz@1tc020N*)*Z*%eK=%XNMX3X-cZyKcTSkD)UO(iEuwhREYS3en56NHPiLrM6FsA2 z3N>+`ww>_u?|+PQa;@+8o&p_QOH(foovfbP2p>Dq^pJ2*Wm(0@Qa@Ws_S(r~h^b*mz z8B~o-jUy%V??%t@Aax@4dUFBc?J=lvhmX+}D35o7tnz4#YQ=P8U~aT*9&O@5&TX%P z8%$`QiUA7s_DC144Jk@|UZJueG zTpHv<|E5EP3A*qXSoG0MPz5dr_X-du-gpU9wmI>1s`6i%YY)pOzOIjF>Vf~K; zWDw{80m!EA*w_)WfNn?Ly(P=^Ouss6V?+JBE;CV?x!r5S`NQhxG2d>DrvYOc+0zgJ zOKf#uT_o#^6}lk3_w;}$-Nh}s0=rs8ia^h9)ikjLnq@UVrRxo4IlA(6qBR|reW*V> zUGLu!D%@X(X7$LGtOJJoQk^=2a^M&l^AM9L>&L@lDFi9qfBL}AZPLMBAP8kxF38GH z>E8fBG`tOOw>IVldB8{<^`?(-g77EsBfKB>slDP5usv9Ylwx9P`d|c;E;E_s5 z&B+ZW3D%DHJh9V)jo|Q72o&&7mM56}j%cY-okuipI#vJbGa}rd!+qn+1n_pHWJjUO zpc7Z1YQrC$!w+#AfefIISiGjGOaumwRqw}nA=K*PAIKWacKr(aLssKbG!`NEu4~N*PxKfZHt3m z83h(=OY^k~;NoOZLTG^7dtT*5qz~QRLWMNUSl&8B_|&8r&KUMqSFkavc|w#NcNpYx zKC}~Iq#mzg#ws5{mYKpE7VH{Rvgr;W*4K}+)$gz4D2smv7JOKVX&0QC=z}mtyeDR3 zXlDnwG_vv_4ZRVVIz!Ro|KzpSM4d*LbjLu;-dzGbAts;$giz(80o?DDZ!l?jJhv9hjC2W>n^$SIKD`N?_hFd-{e_$*qtSGXWV0kYa$);i>VV zpR0y{GBANGpy|zpw;}qaA&bqVBwgGM`E_vz3|$BX2`*R?Ue(lLi{L4q+^eU%eY{E& zL>I}7od6fBK#ZMggDSt-uM(XPGx%B?1@`45(%|(Wv8bfjLM?3^|HP6-gFBlmJc1P* zP*bH_IEC{fvjC2)Jm1mBKnTSh;3-7^YMq*HF*8diJnlq9;ZJ+~;6>vUletD4SKTzc zybcI>C;6b($Ftru$Sg#U#kMmbXk%6_l4q{PxNyw+7V^2YW#c|%>QFPr@QF(E!=~8w z*nX6Xf@8=Iay2P4jEsG?ajO*5+`$@PZ#uv5TazioJ&@7YVnkD4*UO1)aXkEjT}EM+ zu`miD-_0tTlc=%E^eQZ*rZShbo~5V zc?iSh_E3bn*wyQcWo_0Tp79&MQ!hf@@Qza7t+tx2+GvIIFXnJnP01keKsPc{Dr*a*5<_PBE{aTk5)dm{uD;(=;DB5oZL*ILD8(S@3 z$n(a_#<~uNHzTrQ9^bW;$S=LT_Snj0A%Gs465LDvWU>i=1&bPD~1oT z61fQ?qi}!Y`x^%WK-LJBRve?!(h~g+ez!Z1IhZQuT?Z z=V>$$9g}S2iz!eWgBK6h|FK(;=B@eAe(^HDERg17{;Xr}(4AkVxzVQMrc%ZIax9hY z|DwsIFY2pY-(TMMetWtJtC0na?B_+q{%+O_+Q}@(hvvt>XNb%>ZN+L##L~2w5R110 zuOScAXk6+NF1Y6nJE995vzA#urO=@F9E~VZUPsPEf=&>7i5V^szZ_8Mi|q4&$?qdk zVIj8(aAd{gcPGYo#e z-DyV+$e53Vrf4deeBFo~doY+|vu8I>0!nt5VTnggy=8%?WRzbe7icv)34Ig?+%s+E zmHU~`?*)GHa%4N*g}Uq=*fqWxwP|VAimCQM)WnQEooR-5!Evq}_cCxP*ffHHE?_v( z)2!`Ma0t7!)P1(~Be(o+PgtpmjZKhv5<+*okvXyQGkw12gLr`ip+zmWG3K)g7Gyvh zm6EgL-xPn{*#cb&I~FCO--ARj3NV;ii@C_~g}8hydJK!z|AzqZ4+p{>t{&Mf-8X|S z^p=C~qwVi>ynI(7^t)rwhyx?mtxq*P-qg1#TRPtTFW^QfPqsbPet;Xa=PKbv=||Go z<6W<@Y8S67Fv1#hei_N?I`2URjy9pX?lSor(uyleMgV*n*^?0)Q9SozdvrG;>vlIs zVhXbFdvvb{M3Uz9^GOY*05s)>9eW4tl+1T%pWn3_Bo|%z+m-MxO%hfeh#vHfIC+>y zi~5B^IwQ`bpRG9Wu13g+NE$el?j0l@?wG83w}z255X}{=g(kUC_I7!Lc6N|W$2siz z%b=4XL3C3E2-T?_P}A9UV;=mTjv9S@2J9^QZ$dVQEH0pG$S_j$SF545UAZmvBYEL_ zHaEj6UaSVCj?4*0|GVL}_Io@4iVAj{@S)8?{EWXD@(FK%A_xzwp5Dj!H=gF%iZt z6%ar$MF)gJMyW+U<%+aZGg|%Ue{DA!8Bg!YOm|$!FeR=mS8~c#{IvV%G|lkz*=XE zta(gr*OA{;%htV(`(2anRUaZ5|dt6icWq-eL?#J#YP?KLvQYK$ptQboQM< zM%ibJbj*&xPV(HZ=O1xY#Ih1qX;BklCWqOf1LqnKo^s*@3rXCN0hl6O`mc?Gwj3d% z8a>@8Jsi_$JXs&U~HYs!8a{Ucc2>$}2>{WJnN!z~ivmpXzg)Ovd7!xwoP*XV6 zxZA=6IS+{No8!_rmi({d#V}f?**4x68`IlGp}lJVi&+H5T{-tidOSQl3X(_mUf9>0 zR2mjlc#1_C(!snhuyY!vE+T`fcO2|-x(93B)hqZR$gT0^-N?HJj##P->eNrz+=~Tz zIYPM``IB|7MGawzncfW)vp5ygX~xg1r$+9xDei(W;aXDDLZ?P^YzSa4yXz=Az8Rs~ zyO< zlOaQ5?iaqT#0{ZS@-jMJ3y`R)`&XTzX~hL7JV@Ol*!0E|p-{#!8j}fkut;%F-Ybn) zd=nM8kNTvaJ1VYr6>d&oLGvdMPH`{1>O4Vs-}6J((0M4lVDMv4$Mfj;a)PYiP4c+s zE^X9__)qz-h4$Kpz1%?x`bDOHk6ZDe^4jc}M37>v`r&9Ue_DfI8aA?+=J^Z^cYyL) z$dL4z`W_;AH(?w`@sP0`JMz`F6`vep7Sk%#o2Ed$QFfz=dH=95`nKdsKY8;Ny@A)N zn$K!0R%^-z&o=U>lS!RDxwi|dSS%~auh?{<1>M>m5Y6&H1@I18s@|S==d7siz(H`1 zE42Jk`<@CgyESEv4HQ;7wD(ECL~XLF``}0XgLL{= zM$JpYzZp*27-t$8=;e5jF&q#5-M2T=8wF{|M|2zB2onTyuQ(q+1og(q{Z~b(Wj2kD ze_2a&2e$%&seao?g&59(>f3|9JJY3FPi^`=p}Lo*1aUZ8zaRy#r30&*xI?=dY(2k$9+hQ0=cB02Wv1op-0F!-a zdj{;NGyrGE$f~m(K?{=b7R9QG!$g2rH1)hYlpc=L!D1cq?f?g}+m>XYJ2dT!1Wj;r&Cc z!}aH*4q@tRI|Pi6mY@W9MObp8aRn|)cLQ|LQaiQ5edt3y5USbCx*OPUARBx>YW2D^ zA#>L;=fjq2co*&*`Ha8=V$)XLZABUpT_fj*lqmcdDy}WtB*_)9&hiotM3|&AUrT7r zYJu#t5D_vyzgj4lZ-sn5XlfBsW`D3lO2zhkDwrG(PC+1=NwLpF$5>MJ_N1#T8b4vw z#PxVtEziyH(eHatv5IQgh}}WV$rB(*7d@h4Xur9jkM>M4TDY#Nnl!dQi)bpy!7wgjryO zyi}^EQkA};4TFE8bD zcBQl`4+M*%MQ=c1!M45Ka`BFRCmGFY4SCcWEmolUUSN(0-^T?zK8Dp)_kQWINeDEJOjL zwm1^(*o!6|aqAuiykXm-|I({h?zy!5VI7IG%3zheLx}hM=ly)tpka-os!3OFBd^_- z-Z0&1=+@N(an@QL@sf+*mff`1U{!$AQzd23grKwKBjTM4OeO#ywH)qB&5wV1@^z8; zH_|S=5`E!U1-Ga$9*3_?A-~Py6o>;$c~jLU>#NX5Q!G68+l3LFdLMW#`JSMkS_5I| zGk}e!Bq2R(yIVo`hus{!H1Qmz(+Z3T6R0LxTp*vOT8xOcwoAlfWupD3)+1`yLdNNm zwi6LCxze8$W4z&k6ryVvsi6o1UNRpNnyaW`9P~&oG^HT)4CZ$1qEE{MIA|iw?q(PX zPjO>)O#0sf5|V}#4FucDMHPtN0np+k(^``E`*uVef_tXQ?x~Aq)H{fnw+J=!mF|HhT+NBic~4c0kF(9?7ps z8}EwxTK=3CL3h4LCdIU>lvgtI%GaA2?z-qiF9;)612MyM4?gYV%V$FuPP zWlEwdHfIytA`OFFtt1h{o|wxhroTPUxjg`UffCQ3`V79n4YmzuXX*Axo1>Xf<@5 zUNt8Mtnv!Re7>*BCWyn;7%(Vb((Jo;~e$JfI>`gLRCi-3IX7z@o|uW}FvolafG0xSzQ6 z*ja>$Clvl^;s*Rq?vb8{z0OdKy)g~S7#Q?qj$TK7yZHu>Y`LS}l0_aIp#Z~pY`L*N zRkV*Rwiog%)l6oh$sg*G1)%xTA^VJ+rN-KvIgQfVMi?V&99dK^osm6h(dRM>_+G5P zLO)ugZD&x8r}~MDv=s7HBea725_1veJK3@8XO#N|>Nv9IHmDLYbbzX5aQU5Dz|DER zY~et=`m|%^C}%*b|Buu350BV3B{3(+sA!myIP(YBK-@kh>76B&>99d+U|-VKwFQx@ zWTua>FJkWt*{Lrcy87TNtK7`I&OwQ?(BD{88mb}lxu&tnGSInpKtnF% z88+nL$)SdN(xeuA?AnStOu>knj_-xKjbX*^obTr06LUgh~Da4@hepgcJdWhwd( zsjd^`g_MD!@A|J(N8I?&pVOTIt#&>#c(1vbZ>ZFHTaQ1^PrvU#>R`WD+85AZ0hffx zGHuf$#c8^dyPjEZ#4zFX33Jw(HjS3`@N{x2K0k!+Df{t4x0YHK_VzcFOQ?=~<}ODn zEK2riPF4k(+3}0Abd+7wG2Kq(n4PDIe{4<-jRpMnLMvZ`kJq z(Agxhyf0K)&;K*`kzt3et`kWb;H5N?b?7K3$XzH(Xkt3DIA3|fx#QyXPt&r&Jg~>P z*)?jHlVGU~JUIhrq_d)Z8ee|k%vmk2a|N{NwJ^BRdyD~JWuB8aaLS^rru828dXyxA z+q`3MBM>*}Bz{tI!9^XmDV}W-$>{=4Z)_E*7RcMn&_qw5Ol*P%>g%7x! z^t4Isj+KP+MLk+1>cx2D&iF_4n?nzmgLuD)ne^%POju`-W~Bn#q#yvNG~$-wkXekBRov?IpEHHO{jQ6? zrBZ?KEtZ5{1p^GRv5X!D+)Mj zzx`acd^L>yIBGOlkB7v_dy17FH#Z1*m!_Plj?EUFIo@#jAuwBUUzanBE^zh( z5X(PqN4pu$8^jlPkB)p%Vm|bmqs@~B9=Ijx?!;yyB}iA!C$^jJG2d4}$y~@>i_QVH3AH0jNrTY`^EEtGzDdjq{cxgPo}I7qPAQOc zA5lw`Zmn8#?W*`j>R$PSK2Va*er3JO14rn6_%?O{-S3gVfHA{(0q(`Uj8v>G8Lm># zIEUZeEW>km#?ocU>;+%?a;?*(<3>DygW=rCqM5#~Brk2IuWyhKzo%C7aX6aK-!fibF+btQf>}&;te4ql9HXU%IEw8>Ig8`D>^M z0h_N2s5D7!$&2~*g*r)YugIBIkR)p)YS_EJQP&p@V!GU$b^P?w0#wM8d+ks{ zMXV9o%5oO-sY5}vlvhE_6iS&>`8wydHr#=JmTmvW*O~j4}I_elqWDqOBGMu0 zqI72NjwraEIt3T9oMoJY+@hiRnz~W04dh>mP`t;v+Yx6`YX;P)wM)_D8)!9p|9p_5 zcOU?x))@-17?nEMIGuxi{n)chT(PZWYkG<25e(BrY|E!fyjf2h{Q9ye4$@Fa6W-j4 zxW#3HPN_?sU!#)TBN8jo`m;GVo&c}+f?lU*t8(~#fT+ZOCuLe^PbrNaoo2<{oB(hi zs`u5^Zggq?&QlapbW&gIHI!r*ed~&Lp0AXNhe1B7xl#3i-0kvz z;EB_;Iczp(-=d5&?8q}Y&STDqY&QO0PEIhw zGuNol`T(S$xAHN)PtCM4OfhViI5q-%*P)y(7Q%p=wan*+y!hL1BF(-58CVK!SdURErJEpyy|<&?sQYk1zXW;O_fm= zr;MEhOL-n%S>dEiQk-}0m2S`Y3=kao&~3TBBeOi1A%jMq__8_JV$B#Bn+B@+ACwuI zPRa^36(!hnLCy7!h$WNfy~90@Fk@4DAShY7I@{HB8xA0Rnh?(|Eys#VMX(NxQ zdOGD1`~*@LOr;6yPx^Fw91>SUqp}>&4zs@!t?IC%&A<~sbM)cF1Jd+jdxr5#jS`9uf7nAKSl+&sY%hhbKTtg*-F34LE!_EZ(C?V& z*$J{tD{ZO(rzA{TGI2rkb=C|aPX`GVD0mM=_8tq^T34~(0jXdTQ^8|ni$ZgHA~N`a zPHLIt>iX%1FVm}anvPMXr_W*p4bq>~0Lgpe=37W)C<=XEAWvp&#eTZ@+84p&z zYr1pA(Q8?Y&%f1rhrcu6%S~Cm1bAc}LmAi!MgOIqwLaKQixU_sznGz(L_+x` z_yApyfuI}~b76bgk{My3+G(JjHV!y@BjgJ;%8>GfI}n0Rb7zq$q|rv z2QO^r7H6q2?Dqiu$)~2=c%^ZCL3BJK9qomjP9Zmaq%|FtYVm>oG4RSq>Ftfv@`W|J z<^UL~Cua=xE)3U+9I_B0%TZIgZ_&}dl$tYtxbrdCy+HhwOj8~==c#bGVl6C|(1_!T z217eS?elTrcB42nY!``7yx>rH$w#8$g$3Kv2p#uGU;$buxjwv1N^|j6h_vuPK@jMo zH-~byxhebT6iZAkJ9s1=OPlOw#wdVNIA!2=h2+r_@dEEhT16i_QtI z^^x9Ky(rtorsgaE92c%JG6*Gr9hlx-Xy{qdcrTnD4AFzbED!3GGxf)})w?iAMbCv2 z^QD3Ggnm_6DcWQ@I(2{}(u^nsDc2uOD8X~g^EQXNYBox!nB@rY(j1X9n^roLT+Bd| z_T0~+;l7B54Ke^+F-4sb(8<;CA5RQ5a6ul$=(EtvlSGL z6B=)-yRDal`{-@^6IB12Gv~T=;7YK>gfLH}Zpof2ZO{QQPk%hL#iF_}o;XmiP^)oZ z-n#$+;Bk%w>#bV690jfNCc9KlgFerM!tBD8W0`u*O+WTn<2fY;dsGE1TQKOFPm~ih z@!C7%Pde&qhtzp4B9La#@w?aj>beUUs?veT{6H_<-pp6B`vNQiIWye^%n<)907F2$ zznz;bsih28**mPa*SB7w3r|gT-iy#DmY4?@hK!U9<%V4Ws6(42R0kB(^bn*(81jJy z$qjxy*lEBbD??TVkW~wIrNF7hhc(j~tpFDN?AOK5iDYw`IuODLPcR%e@VJGb?MhBt z*n+t(R_>fYBM*!D=C!w7NR1d(nn&e*_WCu%GMQ0>Oyt^nqBf}ekzNmFtT{m+mZWDU za#nbiki7}OXlBNbrcnYdM6Z>*s1#Z#IC5tf@#Y%;f}OmmP1$t$F7{u zp6~T7ve!8m*YrL1NI6>u-2FOd^$eIM9Y0bmRf!Nr0P-veziS(7-Zk9H-a)fjuSOb! zJykjgIf@qY-jZZK@K%S%ua|nv))s2*sLe4kQ^R7aBnJ_=!UqRM3El_oLP##)_IMJ1 z0}&To@gWy6zxb#*Xqm2Vw#QP%H0~3(qCc8FkMk=}@H}IZX*KOm+R=?IukPE$$N{~` zvJrDFFl&OkfkGBCM!@U}b4{}WC3{^rB>9+KejvXsC+#esi)wWmcPLl-6X(vq5!P5c zB-FVZXe$BF1sX`G0KH8*+ttFAiIVNrg4;)S1BYa3qV;AO0v8In%bDGw3eF8nM|gbP zPbwc5u`1i7A+WL~KL?jB9j=)mqI|z~LM*eVJ;>R*iVfg%MET+$>l9LrCJ{zDiTZQv zqN`WkoMDlMjfHxZ85!Yt{mcit4KhKNHdw4t0YBKleVP&;s27I6MSse&{3RBPZpVXG z3fj4U zmexB!txBAeaCQb4nT}b7{3WShm-ASpA+48RrkL;o53f{21x64lb|!S^P@5t5opq6`k9>mQi3h_nm-3}|aQ%N5IW@c5f+RB5 zs}9x~Yz|VWy8Stkd}`l+0GbOT9Ios=@%zbxm^HA^q&fE*MHAt$rDudPnSyMLpsW*E2Kc~Me*EB*3eA@?RW!9wSlTQ7xCAccE zizqWkUz>~7_iDqg&iX9nC9iEFX_Yp-{ydI~_5d8Vpx8w3o)jYlT;R(g92`go6?ve9 zERasg2S;sM7<3?VZ()w1(NJMQ8p#^69Llv`bj0eC?N?T^j6<8jg5Aa_9{@52ba#}W zM(I}`&mc}!HopHH6SahzF)8F_0gffmZdY#Ti_PZf{Jy(#X<1s%WzqmJk;~nt6!+?A zWV&!$vr7gPz_g4+y76*(TfdyiM6Di@ayPIOJo9yZJ_9NOp+j&Gy9E@d{M6K$vDmx= z8b0bGdTv~azS1f|uH^H#@FcL!sWM*d&Yw9dj9a0wa%;YY1Zt-pCYlF9X}Vojz^;^} zVW*RAKSNn_6r3ir_Agu;m@nc0WdKr+5&JsYNRp5Z?qyyP82|qBjDii}MYi$vT&B+= zyQnv(uAD!tEC}U_fo2b{FlmsRd#6Tt!Ws;A9 zOSlyq+vFYtjshtSh=F+j{be{oSJw*E53KYoSjeT`Dh5YJ#bd9Qx|gKYwYq0lbuX|d zUVbv1a(9G?di<&GkxvHdslfu~M~{GOHlTgKQO2rq(rKV{0k5UfXXedA0S5K{2$tz8 zwdu>XuG$<0im>S-8K#v4#}yDUe>|HdrUYY+v3J>03_3kBQ_T`!(O#a>WAn{gz9Cz` z&G$`AGYeY-wvks*xL!gVVisNO(hvz0bgaiLIQf8T^E_A{8WM4vl^|@I&7h^M;;lN{ zC^eGFRtbzYh((261K|FD9;;t-D>Q!E7#A4n=JorNmno^u->x|ox&;$c1(PgFBjh2u zi*J>+8XtJq-tagp1jNqo9jR)Bb@(PdY_R@UAXg9jNbz7Bg{*4GZ26 z@mq#n=As<>twyI6|8pqN6c$=MGNe+8>ZBxhB%Is9FBFR(_Ju(4!o^f3!k0qPmxiaV zYytH|j%5G^vDIt9iNTe2;+t^-C& zhUH$(kL79C8}tClDzL2@Ss1!!@S3QQ!yLr%E-4xkRW!+_{FbSE@vx|Zi{B8@>~CP`Z} zg9Ok~IFvGSC6k2;ko*M3OV;Ilrk1Fl0)Xk9$~>K^s1FA_+IK9GrlK56Sit!ELbz>u z!3BJtVZhCv&&7hK(ekc%#IH+4fN^is5c`JeYD!V22!kv-hmmM>Pk0r61M#VsP5y93 zHg^j%KztRlu=UrFv4Z3AMr*u*{p)O8V}VZ`eX{up28RbiST}IzSIi)y)(+BoELKl! zSPv^?8v+irRj%f=AtR%N$mud;t5JyU!wYnzuM8f z2AJ^sniq0Vg00GH|5x+xX8bAf)Xp}k2C}zRcIfznO8>dr#*zor;#RSd6hxNHN(0q= z4i^tA>sC02L^v9bzb~Q#z_a=R&f)Fd*`^uA_hf3sLWh0IXSn;(6jJv{5!G8>U1?7W?R!XKxicHfVHdJ zRjEWk+@!s!c^<^KCv$S)Wf~`CG?dVj2MfiC_f1FUXHN=Gtljb8)|o1%H{623owQI0fmDSag=55{)ij#b=$Fj-=pRC#q;%k{`eF$y<&Z`T1cYx{_E-%Ii@<6%|c2 z=)F=FpZqS7@aTnZ0gJ(?&oh}4w&f5-T(t2@Qampe_wnH(Num1hMpu?+#KGVDPue)< zmUBtxBjSEm7s)m*dI4f3XGjTj2f#*a4!PtRKhP! z-pC517&yhlXK^&8NsQ_A`{gv>%Zd?D1Z3UQx!&)`nQLn9Hr(9g;}{V-NA9UKphgc2 z6D0&yq0I7(fWX-}YFf;1np6i9LKefFdV4)cHO!*QZ5@k$kBjXoCtT*J?@#P?PSrAb z(Kdgl3yNinlKoES_YTi!(1>M)!(&oNdCI8pdnkp$%tC4NdU&lG1{&-Ad=S_AZd0yc z%}yP!VNa!gp)-W89VS=m+d79kLXVEci zSHE4*WLFV?F{t+RBDbcJ8zkevqP17*X@O>vA;#RmIOIw)*#RW#))dpWPtOPVv8#|+Cz|sVm*(1B4=xzk1N)t7#MpycC5%*C+b4xOE_yF>{)KpZxNg6$bvw50o1Wc z0k9$J3j2WPyUbY!++6c|NCH_7$wEUQWCQML*&#A}#-Zyb*Bk)ZjC>dx$w+%nx9AOF zMP8t|gCe7Fw>f*v-23@WDaE%lm5fq?H&xY?;gF?0FC>(kX>MTO-Mx)8d^8osWtc~s zF{}rDXtP%S5yNhdrhdaC8&Rrp}eI98}nkru*)uw<7S9`HvzSZi3&Oe@`n~ zjAZr8^SxO>w~mP!KmY~EBRx?8Jm;`?G(OL;(+-BbY{95WiFb8O55b~CMx@H_HMx;N zTNFtMuy(P$p9fNunUf=@;`EGoO6b|u05N7m($2J=M7b%4<+IqM7EIny9NIIwSZz+c z{kD}^G@ht71nv<@kjnt3B7M=7wXs)^r_LD&y8Bte1jIVO;5H)diP{t_*fir(dPDt( zY~Z{RF+Xe1iac0Mm)0#VOFl;NnGIjPbv->1(TPaGlU%m3(P2Pa=P8)jgUsf;P--S@ zVNQdeP<5=_>pzsRwTrcQ>drEntPJTo{dtjrctfnMj)}c-2oP7`Xzs?ds{T?K7d78U zRN>jvGRFxde2X) zd@qkXR1d>X}M2k3ju;R3{SA~eezt>^@g=!;*osj#QU>Fm$~mm#sILXWcQ>bmn2@-Fo_mLYD( zQNkhA0#-x*-;D(Nlal}sM8pZo7?3)ZVtykmU-OIRf6@qo66F6)hu|iBaX1x5)ok*J zsL+a3SFa7;Ogz%vyi|3wJc@GBfJ~V}*R=)3OM{%QUmp^)pzZvkY=Qu*{^AD#O-m3q z)o2POTUU!t$=N<4C@`C0(p7Jrk5B}xD)JV4r+n2I0<63;v-2gf`N|j8i_T|Mn>Tq+ zFduAtT|&5LzSlGS~(b_JqG)RDpoWkP_P%JrMoEj9V){p`^;WJrVI%wEcGB9ssK|%_wX4M} zV^0MOu($E4H4CT^bKl|e&{yfzyFL)O2rSGFj$kbzzqEFCI26GUPH(9FAZgw~3+w$bKt+Iwa~ z#}iR2dKLiD^fe+dYOtlx^@a@cU+YS=2d~tiLC3J2NmYI%TK2f;qCC%=J_}`|yO6A_ zZ(O!v<>2uo7RF&nJ^8VLk;slGA6G=0EVXo-%tt+@cT%)JSp9<2v*`tLeyX2zm#P*Q^dC>MqypU+5+LvmL@B}I;5^aZ-ss(gBY39f7>xw*8F zmCHUgqT&csrW1H;Sa($wa5rMI05{ankjw+eMDiwGi%6)}p!BMS!JvpeAkD%YnSdUX z^q<50lh<6(8C9zxenjSJu>#9ertmJ;Kqjk1yF?{H1}x|-8pka&t4WyE7YOVUqr_s@ zL+Pl_wfFvm=rX-BcYsH(Z00&TQvR6h7*uQtf;rrO7cn_yc67RNfZx9XAzC>o|B5P0 z=b2$pK@VH*Qfn3lLj9=u(mvr`t9~>#wCcdP0TFB@L&P^lC>u8^|6)p43vgeXbPvWO zW=iW6mm2X7o1f4S4&$E3M)Lfe7Z(l<_`A!L#?xK!;F{cSx}tfVP_~_a2XA&qO~CRZ zw1=9Bg;2^;TC1Q`S6)K6KYa1e!k4x&v=(s6giuYHim4^ZmA*WZtl=yZ`vSxILr+QT zn4PPS-T0*F+2$!6>ZqSqcS}qYGpW=WP>Ao9S8E@IRg!?E-y6RR56FD;GF(UT%CDg_ zT()J;TDEer7#4PbNwIQd;OmXSoHk}tJ-~9=57EaUjLI=$c)RlBbqN0z`xi$C`odY6 zy%F8`WdMbNVQmHRGP4}?mpJIPZ}8$2$;UqO^{T*{2^2eO+GS|9nz7#@;XzO3fT{ZU z+wu)Ol?jZN?3&gBe%+Rj_5a3Wp1W(>cv7g4fGh?5;wa zwB30~gOIuM%g1kOU%RtFt!p`k%F;f-SE5r*Q~_;=I|+i9c=5WYG~ zqn_OWE6>>3l(H)8RJP^(+zrjdu%R*M~xSj26#Gu5zCBHdv` z4F)Y3iopUo7h||Kpa9H2K#ZDM=SjAjD)3%11@k9Tfu1*n9_-apvv+$WW>Cq5G$iJB zHzAI^0J*A)yGMJxOzrZV{pyGKlHGn=-vmNqQav?usuzX;O#1Wp$ z@KOq>NTIfQ{-^7ni+lUM5SwkpUQvOSawfaA8#k0y(cdk$`?T1-<7KuFDIiU!4rBQ* zf<@DFY|dh?BXMA2T#7j!~KpBF?h(m8)cdD8+O2mqkY0|8+$&Hk^x zC7tv|mD)PKZZZq=pp;IxbgW zDef?(kB1oU?4^ESI~5^X6^ zqOdfzU~hC_z2Q~mo0y7F2&xJYheMDfBWrcxkZU54oNDL0)J5B#y*~60=+AP+%*n0_G!6v9ab)P!zFlk^mm~?HVZfWR!{o{&o z_`lhX{I^+G0ORkH!zbJ^67-kdEBp^veBQgxHvFypR%YDc_K(U9fJNsdpWK2XnEA7Z z4h2Ny{3THQz;Jn7ke}ss8}46}Q)@0>Dk29-;drQFnZ9J(NW34z1fQTff?KLB?0(#? zTrd@>a)%%Zw@Xh;@96wb#{r!Ct&6Yd5_B;J=S$`vKtDuP2};(vwePoDLK;|+A9yh3 zmGvj#rmgMuLPCAT_;x`|pQ1Jn4~oWvjVt9imiioG3AX;=Byklbffd0!u2qTkrQ9N` zC)C6{z4|97rvT+_cx4Y2l`uH=>XF6@hUoZ-$^kme6Vz6hqb6-|3F}>$JF+YFHVD*D z$!yax)_nF2Jp6O4(cVkTjSZf(vjpfwC5j}g?AB%x2SS?kYeVWznstk$$-g(9Hv!UP z&oVn$_TyI(TB_6DeeGg1kP|nW$9%+)c5=~<8{R2g?O)x!nN7~dBI#O8r}z607>n99 z|J9+OAN6D^%yS$xZDW(8_p*4NE?P%nZ8K*LDg^t0iMhI|Z^sIhyJ0tW1s-HU=AIzS z2GUljuuYaD`ho*6*uKD!qQN<_-#Hgrx#s$tTF+w^ZS+U`_oulzMTJ&v%4bGWooHd) zTYW1|p_i!-Ln@2cZHczjiiTbS7sT@m-ntXEIWQFx z0IfVuq$YCge9C{;ZKwaPCc40kVQrRY{d%NTBUDQqh$K-_U#z+aUFOnzbiBLbs0^cn z4QMCIeedFE7fT2>=X3CMU_C-1Q2$6^c`;C(7XHX-QQ!(&^;vKc&KE<6PD@c1=#b~@ z<6%LJTNRnId`E$_+rub=Ea(_DJ-fpi~RWofM?Cl;S%Z7WB?#CNe>3XUjSNPkz0S3jyhxcy2Cx zt?$0l?Zf+$96~{84}1RxlLtbnaxBL zg8|e>rEy!2p^yTR+#7l3A-Pdjc$m%Lcsq`N|0J;->e%?p2U9`4QwOUy1nx_aVhF{% zUl+E4f_zRku&>UJY0h5DtLt{JydZZ8!$gXOp>VnN6$-s&cEo7Ln=bWLfa)# zDvQl-$y{fQ%M8IA;M?{gk;m9;PEh9x(RRvWu8?d*5!Z_p@)y3{wf`cNwPvVmLgo}B znE(Os8Kg9XahW*K#tluq4}s6JEl}`b)-8~vb`dgWfO=lq#yFC2-LtEK!v?;4%VWGt z3#$-!!_7jIpKuz_7uk@V$n|Uhqk(9?vt?k>(Gpax*dG|s6<%Y)Z^rKI_|+H8rVL(T zJ*^8VrB~&4aj?t?nIXL+pkG)xFp^Ec=*FImtJV}xx%~;ZpnEGS=q9Kl=d3e|>B#us zL9EN0bR;xGAaSZ@5U^2VZ_~N3<4^*f9%57DlmTWC$D|-$ats0#I9j~BE#+{kKF9!` zTR*=lXzSpB@>@utjdAWSwAay-jE4lkCeIgleg{qt_%@sI*xCo8=M{%0 zro*3Afk-+%E7A%dedI)L#H!|~09S!1zJv#fkjSXq?>!9vv|qwOnuCbCG7^jF7a#M0gWs>{<00)5KFwQNx)x12 zn2FEHo6TulZAa8&0+uWSSkIw*!A6L+;h+-15$Qaoslmy2Fjj z_+{N&7sqYL_>tT_mk}=xUEwubxZn4zw&M!S4|81B_g;nr5eNrSBl`f#zvJtQ#uCAA zUr=5Q$qg6Q*T)iiX@h+qvFFt^!)Fn;2{R&-83A5Q-F8c+$*g2Fm?-`lP)L~8AxEpf zZ=KsGhVs)hfIqKcjpM3z#cy7IzOQ%S)3qY0o!tDts`Y(jIr z7v4k4ci77xZ60cdpux5M0Gdax!DyfG7VV?kYGZL{hs?-QDRKorl8OaspCg;cgU663 zD=}%earb2=^US;PmSsu#kmt0;%3`?kulg?dQtEh2G?)z{aba+xMrQ7kLON2{LH6+_Is)o%aQVE3P1oyKYLZ#c|9FJ zbZV>$0#mrv*mL*O!dOord&0G4Cg=Jvl6(0Q8xynqbUxORqKQzWbE}RNBae^kzm**9 zvT6+l)DUf>*>}(Bnq4`pSLyXgk6AfKCg?1vo-o11Ux1Z$CyGBD*tVgin(wvoR3kve zez#g`57L&OP2UfRJsEF&F*Ezb3cA)ybm%X^@I5J@(Q!Ekbgrmbbs$n^IfEu8I`}qp zP#$z4LQZ|=Z{*>yZH<|1&Q#m+uX_{q6J0+NsKf$kH2b<_>%q-JPJi2=cUU($V*wfR zM6R{YsbE4R29Oj^%_iX-#+~w2$ zHf6^@W_1X1=v4GiL4503{!F=n{e%|}APA3B%#Ae?;UxV5xd!B6U%be;UwQ0^6|t%@ zfXJ(TrfUB46Q*0j|0L04VDEU_x*v2qv_X-kYmYD>q)_IObatVGSBN@0zt;fs`0wBL zKGA2dWn+IZJloi3;}&-HWb$@L@Lv@i9*bzdloH1E?)WadyuJO!??#r{e7-$jDr?In zey~i}j%_Lh81QDkk2t(Jc}$hp5agt~GP&VS&XDFgCl@ooBQHFU^NHtGH zapStyw*+~VG#5tFm=YOz(jMMDG4V@IOf37LyZ8ng*2z$$D`|R&Vmr$FQLQ~=JK~4n zreb*ij_x;Xw|Yzf0}5{CD@F>d^p6iDiCf*4O zjm=cxgA=I}G7gZW3>&XAip1+T-Re2Gw>MWxOYJE&Db7$Vlw0s9Bk6fh>=<&pC-Gy% zs&HnTDeZ?2J7_Ie6ZWD9+?ZBEqPj@MgH%NJ9-oQe#46h@uW}M#)@I-?iUsY@FF=Rx zPlk@R3X;Wdmm=ChlU$tTse0}rMohdgpv~ZgT$ZZFTi%f&6$>IpAGUh>qB6fP)0|&P zPwYea1YLrt*rW|YmRR&1>m{B-s->2|l{Z0C+uxR~<R$eXkk{dzPkc1ya5pK77) zsk#C1MnIoN%ZD=61(X8kWo&!kV^-yNYSQ`hfsFlHBJC32?3)vy!InQ9PZ~y$^>U|h>P1AA zj~!|jYWoPZP2Z^!Dh2;TCGwBHpj&n6Q|}wPj}2;pv+U1`um@jmh|66wE9wTWY~N`l zkqSOcEyAy#V7x|fy8!r|%q|Nwj*}rtX>wL1HvoJQOLUAJ><=yP^9HH5Q%_y7!~LWu zuHmRX*}(dz>f+gHr1pX^S4#k>gh~nUMg-zX#j7HGmev}-6+FH-+5ZS5>%E7Z?joEithvdMZwB#(&pcSgNHU|$`PYPVqYkQY0h4HCn2d@0t zY&9wzq8_Y~^gfMP`RFu=(R4;O;R?3n?lDor7lnv=0DA9$G#?tFOwz9(HVd`z$L9;0 zwlu*JAq9RXg+uoXBW^Y>HT6F?$iaKi5^)0U5vvlRjKM8g&#z21P0-Z5CPG(f#IJe6 zVC62Od=o_WAlTVWtFC-P`$$xMFDMY zk1h4OFK8vhkMhpG2goz$+)66w`Dhs`O635L z437mf+rzW~!!DBWK|kbEPh9l~P0XZJO2%^9H!&#ldac)2&g-m>`+Sa zPMk2adZ96BKNx0r~->oe4wG|6Ex%UH-okfyJa}>qlsiY>NnkhY5LKj+!;PprW%oo1 z(@}O?rj>yGEmAUH*-frcE(i3{w8vcqwYM%~aMFndQkps?g<~$~Th7Z~)|ikNbS({t z*fmDSC_D4Jj&T%TC$msOZY{0(M~n>`dYaN#sqYsaPKb;LW8vRJar*5{`|QomadX-! z>J0{az2Aev#7w8^2G+VMN3t%+^P2Q3R2_J|G7@%xQE7^G@6~JwvJ7!56mP`Ga?Wv@T&^j8}%6CYqcx z#fp{z4!ycc?6ti6`Zj!}Wl5vR6P20eO-kQXvCIPNCkqAs4v}0M z8_p5Hbtr89+K>xvIWi z9uZUNLfOvfgyF*jX0LQ{=Hw~mdK*tn!Syg5edYOr6RbV3QpvO7{P$7mD?*55dz3|O zh$c7^OqpV)|A(+{1VDSXqgQ>?y(;%7gh~lfqN|RQXTn;r>OmvmZ)o`Yapc#VHIgpq z_QX!y6-J_LbI-{3RGXH!eqqalVYQ7_&B;jnCbQi!oArT`k9;TQBw7zhpds>h2|*xb zu>`s4D_uxl5*@s~7RxMg4W^^XA-+h~cr05LuM2$D37^1%2AUhk>1<{R?fo|8sE&u~ zBX#y!tOS*9maEC$KpK_9=oM^ zovzOM$?2}EK$DZn5fhauXLg(xvM|3N{v#=hj-tJ?fPzEhDi0BwEmD<_9qFDOVKfio zP(mcFv8!u*c_{H%mZ4hPkGQ)o7ug`(610z%X(CzyH<@za5+(($Q(EoN0IohRM$R0R zPbBy)VuVz^h2$rb+tG8Des-(10%7h87Fmm`B_>}in;%yf65H-%YbpIN^&7b29%S- zFTMkJM7j7Yv(8dBk=;3`1o|Z=bsp+?Bca(NpA9;jY>AaqFhGl)>)Z8hbvcWP*(V;qY$stwa;U%SsL8HI2k*-1$=F3&fY>9}CNF2H6 z@pSH^O-^rog>fm}wbt z@IBR$LHBj(CuC294bdD4Sje|t0he+qzFo0bVvOZLyD%=$`$)f#ZnU6gHWv|%<^%2) zdqD1KoK{jReF#8UYe8nw()JnvaBZMT)hGbw$9`g0q$wS_+_g z$clqDyPd+phr_tBA>N-DCFoK8J?db0JuglOMyu}HfU5{C#n29QS@E=C8{w4(4e8EJ z<=d8k4^#|%rKjxJI;OK>XGXi<((tpjM~5U_cMDhj>a(1@GdqKH#)ud0ST!cs`j?q) zX>JSY`Y*?o8o8g~N1kuxYC;Z+ZK9rt_o38|l4{`yVSZTA_wsZer0zD09|7lAXN)4N zOO2p)FBV3B*44tbg3a0&f1huO1>dUCOJQwRcsBNN&8xXG2t~P6>(a8VXWprDBVF61 z0N7rQxo{%QM`HH|+zS*~^>tUCoFtX z7pJrIcHlZ$2j%KIfN0{8pour!N9ZGxRd}}eD(m!z9p1=LJ;dL&tRpC?Fqpp5q&|4F zjZa}$gJVO#_e2BXyCyPZ#*qhv@4zCp!KElzWa@?$kylA2|FJ7koqAnhoi%!bcc_7= z9g-pE8~uej`w7IC_Zn9^{&q&I&ugq33!t-<|1VurXlCr&^ssh#FJcHT1@ZWg73OVh zBb^Gd9z;9{mW+39Y%*O57WLYU5W7x^4obc`qrUTC=3RteUULcSKrLK1R zI*v@8i~m5OlZ=*jk+?Yh=McPpU(yiV>q;T%m0bU1x5LS%H`Np|%NT=q#0+bTGwxJP zoPOz&hqn=IP-V9DjKA&4;qQynbwi@nV?+?Ds&q$_N8CPG#;smw@*C-FB-^W zN@MqRn(F?a^O?ScR-@s|KU{n)n*PI>Epxd*=s2uTXq>$4U&KRkkkNOj^=gVb@eD@9 zalrFor%0u&V0|8KXE29rP0<{V_bcI4Ly^V}BT!CDOj2koh=^e?dNG}qY+vnGg5z_M z4mQN}fr5@4Hx86;z8Q36?A9C|nzefYGEhneP!6PfGrshu@Q!@9IIZduwwc2`V3J4k zjlCpVSegUZOKIf0HXu(Xz8hXzLmv7Eg@)Vsda55Ggr>MnUAAJy{ZQwVV`pC{ZhIEe zMbP$pP_m*~;df-+oj=N&sB6{YQG=T_f1Ob5Gn<~bHnr$cj*0qA#t`TeHqyDP0woYx zHqvvh)|^gR5C4xyH&<88h8G;4z;~GJXZ)BH}W}BU+Z1`Rv`KCJqa$& z7$)_W>nv)QEAx2)>pm;gl5q_5^461>N@a(EzeJe2zufQjF=oaMLx2FGl`qIsjOA!Z zLB+PVW~AL9C<(+-GaLhLrQh?IZoh+q6@M;MgN{?8IrLtVoiKnf+rJ9$pwu`<0`Uc; z0A>)yHoFQw>@Lfdd^^pWTP^TybuTLIXBest~v zTho~;aIO!%^9?eeMjl{LWGuF6!n(IgF2Dl7#HPQ`Iwr?9PjQh+9_UW4Nqe+-mukgd zUYRcjp73ds!!Ou}1{?*(n;B}~+LI{4DZ6$Bw6KgJyUOr(sbZS!_jbTQ+A|cvnaS8M zFD4`gPRPugqvRgQB{WKA5Cvv@NQGrI;{Tzdz8MyC%O%_z1!qH($tP!c7(!+M- z4kAYojSqagD%m)|Bx#EEUcm$V89!30Pmv*ePEk!8E2wReG$#c*tX$WXxgX!}=!p!b z7f-w^ELi={QqD*!eT+mfG>IAZGn}mnGvSK`+lO*tT2XXI<00m=RZ7j>?2(rG9rrud zhdf4I2~0q79CVE_iJqZ`OE;SZ_Ok4bz>!jo9q4f*b@7TI|6TsCvBPf%)*eVCK1Ido z8sUEz%%!NC5DAJ&R>H-tS)_l%Q;epMlsk?n!2kXkS6k*vXJ}>_&&tKFLsal8cI!S} z>*p#ACn~FsLS}0zOoz;;x7dQv&jz~|jHwR&n{<^Da5IIW0j1rv#=9lUuTFtVXTQ?4 zwK{K*$Fpa|rxJVi^E&{(sr2Ih4Afe1_XeJFN!xPKat4OUCoEl}Kr9UOVbs$+(r;0~ zJnRfs6KU33cksxh*4i-D5WkCejU0P(Iiq|O?B3H&MP99@&2_#IcQU1QZpR(ED|}6{ zdI;Z*xw-1x!-8^1U&9$3x>&Ck%Tf_ttbPp3XM*g^xRfHIBL8M7E=G5981=CB6nhD+ zUcH(nB#{2Ob^7>V?4-Q!Gq3|6ci=Y4tRB!7SWWAY)J!h}BVi89V=13At!x!?2+T&& z(M6`XvxWwT+00N(b|K;aT7R1>wc%;&_}Yzd$ckLtKfu%3DjjrKjdyf4j*g$uv0AQ3 z2Y+`OTQt>-8>3;Q36B>=SBNN}{LJl1urqOmLN!YkTf$SBIM3@%S&-3nG^i-wd%STn zfb9{4V-xa_Uq$0FAPp&i9`l*{sgB(=tw!V5JtoLGB#y{EHX>Y5DV;weyU9A%wL$O+ zt?o9fw>SV#=)$?EhMg1tgOU{agKrvf>>R)aa^jcWp>jHi9-27YqqPdZUen?LuimNr z!PA4Vu5ns)2J$!*(!Pkel5{vvrdDP(Bzp0uL9PD5viy`q+U5~LynEgqA|W$f+TL=L z4m?B)-ct;n@@lOE9H4wsfFI)Eb+^R48|KR(J~|J2g#Cr*7TVexv-QpPV1S9imOGaQ zwQJ#HuWTQ7?jAL&N7m&Y%&G@SpfoRsS~Hb>r0Sq~_iT1a<&)%3B>Orc1d`5|clo^unalKZuLd&0rMO!S0--^8HYLs5@o5BUc zO}4fHDQH!o394;y#)eg3M2QH5<@8uGj0!rt`K%cu!9qki4#KfYtK}Uz)k~(zF$!H~ z;o3QzbzERrY!J6Ai37;K0v(Mxv&e*Gf~t zO?EYPe%Q0*1byRBAC#v>wj)9mLrQg+5vmblAmM@V0EgN_b}IQ&wnT%I@;x zOrmdR2e)mW3cZ_JqRXit>4c6GK*cu)7P#ut0?Si$)OUcW&hJ-=y8Aan$2Yg@IzVlb z0L}(1hPL=oqbiNf+x35 z#qnWOZlw|Z1hr|$rkka9cruq+IQ8Id$jbrpSIT2y?|Q=P@ge#9>XFpV@Md3XQ=3r9 zTk|T%O=9S5ZYNld!#7HAGzU^)l_a*~5}8-3T`Tgf3ZX4i6~lavMe?-*lUp74Bw?{9 zgy8;H-=6{cxc-~*-r#GQC+u+xv1i>e9c_VztaLAn6zD}fyH-33DZmueHXkeuaT~h4ysE(?1R5M}zxStdTgW6kifW=% z&m^_d@1JOsOZK_~r0pX5`1xDL{l;YG%^6|I0Fz!{-aQMz)@gKP8t4Had2k8>`TBH} zFq1%X5I0s>(*Lhb$PkNWJhK4-0Nn7!*IHI;LqifCWS8CDBHU0cqi_GF<5d5tm5<=4 zXin$AybHuB=38;?o|g@eZ54+)A3Lf)41<;>%@%(s?+%5PH<*nn%88+onnCB8yn`^% zMUl&=#H}Vg5+SE`Spd#f&he05BNn8F4$dV5SbAG}Q;krE3jA@Uq=jtznmbg+2i;KR z9_eroLR}I751I=6h|PwKg+Mr6xW^!$LLm97pl*TY?9_mIEF-yb@Unsz6yS6)gk2Il zDR`m*TCpTiKrab9@@wWOgWkm((tdcODQeQ1%DopYki=cu@EOsX1gEY$aca2U z+xO0l6g&Kzf9qh+? zI6%T-Xs=BklomnBf9{L#4f?-(gsFykwE5$2bH;@MI<8xlOXO{SqSD1Izuj&a1V3LN z8PUj?H4q1gxMI_O03K_4Mte5OI&*P2OVG#=hGTbP!^#|EE6*4RoQpK2TLpFoL#XLX z`Z-2_B+>_8hLvtg6C2O!@~;+Qo%gE(uVsW7{g}2kF9GC=kf@s3;_{2vr4e|ho7#yA zI&CZa7GT0yWeJs57Q0~kv+=YP(}BA7d?`o$pS){ReYNkTIt&RnIP)$kGPSImux=#3uBR&VVa?Pv=u$N1M%GK4>bR4!sIKXz5o;hGjhr#Q zV}NQ9_n?n%#f#y{uA@)ToJMwA=nMjTTOZ*vp49clvPBgEC#q3lMVM!Ot-&c6B;a9= zbt_}T%28-`09ODBrg4ZmFFQzs-AFCoI&_w!LymEiLCs&_2vuGdoL||u%8KjnTjj_) ziv?JEudT;)pchis!Gp_*nkjkWT2i`!OuA%L;FJ#@{u{?{1s)E1*j^MtA^@N{u5ZdH=7s zETz}8g*#agdu)^P475uwV2Y23PONM` z;S~|*8Ec$o&svA>2`;zW89qN-` zWlZW6VcvA*{(y?oybMn?ol-ViP!$3~-!Eq*--DoyYqY z7gCTmCpS4*xt0yIr{zWywA#Lmoz%S!jas4&TTw(Vp#aOW4)Kn*kTwji&h5pj&tc@h zN$iy0_)WI^^0X}kM`{>=fH%Q6U|6u^Y^~E+E#4g&cUWq6!|e)O_^0O=VQQtIAk@R7Y$F?PcOyaJ-b$+)ad0ko(r zoyO{7VuXo}{!hqcAOFVAWn1s)Zlrxj)4`i~P;hKEUtxs509@eOl7Su8tc}!s8x=$) z4~orXF)?+``_zC;I{~FE{5*!6AmZKUc|BcXOpD+;ny>NElqpI=wv1j#IgHR*&xLCwi>9PA0_i2c3df_0yiGGNY2oP zJI!sSk>66;eJu=!Xa0}-%d=;Kc!3Nt*W+K1_>(Tq`#k!0jT{B8UT18^FJLTp@ZP$v ziy-LLsvy<18c&TY&TfVZ;DF;nNTmSst8x<(j}(5<6Revy$0Dm~x~K z={)aASV-Fsfpubn;+|mTb8zJPxt2M^uA!ivC6VOMd|~A`EPJxNpFpneRjzt^7Jra= z$Bw9BZvcnxm<<_|VA_z9F@GoxHAz7RKdS3VFyNw`jvGgho-_SV@bK*415-nYv+MOo zLvy-%b+H<(8Wm1v;_W0jk)qCXkKVBbENT5j-N?y1&1@E-MW!OujtGUHO514ke(?cG zJ$0uFD$<6s4Tpk!DEQ%)O%{PAT(Lo8;~f*TH0$fu}t@#PR7x4~+kpw>xo zB`MB_00(ZM+e`OhEuVI#HW>8nYSY9pkdNZ`!bc-aZcZyQOtHK^X6R_+WeS2RpCBcM znt}f#avW?B={;}D6WddCh!&Fl0_eN{;`Da$Fg9eQRc!Y*KG@@ukBXEIipL!E(-0{K zqa{Y0#xHiwNa89u8$&4sq(PAndpk7IpceJC`;l3~D`W{+ZW?&tc7lqQS66X%dOzzx^9xK$+G-x9r_-)M7tGir~_c8D9)CV0w|;&Nq& z#Y0-C<4Wj+=~v}8xHUv)LN6$58N=5>(TeJzAGqZntT!aPLx15L56wQStLA zo#kdyNU;Dp$OA&xL_3lEQWP92Qk}&ywF;K`@GRp^uapWkf?B~( z05?xw2^dipSfZ~@(1t)+cDnIJCTM#@_D%K&4V$#;y~>3(_iYWgx(wiaYNh^1^$ZuQ z#V}e>t7pC?#9p{@U}mmiw*UIGVaUbWotSyX5^(oCktmJJKw&S<7;5Gvlv*En!mL8f zZ%F_=KP(_TQ!Y+e>05^&G4K*CvYg-GExImszl**AS}YeRf&lUJBedNVqZJ@c*Rarx zx@yV(Tlk+)+cAP;$H!O%QzX2hhs_zo@N?dk0PNV1z0X}Ew+Uoi%l5O)9At{R#dr$L z$hvwRfo^HsPrT)t31`u z^m+J?rGcJz<2`Bg_B=$(1Jv?b4nty+M`QX%8-_sl3FF~wPnuP zgCTODRvwfG9})kNbk`Dd=aKmZxD8N+^tU}(00STI$MrF^gGV*sii z|J#A%9xcDx>P3OG^&RB=b=m}3BC%tvzQVvVpcvDI%F0u29Z>XOl3mlGU=wySgdNDI z6Q|Gfn(L?_cate$`GGdO$*$;-H#{T?l zF_Urlt%^8E(n;Y8{lk9j)7WdM>_zcU!uwZ(`vLz6-Lx_Q09Wr=QG15hz^9wsP_dk2 zSb{HZiI(+_<)!%5M$3LpW0p*8Ghx_u3RWlpp=cRa8WD1l%vVUcDREc$Gj47S%&N6D zU=+6K(Fft^>_m#v-P9rF2rUhsf0<=yV5;nBw)b45F$y;uyS-XoT7Y^pk~dZ5wfJx0 zKQ1$X;=v6^Yvpp|G4AwHmjSWuM21D(Dg`<$W2BGLJBy|9Yu7b!7PI~hDt9pLxCL6x zrbXmRJJ24vmF~;?jK0*~kfL+SvkiXVu8(U*y3i?DW{_hJzf!9I21(03#G?z)z`;}} z)cuLU#sDaRV!^Y#;8z~=x9lhkMkho)Ms3Z(nUz+itOAzZ8X){V9zeSkJ1}SyIDk+w zGSGw+m-%~kuukOgf(Zm6V^RId2|Dzz>@rSnOHuENQkC%}%JkF-%v*%iDRAUUDdw7e z01IKoVzyoeL)WH6=?%+ zjZCna2%>X*-?8vQyJ!~n5i))EG*PG+TC9-P`MS6&YULV4xjB(2WZO8~7?m`=wL?yA z6SH1%E(pF-n%!(f4gTCr9WK^AsS`T;%v-SZx9Wc+JoTE-ATvHP#YNe%AHeWL@mjym zPdb&y4*lI%sE%@B;5iR&qfpk%lE{4 zj&C^R4&@ghAI5vF&H>ZzvpCLVwrFW0#_+R^oe0KQIvY_yq$5iKFQcZ`*^Dr#vj z=;Vzl@Q!wV9An5YWgLwBah_f!ZXhyA+?!S%h^Ln^m2ny1gtUs9(4>uJfouEb~JIS!n_o903NTvnXYZ9dQ0bI zIa5w>$+_tRw{{ay#3UbapGji?77rp?;F5!XVhb@uPLIL@WaHjFz8s z*d!45Oec>0@4!Jb26>l0r;gXF2^>x?Aiy-wn0#>vjWtPWq%`X=_bUzh7LvGmT`oxH z+nUif(UzgJeFMeL8<4xEtuS)#A6}T+4HO+u5h&KY_fTI{LOf!26Wa5Xbvr#d{q-vv zo5M=(r0r_f-@Y1nY&k>B@iO8Cc-6xc@U5=iZUy_qhxt#{ouh%i=m;LxmHrSVVd3Y& zIN+NZ-5Oq^g`Ih}Jq7CbUAA|=c?Q{FfSdMKMe_$4}XUK>p~JH=MqPrydZ) zr1w6w^7`t6=~x96fNO!aJ<~w(VvlTPLScY)I|wT2eJYYApNN)C?wfO{ZmF*BylkWX zm8?wJ4wenwxx-`2Yr`6GxRL~x=b>wn$u#X34-mdz&I)|m z)CJh>%jZMH9aVI0T?`PV38;LGwno+m6RKaVez2PW5hhb+=%|1GlxFF-| z2Cjfk0H6GDHyF?@yD3%E_w~x50C&Ik^iJY&gB3_AyF$b7TC(*hI1Ds2$B{!)^<8hk z6N9TvAKa+NoXfd1a#kRu|6}}@JR*1IhgIZ9k2(aMkkpFZ-P(_Tx4x-MPN#P8hC*T- zcL-$;a?Jmmr&!v7E_Q{jqgr?6$q)%8B=n(Y@@UpXB{EER6Br%`*BA~`h(g++r!HWr zBUX;6Z}DcIbT<%_D=iX9rVDG=Rvx(jFbh_F@Ah#pLW1*sQr^+Pl)M2jXFNY5$&hA4Oe2w(q?MMr)ZQ3hWu=3^=R!*j<_a(FrU7zB7Fq(x=IRlo4fz77l z&qc>4Jk4g3UMc++r|;hbmSUNXK`R@GHG;h6 zm;qW9Ey|t)dbcyU4{_=kM=NX`+xPqb5tA74(_`^i&2t&AdpgZ&Hbcep@Z#zWSDpjD z-QQySBx)`!B=M;ud2&10Ea$to>AC>G-5oSyLT9iH0%8)hsW3-u$WkiS0(o(*q0>1b z=0O;D1Tt-;!vi}%g;AnuyUX9)GAd$ImXMQxC|zKVKk|b-Q;m%pe?G{_81@LXcn>lq zjxjM$;jOg*sY6A7A5xN6?+4h?!C4CB#9nEW;3qfITH(!2s3!9%{i4zAO)QnZE_2+q zTni4u_*WFT@j^P)Dpa*}dkMp-Qb#p~|KPBNF5JjKs>cihS=19~R~x!fp-!ZbAxqSm zi0DWxJm;@Qc~pEf5Z{-+oZEn zlmSm^ph3SqN)H3G1kHNEbupO91W6!j?-I|o${RNMM^aBF@4`zwo)|ruzAGhft@+@n zU(Z+EYdbAtKmIdRZo|=_d7OptxYAM)sxm}y~aGAOh zFt8vst`LfrfEv_S6sLI94CdGyzaziNjzpb6Z!4@)pP3@XJq!0bhbEYyV$aC{DU9S((~D zu{uRZIw9>aiZ5^z?1$r#xS&hJ3X6JTIzRQ!&>6=eW4dWQ2hAgHyVIV2X6t|MRIYou z42@PGVA;KIBZ~Vg%f?83uwOL#4Lc(COben7-xTuj5FzJ{L>d$(v1xWI)ZzBEH%&)) z3XJQc+*w{l8>;_r?`&zZWnPQ$k>-|J%IL04uK5Mf+IldFu0s)(^bFP6^;vzO;B~~G>of-DM-T`U_N=ajVpm{cf_)Rs zmb~YP4V+KJi)kDI@Bh42)Bc>R?TUfH6;y-2p0ph!jQBL!*%rMGWJjD0pNBiR99aAmtN(1@%`F0yzR02BC%-A)<3eqDN-V4>Tf-ny+; z=vKB`CHzel{=MABEa8S8{#qd5Yqk1Nxk zcnPJ==0to2v7El`8hEn@BW1Wnn(q_NON}hh59o<&LkKt!Hy-!xt6CHvvpSV_YoYay z>}Lyz45set`opXlZU04!g>Yi7&?S@AAHm<;vDbz%H3B{-5j3~V^$40z7nx( z4ET4_nbcRFQ!8{7jX`oJwy(?4A_nwAq(1a>$%#sGY+& zM2AL>H$c~PqutCwq zQ3lODNT}LUCwHIx&x~;ZuwY_ zp7+Zw^PaJRz2?DlVVt4F%wqsSH!nCQt!x8~x_&dQq%M-efb{Ia1h;PNJdj*`0= zCw)CF_rptV?*i3hnuPg}*+;{Wk812eeL8XE5b-rn>MTBfIeX?2&Ud*eL3PBy*BnIaCDw-!jIM4D z(+6i_b%)mRH5{gmA!cc_ODU_mB3@}LDCDYvO)P%fD>jb%DSb9IZe}tBBa*<}CA5Xp zA8^>dZwRsrOv&i>&l4exn(~`ga;;5kQse14q0q5Ty%a z_i$Wc;o_oe1_APdHW0}0$&fsO9YQh)5`pTU%pVKuXlP3NKfz--uBPgW#iV4rW1gVR z==_*ukKC2qs=!%mn$7{eBWIjBG^7K8ea!j@3|QtYMFY9ujI(21N?a=7z(vpg08m_1 zU3R8Zk07@(gc+7*_qw zNF!M*c&)=ggGb2|1|b8+UapHKmR;oen^httbfFA!N=nGKP2dA<$f~-S1EL#Ddqp2d2>%P zf)+1sZwxbnY*&!Yl)q});dCnb_6x-1K@rZQyu$=#YbSj08w=;DCg@P#-N&~9g%pV^ zK*b8X=)nOt!+3lD2iCe=JTv)$Y833H1DcCScxesdanl=7X>;iS0kBSrVKn#!*p#|e zx10#?{L@#KP};?lwEpGGXOl%+wT#*pQK4}MLGdeV&ytufGyF-G3Z9LeF}`DfY39<+ zHm5xms5jwkFHgImU@)w%y%%-;KP;i|ze-hpF49|hg#pS!l)Gri6r?3~4(x#-gTf?s z;C)!Gwn*?x9+cBTsk2T(R`_V-FK+je>nHg}B$f;w4Jh6;#h?msOMRH>zbKk<=3q1k zT3M!}&4uA2@Ou4)e#KI(G&TRQF30xgH7@z|o%L&Oe;_Z0(S&WKDWm}at{HZs^7Y&} zkAzj>0Fp|uPXUxJHRb!IoXvLDgblBkgiES*(zdw<6E*Zi0l#= zBWO_q>ptO=FKb`2Yeq%`wjO||D_ewXz^fEazK1>+w#h#l^@%zP*N3LIYLy@Oj7k2q z*F49HIQjmc@A2kq)6d!f{Fv0|Tg6b^^<9q}%H;zh?_pSoY?! zk*9=)t5-YXu3DX4=vz=N`?pNT$`H>GP2u2K4TJm80FUix=Pz4oy;=EBqiMFUg1xk3t}MMy`I87)1(5o zTm_+I-zfQshiEMEGLbpYk%3TRZn6~hOGQj@;^NQFF79`@RW*`zXLC2v>Z4=Wt55i4 zJTRQ7{!AGVJ;b=O$1=Jr(W3=!!x`8X#29+~bF-SK70&+5x9~zur8T|3Wc*&sN2iuu z{p~0cn{0*T->g;gClZUk3YLP>v|3olQs^;% zZ)jEaf9Zh5#`B#HWr=eMEJnd>iUj=b4O(uBr__in53(icLx*GLe#2RZqT@*SQ~hDp8dQIkCGg#kp`UAWGhA5L_qcJKoHj(R z@#46fq%Q(bw4AhV{>P{IG3*r+m*M+sOKlH_rnt9FI!ol0$p(JCsO?nYpugan1CoQk zCYM?X6!c08N~(h{S{>J+*Je#$owu=AzbPua8KH9_wC!bhuY{GbfOaEjB~$J^)zH(LMQ1goNFp__i^+dte5`LYJaMsS8-Ylaw_SI-<0kiFy8UW zPi-ix=E?|XaF(e-NZ2#Q!8EX+s%wRWTQJ|zL8$m&UirKLC6QfDRp!H+sB7;prTS+L zJYtK?T73x-1tO7r^CY-P5cTW8Hct2QVDrZ!v!j)xsC-qM;5I=ZD=+!d4nTw3JGPj9 zCw9J3x^Rz?+(&Z8T z{BI2iR(oZjGbOE}dA;n3Xjs3t@AQEJLpxl3Bfbb|2!&!wh*Hks=3i(vm60Smax+UF zt`5NvL1kL4Hk$`Ew#vovLFv}|2FY|+0JOb3PEq4lV$30(uDF}ST$){mDzD+n|HoV^ z``7lXIlueHSXBSA(SHvgK&>GVJ zwDd&)BBwRyZBlUWN>3BJDR?QGz3nr^mhH2^6BJL9e;v!#FH=ti#_)B&cNp|sQWsKZ z^ZQQKp$Z#|+glp5Fq6)B<*zQI3+)f$6-?l`+jr59E|&OJ6-oeOOvLNK*m9V*=J;h{ zq#oGiwzrx@5(G>V#c|;OUJ!J%A$7wKQHw@thpe^gfMJM{QWkJs z9A0;+A6sOkLC$Li`c@>)rBw+Uq%nXdJw9G09|CXcxNZC7(x-afR6k0i9Kdn!U8129 zR%^mfFo6j+PJV}fqo_T$V|P1n#=KuM;E*N$-IwLlCg*#niLgwR64TH%AZO5qNC(&u z#GuywvPN(s=Wu;{>SiWmv-EAHdR~gvF5_cNn63CTaC4J85M!dwA7dlzm1xcschbzc zsVd*}1Brb-Ch~1$2(=VNIev)AzgCpc#kZZW#NY-%&6e z4-dv!yyv2gvW_%mzML2ppm%HkHAgSCC3R6Oaz!;A#Gc%R@)|i6KQ2MAC_)Rj0f&YnA&JK|jFKyw6L9w!C3C-3_(Cdu47 z2(Q8>;QOR(sR1hax>Xy09Q4hO`r#CAi%gQy;$R(8%qF zY7KoL#iQ)pUSlu|W_cuHcJS-)PU~IFF@cvp$#1qb2+q!XWeQHOX{Xbj@rP#lXL36z zrrd4#@CaR(6w(tUPPN-%V%47`T07wWm;h8|IEL!>CUUqa9i0k501xu-v7 zAasp1k0+~xHs;N{a!&$4-%(ntYtu{gYuo%RBE@u@{V1Jn>izkOPTpA;t^wEwVLU|X z`4mO;-c1wp*kQScGWo|!cbpu!1JD?@6mjZhBQYXzb`vd83AGpcD3(=`Xc})n% zvM{?ks%0@i%-@K7TQdTDS=ur~Xx_B;BVEVv7{fpOi|{>)H?s+kuCAcs634`x*F0yK z&*JkkyN%^~mypO4*^IB7)up|}WU=3Fvf0u@A1Z`&*GW7j5pi)J0_`1nokteHjMX%st=tGs#dJ~A; zZFNb^tnraNQV1~%suY3@cmk!GcGUA@^kGC~zAY4RiCLEbnRbX-&$Eu!UgNb5eP5b< zmSg=nWfs4&CB@hd^P(45*x)h>8y}g6^>$rx*$f@I;~?Wwlos`ajTM}d$`Aqf)~b@1_aY|gW<@87YC z)+J9F3^5nH&g~_B68{QFN194%Z~Yfb_P80Y^E^CXSz$q0W(z8x-qRGLY|*)b#@nbT z8&KxD>~D0qYA+`RIWDYL#7tyZ!MEbU_j%nvFv@o;Z-ygAPLtnqFCdnq?ey4}3F4G- z&-FHP#E9P<`qIodBNOQ*m$Mduno6+t@Qr@8lvhE_2{SK<`tx@7wF}GqR9RdQhhcy; z_sWvFX8i1Sy2{0>K?LeQ4*MCf^X9C?nYdbcaZCfkKz49+%*X|7xyGZ=c8@8y=b+(K zkh#Rs^?kmu1sfV{YfiqZ1EBsL3g!jv|#h^-}i%ewtF^u^R>F4ebfv+K&E$|#MrQpYk=4B5?ED! zXpG{fzP575Ci-htLHT?t*hRB=Rs6UCnUq!?a|5EEsO~%MX2M2;benGU!aiZ&pSQPC zxdzW-=q-eQ0g9uFWvSfloDz+p8*2f`iQH}rvZ__R0-U=XN&ps*p~XYQq>GdV z5}87@?l5DjI4fq#()T95fVJdYZ4~SS{-!0{JLT=uq4%Xw)KPmYIuvixQiK{HZ%AH3 zlDZAL`6o}s&17rw@pFggoH#f5T#|}Oue72~e^@3xryTHyLXR>Gpem zf8Ny2-UABQ`EZFj=lM9K3H{*R|hLtMnQ?K{xRMr6&z zk`Rq#t>UwBmch5!vS7D)n~$wo6J1gv3X_wfe?#`E;l~hg&r!Qvc;%P#(UR;*u&q-NZi#kAk=?gz`1G^dD*dc-?8k2fR!eObPf-|ud>FNQz#+^n0cB^8*crbd;D|g~ z8dsk$$wf(1*%!QTj$yoGVmRQsSU!?Wquh@%YD2gvh@Rrk=wTxy+Gx8fNepi}Cw%)j+3BtN65a^h%e zxt=b1#4^Fcf>ofL=xW4)x7m`zB6t@n8_dAh?U;2@-eIlutIoP-nq%yfA5FbeJE-Z{EH5oOl8xaUO9zHa@pMG}X`fl$K0NOUDlU^Y~#`v%Ei5&gA8P)g7nf zI(IDQn#&m-@9)v4X7);6ryM3Iq9+EM@XS?KjcHEjecw#lc;=rLV+hNEfY$MU3TJ0V zG+n8VC=1.0.2" } }, "sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], - - "readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="], - - "semver": ["semver@5.3.0", "", { "bin": { "semver": "./bin/semver" } }, "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw=="], - - "string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], - - "sup-proton-bridge": ["sup-proton-bridge@workspace:proton-bridge"], - - "sup-server": ["sup-server@workspace:server"], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "utf7": ["utf7@1.0.2", "", { "dependencies": { "semver": "~5.3.0" } }, "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw=="], } } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 86938a6..5f178f3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,8 +9,14 @@ services: - PORT=8080 - API_KEY=${API_KEY:-} - VERBOSE=${VERBOSE:-false} + # ProtonMail integration (optional) + - BRIDGE_IMAP_USERNAME=${BRIDGE_IMAP_USERNAME:-} + - BRIDGE_IMAP_PASSWORD=${BRIDGE_IMAP_PASSWORD:-} + - PROTON_BRIDGE_HOST=protonmail-bridge + - SUP_TOPIC=${SUP_TOPIC:-Proton Mail} volumes: - signal-data:/root/.local/share/signal-cli + - sup-data:/root/.local/share/sup restart: unless-stopped protonmail-bridge: @@ -19,24 +25,10 @@ services: profiles: ['protonmail'] volumes: - proton-bridge-data:/root - - /tmp/bridge-updates:/root/.local/share/protonmail/bridge-v3/updates:ro # Disable auto-updates - restart: unless-stopped - - sup-proton-bridge: - build: ./proton-bridge - container_name: sup-proton-bridge - profiles: ['protonmail'] - depends_on: - - sup-server - - protonmail-bridge - environment: - - BRIDGE_IMAP_USERNAME=${BRIDGE_IMAP_USERNAME} - - BRIDGE_IMAP_PASSWORD=${BRIDGE_IMAP_PASSWORD} - - SUP_API_KEY=${API_KEY} - - SUP_TOPIC=${SUP_TOPIC:-Proton Mail} - - VERBOSE=${VERBOSE:-false} + - /tmp/bridge-updates:/root/.local/share/protonmail/bridge-v3/updates:ro restart: unless-stopped volumes: signal-data: + sup-data: proton-bridge-data: diff --git a/docker-compose.yml b/docker-compose.yml index e01df69..6518480 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,14 @@ services: - PORT=8080 - API_KEY=${API_KEY:-} - VERBOSE=${VERBOSE:-false} + # ProtonMail integration (optional) + - BRIDGE_IMAP_USERNAME=${BRIDGE_IMAP_USERNAME:-} + - BRIDGE_IMAP_PASSWORD=${BRIDGE_IMAP_PASSWORD:-} + - PROTON_BRIDGE_HOST=protonmail-bridge + - SUP_TOPIC=${SUP_TOPIC:-Proton Mail} volumes: - signal-data:/root/.local/share/signal-cli + - sup-data:/root/.local/share/sup restart: unless-stopped protonmail-bridge: @@ -17,24 +23,10 @@ services: profiles: ['protonmail'] volumes: - proton-bridge-data:/root - - /tmp/bridge-updates:/root/.local/share/protonmail/bridge-v3/updates:ro # Disable auto-updates - restart: unless-stopped - - sup-proton-bridge: - image: ghcr.io/lone-cloud/sup-proton-bridge:latest - container_name: sup-proton-bridge - profiles: ['protonmail'] - depends_on: - - sup-server - - protonmail-bridge - environment: - - BRIDGE_IMAP_USERNAME=${BRIDGE_IMAP_USERNAME} - - BRIDGE_IMAP_PASSWORD=${BRIDGE_IMAP_PASSWORD} - - SUP_API_KEY=${API_KEY} - - SUP_TOPIC=${SUP_TOPIC:-Proton Mail} - - VERBOSE=${VERBOSE:-false} + - /tmp/bridge-updates:/root/.local/share/protonmail/bridge-v3/updates:ro restart: unless-stopped volumes: signal-data: + sup-data: proton-bridge-data: diff --git a/package.json b/package.json index 1bbf883..e1a0979 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sup-monorepo", + "name": "sup", "version": "0.1.0", "description": "Privacy-preserving push notifications using Signal as transport", "private": true, @@ -13,10 +13,6 @@ "type": "git", "url": "https://github.com/lone-cloud/sup" }, - "workspaces": [ - "server", - "proton-bridge" - ], "scripts": { "check": "tsc --noEmit && biome check .", "fix": "biome check --write .", diff --git a/proton-bridge/.env.example b/proton-bridge/.env.example deleted file mode 100644 index a2b1b43..0000000 --- a/proton-bridge/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# ProtonMail Bridge Configuration -PROTON_BRIDGE_HOST=localhost -PROTON_BRIDGE_PORT=1143 -PROTON_EMAIL=your-email@protonmail.com -PROTON_PASSWORD=bridge-generated-password - -# SUP Server Configuration -SUP_SERVER_URL=http://localhost:8080 -SUP_API_KEY=your-api-key -SUP_TOPIC=Proton Mail diff --git a/proton-bridge/.gitignore b/proton-bridge/.gitignore deleted file mode 100644 index 3931b2e..0000000 --- a/proton-bridge/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -.env -*.log -dist/ diff --git a/proton-bridge/Dockerfile b/proton-bridge/Dockerfile deleted file mode 100644 index b9ebb17..0000000 --- a/proton-bridge/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM oven/bun:1.1.42-alpine AS builder - -WORKDIR /app - -COPY package.json bun.lock* ./ -RUN bun install --frozen-lockfile - -COPY . . - -RUN bun build --compile src/index.ts --outfile sup-proton-bridge - -FROM alpine:3.21 - -COPY --from=builder /app/sup-proton-bridge /usr/local/bin/sup-proton-bridge - -CMD ["sup-proton-bridge"] diff --git a/proton-bridge/README.md b/proton-bridge/README.md deleted file mode 100644 index cefe26f..0000000 --- a/proton-bridge/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# SUP ProtonMail Bridge - -IMAP bridge that monitors a ProtonMail account via Proton Bridge and sends notifications to a SUP server. - -## Architecture - -``` -ProtonMail (E2EE) → Proton Bridge (IMAP) → This Bridge → SUP Server → Signal → Phone -``` - -## Prerequisites - -1. **Proton Bridge** installed and running locally - - Download from: https://proton.me/mail/bridge - - Set up your ProtonMail account - - Note the generated IMAP password (not your ProtonMail password!) - -2. **SUP Server** running - - See main README for SUP server setup - -## Configuration - -1. Copy `.env.example` to `.env`: - ```bash - cp .env.example .env - ``` - -2. Configure your settings: - - ```env - PROTON_EMAIL=your-email@protonmail.com - PROTON_PASSWORD=bridge-generated-password # From Proton Bridge settings - SUP_SERVER_URL=http://localhost:8080 - SUP_API_KEY=your-api-key # Optional - SUP_TOPIC=protonmail - ``` - -## Running Locally - -```bash -# Install dependencies -bun install - -# Run in development mode (auto-reload) -bun dev - -# Run in production mode -bun start -``` - -## Running with Docker - -```bash -# Build -docker build -t sup-proton-bridge . - -# Run -docker run -d \ - --name sup-proton-bridge \ - --env-file .env \ - --network host \ - sup-proton-bridge -``` - -## Running with Docker Compose - -See the main `docker-compose.yml` in the repo root. - -## How It Works - -1. Connects to Proton Bridge via IMAP (localhost:1143) -2. Opens INBOX and enters IDLE mode -3. When new mail arrives, fetches sender and subject -4. Sends notification to SUP server at `/notify/protonmail` -5. SUP delivers notification via Signal to your phone - -## Security Notes - -- Proton Bridge runs locally and handles E2EE decryption -- This bridge only accesses the already-decrypted IMAP interface -- No credentials are sent to SUP server -- Only email metadata (sender, subject) is transmitted diff --git a/proton-bridge/package.json b/proton-bridge/package.json deleted file mode 100644 index 6419bbf..0000000 --- a/proton-bridge/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "sup-proton-bridge", - "version": "0.1.0", - "description": "ProtonMail IMAP bridge for SUP notifications via Signal", - "type": "module", - "scripts": { - "dev": "bun --watch src/index.ts", - "start": "bun src/index.ts", - "build": "bun build --compile src/index.ts --outfile sup-proton-bridge" - }, - "dependencies": { - "chalk": "^5.6.2", - "imap": "^0.8.19" - }, - "devDependencies": { - "@types/imap": "^0.8.43" - } -} diff --git a/proton-bridge/src/index.ts b/proton-bridge/src/index.ts deleted file mode 100644 index 51b3d2f..0000000 --- a/proton-bridge/src/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -import chalk from 'chalk'; -import Imap from 'imap'; - -const PROTON_BRIDGE_HOST = process.env.PROTON_BRIDGE_HOST || 'protonmail-bridge'; -const PROTON_BRIDGE_PORT = Number.parseInt(process.env.PROTON_BRIDGE_PORT || '143', 10); -const BRIDGE_IMAP_USERNAME = process.env.BRIDGE_IMAP_USERNAME; -const BRIDGE_IMAP_PASSWORD = process.env.BRIDGE_IMAP_PASSWORD; // Generated by Proton Bridge - -const SUP_SERVER_URL = process.env.SUP_SERVER_URL || 'http://sup-server:8080'; -const SUP_API_KEY = process.env.SUP_API_KEY; -const SUP_TOPIC = process.env.SUP_TOPIC || 'Proton Mail'; -const VERBOSE = process.env.VERBOSE === 'true'; - -const log = (...args: unknown[]) => VERBOSE && console.log(...args); - -if (!BRIDGE_IMAP_USERNAME || !BRIDGE_IMAP_PASSWORD) { - console.error( - chalk.red('Missing required env vars: BRIDGE_IMAP_USERNAME and BRIDGE_IMAP_PASSWORD'), - ); - console.error( - chalk.yellow( - 'Run: docker run --rm -it -v proton-bridge-data:/root shenxn/protonmail-bridge init', - ), - ); - console.error(chalk.yellow('Then use `login` and `info` commands to get IMAP credentials')); - process.exit(1); -} - -console.log( - chalk.blue(`🔗 Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`), -); -console.log(chalk.blue(`📨 Monitoring mailbox: ${BRIDGE_IMAP_USERNAME}`)); -console.log(chalk.blue(`🔔 Sending notifications to: ${SUP_SERVER_URL}/notify/${SUP_TOPIC}`)); - -const imap = new Imap({ - user: BRIDGE_IMAP_USERNAME, - password: BRIDGE_IMAP_PASSWORD, - host: PROTON_BRIDGE_HOST, - port: PROTON_BRIDGE_PORT, - tls: true, - tlsOptions: { rejectUnauthorized: false }, // Proton Bridge uses self-signed cert - keepalive: true, -}); - -async function sendNotification(title: string, message: string) { - try { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (SUP_API_KEY) { - headers.Authorization = `Bearer ${SUP_API_KEY}`; - } - - const response = await fetch(`${SUP_SERVER_URL}/notify/${SUP_TOPIC}`, { - method: 'POST', - headers, - body: JSON.stringify({ title, message }), - }); - - if (!response.ok) { - new Error(`SUP server responded with ${response.status}`); - } - - console.log(chalk.green(`✅ Notification sent: ${title}`)); - } catch (error) { - console.error(chalk.red('❌ Failed to send notification:'), error); - } -} - -function openInbox() { - imap.openBox('INBOX', false, (err, box) => { - if (err) { - console.error(chalk.red('Failed to open inbox:'), err); - } - - log(`✅ Connected to inbox (${box.messages.total} messages)`); - - imap.on('mail', async (numNewMsgs: number) => { - log(`📬 ${numNewMsgs} new message(s) received`); - - const fetch = imap.seq.fetch(`${box.messages.total}:*`, { - bodies: 'HEADER.FIELDS (FROM SUBJECT)', - struct: true, - }); - - fetch.on('message', (msg) => { - msg.on('body', (stream) => { - let buffer = ''; - stream.on('data', (chunk) => { - buffer += chunk.toString('utf8'); - }); - stream.once('end', () => { - const header = Imap.parseHeader(buffer); - const from = header.from?.[0] || 'Unknown sender'; - const subject = header.subject?.[0] || 'No subject'; - - sendNotification('New ProtonMail', `From: ${from}\n${subject}`); - }); - }); - }); - }); - - imap.on('update', () => { - log('📊 Mailbox updated'); - }); - }); -} - -imap.once('ready', () => { - log('✅ IMAP connection ready'); - openInbox(); -}); - -imap.once('error', (err: Error) => { - console.error(chalk.red('❌ IMAP error:'), err); -}); - -imap.once('end', () => { - log('⚠️ IMAP connection ended, reconnecting...'); - setTimeout(() => imap.connect(), 5000); -}); - -imap.connect(); - -process.on('SIGTERM', () => { - log('Shutting down...'); - imap.end(); - process.exit(0); -}); - -process.on('SIGINT', () => { - log('Shutting down...'); - imap.end(); - process.exit(0); -}); diff --git a/scripts/install-signal-cli.ts b/scripts/install-signal-cli.ts index fe03ba0..2bc95a3 100755 --- a/scripts/install-signal-cli.ts +++ b/scripts/install-signal-cli.ts @@ -1,3 +1,5 @@ +import { chmod, rename } from 'node:fs/promises'; + const SIGNAL_CLI_VERSION = '0.13.22'; const SIGNAL_CLI_URL = `https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}.tar.gz`; const SIGNAL_CLI_DIR = `${import.meta.dir}/../signal-cli`; @@ -14,23 +16,15 @@ async function installSignalCli() { throw new Error(`Failed to download: ${response.statusText}`); } - const tarball = await response.arrayBuffer(); - console.log('Extracting signal-cli...'); - const proc = Bun.spawn(['tar', 'xzf', '-'], { - stdin: 'pipe', - cwd: `${import.meta.dir}/..`, - }); - - proc.stdin.write(new Uint8Array(tarball)); - proc.stdin.end(); - await proc.exited; + const archive = new Bun.Archive(await response.blob()); + await archive.extract(`${import.meta.dir}/..`); const extractedDir = `${import.meta.dir}/../signal-cli-${SIGNAL_CLI_VERSION}`; - await Bun.spawn(['mv', extractedDir, SIGNAL_CLI_DIR]).exited; + await rename(extractedDir, SIGNAL_CLI_DIR); - await Bun.spawn(['chmod', '+x', `${SIGNAL_CLI_DIR}/bin/signal-cli`]).exited; + await chmod(`${SIGNAL_CLI_DIR}/bin/signal-cli`, 0o755); console.log('✓ signal-cli installed successfully'); } diff --git a/server/bun.lock b/server/bun.lock new file mode 100644 index 0000000..59bf2b5 --- /dev/null +++ b/server/bun.lock @@ -0,0 +1,41 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "sup-server", + "dependencies": { + "chalk": "^5.6.2", + "imap": "^0.8.19", + }, + "devDependencies": { + "@types/imap": "^0.8.43", + }, + }, + }, + "packages": { + "@types/imap": ["@types/imap@0.8.43", "", { "dependencies": { "@types/node": "*" } }, "sha512-POPoqrDax9mxM2N4ITZYCWaFtg1ORVfzJe4S7xwSh9aHawdEb7FwWTJYiAhzIvWp7DM+6BajnzYOwZ1BUrqtow=="], + + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "imap": ["imap@0.8.19", "", { "dependencies": { "readable-stream": "1.1.x", "utf7": ">=1.0.2" } }, "sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], + + "readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="], + + "semver": ["semver@5.3.0", "", { "bin": { "semver": "./bin/semver" } }, "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw=="], + + "string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "utf7": ["utf7@1.0.2", "", { "dependencies": { "semver": "~5.3.0" } }, "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw=="], + } +} diff --git a/server/constants/config.ts b/server/constants/config.ts new file mode 100644 index 0000000..8900382 --- /dev/null +++ b/server/constants/config.ts @@ -0,0 +1,17 @@ +export const PORT = Bun.env.PORT || 8080; +export const API_KEY = Bun.env.API_KEY; +export const VERBOSE = Bun.env.VERBOSE === 'true'; + +export const DEVICE_NAME = 'SUP'; +export const DAEMON_START_MAX_ATTEMPTS = 10; + +export const SUP_ENDPOINT_PREFIX = `[${DEVICE_NAME}:`; +export const LAUNCH_ENDPOINT_PREFIX = '[LAUNCH:'; + +// ProtonMail Integration +export const BRIDGE_IMAP_USERNAME = Bun.env.BRIDGE_IMAP_USERNAME; +export const BRIDGE_IMAP_PASSWORD = Bun.env.BRIDGE_IMAP_PASSWORD; +export const PROTON_BRIDGE_HOST = Bun.env.PROTON_BRIDGE_HOST || 'protonmail-bridge'; +export const PROTON_BRIDGE_PORT = Number.parseInt(Bun.env.PROTON_BRIDGE_PORT || '143', 10); +export const SUP_TOPIC = Bun.env.SUP_TOPIC || 'Proton Mail'; +export const ENABLE_PROTON_ANDROID = Bun.env.ENABLE_PROTON_ANDROID === 'true'; diff --git a/server/constants/paths.ts b/server/constants/paths.ts new file mode 100644 index 0000000..fe3fdc9 --- /dev/null +++ b/server/constants/paths.ts @@ -0,0 +1,9 @@ +const HOME = process.env.HOME || '/root'; + +export const SIGNAL_CLI_BIN = '../signal-cli/bin/signal-cli'; +export const SIGNAL_CLI_SOCKET = '/tmp/signal-cli.sock'; +export const SIGNAL_CLI_DATA_DIR = `${HOME}/.local/share/signal-cli`; +export const SIGNAL_CLI_DATA = `${HOME}/.local/share/signal-cli/data`; + +export const SUP_DATA_DIR = `${HOME}/.local/share/sup`; +export const SUP_DB = `${HOME}/.local/share/sup/store.db`; diff --git a/server/constants/server.ts b/server/constants/server.ts index 694113e..4a264e7 100644 --- a/server/constants/server.ts +++ b/server/constants/server.ts @@ -7,11 +7,10 @@ export const ROUTES = { FAVICON: '/favicon.png', MATRIX_NOTIFY: '/_matrix/push/v1/notify', UP: '/up', - UP_PREFIX: '/up/', + UP_INSTANCE: '/up/:instance', ENDPOINTS: '/endpoints', - NOTIFY_PREFIX: '/notify/', + NOTIFY_TOPIC: '/notify/:topic', TOPICS: '/topics', - NOTIFICATIONS: '/notifications', } as const; export const CONTENT_TYPE = { @@ -21,7 +20,7 @@ export const CONTENT_TYPE = { } as const; export const TEMPLATES = { - LINKED: 'server/templates/linked.html', - LINK: 'server/templates/link.html', - SETUP: 'server/templates/setup.html', + LINKED: 'templates/linked.html', + LINK: 'templates/link.html', + SETUP: 'templates/setup.html', } as const; diff --git a/server/index.ts b/server/index.ts index 20eaf68..d70ab0b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,8 +1,10 @@ import chalk from 'chalk'; +import { API_KEY, BRIDGE_IMAP_PASSWORD, BRIDGE_IMAP_USERNAME, PORT } from './constants/config'; import { CONTENT_TYPE, ROUTES, TEMPLATES } from './constants/server'; +import { checkSignalCli, hasValidAccount, initSignal, startDaemon } from './modules/signal'; import { handleHealth } from './routes/health'; import { handleLink, handleLinkQR, handleLinkStatus, handleUnlink } from './routes/link'; -import { handleGetNotifications, handleNotify, handleTopics } from './routes/notify'; +import { handleNotify, handleTopics } from './routes/notify'; import { handleDiscovery, handleEndpoints, @@ -10,10 +12,7 @@ import { handleRegister, handleUnregister, } from './routes/unifiedpush'; -import { checkSignalCli, hasValidAccount, initSignal, startDaemon } from './signal'; - -const PORT = Bun.env.PORT || 8080; -const API_KEY = Bun.env.API_KEY; +import { checkAuth } from './utils/auth'; let daemon: ReturnType | null = null; @@ -30,77 +29,98 @@ if (hasAccount) { } if (!API_KEY) { - console.warn(chalk.yellow('⚠️ Server running without API_KEY - anyone can register endpoints!')); + console.warn(chalk.yellow('⚠️ Server running without API_KEY')); console.warn(chalk.dim(' Set API_KEY env var for production deployments.')); } -const requireHttps = (req: Request) => { - const proto = req.headers.get('x-forwarded-proto') || 'http'; - const host = req.headers.get('host') || ''; - const isLocalhost = host.startsWith('localhost') || host.startsWith('127.0.0.1'); - - if (API_KEY && proto !== 'https' && !isLocalhost) { - return new Response('HTTPS required when API_KEY is configured', { status: 403 }); - } - - return null; -}; +if (BRIDGE_IMAP_USERNAME && BRIDGE_IMAP_PASSWORD) { + const { startProtonMonitor } = await import('./modules/protonmail'); + await startProtonMonitor(); +} const server = Bun.serve({ port: PORT, idleTimeout: 60, - async fetch(req) { - const url = new URL(req.url); + routes: { + [ROUTES.FAVICON]: Bun.file('assets/favicon.png'), - if (url.pathname === ROUTES.FAVICON) { - const file = Bun.file('server/assets/favicon.png'); - return new Response(file, { headers: { 'content-type': 'image/png' } }); - } + [ROUTES.HEALTH]: handleHealth, - if (url.pathname === ROUTES.HEALTH) return handleHealth(); - if (url.pathname === ROUTES.LINK) return handleLink(); - if (url.pathname === ROUTES.LINK_QR) return handleLinkQR(); - if (url.pathname === ROUTES.LINK_STATUS) return handleLinkStatus(); + [ROUTES.LINK]: { + GET: handleLink, + }, + [ROUTES.LINK_QR]: { + GET: handleLinkQR, + }, + + [ROUTES.LINK_STATUS]: { + GET: handleLinkStatus, + }, + + [ROUTES.LINK_UNLINK]: { + POST: async (req) => { + const response = await handleUnlink(req, daemon); + if (response.status === 303) { + daemon = await startDaemon(); + } + return response; + }, + }, + + [ROUTES.UP]: { + GET: handleDiscovery, + }, + + [ROUTES.ENDPOINTS]: { + GET: (req) => { + const auth = checkAuth(req); + if (auth) return auth; + return handleEndpoints(); + }, + }, + + [ROUTES.TOPICS]: { + GET: (req) => { + const auth = checkAuth(req); + if (auth) return auth; + return handleTopics(); + }, + }, + + [ROUTES.MATRIX_NOTIFY]: { + POST: handleMatrixNotify, + }, + + [ROUTES.UP_INSTANCE]: { + POST: (req) => { + const auth = checkAuth(req); + if (auth) return auth; + return handleRegister(req, new URL(req.url)); + }, + DELETE: (req) => { + const auth = checkAuth(req); + if (auth) return auth; + return handleUnregister(new URL(req.url)); + }, + }, + + [ROUTES.NOTIFY_TOPIC]: { + POST: (req) => { + const auth = checkAuth(req); + if (auth) return auth; + return handleNotify(req, new URL(req.url)); + }, + }, + }, + + async fetch(_req) { if (!(await hasValidAccount())) { const html = await Bun.file(TEMPLATES.SETUP).text(); return new Response(html, { headers: { 'content-type': CONTENT_TYPE.HTML } }); } - if (url.pathname === ROUTES.MATRIX_NOTIFY && req.method === 'POST') { - return handleMatrixNotify(req); - } - - const httpsCheck = requireHttps(req); - if (httpsCheck) return httpsCheck; - - if (url.pathname === ROUTES.LINK_UNLINK && req.method === 'POST') { - const response = await handleUnlink(req, daemon); - - if (response.status === 303) { - daemon = await startDaemon(); - } - - return response; - } - - if (url.pathname.startsWith(ROUTES.UP_PREFIX)) { - if (req.method === 'POST') return handleRegister(req, url); - if (req.method === 'DELETE') return handleUnregister(url); - } - - if (url.pathname === ROUTES.UP && req.method === 'GET') return handleDiscovery(); - if (url.pathname === ROUTES.ENDPOINTS && req.method === 'GET') return handleEndpoints(); - if (url.pathname === ROUTES.TOPICS && req.method === 'GET') return handleTopics(); - if (url.pathname === ROUTES.NOTIFICATIONS && req.method === 'GET') { - return handleGetNotifications(req, url); - } - - if (url.pathname.startsWith(ROUTES.NOTIFY_PREFIX) && req.method === 'POST') { - return handleNotify(req, url); - } - return new Response(null, { status: 404 }); }, }); diff --git a/server/modules/protonmail.ts b/server/modules/protonmail.ts new file mode 100644 index 0000000..995964c --- /dev/null +++ b/server/modules/protonmail.ts @@ -0,0 +1,126 @@ +import chalk from 'chalk'; +import Imap from 'imap'; +import { + BRIDGE_IMAP_PASSWORD, + BRIDGE_IMAP_USERNAME, + ENABLE_PROTON_ANDROID, + LAUNCH_ENDPOINT_PREFIX, + PROTON_BRIDGE_HOST, + PROTON_BRIDGE_PORT, + SUP_TOPIC, + VERBOSE, +} from '../constants/config'; +import { createGroup, sendGroupMessage } from './signal'; +import { getGroupId, register } from './store'; + +const log = (...args: unknown[]) => VERBOSE && console.log(...args); + +export async function startProtonMonitor() { + if (!BRIDGE_IMAP_USERNAME || !BRIDGE_IMAP_PASSWORD) { + console.error( + chalk.red('Missing required env vars: BRIDGE_IMAP_USERNAME and BRIDGE_IMAP_PASSWORD'), + ); + console.error(chalk.yellow('Run: docker compose run --rm protonmail-bridge init')); + console.error(chalk.yellow('Then use `login` and `info` commands to get IMAP credentials')); + return; + } + + console.log( + chalk.blue(`🔗 Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`), + ); + console.log(chalk.blue(`📨 Monitoring mailbox: ${BRIDGE_IMAP_USERNAME}`)); + + const imap = new Imap({ + user: BRIDGE_IMAP_USERNAME, + password: BRIDGE_IMAP_PASSWORD, + host: PROTON_BRIDGE_HOST, + port: PROTON_BRIDGE_PORT, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + keepalive: true, + }); + + async function sendNotification(title: string, message: string) { + try { + const topicKey = `proton-${SUP_TOPIC}`; + const groupId = getGroupId(topicKey) ?? (await createGroup(SUP_TOPIC)); + + if (!getGroupId(topicKey)) { + register(topicKey, groupId, SUP_TOPIC); + } + + const prefix = ENABLE_PROTON_ANDROID + ? `${LAUNCH_ENDPOINT_PREFIX}ch.protonmail.android]\n` + : ''; + await sendGroupMessage(groupId, `${prefix}**${title}**\n${message}`); + + console.log(chalk.green(`✅ Notification sent: ${title}`)); + } catch (error) { + console.error(chalk.red('❌ Failed to send notification:'), error); + } + } + + function openInbox() { + imap.openBox('INBOX', false, (err, box) => { + if (err) { + console.error(chalk.red('Failed to open inbox:'), err); + return; + } + + log(`✅ Connected to inbox (${box.messages.total} messages)`); + + imap.on('mail', async (numNewMsgs: number) => { + log(`📬 ${numNewMsgs} new message(s) received`); + + const fetch = imap.seq.fetch(`${box.messages.total}:*`, { + bodies: 'HEADER.FIELDS (FROM SUBJECT)', + struct: true, + }); + + fetch.on('message', (msg) => { + msg.on('body', (stream) => { + let buffer = ''; + stream.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + }); + stream.once('end', () => { + const header = Imap.parseHeader(buffer); + const from = header.from?.[0] || 'Unknown sender'; + const subject = header.subject?.[0] || 'No subject'; + + sendNotification(`New Mail from ${from}`, subject); + }); + }); + }); + }); + + imap.on('update', () => { + log('📊 Mailbox updated'); + }); + }); + } + + imap.once('ready', () => { + log('✅ IMAP connection ready'); + openInbox(); + }); + + imap.once('error', (err: Error) => { + console.error(chalk.red('❌ IMAP error:'), err); + }); + + imap.once('end', () => { + log('⚠️ IMAP connection ended, reconnecting...'); + setTimeout(() => imap.connect(), 5000); + }); + + imap.connect(); + + process.on('SIGTERM', () => { + imap.end(); + }); + + process.on('SIGINT', () => { + imap.end(); + }); +} diff --git a/server/signal.ts b/server/modules/signal.ts similarity index 79% rename from server/signal.ts rename to server/modules/signal.ts index 6c7c446..ebe93c9 100644 --- a/server/signal.ts +++ b/server/modules/signal.ts @@ -1,10 +1,12 @@ +import { rm } from 'node:fs/promises'; import chalk from 'chalk'; -import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from './types/signal'; +import { DAEMON_START_MAX_ATTEMPTS, DEVICE_NAME, VERBOSE } from '../constants/config'; +import { SIGNAL_CLI_BIN, SIGNAL_CLI_DATA, SIGNAL_CLI_SOCKET } from '../constants/paths'; +import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from '../types'; -const SIGNAL_CLI_PATH = 'signal-cli/bin/signal-cli'; -const SOCKET_PATH = '/tmp/signal-cli.sock'; +// Local development uses relative path, Docker uses PATH +const SIGNAL_CLI_PATH = (await Bun.file(SIGNAL_CLI_BIN).exists()) ? SIGNAL_CLI_BIN : 'signal-cli'; const MESSAGE_DELIMITER = '\n'; -const DEVICE_NAME = 'SUP'; let account: string | null = null; let currentLinkUri: string | null = null; let rpcId = 1; @@ -30,7 +32,7 @@ async function rpcCall(method: string, params: Record) { let response = ''; Bun.connect({ - unix: SOCKET_PATH, + unix: SIGNAL_CLI_SOCKET, socket: { data(socket, data) { response += new TextDecoder().decode(data); @@ -73,7 +75,9 @@ async function rpcCall(method: string, params: Record) { } export async function generateLinkQR() { - const result = (await rpcCall('startLink', { deviceName: DEVICE_NAME })) as StartLinkResult; + const result = (await rpcCall('startLink', { + deviceName: DEVICE_NAME, + })) as StartLinkResult; const uri = result.deviceLinkUri; if (!uri) { @@ -101,9 +105,8 @@ export async function unlinkDevice() { account = null; currentLinkUri = null; - const dataPath = `${process.env.HOME}/.local/share/signal-cli/data`; try { - await Bun.spawn(['rm', '-rf', dataPath], { stdout: 'pipe' }); + await rm(SIGNAL_CLI_DATA, { recursive: true, force: true }); } catch {} } @@ -146,13 +149,14 @@ export async function hasValidAccount() { } export async function startDaemon() { - const proc = Bun.spawn([SIGNAL_CLI_PATH, 'daemon', '--socket', SOCKET_PATH], { + let authError = false; + let cleaned = false; + + const proc = Bun.spawn([SIGNAL_CLI_PATH, 'daemon', '--socket', SIGNAL_CLI_SOCKET], { stdout: 'pipe', stderr: 'pipe', }); - const verbose = Bun.env.VERBOSE === 'true'; - (async () => { for await (const chunk of proc.stderr) { const text = new TextDecoder().decode(chunk); @@ -160,9 +164,13 @@ export async function startDaemon() { if (!trimmed) continue; + if (trimmed.includes('Authorization failed') || trimmed.includes('AccountCheckException')) { + authError = true; + } + if (trimmed.includes('ConcurrentModificationException')) continue; - if (verbose) { + if (VERBOSE) { if (trimmed.includes('ERROR')) { console.error(chalk.red('[signal-cli]'), trimmed); } else if (trimmed.includes('WARN')) { @@ -182,10 +190,10 @@ export async function startDaemon() { })(); let attempts = 0; - while (attempts < 30) { + while (attempts < DAEMON_START_MAX_ATTEMPTS) { try { const socket = await Bun.connect({ - unix: SOCKET_PATH, + unix: SIGNAL_CLI_SOCKET, socket: { data() {}, error() {}, @@ -195,6 +203,14 @@ export async function startDaemon() { console.log(chalk.green('✓ signal-cli daemon started')); return proc; } catch (_error) { + if (authError && attempts > 5 && !cleaned) { + console.log(chalk.yellow('⚠ Detected stale account data, cleaning up and retrying...')); + proc.kill(); + await unlinkDevice(); + cleaned = true; + return startDaemon(); + } + await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } diff --git a/server/modules/store.ts b/server/modules/store.ts new file mode 100644 index 0000000..f6a3e3c --- /dev/null +++ b/server/modules/store.ts @@ -0,0 +1,50 @@ +import { Database } from 'bun:sqlite'; +import { SUP_DATA_DIR, SUP_DB } from '../constants/paths'; + +interface EndpointMapping { + endpoint: string; + groupId: string; + appName: string; +} + +await Bun.write(`${SUP_DATA_DIR}/.keep`, ''); + +const db = new Database(SUP_DB); + +db.run(` + CREATE TABLE IF NOT EXISTS mappings ( + endpoint TEXT PRIMARY KEY, + groupId TEXT NOT NULL, + appName TEXT NOT NULL + ) +`); + +export const register = (endpoint: string, groupId: string, appName: string) => { + db.run('INSERT OR REPLACE INTO mappings (endpoint, groupId, appName) VALUES (?, ?, ?)', [ + endpoint, + groupId, + appName, + ]); +}; + +export const getGroupId = (endpoint: string) => { + const row = db.query('SELECT groupId FROM mappings WHERE endpoint = ?').get(endpoint) as + | { groupId: string } + | undefined; + return row?.groupId; +}; + +export const getAppName = (endpoint: string) => { + const row = db.query('SELECT appName FROM mappings WHERE endpoint = ?').get(endpoint) as + | { appName: string } + | undefined; + return row?.appName; +}; + +export const getAllMappings = () => { + return db.query('SELECT endpoint, groupId, appName FROM mappings').all() as EndpointMapping[]; +}; + +export const remove = (endpoint: string) => { + db.run('DELETE FROM mappings WHERE endpoint = ?', [endpoint]); +}; diff --git a/server/unifiedpush.ts b/server/modules/unifiedpush.ts similarity index 83% rename from server/unifiedpush.ts rename to server/modules/unifiedpush.ts index 074cf4f..1b9a129 100644 --- a/server/unifiedpush.ts +++ b/server/modules/unifiedpush.ts @@ -1,3 +1,5 @@ +import { SUP_ENDPOINT_PREFIX } from '../constants/config'; + export interface UnifiedPushMessage { endpoint: string; title?: string; @@ -5,8 +7,6 @@ export interface UnifiedPushMessage { data?: Record; } -const formatUpPrefix = (endpoint: string) => `[UP:${endpoint}]`; - export const parseUnifiedPushRequest = async (req: Request) => { const url = new URL(req.url); const endpointId = url.pathname.split('/').pop() ?? ''; @@ -36,9 +36,7 @@ export const parseUnifiedPushRequest = async (req: Request) => { }; export const formatAsSignalMessage = (msg: UnifiedPushMessage) => { - const parts: string[] = []; - - parts.push(formatUpPrefix(msg.endpoint)); + const parts: string[] = [`${SUP_ENDPOINT_PREFIX}${msg.endpoint}]`]; if (msg.title) { parts.push(`**${msg.title}**`); @@ -52,5 +50,5 @@ export const formatAsSignalMessage = (msg: UnifiedPushMessage) => { parts.push(JSON.stringify(msg.data, null, 2)); } - return parts.join('\n\n') || `${formatUpPrefix(msg.endpoint)}\nEmpty notification`; + return parts.join('\n'); }; diff --git a/server/package.json b/server/package.json index d5301ad..8e5fa0d 100644 --- a/server/package.json +++ b/server/package.json @@ -7,13 +7,18 @@ "private": true, "license": "AGPL-3.0-or-later", "dependencies": { - "chalk": "^5.6.2" + "chalk": "^5.6.2", + "imap": "^0.8.19" + }, + "devDependencies": { + "@types/imap": "^0.8.43" }, "scripts": { "postinstall": "bun run ../scripts/install-signal-cli.ts || true", "dev": "bun --watch index.ts", "start": "bun run index.ts", "build": "bun build --compile index.ts --outfile sup-server", - "check": "tsc --noEmit" + "check": "tsc --noEmit && biome check .", + "fix": "biome check --write ." } } diff --git a/server/routes/health.ts b/server/routes/health.ts index 8f717de..c3dace1 100644 --- a/server/routes/health.ts +++ b/server/routes/health.ts @@ -1,4 +1,4 @@ -import { checkSignalCli, hasValidAccount } from '../signal'; +import { checkSignalCli, hasValidAccount } from '../modules/signal'; export const handleHealth = async () => { const signalOk = await checkSignalCli(); diff --git a/server/routes/link.ts b/server/routes/link.ts index 3401be9..e1dbd4d 100644 --- a/server/routes/link.ts +++ b/server/routes/link.ts @@ -1,12 +1,19 @@ +import { API_KEY } from '../constants/config'; import { CONTENT_TYPE, ROUTES, TEMPLATES } from '../constants/server'; -import { finishLink, generateLinkQR, hasValidAccount, initSignal, unlinkDevice } from '../signal'; +import { + finishLink, + generateLinkQR, + hasValidAccount, + initSignal, + unlinkDevice, +} from '../modules/signal'; export const handleLink = async () => { const linked = await hasValidAccount(); const template = linked ? TEMPLATES.LINKED : TEMPLATES.LINK; let html = await Bun.file(template).text(); - if (linked && Bun.env.API_KEY) { + if (linked && API_KEY) { const passwordField = ''; html = html.replace('{{PASSWORD_FIELD}}', passwordField); @@ -41,11 +48,10 @@ export const handleLinkStatus = async () => { }; export const handleUnlink = async (req: Request, daemon: ReturnType | null) => { - const API_KEY = Bun.env.API_KEY; - if (API_KEY) { - const formData = await req.formData(); - const password = formData.get('password'); + const body = await req.text(); + const params = new URLSearchParams(body); + const password = params.get('password'); if (password !== API_KEY) { return new Response(null, { status: 403 }); diff --git a/server/routes/notify.ts b/server/routes/notify.ts index d4fa3b9..d6e6201 100644 --- a/server/routes/notify.ts +++ b/server/routes/notify.ts @@ -1,5 +1,5 @@ -import { createGroup, sendGroupMessage } from '../signal'; -import { addNotification, getAllMappings, getGroupId, getNotifications, register } from '../store'; +import { createGroup, sendGroupMessage } from '../modules/signal'; +import { getAllMappings, getGroupId, register } from '../modules/store'; interface NotificationMessage { topic: string; @@ -18,12 +18,6 @@ const formatNotification = (notification: NotificationMessage) => { }; export const handleNotify = async (req: Request, url: URL) => { - const API_KEY = Bun.env.API_KEY; - - if (API_KEY && req.headers.get('authorization') !== `Bearer ${API_KEY}`) { - return new Response(null, { status: 401 }); - } - const topic = url.pathname.split('/')[2]; if (!topic) { return new Response('Topic required', { status: 400 }); @@ -52,12 +46,6 @@ export const handleNotify = async (req: Request, url: URL) => { const signalMessage = formatNotification(notification); await sendGroupMessage(groupId, signalMessage); - addNotification({ - topic, - title, - message: body, - }); - return Response.json({ success: true, topic, groupId }); }; @@ -72,12 +60,3 @@ export const handleTopics = () => { return Response.json({ topics }); }; - -export const handleGetNotifications = (_req: Request, url: URL) => { - const topic = url.searchParams.get('topic') || undefined; - const endpoint = url.searchParams.get('endpoint') || undefined; - - const notifications = getNotifications({ topic, endpoint }); - - return Response.json({ notifications }); -}; diff --git a/server/routes/unifiedpush.ts b/server/routes/unifiedpush.ts index 231e066..eee16f3 100644 --- a/server/routes/unifiedpush.ts +++ b/server/routes/unifiedpush.ts @@ -1,7 +1,7 @@ import { ROUTES } from '../constants/server'; -import { createGroup, sendGroupMessage } from '../signal'; -import { getAllMappings, getGroupId, register, remove } from '../store'; -import { formatAsSignalMessage, parseUnifiedPushRequest } from '../unifiedpush'; +import { createGroup, sendGroupMessage } from '../modules/signal'; +import { getAllMappings, getGroupId, register, remove } from '../modules/store'; +import { formatAsSignalMessage, parseUnifiedPushRequest } from '../modules/unifiedpush'; export const handleMatrixNotify = async (req: Request) => { const message = await parseUnifiedPushRequest(req); @@ -18,12 +18,6 @@ export const handleMatrixNotify = async (req: Request) => { }; export const handleRegister = async (req: Request, url: URL) => { - const API_KEY = Bun.env.API_KEY; - - if (API_KEY && req.headers.get('authorization') !== `Bearer ${API_KEY}`) { - return new Response(null, { status: 401 }); - } - const endpointId = url.pathname.split('/')[2] ?? ''; const { appName } = (await req.json()) as { appName: string; diff --git a/server/store.ts b/server/store.ts deleted file mode 100644 index 75d7bd9..0000000 --- a/server/store.ts +++ /dev/null @@ -1,73 +0,0 @@ -interface EndpointMapping { - endpoint: string; - groupId: string; - appName: string; -} - -interface Notification { - id: string; - topic: string; - endpoint?: string; - time: number; - title?: string; - message: string; - priority?: number; - tags?: string[]; - click?: string; -} - -const mappings = new Map(); -const notifications: Notification[] = []; -const MAX_NOTIFICATIONS = 1000; -const RETENTION_DAYS = 7; - -export const register = (endpoint: string, groupId: string, appName: string) => { - mappings.set(endpoint, { endpoint, groupId, appName }); -}; - -export const getGroupId = (endpoint: string) => mappings.get(endpoint)?.groupId; - -export const getAppName = (endpoint: string) => mappings.get(endpoint)?.appName; - -export const getAllMappings = () => Array.from(mappings.values()); - -export const remove = (endpoint: string) => { - mappings.delete(endpoint); -}; - -export const addNotification = (notification: Omit) => { - const id = crypto.randomUUID(); - const time = Date.now(); - - notifications.unshift({ - id, - time, - ...notification, - }); - - if (notifications.length > MAX_NOTIFICATIONS) { - notifications.splice(MAX_NOTIFICATIONS); - } - - const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000; - const firstOldIndex = notifications.findIndex((n) => n.time < cutoff); - if (firstOldIndex > 0) { - notifications.splice(firstOldIndex); - } - - return id; -}; - -export const getNotifications = (filter?: { topic?: string; endpoint?: string }) => { - let result = notifications; - - if (filter?.topic) { - result = result.filter((n) => n.topic === filter.topic); - } - - if (filter?.endpoint) { - result = result.filter((n) => n.endpoint === filter.endpoint); - } - - return result; -}; diff --git a/server/types/signal.ts b/server/types.d.ts similarity index 100% rename from server/types/signal.ts rename to server/types.d.ts diff --git a/server/utils/auth.ts b/server/utils/auth.ts new file mode 100644 index 0000000..10957bf --- /dev/null +++ b/server/utils/auth.ts @@ -0,0 +1,19 @@ +import { API_KEY } from '../constants/config'; + +export const checkAuth = (req: Request) => { + if (!API_KEY) return null; + + const proto = req.headers.get('x-forwarded-proto') || 'http'; + const host = req.headers.get('host') || ''; + const isLocalhost = host.startsWith('localhost') || host.startsWith('127.0.0.1'); + + if (proto !== 'https' && !isLocalhost) { + return new Response('HTTPS required when API_KEY is configured', { status: 403 }); + } + + if (req.headers.get('authorization') !== `Bearer ${API_KEY}`) { + return new Response(null, { status: 401 }); + } + + return null; +};