fuzzyworld

word-waltz / commit

added push support, by way of firebase

51cb47e736739b4e59b59351c64c88a4d7e1a2d2 · Pavel Safronov · 2026-03-25 22:21:08 -0700 · parents 8008bb36ad7e

added push support, by way of firebase
 .env.example                |   15 +
 cspell.json                 |    4 +
 firebase.json               |   12 +-
 functions/index.js          |   88 ++
 functions/package-lock.json | 2777 +++++++++++++++++++++++++++++++++++++++++++
 functions/package.json      |   15 +
 package.json                |    4 +-
 scripts/setup-push.js       |  164 +++
 src/firebase/gameService.ts |   34 +-
 src/main.ts                 |   42 +-
 src/public/sw.js            |   42 +
 src/types.ts                |   34 +-
 src/utils/notifications.ts  |  128 +-
 src/vite-env.d.ts           |    9 +
 14 files changed, 3339 insertions(+), 29 deletions(-)

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..048b625
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,15 @@
+# Copy this file to .env and fill in real values.
+# .env is gitignored — never commit real secrets.
+
+# Firebase project config (same values as src/firebase/config.ts)
+VITE_FIREBASE_API_KEY=
+VITE_FIREBASE_AUTH_DOMAIN=
+VITE_FIREBASE_PROJECT_ID=
+VITE_FIREBASE_STORAGE_BUCKET=
+VITE_FIREBASE_MESSAGING_SENDER_ID=
+VITE_FIREBASE_APP_ID=
+
+# Web Push (VAPID) public key — safe to expose in the client.
+# Generate with: npx web-push generate-vapid-keys
+# The private key goes into Firebase Functions secrets (see functions/index.js).
+VITE_VAPID_PUBLIC_KEY=
diff --git a/cspell.json b/cspell.json
index fe0c97b..f2f9255 100644
--- a/cspell.json
+++ b/cspell.json
@@ -19,7 +19,11 @@
         "QWERTYUIOP",
         "qrcode",
         "serialisation",
+        "serialised",
         "unencrypted",
+        "vapid",
+        "VAPID",
+        "webpush",
         "Wordle"
     ],
     "ignorePaths": [
diff --git a/firebase.json b/firebase.json
index 201ec0a..b66b86b 100644
--- a/firebase.json
+++ b/firebase.json
@@ -1,5 +1,15 @@
 {
     "firestore": {
         "rules": "firestore.rules"
-    }
+    },
+    "functions": [
+        {
+            "source": "functions",
+            "codebase": "default",
+            "ignore": [
+                "node_modules",
+                ".git"
+            ]
+        }
+    ]
 }
\ No newline at end of file
diff --git a/functions/index.js b/functions/index.js
new file mode 100644
index 0000000..a4bfe18
--- /dev/null
+++ b/functions/index.js
@@ -0,0 +1,88 @@
+/**
+ * Word Waltz — Firebase Cloud Functions
+ *
+ * sendPushOnTurnChange: Triggered on every Firestore write to a game document.
+ * If pendingNotification is set, it sends a Web Push message to the indicated
+ * player's subscription, then clears pendingNotification.
+ *
+ * Setup (one-time):
+ *  1. Install the Firebase CLI: npm install -g firebase-tools
+ *  2. Upgrade to the Blaze (pay-as-you-go) plan — required for Cloud Functions.
+ *  3. Generate VAPID keys:
+ *       npx web-push generate-vapid-keys
+ *  4. Store secrets in Firebase:
+ *       firebase functions:secrets:set VAPID_SUBJECT   # e.g. mailto:you@example.com
+ *       firebase functions:secrets:set VAPID_PUBLIC_KEY
+ *       firebase functions:secrets:set VAPID_PRIVATE_KEY
+ *  5. Set VITE_VAPID_PUBLIC_KEY (the same public key) in your .env and as a
+ *     GitLab CI/CD variable so the client can subscribe to push.
+ *  6. Deploy:
+ *       cd functions && npm install
+ *       firebase deploy --only functions
+ */
+
+/* eslint-disable @typescript-eslint/no-var-requires */
+const { onDocumentWritten } = require('firebase-functions/v2/firestore');
+const { defineSecret } = require('firebase-functions/params');
+const admin = require('firebase-admin');
+const webpush = require('web-push');
+
+admin.initializeApp();
+
+const VAPID_SUBJECT = defineSecret('VAPID_SUBJECT');
+const VAPID_PUBLIC_KEY = defineSecret('VAPID_PUBLIC_KEY');
+const VAPID_PRIVATE_KEY = defineSecret('VAPID_PRIVATE_KEY');
+
+exports.sendPushOnTurnChange = onDocumentWritten(
+    {
+        document: 'games/{gameId}',
+        secrets: [VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY],
+    },
+    async (event) => {
+        const after = event.data?.after;
+        if (!after?.exists) return;
+
+        const data = after.data();
+
+        // Nothing to do if there's no pending notification.
+        const notification = data.pendingNotification;
+        if (!notification || !notification.player || !notification.message) return;
+
+        const player = notification.player; // 'a' or 'b'
+        const subJson = data.pushSubs?.[player];
+        if (!subJson) return;
+
+        let subscription;
+        try {
+            subscription = JSON.parse(subJson);
+        } catch {
+            console.warn('Malformed push subscription JSON for player', player);
+            return;
+        }
+
+        webpush.setVapidDetails(
+            VAPID_SUBJECT.value(),
+            VAPID_PUBLIC_KEY.value(),
+            VAPID_PRIVATE_KEY.value(),
+        );
+
+        const payload = JSON.stringify({
+            title: 'Word Waltz',
+            body: notification.message,
+            url: notification.gameUrl ?? '', // home screen URL (no key)
+        });
+
+        try {
+            await webpush.sendNotification(subscription, payload);
+        } catch (err) {
+            // 410 Gone = subscription expired; clear it so we don't retry.
+            if (err?.statusCode === 410) {
+                await after.ref.update({ [`pushSubs.${player}`]: null });
+            }
+            console.error('Push send failed:', err?.statusCode, err?.body);
+        }
+
+        // Clear the pendingNotification so this function is not re-triggered.
+        await after.ref.update({ pendingNotification: null });
+    },
+);
diff --git a/functions/package-lock.json b/functions/package-lock.json
new file mode 100644
index 0000000..4a67565
--- /dev/null
+++ b/functions/package-lock.json
@@ -0,0 +1,2777 @@
+{
+    "name": "word-waltz-functions",
+    "version": "1.0.0",
+    "lockfileVersion": 3,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "word-waltz-functions",
+            "version": "1.0.0",
+            "dependencies": {
+                "firebase-admin": "^12.0.0",
+                "firebase-functions": "^5.0.0",
+                "web-push": "^3.6.7"
+            },
+            "engines": {
+                "node": "20"
+            }
+        },
+        "node_modules/@fastify/busboy": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz",
+            "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
+            "license": "MIT"
+        },
+        "node_modules/@firebase/app-check-interop-types": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz",
+            "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==",
+            "license": "Apache-2.0"
+        },
+        "node_modules/@firebase/app-types": {
+            "version": "0.9.2",
+            "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz",
+            "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==",
+            "license": "Apache-2.0"
+        },
+        "node_modules/@firebase/auth-interop-types": {
+            "version": "0.2.3",
+            "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz",
+            "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==",
+            "license": "Apache-2.0"
+        },
+        "node_modules/@firebase/component": {
+            "version": "0.6.9",
+            "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz",
+            "integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@firebase/util": "1.10.0",
+                "tslib": "^2.1.0"
+            }
+        },
+        "node_modules/@firebase/database": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz",
+            "integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@firebase/app-check-interop-types": "0.3.2",
+                "@firebase/auth-interop-types": "0.2.3",
+                "@firebase/component": "0.6.9",
+                "@firebase/logger": "0.4.2",
+                "@firebase/util": "1.10.0",
+                "faye-websocket": "0.11.4",
+                "tslib": "^2.1.0"
+            }
+        },
+        "node_modules/@firebase/database-compat": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz",
+            "integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@firebase/component": "0.6.9",
+                "@firebase/database": "1.0.8",
+                "@firebase/database-types": "1.0.5",
+                "@firebase/logger": "0.4.2",
+                "@firebase/util": "1.10.0",
+                "tslib": "^2.1.0"
+            }
+        },
+        "node_modules/@firebase/database-types": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz",
+            "integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@firebase/app-types": "0.9.2",
+                "@firebase/util": "1.10.0"
+            }
+        },
+        "node_modules/@firebase/logger": {
+            "version": "0.4.2",
+            "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz",
+            "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "tslib": "^2.1.0"
+            }
+        },
+        "node_modules/@firebase/util": {
+            "version": "1.10.0",
+            "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz",
+            "integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "tslib": "^2.1.0"
+            }
+        },
+        "node_modules/@google-cloud/firestore": {
+            "version": "7.11.6",
+            "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz",
+            "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "@opentelemetry/api": "^1.3.0",
+                "fast-deep-equal": "^3.1.1",
+                "functional-red-black-tree": "^1.0.1",
+                "google-gax": "^4.3.3",
+                "protobufjs": "^7.2.6"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/@google-cloud/paginator": {
+            "version": "5.0.2",
+            "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
+            "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "arrify": "^2.0.0",
+                "extend": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/@google-cloud/projectify": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
+            "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/@google-cloud/promisify": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
+            "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/@google-cloud/storage": {
+            "version": "7.19.0",
+            "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz",
+            "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "@google-cloud/paginator": "^5.0.0",
+                "@google-cloud/projectify": "^4.0.0",
+                "@google-cloud/promisify": "<4.1.0",
+                "abort-controller": "^3.0.0",
+                "async-retry": "^1.3.3",
+                "duplexify": "^4.1.3",
+                "fast-xml-parser": "^5.3.4",
+                "gaxios": "^6.0.2",
+                "google-auth-library": "^9.6.3",
+                "html-entities": "^2.5.2",
+                "mime": "^3.0.0",
+                "p-limit": "^3.0.1",
+                "retry-request": "^7.0.0",
+                "teeny-request": "^9.0.0",
+                "uuid": "^8.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/@google-cloud/storage/node_modules/uuid": {
+            "version": "8.3.2",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+            "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+            "license": "MIT",
+            "optional": true,
+            "bin": {
+                "uuid": "dist/bin/uuid"
+            }
+        },
+        "node_modules/@grpc/grpc-js": {
+            "version": "1.14.3",
+            "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
+            "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "@grpc/proto-loader": "^0.8.0",
+                "@js-sdsl/ordered-map": "^4.4.2"
+            },
+            "engines": {
+                "node": ">=12.10.0"
+            }
+        },
+        "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
+            "version": "0.8.0",
+            "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
+            "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "lodash.camelcase": "^4.3.0",
+                "long": "^5.0.0",
+                "protobufjs": "^7.5.3",
+                "yargs": "^17.7.2"
+            },
+            "bin": {
+                "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/@grpc/proto-loader": {
+            "version": "0.7.15",
+            "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+            "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "lodash.camelcase": "^4.3.0",
+                "long": "^5.0.0",
+                "protobufjs": "^7.2.5",
+                "yargs": "^17.7.2"
+            },
+            "bin": {
+                "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/@js-sdsl/ordered-map": {
+            "version": "4.4.2",
+            "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+            "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+            "license": "MIT",
+            "optional": true,
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/js-sdsl"
+            }
+        },
+        "node_modules/@opentelemetry/api": {
+            "version": "1.9.1",
+            "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+            "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "engines": {
+                "node": ">=8.0.0"
+            }
+        },
+        "node_modules/@protobufjs/aspromise": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+            "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/base64": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+            "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/codegen": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+            "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/eventemitter": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+            "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/fetch": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+            "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "@protobufjs/aspromise": "^1.1.1",
+                "@protobufjs/inquire": "^1.1.0"
+            }
+        },
+        "node_modules/@protobufjs/float": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+            "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/inquire": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+            "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/path": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+            "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/pool": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+            "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@protobufjs/utf8": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+            "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@tootallnate/once": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+            "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@types/body-parser": {
+            "version": "1.19.6",
+            "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+            "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/connect": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/caseless": {
+            "version": "0.12.5",
+            "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
+            "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/@types/connect": {
+            "version": "3.4.38",
+            "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+            "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/cors": {
+            "version": "2.8.19",
+            "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+            "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/express": {
+            "version": "4.17.3",
+            "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz",
+            "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/body-parser": "*",
+                "@types/express-serve-static-core": "*",
+                "@types/serve-static": "*"
+            }
+        },
+        "node_modules/@types/express-serve-static-core": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+            "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*",
+                "@types/qs": "*",
+                "@types/range-parser": "*",
+                "@types/send": "*"
+            }
+        },
+        "node_modules/@types/http-errors": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+            "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+            "license": "MIT"
+        },
+        "node_modules/@types/jsonwebtoken": {
+            "version": "9.0.10",
+            "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
+            "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/ms": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/long": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+            "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/@types/ms": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+            "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+            "license": "MIT"
+        },
+        "node_modules/@types/node": {
+            "version": "22.19.15",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+            "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+            "license": "MIT",
+            "dependencies": {
+                "undici-types": "~6.21.0"
+            }
+        },
+        "node_modules/@types/qs": {
+            "version": "6.15.0",
+            "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
+            "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
+            "license": "MIT"
+        },
+        "node_modules/@types/range-parser": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+            "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+            "license": "MIT"
+        },
+        "node_modules/@types/request": {
+            "version": "2.48.13",
+            "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz",
+            "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "@types/caseless": "*",
+                "@types/node": "*",
+                "@types/tough-cookie": "*",
+                "form-data": "^2.5.5"
+            }
+        },
+        "node_modules/@types/send": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+            "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/serve-static": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+            "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/http-errors": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/tough-cookie": {
+            "version": "4.0.5",
+            "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+            "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/abort-controller": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+            "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "event-target-shim": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=6.5"
+            }
+        },
+        "node_modules/accepts": {
+            "version": "1.3.8",
+            "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+            "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+            "license": "MIT",
+            "dependencies": {
+                "mime-types": "~2.1.34",
+                "negotiator": "0.6.3"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/agent-base": {
+            "version": "7.1.4",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+            "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 14"
+            }
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/array-flatten": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+            "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+            "license": "MIT"
+        },
+        "node_modules/arrify": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+            "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/asn1.js": {
+            "version": "5.4.1",
+            "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+            "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+            "license": "MIT",
+            "dependencies": {
+                "bn.js": "^4.0.0",
+                "inherits": "^2.0.1",
+                "minimalistic-assert": "^1.0.0",
+                "safer-buffer": "^2.1.0"
+            }
+        },
+        "node_modules/async-retry": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+            "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "retry": "0.13.1"
+            }
+        },
+        "node_modules/asynckit": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+            "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/base64-js": {
+            "version": "1.5.1",
+            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+            "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/bignumber.js": {
+            "version": "9.3.1",
+            "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+            "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/bn.js": {
+            "version": "4.12.3",
+            "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+            "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+            "license": "MIT"
+        },
+        "node_modules/body-parser": {
+            "version": "1.20.4",
+            "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+            "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+            "license": "MIT",
+            "dependencies": {
+                "bytes": "~3.1.2",
+                "content-type": "~1.0.5",
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "destroy": "~1.2.0",
+                "http-errors": "~2.0.1",
+                "iconv-lite": "~0.4.24",
+                "on-finished": "~2.4.1",
+                "qs": "~6.14.0",
+                "raw-body": "~2.5.3",
+                "type-is": "~1.6.18",
+                "unpipe": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8",
+                "npm": "1.2.8000 || >= 1.4.16"
+            }
+        },
+        "node_modules/buffer-equal-constant-time": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+            "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/bytes": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+            "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/call-bind-apply-helpers": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+            "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "function-bind": "^1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/call-bound": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+            "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bind-apply-helpers": "^1.0.2",
+                "get-intrinsic": "^1.3.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/cliui": {
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+            "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+            "license": "ISC",
+            "optional": true,
+            "dependencies": {
+                "string-width": "^4.2.0",
+                "strip-ansi": "^6.0.1",
+                "wrap-ansi": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/combined-stream": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+            "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "delayed-stream": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/content-disposition": {
+            "version": "0.5.4",
+            "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+            "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+            "license": "MIT",
+            "dependencies": {
+                "safe-buffer": "5.2.1"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/content-type": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+            "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/cookie": {
+            "version": "0.7.2",
+            "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+            "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/cookie-signature": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+            "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+            "license": "MIT"
+        },
+        "node_modules/cors": {
+            "version": "2.8.6",
+            "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+            "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+            "license": "MIT",
+            "dependencies": {
+                "object-assign": "^4",
+                "vary": "^1"
+            },
+            "engines": {
+                "node": ">= 0.10"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/express"
+            }
+        },
+        "node_modules/debug": {
+            "version": "2.6.9",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+            "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "2.0.0"
+            }
+        },
+        "node_modules/delayed-stream": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+            "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/depd": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+            "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/destroy": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+            "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8",
+                "npm": "1.2.8000 || >= 1.4.16"
+            }
+        },
+        "node_modules/dunder-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+            "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bind-apply-helpers": "^1.0.1",
+                "es-errors": "^1.3.0",
+                "gopd": "^1.2.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/duplexify": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
+            "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "end-of-stream": "^1.4.1",
+                "inherits": "^2.0.3",
+                "readable-stream": "^3.1.1",
+                "stream-shift": "^1.0.2"
+            }
+        },
+        "node_modules/ecdsa-sig-formatter": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+            "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "safe-buffer": "^5.0.1"
+            }
+        },
+        "node_modules/ee-first": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+            "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+            "license": "MIT"
+        },
+        "node_modules/emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/encodeurl": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+            "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/end-of-stream": {
+            "version": "1.4.5",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+            "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "once": "^1.4.0"
+            }
+        },
+        "node_modules/es-define-property": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+            "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-errors": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+            "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-object-atoms": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+            "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-set-tostringtag": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+            "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "get-intrinsic": "^1.2.6",
+                "has-tostringtag": "^1.0.2",
+                "hasown": "^2.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/escalade": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+            "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/escape-html": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+            "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+            "license": "MIT"
+        },
+        "node_modules/etag": {
+            "version": "1.8.1",
+            "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+            "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/event-target-shim": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+            "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/express": {
+            "version": "4.22.1",
+            "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+            "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+            "license": "MIT",
+            "dependencies": {
+                "accepts": "~1.3.8",
+                "array-flatten": "1.1.1",
+                "body-parser": "~1.20.3",
+                "content-disposition": "~0.5.4",
+                "content-type": "~1.0.4",
+                "cookie": "~0.7.1",
+                "cookie-signature": "~1.0.6",
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "etag": "~1.8.1",
+                "finalhandler": "~1.3.1",
+                "fresh": "~0.5.2",
+                "http-errors": "~2.0.0",
+                "merge-descriptors": "1.0.3",
+                "methods": "~1.1.2",
+                "on-finished": "~2.4.1",
+                "parseurl": "~1.3.3",
+                "path-to-regexp": "~0.1.12",
+                "proxy-addr": "~2.0.7",
+                "qs": "~6.14.0",
+                "range-parser": "~1.2.1",
+                "safe-buffer": "5.2.1",
+                "send": "~0.19.0",
+                "serve-static": "~1.16.2",
+                "setprototypeof": "1.2.0",
+                "statuses": "~2.0.1",
+                "type-is": "~1.6.18",
+                "utils-merge": "1.0.1",
+                "vary": "~1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.10.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/express"
+            }
+        },
+        "node_modules/extend": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+            "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/farmhash-modern": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz",
+            "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=18.0.0"
+            }
+        },
+        "node_modules/fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/fast-xml-builder": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
+            "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/NaturalIntelligence"
+                }
+            ],
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "path-expression-matcher": "^1.1.3"
+            }
+        },
+        "node_modules/fast-xml-parser": {
+            "version": "5.5.9",
+            "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz",
+            "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/NaturalIntelligence"
+                }
+            ],
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "fast-xml-builder": "^1.1.4",
+                "path-expression-matcher": "^1.2.0",
+                "strnum": "^2.2.2"
+            },
+            "bin": {
+                "fxparser": "src/cli/cli.js"
+            }
+        },
+        "node_modules/faye-websocket": {
+            "version": "0.11.4",
+            "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+            "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "websocket-driver": ">=0.5.1"
+            },
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
+        "node_modules/finalhandler": {
+            "version": "1.3.2",
+            "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+            "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+            "license": "MIT",
+            "dependencies": {
+                "debug": "2.6.9",
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "on-finished": "~2.4.1",
+                "parseurl": "~1.3.3",
+                "statuses": "~2.0.2",
+                "unpipe": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/firebase-admin": {
+            "version": "12.7.0",
+            "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz",
+            "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@fastify/busboy": "^3.0.0",
+                "@firebase/database-compat": "1.0.8",
+                "@firebase/database-types": "1.0.5",
+                "@types/node": "^22.0.1",
+                "farmhash-modern": "^1.1.0",
+                "jsonwebtoken": "^9.0.0",
+                "jwks-rsa": "^3.1.0",
+                "node-forge": "^1.3.1",
+                "uuid": "^10.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "optionalDependencies": {
+                "@google-cloud/firestore": "^7.7.0",
+                "@google-cloud/storage": "^7.7.0"
+            }
+        },
+        "node_modules/firebase-functions": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz",
+            "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/cors": "^2.8.5",
+                "@types/express": "4.17.3",
+                "cors": "^2.8.5",
+                "express": "^4.17.1",
+                "protobufjs": "^7.2.2"
+            },
+            "bin": {
+                "firebase-functions": "lib/bin/firebase-functions.js"
+            },
+            "engines": {
+                "node": ">=14.10.0"
+            },
+            "peerDependencies": {
+                "firebase-admin": "^11.10.0 || ^12.0.0"
+            }
+        },
+        "node_modules/form-data": {
+            "version": "2.5.5",
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
+            "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "asynckit": "^0.4.0",
+                "combined-stream": "^1.0.8",
+                "es-set-tostringtag": "^2.1.0",
+                "hasown": "^2.0.2",
+                "mime-types": "^2.1.35",
+                "safe-buffer": "^5.2.1"
+            },
+            "engines": {
+                "node": ">= 0.12"
+            }
+        },
+        "node_modules/forwarded": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+            "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/fresh": {
+            "version": "0.5.2",
+            "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+            "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/function-bind": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+            "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/functional-red-black-tree": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+            "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/gaxios": {
+            "version": "6.7.1",
+            "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+            "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "extend": "^3.0.2",
+                "https-proxy-agent": "^7.0.1",
+                "is-stream": "^2.0.0",
+                "node-fetch": "^2.6.9",
+                "uuid": "^9.0.1"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/gaxios/node_modules/uuid": {
+            "version": "9.0.1",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+            "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+            "funding": [
+                "https://github.com/sponsors/broofa",
+                "https://github.com/sponsors/ctavan"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "bin": {
+                "uuid": "dist/bin/uuid"
+            }
+        },
+        "node_modules/gcp-metadata": {
+            "version": "6.1.1",
+            "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
+            "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "gaxios": "^6.1.1",
+                "google-logging-utils": "^0.0.2",
+                "json-bigint": "^1.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/get-caller-file": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+            "license": "ISC",
+            "optional": true,
+            "engines": {
+                "node": "6.* || 8.* || >= 10.*"
+            }
+        },
+        "node_modules/get-intrinsic": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+            "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bind-apply-helpers": "^1.0.2",
+                "es-define-property": "^1.0.1",
+                "es-errors": "^1.3.0",
+                "es-object-atoms": "^1.1.1",
+                "function-bind": "^1.1.2",
+                "get-proto": "^1.0.1",
+                "gopd": "^1.2.0",
+                "has-symbols": "^1.1.0",
+                "hasown": "^2.0.2",
+                "math-intrinsics": "^1.1.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+            "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+            "license": "MIT",
+            "dependencies": {
+                "dunder-proto": "^1.0.1",
+                "es-object-atoms": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/google-auth-library": {
+            "version": "9.15.1",
+            "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
+            "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "base64-js": "^1.3.0",
+                "ecdsa-sig-formatter": "^1.0.11",
+                "gaxios": "^6.1.1",
+                "gcp-metadata": "^6.1.0",
+                "gtoken": "^7.0.0",
+                "jws": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/google-gax": {
+            "version": "4.6.1",
+            "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz",
+            "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "@grpc/grpc-js": "^1.10.9",
+                "@grpc/proto-loader": "^0.7.13",
+                "@types/long": "^4.0.0",
+                "abort-controller": "^3.0.0",
+                "duplexify": "^4.0.0",
+                "google-auth-library": "^9.3.0",
+                "node-fetch": "^2.7.0",
+                "object-hash": "^3.0.0",
+                "proto3-json-serializer": "^2.0.2",
+                "protobufjs": "^7.3.2",
+                "retry-request": "^7.0.0",
+                "uuid": "^9.0.1"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/google-gax/node_modules/uuid": {
+            "version": "9.0.1",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+            "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+            "funding": [
+                "https://github.com/sponsors/broofa",
+                "https://github.com/sponsors/ctavan"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "bin": {
+                "uuid": "dist/bin/uuid"
+            }
+        },
+        "node_modules/google-logging-utils": {
+            "version": "0.0.2",
+            "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
+            "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/gopd": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+            "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/gtoken": {
+            "version": "7.1.0",
+            "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+            "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "gaxios": "^6.0.0",
+                "jws": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/has-symbols": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+            "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-tostringtag": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+            "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "has-symbols": "^1.0.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/hasown": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+            "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "function-bind": "^1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/html-entities": {
+            "version": "2.6.0",
+            "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
+            "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/mdevils"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://patreon.com/mdevils"
+                }
+            ],
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/http_ece": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
+            "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=16"
+            }
+        },
+        "node_modules/http-errors": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+            "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+            "license": "MIT",
+            "dependencies": {
+                "depd": "~2.0.0",
+                "inherits": "~2.0.4",
+                "setprototypeof": "~1.2.0",
+                "statuses": "~2.0.2",
+                "toidentifier": "~1.0.1"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/express"
+            }
+        },
+        "node_modules/http-parser-js": {
+            "version": "0.5.10",
+            "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+            "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+            "license": "MIT"
+        },
+        "node_modules/http-proxy-agent": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+            "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "@tootallnate/once": "2",
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/http-proxy-agent/node_modules/agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
+        },
+        "node_modules/http-proxy-agent/node_modules/debug": {
+            "version": "4.4.3",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+            "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/http-proxy-agent/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/https-proxy-agent": {
+            "version": "7.0.6",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+            "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+            "license": "MIT",
+            "dependencies": {
+                "agent-base": "^7.1.2",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 14"
+            }
+        },
+        "node_modules/https-proxy-agent/node_modules/debug": {
+            "version": "4.4.3",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+            "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/https-proxy-agent/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "license": "MIT"
+        },
+        "node_modules/iconv-lite": {
+            "version": "0.4.24",
+            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+            "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+            "license": "MIT",
+            "dependencies": {
+                "safer-buffer": ">= 2.1.2 < 3"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+            "license": "ISC"
+        },
+        "node_modules/ipaddr.js": {
+            "version": "1.9.1",
+            "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+            "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.10"
+            }
+        },
+        "node_modules/is-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-stream": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/jose": {
+            "version": "4.15.9",
+            "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+            "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/panva"
+            }
+        },
+        "node_modules/json-bigint": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+            "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "bignumber.js": "^9.0.0"
+            }
+        },
+        "node_modules/jsonwebtoken": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+            "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+            "license": "MIT",
+            "dependencies": {
+                "jws": "^4.0.1",
+                "lodash.includes": "^4.3.0",
+                "lodash.isboolean": "^3.0.3",
+                "lodash.isinteger": "^4.0.4",
+                "lodash.isnumber": "^3.0.3",
+                "lodash.isplainobject": "^4.0.6",
+                "lodash.isstring": "^4.0.1",
+                "lodash.once": "^4.0.0",
+                "ms": "^2.1.1",
+                "semver": "^7.5.4"
+            },
+            "engines": {
+                "node": ">=12",
+                "npm": ">=6"
+            }
+        },
+        "node_modules/jsonwebtoken/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "license": "MIT"
+        },
+        "node_modules/jwa": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+            "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+            "license": "MIT",
+            "dependencies": {
+                "buffer-equal-constant-time": "^1.0.1",
+                "ecdsa-sig-formatter": "1.0.11",
+                "safe-buffer": "^5.0.1"
+            }
+        },
+        "node_modules/jwks-rsa": {
+            "version": "3.2.2",
+            "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz",
+            "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/jsonwebtoken": "^9.0.4",
+                "debug": "^4.3.4",
+                "jose": "^4.15.4",
+                "limiter": "^1.1.5",
+                "lru-memoizer": "^2.2.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/jwks-rsa/node_modules/debug": {
+            "version": "4.4.3",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+            "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/jwks-rsa/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "license": "MIT"
+        },
+        "node_modules/jws": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+            "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+            "license": "MIT",
+            "dependencies": {
+                "jwa": "^2.0.1",
+                "safe-buffer": "^5.0.1"
+            }
+        },
+        "node_modules/limiter": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+            "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
+        },
+        "node_modules/lodash.camelcase": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+            "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/lodash.clonedeep": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+            "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.includes": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+            "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.isboolean": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+            "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.isinteger": {
+            "version": "4.0.4",
+            "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+            "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.isnumber": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+            "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.isplainobject": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+            "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.isstring": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+            "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+            "license": "MIT"
+        },
+        "node_modules/lodash.once": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+            "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+            "license": "MIT"
+        },
+        "node_modules/long": {
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+            "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+            "license": "Apache-2.0"
+        },
+        "node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "license": "ISC",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/lru-memoizer": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
+            "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
+            "license": "MIT",
+            "dependencies": {
+                "lodash.clonedeep": "^4.5.0",
+                "lru-cache": "6.0.0"
+            }
+        },
+        "node_modules/math-intrinsics": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+            "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/media-typer": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+            "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/merge-descriptors": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+            "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/methods": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+            "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+            "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+            "license": "MIT",
+            "optional": true,
+            "bin": {
+                "mime": "cli.js"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            }
+        },
+        "node_modules/mime-db": {
+            "version": "1.52.0",
+            "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+            "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime-types": {
+            "version": "2.1.35",
+            "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+            "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+            "license": "MIT",
+            "dependencies": {
+                "mime-db": "1.52.0"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/minimalistic-assert": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+            "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+            "license": "ISC"
+        },
+        "node_modules/minimist": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+            "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/ms": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+            "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+            "license": "MIT"
+        },
+        "node_modules/negotiator": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+            "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/node-fetch": {
+            "version": "2.7.0",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+            "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "whatwg-url": "^5.0.0"
+            },
+            "engines": {
+                "node": "4.x || >=6.0.0"
+            },
+            "peerDependencies": {
+                "encoding": "^0.1.0"
+            },
+            "peerDependenciesMeta": {
+                "encoding": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/node-forge": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
+            "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
+            "license": "(BSD-3-Clause OR GPL-2.0)",
+            "engines": {
+                "node": ">= 6.13.0"
+            }
+        },
+        "node_modules/object-assign": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+            "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/object-hash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+            "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/object-inspect": {
+            "version": "1.13.4",
+            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+            "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/on-finished": {
+            "version": "2.4.1",
+            "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+            "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+            "license": "MIT",
+            "dependencies": {
+                "ee-first": "1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+            "license": "ISC",
+            "optional": true,
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/p-limit": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "yocto-queue": "^0.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/parseurl": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+            "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/path-expression-matcher": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
+            "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/NaturalIntelligence"
+                }
+            ],
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/path-to-regexp": {
+            "version": "0.1.12",
+            "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+            "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+            "license": "MIT"
+        },
+        "node_modules/proto3-json-serializer": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
+            "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "protobufjs": "^7.2.5"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/protobufjs": {
+            "version": "7.5.4",
+            "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+            "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+            "hasInstallScript": true,
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "@protobufjs/aspromise": "^1.1.2",
+                "@protobufjs/base64": "^1.1.2",
+                "@protobufjs/codegen": "^2.0.4",
+                "@protobufjs/eventemitter": "^1.1.0",
+                "@protobufjs/fetch": "^1.1.0",
+                "@protobufjs/float": "^1.0.2",
+                "@protobufjs/inquire": "^1.1.0",
+                "@protobufjs/path": "^1.1.2",
+                "@protobufjs/pool": "^1.1.0",
+                "@protobufjs/utf8": "^1.1.0",
+                "@types/node": ">=13.7.0",
+                "long": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/proxy-addr": {
+            "version": "2.0.7",
+            "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+            "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+            "license": "MIT",
+            "dependencies": {
+                "forwarded": "0.2.0",
+                "ipaddr.js": "1.9.1"
+            },
+            "engines": {
+                "node": ">= 0.10"
+            }
+        },
+        "node_modules/qs": {
+            "version": "6.14.2",
+            "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+            "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "side-channel": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=0.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/range-parser": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+            "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/raw-body": {
+            "version": "2.5.3",
+            "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+            "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+            "license": "MIT",
+            "dependencies": {
+                "bytes": "~3.1.2",
+                "http-errors": "~2.0.1",
+                "iconv-lite": "~0.4.24",
+                "unpipe": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/require-directory": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+            "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/retry": {
+            "version": "0.13.1",
+            "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+            "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">= 4"
+            }
+        },
+        "node_modules/retry-request": {
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
+            "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "@types/request": "^2.48.8",
+                "extend": "^3.0.2",
+                "teeny-request": "^9.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "license": "MIT"
+        },
+        "node_modules/safer-buffer": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+            "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+            "license": "MIT"
+        },
+        "node_modules/semver": {
+            "version": "7.7.4",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+            "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/send": {
+            "version": "0.19.2",
+            "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+            "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+            "license": "MIT",
+            "dependencies": {
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "destroy": "1.2.0",
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "etag": "~1.8.1",
+                "fresh": "~0.5.2",
+                "http-errors": "~2.0.1",
+                "mime": "1.6.0",
+                "ms": "2.1.3",
+                "on-finished": "~2.4.1",
+                "range-parser": "~1.2.1",
+                "statuses": "~2.0.2"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/send/node_modules/mime": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+            "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+            "license": "MIT",
+            "bin": {
+                "mime": "cli.js"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/send/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "license": "MIT"
+        },
+        "node_modules/serve-static": {
+            "version": "1.16.3",
+            "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+            "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+            "license": "MIT",
+            "dependencies": {
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "parseurl": "~1.3.3",
+                "send": "~0.19.1"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/setprototypeof": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+            "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+            "license": "ISC"
+        },
+        "node_modules/side-channel": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+            "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "object-inspect": "^1.13.3",
+                "side-channel-list": "^1.0.0",
+                "side-channel-map": "^1.0.1",
+                "side-channel-weakmap": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/side-channel-list": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+            "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "object-inspect": "^1.13.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/side-channel-map": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+            "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bound": "^1.0.2",
+                "es-errors": "^1.3.0",
+                "get-intrinsic": "^1.2.5",
+                "object-inspect": "^1.13.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/side-channel-weakmap": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+            "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bound": "^1.0.2",
+                "es-errors": "^1.3.0",
+                "get-intrinsic": "^1.2.5",
+                "object-inspect": "^1.13.3",
+                "side-channel-map": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/statuses": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+            "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/stream-events": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+            "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "stubs": "^3.0.0"
+            }
+        },
+        "node_modules/stream-shift": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+            "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "node_modules/string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strnum": {
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
+            "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/NaturalIntelligence"
+                }
+            ],
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/stubs": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+            "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/teeny-request": {
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
+            "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
+            "license": "Apache-2.0",
+            "optional": true,
+            "dependencies": {
+                "http-proxy-agent": "^5.0.0",
+                "https-proxy-agent": "^5.0.0",
+                "node-fetch": "^2.6.9",
+                "stream-events": "^1.0.5",
+                "uuid": "^9.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
+        "node_modules/teeny-request/node_modules/agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
+        },
+        "node_modules/teeny-request/node_modules/debug": {
+            "version": "4.4.3",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+            "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/teeny-request/node_modules/https-proxy-agent": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/teeny-request/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/teeny-request/node_modules/uuid": {
+            "version": "9.0.1",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+            "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+            "funding": [
+                "https://github.com/sponsors/broofa",
+                "https://github.com/sponsors/ctavan"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "bin": {
+                "uuid": "dist/bin/uuid"
+            }
+        },
+        "node_modules/toidentifier": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+            "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.6"
+            }
+        },
+        "node_modules/tr46": {
+            "version": "0.0.3",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+            "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/tslib": {
+            "version": "2.8.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+            "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+            "license": "0BSD"
+        },
+        "node_modules/type-is": {
+            "version": "1.6.18",
+            "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+            "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+            "license": "MIT",
+            "dependencies": {
+                "media-typer": "0.3.0",
+                "mime-types": "~2.1.24"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/undici-types": {
+            "version": "6.21.0",
+            "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+            "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+            "license": "MIT"
+        },
+        "node_modules/unpipe": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+            "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+            "license": "MIT",
+            "optional": true
+        },
+        "node_modules/utils-merge": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+            "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4.0"
+            }
+        },
+        "node_modules/uuid": {
+            "version": "10.0.0",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+            "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+            "funding": [
+                "https://github.com/sponsors/broofa",
+                "https://github.com/sponsors/ctavan"
+            ],
+            "license": "MIT",
+            "bin": {
+                "uuid": "dist/bin/uuid"
+            }
+        },
+        "node_modules/vary": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+            "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/web-push": {
+            "version": "3.6.7",
+            "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
+            "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
+            "license": "MPL-2.0",
+            "dependencies": {
+                "asn1.js": "^5.3.0",
+                "http_ece": "1.2.0",
+                "https-proxy-agent": "^7.0.0",
+                "jws": "^4.0.0",
+                "minimist": "^1.2.5"
+            },
+            "bin": {
+                "web-push": "src/cli.js"
+            },
+            "engines": {
+                "node": ">= 16"
+            }
+        },
+        "node_modules/webidl-conversions": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+            "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+            "license": "BSD-2-Clause",
+            "optional": true
+        },
+        "node_modules/websocket-driver": {
+            "version": "0.7.4",
+            "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+            "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "http-parser-js": ">=0.5.1",
+                "safe-buffer": ">=5.1.0",
+                "websocket-extensions": ">=0.1.1"
+            },
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
+        "node_modules/websocket-extensions": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+            "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+            "license": "Apache-2.0",
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
+        "node_modules/whatwg-url": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+            "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "tr46": "~0.0.3",
+                "webidl-conversions": "^3.0.0"
+            }
+        },
+        "node_modules/wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+            "license": "ISC",
+            "optional": true
+        },
+        "node_modules/y18n": {
+            "version": "5.0.8",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+            "license": "ISC",
+            "optional": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+            "license": "ISC"
+        },
+        "node_modules/yargs": {
+            "version": "17.7.2",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+            "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "cliui": "^8.0.1",
+                "escalade": "^3.1.1",
+                "get-caller-file": "^2.0.5",
+                "require-directory": "^2.1.1",
+                "string-width": "^4.2.3",
+                "y18n": "^5.0.5",
+                "yargs-parser": "^21.1.1"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/yargs-parser": {
+            "version": "21.1.1",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+            "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+            "license": "ISC",
+            "optional": true,
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/yocto-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        }
+    }
+}
diff --git a/functions/package.json b/functions/package.json
new file mode 100644
index 0000000..a65d91d
--- /dev/null
+++ b/functions/package.json
@@ -0,0 +1,15 @@
+{
+    "name": "word-waltz-functions",
+    "version": "1.0.0",
+    "description": "Firebase Cloud Functions for Word Waltz push notifications",
+    "main": "index.js",
+    "engines": {
+        "node": "20"
+    },
+    "dependencies": {
+        "firebase-admin": "^12.0.0",
+        "firebase-functions": "^5.0.0",
+        "web-push": "^3.6.7"
+    },
+    "private": true
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 3e7c7ff..baa42b6 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
     "prepare": "git config core.hooksPath .githooks || true",
     "generate-assets": "tsx scripts/generate-assets.ts",
     "typecheck": "tsc --noEmit",
-    "spell": "cspell \"**\" --no-progress"
+    "spell": "cspell \"**\" --no-progress",
+    "setup-push": "node scripts/setup-push.js",
+    "deploy-functions": "cd functions && npm install && cd .. && npx --package=firebase-tools firebase --project word-waltz deploy --only functions"
   },
   "dependencies": {
     "firebase": "^10.12.0",
diff --git a/scripts/setup-push.js b/scripts/setup-push.js
new file mode 100644
index 0000000..db76238
--- /dev/null
+++ b/scripts/setup-push.js
@@ -0,0 +1,164 @@
+#!/usr/bin/env node
+/**
+ * scripts/setup-push.js — one-time Web Push / VAPID key setup
+ *
+ * Generates a VAPID key pair, writes the public key to .env, and stores all
+ * three secrets in Firebase Secret Manager. Run once (or to rotate keys).
+ * After running, deploy the Cloud Function with: npm run deploy-functions
+ *
+ * Prerequisites:
+ *  - firebase login
+ *  - Project must be on the Blaze plan
+ */
+
+import { execSync } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import readline from 'readline';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(__dirname, '..');
+const ENV_FILE = path.join(ROOT, '.env');
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function ask(question) {
+    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+    return new Promise((resolve) => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
+}
+
+/** Read .env as a key=value map (ignores comments and blanks). */
+function readEnv() {
+    if (!fs.existsSync(ENV_FILE)) return {};
+    const lines = fs.readFileSync(ENV_FILE, 'utf8').split('\n');
+    const map = {};
+    for (const line of lines) {
+        const trimmed = line.trim();
+        if (!trimmed || trimmed.startsWith('#')) continue;
+        const idx = trimmed.indexOf('=');
+        if (idx === -1) continue;
+        map[trimmed.slice(0, idx)] = trimmed.slice(idx + 1);
+    }
+    return map;
+}
+
+/** Write a single key=value into .env, updating it if the key already exists. */
+function writeEnvKey(key, value) {
+    let content = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
+    const regex = new RegExp(`^${key}=.*$`, 'm');
+    if (regex.test(content)) {
+        content = content.replace(regex, `${key}=${value}`);
+    } else {
+        content += (content.endsWith('\n') ? '' : '\n') + `${key}=${value}\n`;
+    }
+    fs.writeFileSync(ENV_FILE, content);
+    console.log(`  ✓ Written ${key} to .env`);
+}
+
+/** Set a Firebase Functions secret non-interactively by piping the value. */
+function setFirebaseSecret(name, value) {
+    console.log(`\n$ firebase functions:secrets:set ${name}  [piped]`);
+    execSync(`npx --package=firebase-tools firebase --project word-waltz functions:secrets:set ${name}`, {
+        input: value,
+        stdio: ['pipe', 'inherit', 'inherit'],
+        cwd: ROOT,
+    });
+}
+
+// ---------------------------------------------------------------------------
+// Main
+// ---------------------------------------------------------------------------
+
+async function main() {
+    console.log('=== Word Waltz — Web Push setup ===\n');
+
+    // ---- 1. Generate VAPID keys ----
+    console.log('Step 1: Generating VAPID key pair…');
+    // We need web-push available. Try the local functions/ installation first,
+    // fall back to npx.
+    let generateKeys;
+    try {
+        const wpPath = path.join(ROOT, 'functions', 'node_modules', 'web-push', 'src', 'vapid-helper.js');
+        if (fs.existsSync(wpPath)) {
+            const { generateVAPIDKeys } = await import(wpPath);
+            generateKeys = generateVAPIDKeys;
+        }
+    } catch {
+        // will fall back to npx
+    }
+
+    let publicKey, privateKey;
+    if (generateKeys) {
+        ({ publicKey, privateKey } = generateKeys());
+    } else {
+        // Use npx web-push and parse stdout
+        const output = execSync('npx --yes web-push generate-vapid-keys --json', {
+            cwd: ROOT,
+            encoding: 'utf8',
+        });
+        try {
+            const parsed = JSON.parse(output);
+            publicKey = parsed.publicKey;
+            privateKey = parsed.privateKey;
+        } catch {
+            // Older web-push versions print human-readable text
+            const pubMatch = output.match(/Public Key:\s*\n?\s*(\S+)/);
+            const privMatch = output.match(/Private Key:\s*\n?\s*(\S+)/);
+            if (!pubMatch || !privMatch) {
+                console.error('Could not parse web-push output:\n', output);
+                process.exit(1);
+            }
+            publicKey = pubMatch[1];
+            privateKey = privMatch[1];
+        }
+    }
+
+    console.log(`  Public key : ${publicKey}`);
+    console.log(`  Private key: ${privateKey.slice(0, 6)}… (kept secret)`);
+
+    // ---- 2. Write public key to .env ----
+    console.log('\nStep 2: Writing public key to .env…');
+    const existing = readEnv();
+    if (existing['VITE_VAPID_PUBLIC_KEY'] && existing['VITE_VAPID_PUBLIC_KEY'] !== publicKey) {
+        const overwrite = await ask(
+            `  .env already has a VAPID public key. Overwrite? [y/N] `,
+        );
+        if (!overwrite.toLowerCase().startsWith('y')) {
+            console.log('  Skipped — keeping existing key. Re-run if you want fresh keys.');
+            process.exit(0);
+        }
+    }
+    writeEnvKey('VITE_VAPID_PUBLIC_KEY', publicKey);
+
+    // Also update .env.example placeholder without the real value
+    const exampleFile = path.join(ROOT, '.env.example');
+    if (fs.existsSync(exampleFile)) {
+        let ex = fs.readFileSync(exampleFile, 'utf8');
+        ex = ex.replace(/^VITE_VAPID_PUBLIC_KEY=.*$/m, 'VITE_VAPID_PUBLIC_KEY=');
+        fs.writeFileSync(exampleFile, ex);
+        console.log('  ✓ Cleared VITE_VAPID_PUBLIC_KEY in .env.example');
+    }
+
+    // ---- 3. Set Firebase Functions secrets ----
+    console.log('\nStep 3: Setting Firebase Functions secrets…');
+    const subject = 'mailto:pavel@fuzzyworld.net';
+
+    setFirebaseSecret('VAPID_SUBJECT', subject);
+    setFirebaseSecret('VAPID_PUBLIC_KEY', publicKey);
+    setFirebaseSecret('VAPID_PRIVATE_KEY', privateKey);
+    console.log('  ✓ All three secrets stored in Firebase.');
+
+    console.log('\n✅  VAPID setup complete!');
+    console.log('   Next steps:');
+    console.log('   1. Run: npm run deploy-functions');
+    console.log('   2. Set this as a masked CI/CD variable in the word-waltz GitLab project:');
+    console.log(`\n      VITE_VAPID_PUBLIC_KEY=${publicKey}\n`);
+}
+
+main().catch((err) => {
+    console.error('\n❌  Setup failed:', err.message ?? err);
+    process.exit(1);
+});
diff --git a/src/firebase/gameService.ts b/src/firebase/gameService.ts
index e987d6f..225de18 100644
--- a/src/firebase/gameService.ts
+++ b/src/firebase/gameService.ts
@@ -9,13 +9,17 @@
  * This is the ONLY plaintext data in the document and is used solely for
  * automated cleanup of stale games. It reveals no game content.
  *
+ * Push notification fields (pushSubs, pendingNotification) are also plaintext
+ * and are described in types.ts. They reveal no game content — only that a
+ * player is subscribed to notifications and that it is someone's turn.
+ *
  * None of these functions know about encryption — that is strictly the
  * responsibility of the layer above (main.ts / route handlers).
  */
 
 import { doc, getDoc, setDoc, updateDoc, deleteDoc, onSnapshot } from 'firebase/firestore';
 import { db } from './config.js';
-import type { Encrypted, EncryptedEnvelope } from '../types.js';
+import type { Encrypted, EncryptedEnvelope, PendingNotification, PlayerId } from '../types.js';
 
 /** Create a new game document. */
 export async function createGame(gameId: string, encrypted: Encrypted): Promise<void> {
@@ -33,9 +37,33 @@ export async function getGame(gameId: string): Promise<EncryptedEnvelope | null>
 /**
  * Overwrite the game document with new encrypted state.
  * Always updates lastPlayed to the current time.
+ * Optionally writes a pendingNotification for the Cloud Function to consume.
+ */
+export async function updateGame(
+  gameId: string,
+  encrypted: Encrypted,
+  pendingNotification?: PendingNotification | null,
+): Promise<void> {
+  const update: Partial<EncryptedEnvelope> = {
+    ...encrypted,
+    lastPlayed: Date.now(),
+    pendingNotification: pendingNotification ?? null,
+  };
+  await updateDoc(doc(db, 'games', gameId), update);
+}
+
+/**
+ * Store or clear this player's push subscription JSON in the game document.
+ * This is a partial update — it does not touch the encrypted game state.
  */
-export async function updateGame(gameId: string, encrypted: Encrypted): Promise<void> {
-  await updateDoc(doc(db, 'games', gameId), { ...encrypted, lastPlayed: Date.now() });
+export async function storePushSubscription(
+  gameId: string,
+  player: PlayerId,
+  subscriptionJson: string | null,
+): Promise<void> {
+  await updateDoc(doc(db, 'games', gameId), {
+    [`pushSubs.${player}`]: subscriptionJson,
+  });
 }
 
 /** Subscribe to real-time updates for a game document. */
diff --git a/src/main.ts b/src/main.ts
index 7648150..c99d796 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,7 +10,7 @@
 
 import { parseLink } from './utils/shareLink.js';
 import { generateKey, exportKey, importKey, encrypt, decrypt } from './crypto/encryption.js';
-import { createGame, getGame, updateGame, subscribeToGame, deleteGame } from './firebase/gameService.js';
+import { createGame, getGame, updateGame, subscribeToGame, deleteGame, storePushSubscription } from './firebase/gameService.js';
 import { getStoredGames, upsertStoredGame, removeStoredGame, clearStoredGames } from './utils/gameRegistry.js';
 import { buildLink } from './utils/shareLink.js';
 import { mountWordPicker } from './components/WordPicker.js';
@@ -18,9 +18,9 @@ import { mountWordleBoard } from './components/WordleBoard.js';
 import { mountScoreboard } from './components/Scoreboard.js';
 import { mountSoloScoreboard } from './components/SoloScoreboard.js';
 import { mountShareDialog } from './components/ShareDialog.js';
-import { notifyMyTurn, clearNotification } from './utils/notifications.js';
+import { notifyMyTurn, clearNotification, registerServiceWorker, subscribeToPush } from './utils/notifications.js';
 import { getRandomWord } from './utils/wordList.js';
-import type { GameState, PlayerId, Round, SoloGameState, SoloRound } from './types.js';
+import type { GameState, PlayerId, Round, SoloGameState, SoloRound, PendingNotification } from './types.js';
 
 // ---------------------------------------------------------------------------
 // Waiting status helper
@@ -102,9 +102,14 @@ function navigateToGame(gameId: string, key: string): void {
 // Firestore write helper
 // ---------------------------------------------------------------------------
 
-async function saveState(gameId: string, key: CryptoKey, state: GameState): Promise<void> {
+async function saveState(
+    gameId: string,
+    key: CryptoKey,
+    state: GameState,
+    pendingNotification?: PendingNotification | null,
+): Promise<void> {
     const envelope = await encrypt(JSON.stringify(state), key);
-    await updateGame(gameId, envelope);
+    await updateGame(gameId, envelope, pendingNotification);
 }
 
 // ---------------------------------------------------------------------------
@@ -178,6 +183,7 @@ function render(
         const challenger = nextChallenger(state);
 
         if (challenger === myRole) {
+            const solver: PlayerId = myRole === 'a' ? 'b' : 'a';
             cleanupMain = mountWordPicker(mainEl, {
                 onWordChosen: async (word: string) => {
                     const newRound: Round = {
@@ -187,13 +193,18 @@ function render(
                         solved: false,
                         score: 0,
                     };
+                    const notify: PendingNotification = {
+                        player: solver,
+                        message: `${state.players[myRole].name || 'Your opponent'} challenged you — your turn to solve!`,
+                        gameUrl: shareUrl.split('#')[0],
+                    };
                     await saveState(gameId, key, {
                         ...state,
                         rounds: [...state.rounds, newRound],
                         currentRound: state.rounds.length,
                         turn: 'solve',
                         updatedAt: Date.now(),
-                    });
+                    }, notify);
                 },
             });
         } else {
@@ -686,7 +697,12 @@ async function bootstrap(): Promise<void> {
                         updatedAt: Date.now(),
                     };
                     const updatedEnvelope = await encrypt(JSON.stringify(updated), cryptoKey);
-                    await updateGame(gameId, updatedEnvelope);
+                    const joinNotify: PendingNotification = {
+                        player: 'a',
+                        message: `${name} has joined! Pick a word to challenge them.`,
+                        gameUrl: buildLink(gameId, key).split('#')[0],
+                    };
+                    await updateGame(gameId, updatedEnvelope, joinNotify);
                     state = updated;
                     setRole(gameId, 'b');
                     myRole = 'b';
@@ -703,6 +719,18 @@ async function bootstrap(): Promise<void> {
     const role = myRole as PlayerId;
     const shareUrl = buildLink(gameId, key);
 
+    // Register the service worker and subscribe to push notifications.
+    // Fire-and-forget: push is a best-effort enhancement.
+    void (async () => {
+        await registerServiceWorker();
+        const subJson = await subscribeToPush();
+        if (subJson) {
+            storePushSubscription(gameId, role, subJson).catch((err: unknown) =>
+                console.warn('Failed to store push subscription:', err),
+            );
+        }
+    })();
+
     render(app, state, role, gameId, cryptoKey, key, shareUrl);
 
     // Track whether it was already my turn when we first loaded so we only
diff --git a/src/public/sw.js b/src/public/sw.js
new file mode 100644
index 0000000..a73ecab
--- /dev/null
+++ b/src/public/sw.js
@@ -0,0 +1,42 @@
+/**
+ * Word Waltz — Service Worker
+ *
+ * Handles incoming Web Push events and notification clicks.
+ * Registered from notifications.ts at runtime.
+ */
+
+/* global self, clients */
+
+// Derive the base icon path from the SW's own location so it works both on
+// localhost (scope /) and on GitLab Pages (scope /word-waltz/).
+const BASE = new URL('./', self.location.href).href; // e.g. "/word-waltz/"
+
+self.addEventListener('push', (event) => {
+    const data = event.data?.json() ?? {};
+    const title = data.title ?? 'Word Waltz';
+    const options = {
+        body: data.body ?? "It's your turn!",
+        icon: `${BASE}apple-touch-icon.png`,
+        badge: `${BASE}favicon-32x32.png`,
+        tag: 'word-waltz-turn',   // replace previous notification
+        renotify: true,
+        data: { url: data.url ?? BASE },
+    };
+    event.waitUntil(self.registration.showNotification(title, options));
+});
+
+self.addEventListener('notificationclick', (event) => {
+    event.notification.close();
+    const target = event.notification.data?.url ?? BASE;
+    event.waitUntil(
+        clients
+            .matchAll({ type: 'window', includeUncontrolled: true })
+            .then((list) => {
+                // Focus an existing tab that is on the same origin+scope
+                for (const c of list) {
+                    if (c.url.startsWith(BASE) && 'focus' in c) return c.focus();
+                }
+                return clients.openWindow(target);
+            }),
+    );
+});
diff --git a/src/types.ts b/src/types.ts
index 3328ab5..3baf8d5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -51,12 +51,40 @@ export interface SoloGameState {
     updatedAt: number;
 }
 
-/** The shape stored in Firestore (encrypted envelope + one plaintext field). */
-export interface EncryptedEnvelope {
+// ---------------------------------------------------------------------------
+// Push notifications — stored in plaintext alongside the encrypted envelope.
+//
+// pushSubs: each player's Web Push subscription JSON, stored so the Cloud
+//   Function can send a push without decrypting the game state.
+//
+// pendingNotification: written by the acting player when the turn changes;
+//   consumed (and cleared) by the Cloud Function to fire the push.
+//   The gameUrl intentionally omits the #key fragment so the encryption key
+//   is never stored in Firestore — the recipient's device already has it in
+//   localStorage, and the notification click opens the home screen.
+// ---------------------------------------------------------------------------
+
+export interface PendingNotification {
+    player: PlayerId;
+    message: string;
+    /** Base URL of the app (e.g. https://host/word-waltz/) — no key. */
+    gameUrl: string;
+}
+
+/** Firestore document fields added by the push notification layer. */
+export interface PushFields {
+    /** JSON-serialized PushSubscription per player (may be absent). */
+    pushSubs?: { a?: string | null; b?: string | null };
+    /** Set by the acting player; consumed + cleared by the Cloud Function. */
+    pendingNotification?: PendingNotification | null;
+}
+
+/** The shape stored in Firestore (encrypted envelope + plaintext fields). */
+export interface EncryptedEnvelope extends PushFields {
     iv: string;
     ciphertext: string;
     lastPlayed: number;
 }
 
 /** The encrypted part only — passed in by callers of createGame / updateGame. */
-export type Encrypted = Omit<EncryptedEnvelope, 'lastPlayed'>;
+export type Encrypted = Pick<EncryptedEnvelope, 'iv' | 'ciphertext'>;
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index badf7d9..31f92e2 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -1,18 +1,106 @@
 /**
- * notifications.ts — tab title + OS notification helpers
+ * notifications.ts — service worker registration, push subscriptions, and
+ * in-tab notification helpers.
  *
  * Strategy:
- *  - Always update document.title so the user sees an alert in the tab bar
- *    even without notification permission.
- *  - Fire a Notification API alert when permission is granted and the tab is
- *    not currently focused (no point interrupting someone already looking at
- *    the page).
- *  - Request permission lazily on the first "your turn" event so the browser
- *    prompt appears in a meaningful context, not on page load.
+ *  - Register /sw.js so we can use registration.showNotification() across all
+ *    browsers (including Firefox and Safari which don't support new Notification
+ *    without a service worker).
+ *  - Subscribe to Web Push (VAPID) if VITE_VAPID_PUBLIC_KEY is configured.
+ *    The subscription JSON is stored in Firestore so the Cloud Function can
+ *    send a push when it's the player's turn, even if the tab is closed.
+ *  - Always update document.title as a fallback visible in the browser tab.
+ *  - Only fire the OS notification when the tab is not focused (no point
+ *    interrupting someone already looking at the page).
+ *
+ * VAPID setup:
+ *  1. Run: npx web-push generate-vapid-keys
+ *  2. Set VITE_VAPID_PUBLIC_KEY in your .env (and as a GitLab CI variable).
+ *  3. Set VAPID_PRIVATE_KEY + VAPID_SUBJECT in your Firebase Cloud Function
+ *     environment (see functions/index.js).
  */
 
 const BASE_TITLE = 'Word Waltz';
 
+// -------------------------------------------------------------------------
+// Service Worker registration
+// -------------------------------------------------------------------------
+
+let swRegistration: ServiceWorkerRegistration | null = null;
+
+/**
+ * Register the service worker. Safe to call multiple times — subsequent calls
+ * are no-ops once registration is cached. Returns the registration or null if
+ * service workers are not supported.
+ */
+export async function registerServiceWorker(): Promise<ServiceWorkerRegistration | null> {
+    if (swRegistration) return swRegistration;
+    if (!('serviceWorker' in navigator)) return null;
+
+    try {
+        // BASE_URL is '/' in dev and '/word-waltz/' on GitLab Pages.
+        const swUrl = `${import.meta.env.BASE_URL}sw.js`;
+        swRegistration = await navigator.serviceWorker.register(swUrl, {
+            scope: import.meta.env.BASE_URL,
+        });
+        return swRegistration;
+    } catch (err) {
+        console.warn('Service worker registration failed:', err);
+        return null;
+    }
+}
+
+// -------------------------------------------------------------------------
+// Push subscription
+// -------------------------------------------------------------------------
+
+/**
+ * Subscribe this device to Web Push and return the serialised subscription
+ * JSON, or null if push is unavailable / not configured.
+ *
+ * Requires VITE_VAPID_PUBLIC_KEY to be defined at build time.
+ */
+export async function subscribeToPush(): Promise<string | null> {
+    const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY as string | undefined;
+    if (!vapidPublicKey) return null;
+
+    const reg = await registerServiceWorker();
+    if (!reg) return null;
+    if (!('pushManager' in reg)) return null;
+
+    try {
+        // Request notification permission lazily
+        if (Notification.permission === 'default') {
+            await Notification.requestPermission();
+        }
+        if (Notification.permission !== 'granted') return null;
+
+        const existing = await reg.pushManager.getSubscription();
+        if (existing) return JSON.stringify(existing);
+
+        const sub = await reg.pushManager.subscribe({
+            userVisibleOnly: true,
+            applicationServerKey: urlBase64ToUint8Array(vapidPublicKey).buffer as ArrayBuffer,
+        });
+        return JSON.stringify(sub);
+    } catch (err) {
+        console.warn('Push subscription failed:', err);
+        return null;
+    }
+}
+
+// VAPID public key is URL-safe base64; the browser needs a Uint8Array.
+function urlBase64ToUint8Array(base64String: string): Uint8Array {
+    const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
+    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
+    const raw = atob(base64);
+    return Uint8Array.from(raw, (c) => c.charCodeAt(0));
+}
+
+// -------------------------------------------------------------------------
+// In-tab notification helpers
+// -------------------------------------------------------------------------
+
 /** Set the tab title and optionally send an OS notification. */
 export async function notifyMyTurn(message: string): Promise<void> {
     document.title = `⚡ ${message} — ${BASE_TITLE}`;
@@ -23,16 +111,28 @@ export async function notifyMyTurn(message: string): Promise<void> {
     if (Notification.permission === 'default') {
         await Notification.requestPermission();
     }
-
     if (Notification.permission !== 'granted') return;
 
-    // Only fire when the tab is hidden — no need to interrupt an active player
+    // Only fire when the tab is hidden
     if (document.visibilityState === 'visible') return;
 
-    new Notification(BASE_TITLE, {
-        body: message,
-        icon: '/apple-touch-icon.png',
-    });
+    const reg = swRegistration ?? (await registerServiceWorker());
+    if (reg) {
+        // Preferred: service-worker-backed notification (works in all browsers)
+        await reg.showNotification(BASE_TITLE, {
+            body: message,
+            icon: `${import.meta.env.BASE_URL}apple-touch-icon.png`,
+            tag: 'word-waltz-turn',
+            // renotify is supported at runtime but not in all TS lib versions
+            ...({ renotify: true } as object),
+        });
+    } else {
+        // Fallback for browsers without SW support
+        new Notification(BASE_TITLE, {
+            body: message,
+            icon: `${import.meta.env.BASE_URL}apple-touch-icon.png`,
+        });
+    }
 }
 
 /** Reset the title back to the default. */
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11681c8
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,9 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+    readonly VITE_VAPID_PUBLIC_KEY?: string;
+}
+
+interface ImportMeta {
+    readonly env: ImportMetaEnv;
+}