From b23027e1b8afd67c15265a0e19886ead00797997 Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Wed, 27 May 2026 22:27:26 -0700 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E2=9C=A8=20ios=20notifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/src/ZotMeet/AppDelegate.swift | 3 +- package.json | 1 + pnpm-lock.yaml | 879 +++++++++++++++++- src/components/nav/mui-app-shell.tsx | 2 + .../push/native-ios-push-bridge.tsx | 92 ++ src/lib/notification/types.ts | 7 +- src/lib/push/send-push.ts | 121 +++ src/server/data/user/queries.ts | 14 + 8 files changed, 1104 insertions(+), 15 deletions(-) create mode 100644 src/components/push/native-ios-push-bridge.tsx create mode 100644 src/lib/push/send-push.ts diff --git a/ios/src/ZotMeet/AppDelegate.swift b/ios/src/ZotMeet/AppDelegate.swift index 01c0f9e18..7be1e42bf 100644 --- a/ios/src/ZotMeet/AppDelegate.swift +++ b/ios/src/ZotMeet/AppDelegate.swift @@ -11,8 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { -// TODO: if we're using Firebase, uncomment next string - //FirebaseApp.configure() + FirebaseApp.configure() // [START set_messaging_delegate] Messaging.messaging().delegate = self diff --git a/package.json b/package.json index 42911c3c1..41137f19d 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^16.4.5", + "firebase-admin": "^13.10.0", "googleapis": "^148.0.0", "lucide-react": "^0.453.0", "next": "16.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11d4fb4af..5c25470ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,7 @@ importers: version: 7.3.7(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@mui/material-nextjs': specifier: ^7.3.6 - version: 7.3.7(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 7.3.7(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@mui/x-date-pickers': specifier: ^8.27.2 version: 8.27.2(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@mui/material@7.3.7(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@mui/system@7.3.7(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(date-fns@4.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -102,6 +102,9 @@ importers: dotenv: specifier: ^16.4.5 version: 16.6.1 + firebase-admin: + specifier: ^13.10.0 + version: 13.10.0 googleapis: specifier: ^148.0.0 version: 148.0.0 @@ -110,13 +113,13 @@ importers: version: 0.453.0(react@19.2.3) next: specifier: 16.1.1 - version: 16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nuqs: specifier: ^2.7.3 - version: 2.8.8(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 2.8.8(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) pg: specifier: ^8.13.0 version: 8.18.0 @@ -186,7 +189,7 @@ importers: version: 0.28.1 drizzle-orm: specifier: 0.36.4 - version: 0.36.4(@types/pg@8.16.0)(@types/react@19.2.7)(pg@8.18.0)(react@19.2.3) + version: 0.36.4(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(@types/react@19.2.7)(pg@8.18.0)(react@19.2.3) husky: specifier: ^9.1.7 version: 9.1.7 @@ -1014,6 +1017,41 @@ packages: cpu: [x64] os: [win32] + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@firebase/app-check-interop-types@0.3.4': + resolution: {integrity: sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==} + + '@firebase/app-types@0.9.5': + resolution: {integrity: sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==} + + '@firebase/auth-interop-types@0.2.5': + resolution: {integrity: sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==} + + '@firebase/component@0.7.3': + resolution: {integrity: sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==} + engines: {node: '>=20.0.0'} + + '@firebase/database-compat@2.1.4': + resolution: {integrity: sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==} + engines: {node: '>=20.0.0'} + + '@firebase/database-types@1.0.20': + resolution: {integrity: sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==} + + '@firebase/database@1.1.3': + resolution: {integrity: sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==} + engines: {node: '>=20.0.0'} + + '@firebase/logger@0.5.1': + resolution: {integrity: sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==} + engines: {node: '>=20.0.0'} + + '@firebase/util@1.15.1': + resolution: {integrity: sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==} + engines: {node: '>=20.0.0'} + '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -1029,6 +1067,40 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@google-cloud/firestore@7.11.6': + resolution: {integrity: sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==} + engines: {node: '>=14.0.0'} + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.19.0': + resolution: {integrity: sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==} + engines: {node: '>=14'} + + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -1200,6 +1272,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@modelcontextprotocol/sdk@1.6.1': resolution: {integrity: sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==} engines: {node: '>=18'} @@ -1520,6 +1595,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + '@oslojs/asn1@1.0.0': resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} @@ -1541,6 +1620,36 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2186,15 +2295,31 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} + '@tsconfig/bun@1.0.7': resolution: {integrity: sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/conventional-commits-parser@5.0.2': resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.31': resolution: {integrity: sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==} @@ -2220,17 +2345,31 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2285,6 +2424,16 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2418,6 +2567,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -2502,6 +2655,10 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + date-fns-tz@3.2.0: resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} peerDependencies: @@ -2523,6 +2680,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2655,6 +2816,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -2671,6 +2835,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2694,6 +2861,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -2729,6 +2900,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -2757,6 +2932,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + farmhash-modern@1.1.0: + resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} + engines: {node: '>=18.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2777,6 +2956,10 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2786,6 +2969,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2801,10 +2988,22 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} + firebase-admin@13.10.0: + resolution: {integrity: sha512-rbuCrJvYRwqBqvbccMS8fj/x2zsaMisdf5RQbRzQzr14Rbq9r2UlpuBHqWAwrO6c9dIRF56xF/xoepXsD5yDuQ==} + engines: {node: '>=18'} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + engines: {node: '>= 0.12'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2821,14 +3020,25 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + gaxios@6.7.1: resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} engines: {node: '>=14'} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + gcp-metadata@6.1.1: resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} engines: {node: '>=14'} + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -2873,14 +3083,26 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} + google-gax@4.6.1: + resolution: {integrity: sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==} + engines: {node: '>=14'} + google-logging-utils@0.0.2: resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} engines: {node: '>=14'} + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + googleapis-common@7.2.0: resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} engines: {node: '>=14.0.0'} @@ -2923,10 +3145,24 @@ packages: resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} engines: {node: '>=16.9.0'} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3081,9 +3317,17 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + jwks-rsa@3.2.2: + resolution: {integrity: sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==} + engines: {node: '>=14'} + jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -3091,6 +3335,9 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3110,9 +3357,27 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -3122,6 +3387,9 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -3138,6 +3406,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3146,6 +3417,9 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru-memoizer@2.3.0: + resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} + lucide-react@0.453.0: resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} peerDependencies: @@ -3175,14 +3449,27 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3240,6 +3527,11 @@ packages: sass: optional: true + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3249,6 +3541,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3318,6 +3614,10 @@ packages: openid-client@5.6.4: resolution: {integrity: sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3500,6 +3800,14 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.6.1: + resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3587,6 +3895,10 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3622,6 +3934,14 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3776,6 +4096,12 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3792,6 +4118,9 @@ packages: resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==} engines: {node: '>=20'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3803,6 +4132,9 @@ packages: strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -3849,6 +4181,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -3972,9 +4308,21 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4032,6 +4380,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -4954,6 +5306,53 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@fastify/busboy@3.2.0': {} + + '@firebase/app-check-interop-types@0.3.4': {} + + '@firebase/app-types@0.9.5': + dependencies: + '@firebase/logger': 0.5.1 + + '@firebase/auth-interop-types@0.2.5': {} + + '@firebase/component@0.7.3': + dependencies: + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/database-compat@2.1.4': + dependencies: + '@firebase/component': 0.7.3 + '@firebase/database': 1.1.3 + '@firebase/database-types': 1.0.20 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/database-types@1.0.20': + dependencies: + '@firebase/app-types': 0.9.5 + '@firebase/util': 1.15.1 + + '@firebase/database@1.1.3': + dependencies: + '@firebase/app-check-interop-types': 0.3.4 + '@firebase/auth-interop-types': 0.2.5 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/logger@0.5.1': + dependencies: + tslib: 2.8.1 + + '@firebase/util@1.15.1': + dependencies: + tslib: 2.8.1 + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -4971,6 +5370,74 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@google-cloud/firestore@7.11.6': + dependencies: + '@opentelemetry/api': 1.9.1 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 4.6.1 + protobufjs: 7.6.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + optional: true + + '@google-cloud/projectify@4.0.0': + optional: true + + '@google-cloud/promisify@4.0.0': + optional: true + + '@google-cloud/storage@7.19.0': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 5.7.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + html-entities: 2.6.0 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + uuid: 8.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + optional: true + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.1 + yargs: 17.7.2 + optional: true + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.1 + yargs: 17.7.2 + optional: true + '@hookform/resolvers@3.10.0(react-hook-form@7.71.1(react@19.2.3))': dependencies: react-hook-form: 7.71.1(react@19.2.3) @@ -5085,6 +5552,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': + optional: true + '@modelcontextprotocol/sdk@1.6.1': dependencies: content-type: 1.0.5 @@ -5109,11 +5579,11 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 - '@mui/material-nextjs@7.3.7(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@mui/material-nextjs@7.3.7(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.6 '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) - next: 16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 optionalDependencies: '@emotion/cache': 11.14.0 @@ -5334,6 +5804,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@opentelemetry/api@1.9.1': + optional: true + '@oslojs/asn1@1.0.0': dependencies: '@oslojs/binary': 1.0.0 @@ -5355,6 +5828,38 @@ snapshots: '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': + optional: true + + '@protobufjs/base64@1.1.2': + optional: true + + '@protobufjs/codegen@2.0.5': + optional: true + + '@protobufjs/eventemitter@1.1.1': + optional: true + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + optional: true + + '@protobufjs/float@1.0.2': + optional: true + + '@protobufjs/inquire@1.1.2': + optional: true + + '@protobufjs/path@1.1.2': + optional: true + + '@protobufjs/pool@1.1.0': + optional: true + + '@protobufjs/utf8@1.1.1': + optional: true + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -6105,6 +6610,9 @@ snapshots: dependencies: tslib: 2.8.1 + '@tootallnate/once@2.0.1': + optional: true + '@tsconfig/bun@1.0.7': {} '@tybys/wasm-util@0.10.1': @@ -6112,10 +6620,23 @@ snapshots: tslib: 2.8.1 optional: true + '@types/caseless@0.12.5': + optional: true + '@types/conventional-commits-parser@5.0.2': dependencies: '@types/node': 20.19.31 + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.31 + + '@types/long@4.0.2': + optional: true + + '@types/ms@2.1.0': {} + '@types/node@20.19.31': dependencies: undici-types: 6.21.0 @@ -6142,18 +6663,41 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 20.19.31 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.5 + optional: true + '@types/tinycolor2@1.4.6': {} + '@types/tough-cookie@4.0.5': + optional: true + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 through: 2.3.8 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + optional: true + accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + optional: true + agent-base@7.1.4: {} ajv@8.17.1: @@ -6202,6 +6746,17 @@ snapshots: array-ify@1.0.0: {} + arrify@2.0.1: + optional: true + + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + + asynckit@0.4.0: + optional: true + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -6350,6 +6905,11 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + optional: true + commander@14.0.3: {} commander@4.1.1: {} @@ -6421,6 +6981,8 @@ snapshots: dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} + date-fns-tz@3.2.0(date-fns@4.1.0): dependencies: date-fns: 4.1.0 @@ -6439,6 +7001,9 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + delayed-stream@1.0.0: + optional: true + depd@2.0.0: {} detect-libc@2.1.2: {} @@ -6475,8 +7040,9 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.36.4(@types/pg@8.16.0)(@types/react@19.2.7)(pg@8.18.0)(react@19.2.3): + drizzle-orm@0.36.4(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(@types/react@19.2.7)(pg@8.18.0)(react@19.2.3): optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/pg': 8.16.0 '@types/react': 19.2.7 pg: 8.18.0 @@ -6488,6 +7054,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + optional: true + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -6500,6 +7074,11 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + env-paths@2.2.1: {} environment@1.1.0: {} @@ -6516,6 +7095,14 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + optional: true + esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -6611,6 +7198,9 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: + optional: true + eventemitter3@5.0.4: {} events@1.1.1: {} @@ -6660,6 +7250,8 @@ snapshots: extend@3.0.2: {} + farmhash-modern@1.1.0: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6687,10 +7279,19 @@ snapshots: dependencies: reusify: 1.1.0 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6714,10 +7315,41 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 + firebase-admin@13.10.0: + dependencies: + '@fastify/busboy': 3.2.0 + '@firebase/database-compat': 2.1.4 + '@firebase/database-types': 1.0.20 + farmhash-modern: 1.1.0 + fast-deep-equal: 3.1.3 + google-auth-library: 10.6.2 + jsonwebtoken: 9.0.3 + jwks-rsa: 3.2.2 + optionalDependencies: + '@google-cloud/firestore': 7.11.6 + '@google-cloud/storage': 7.19.0 + transitivePeerDependencies: + - encoding + - supports-color + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@2.5.5: + 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 + optional: true + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -6727,6 +7359,9 @@ snapshots: function-bind@1.1.2: {} + functional-red-black-tree@1.0.1: + optional: true + gaxios@6.7.1: dependencies: extend: 3.0.2 @@ -6738,6 +7373,14 @@ snapshots: - encoding - supports-color + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + gcp-metadata@6.1.1: dependencies: gaxios: 6.7.1 @@ -6747,6 +7390,14 @@ snapshots: - encoding - supports-color + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + generator-function@2.0.1: {} get-caller-file@2.0.5: {} @@ -6795,6 +7446,17 @@ snapshots: dependencies: ini: 4.1.1 + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + google-auth-library@9.15.1: dependencies: base64-js: 1.5.1 @@ -6807,8 +7469,29 @@ snapshots: - encoding - supports-color + google-gax@4.6.1: + dependencies: + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.15.1 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.6.1 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + google-logging-utils@0.0.2: {} + google-logging-utils@1.1.3: {} + googleapis-common@7.2.0: dependencies: extend: 3.0.2 @@ -6861,6 +7544,9 @@ snapshots: hono@4.7.4: {} + html-entities@2.6.0: + optional: true + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -6869,6 +7555,25 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-parser-js@0.5.10: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + optional: true + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: agent-base: 7.1.4 @@ -6991,12 +7696,35 @@ snapshots: jsonparse@1.3.1: {} + jsonwebtoken@9.0.3: + 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.1.1 + ms: 2.1.3 + semver: 7.7.3 + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwks-rsa@3.2.2: + dependencies: + '@types/jsonwebtoken': 9.0.10 + debug: 4.4.3(supports-color@10.2.2) + jose: 4.15.9 + limiter: 1.1.5 + lru-memoizer: 2.3.0 + transitivePeerDependencies: + - supports-color + jws@4.0.1: dependencies: jwa: 2.0.1 @@ -7004,6 +7732,8 @@ snapshots: lilconfig@3.1.3: {} + limiter@1.1.5: {} + lines-and-columns@1.2.4: {} lint-staged@16.2.7: @@ -7031,14 +7761,28 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.clonedeep@4.5.0: {} + + 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.kebabcase@4.1.1: {} lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -7055,6 +7799,9 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + long@5.3.2: + optional: true + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -7063,6 +7810,11 @@ snapshots: dependencies: yallist: 4.0.0 + lru-memoizer@2.3.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 6.0.0 + lucide-react@0.453.0(react@19.2.3): dependencies: react: 19.2.3 @@ -7082,12 +7834,23 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: + optional: true + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + optional: true + mime-types@3.0.2: dependencies: mime-db: 1.54.0 + mime@3.0.0: + optional: true + mimic-function@5.0.1: {} minimatch@5.1.6: @@ -7115,7 +7878,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.1 '@swc/helpers': 0.5.15 @@ -7134,23 +7897,32 @@ snapshots: '@next/swc-linux-x64-musl': 16.1.1 '@next/swc-win32-arm64-msvc': 16.1.1 '@next/swc-win32-x64-msvc': 16.1.1 + '@opentelemetry/api': 1.9.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + normalize-path@3.0.0: {} - nuqs@2.8.8(next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + nuqs@2.8.8(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.3 optionalDependencies: - next: 16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) object-assign@4.1.1: {} @@ -7201,6 +7973,11 @@ snapshots: object-hash: 2.2.0 oidc-token-hash: 5.2.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + optional: true + p-limit@4.0.0: dependencies: yocto-queue: 1.2.2 @@ -7352,6 +8129,27 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.6.1 + optional: true + + protobufjs@7.6.1: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 20.19.31 + long: 5.3.2 + optional: true + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7431,6 +8229,13 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -7458,6 +8263,19 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + retry@0.13.1: + optional: true + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -7659,6 +8477,14 @@ snapshots: statuses@2.0.2: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + optional: true + + stream-shift@1.0.3: + optional: true + string-argv@0.3.2: {} string-width@4.2.3: @@ -7678,6 +8504,11 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -7688,6 +8519,9 @@ snapshots: strnum@2.2.3: {} + stubs@3.0.0: + optional: true + styled-jsx@5.1.6(react@19.2.3): dependencies: client-only: 0.0.1 @@ -7747,6 +8581,18 @@ snapshots: - tsx - yaml + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + text-extensions@2.4.0: {} thenify-all@1.6.0: @@ -7847,8 +8693,18 @@ snapshots: vary@1.1.2: {} + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -7909,6 +8765,9 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yocto-queue@0.1.0: + optional: true + yocto-queue@1.2.2: {} zod-to-json-schema@3.24.3(zod@3.24.2): diff --git a/src/components/nav/mui-app-shell.tsx b/src/components/nav/mui-app-shell.tsx index a6d736b3f..73e14de6a 100644 --- a/src/components/nav/mui-app-shell.tsx +++ b/src/components/nav/mui-app-shell.tsx @@ -2,6 +2,7 @@ import { Box, useMediaQuery, useTheme } from "@mui/material"; import { usePathname } from "next/navigation"; +import { NativeIosPushBridge } from "@/components/push/native-ios-push-bridge"; import type { NotificationItem, UserProfile } from "@/lib/auth/user"; import { MuiBottomNav } from "./mui-bottom-nav"; import { MuiTopNav } from "./mui-top-nav"; @@ -37,6 +38,7 @@ export function MuiAppShell({ minHeight: "100vh", }} > + {user ? : null} {!isMobile && } void; + } + >; + }; + } +} + +function getBridgeHandler(name: string) { + return window.webkit?.messageHandlers?.[name]; +} + +function getUserTopic(userId: string) { + return `user_${userId}`; +} + +function postMessage(name: string, payload?: unknown) { + const handler = getBridgeHandler(name); + if (!handler) return; + handler.postMessage(payload ? JSON.stringify(payload) : ""); +} + +type NativeIosPushBridgeProps = { + userId: string; +}; + +export function NativeIosPushBridge({ userId }: NativeIosPushBridgeProps) { + useEffect(() => { + if (!isNativeIosApp()) return; + if (!window.webkit?.messageHandlers) return; + + const topic = getUserTopic(userId); + const subscribe = () => postMessage("push-subscribe", { topic }); + + const handlePermissionState = (event: Event) => { + const detail = (event as CustomEvent).detail; + if ( + detail === "authorized" || + detail === "ephemeral" || + detail === "provisional" + ) { + subscribe(); + postMessage("push-token"); + return; + } + + if (detail === "notDetermined") { + postMessage("push-permission-request"); + } + }; + + const handlePermissionRequestResult = (event: Event) => { + const detail = (event as CustomEvent).detail; + if (detail === "granted") { + subscribe(); + postMessage("push-token"); + } + }; + + window.addEventListener("push-permission-state", handlePermissionState); + window.addEventListener( + "push-permission-request", + handlePermissionRequestResult, + ); + + postMessage("push-permission-state"); + + return () => { + window.removeEventListener( + "push-permission-state", + handlePermissionState, + ); + window.removeEventListener( + "push-permission-request", + handlePermissionRequestResult, + ); + postMessage("push-subscribe", { topic, unsubscribe: true }); + }; + }, [userId]); + + return null; +} diff --git a/src/lib/notification/types.ts b/src/lib/notification/types.ts index 03ddd7483..9c36f39ff 100644 --- a/src/lib/notification/types.ts +++ b/src/lib/notification/types.ts @@ -62,17 +62,18 @@ export const NOTIFICATION_PREF_OPTIONS: { key: "meetingInvites", label: "Meeting Invites", description: - "Receive in-app and email notifications when you're invited to a meeting.", + "Receive in-app, email, and push notifications when you're invited to a meeting.", }, { key: "groupInvites", label: "Group Invites", description: - "Receive in-app and email notifications when you're invited to a group.", + "Receive in-app, email, and push notifications when you're invited to a group.", }, { key: "nudges", label: "Nudges", - description: "Receive in-app and email reminders to add your availability.", + description: + "Receive in-app, email, and push reminders to add your availability.", }, ]; diff --git a/src/lib/push/send-push.ts b/src/lib/push/send-push.ts new file mode 100644 index 000000000..3e71f83f4 --- /dev/null +++ b/src/lib/push/send-push.ts @@ -0,0 +1,121 @@ +import "server-only"; + +import { cert, getApps, initializeApp } from "firebase-admin/app"; +import { getMessaging } from "firebase-admin/messaging"; + +type PushPayload = { + title: string; + message: string; + type: string; + redirect: string; + groupId?: string | null; + createdBy?: string | null; +}; + +const FIREBASE_SERVICE_ACCOUNT_JSON = process.env.FIREBASE_SERVICE_ACCOUNT_JSON; +const FIREBASE_SERVICE_ACCOUNT_BASE64 = + process.env.FIREBASE_SERVICE_ACCOUNT_BASE64; + +let warnedMissingConfig = false; + +function parseServiceAccount(): { + projectId: string; + clientEmail: string; + privateKey: string; +} | null { + let raw = FIREBASE_SERVICE_ACCOUNT_JSON?.trim(); + + if (!raw && FIREBASE_SERVICE_ACCOUNT_BASE64) { + raw = Buffer.from(FIREBASE_SERVICE_ACCOUNT_BASE64, "base64").toString( + "utf8", + ); + } + + if (!raw) return null; + + try { + const parsed = JSON.parse(raw) as { + project_id?: string; + client_email?: string; + private_key?: string; + }; + + if (!parsed.project_id || !parsed.client_email || !parsed.private_key) { + return null; + } + + return { + projectId: parsed.project_id, + clientEmail: parsed.client_email, + privateKey: parsed.private_key, + }; + } catch { + return null; + } +} + +function getOrInitFirebaseMessaging() { + if (getApps().length > 0) { + return getMessaging(); + } + + const serviceAccount = parseServiceAccount(); + if (!serviceAccount) { + if (!warnedMissingConfig) { + console.warn( + "Push notifications are disabled: missing Firebase service account configuration.", + ); + warnedMissingConfig = true; + } + return null; + } + + const app = initializeApp({ + credential: cert(serviceAccount), + }); + return getMessaging(app); +} + +function topicForUser(userId: string) { + return `user_${userId}`; +} + +export async function sendPushToUsers(userIds: string[], payload: PushPayload) { + if (userIds.length === 0) return; + + const messaging = getOrInitFirebaseMessaging(); + if (!messaging) return; + + const sendResults = await Promise.allSettled( + userIds.map((userId) => + messaging.send({ + topic: topicForUser(userId), + notification: { + title: payload.title, + body: payload.message, + }, + data: { + type: payload.type, + redirect: payload.redirect, + title: payload.title, + message: payload.message, + groupId: payload.groupId ?? "", + createdBy: payload.createdBy ?? "", + }, + apns: { + payload: { + aps: { + sound: "default", + }, + }, + }, + }), + ), + ); + + for (const result of sendResults) { + if (result.status === "rejected") { + console.error("Failed to send push notification:", result.reason); + } + } +} diff --git a/src/server/data/user/queries.ts b/src/server/data/user/queries.ts index 6a4bc6071..57ca27f05 100644 --- a/src/server/data/user/queries.ts +++ b/src/server/data/user/queries.ts @@ -15,6 +15,7 @@ import { type NotificationPrefs, toNotificationPrefs, } from "@/lib/notification/types"; +import { sendPushToUsers } from "@/lib/push/send-push"; import { toIlikeContainsPattern } from "@/lib/sql/like-pattern"; export async function getUserIdExists(id: string) { @@ -209,6 +210,7 @@ export async function createNewNotification( const recipientRows = await db .select({ + userId: users.id, memberId: users.memberId, email: users.email, }) @@ -274,6 +276,18 @@ export async function createNewNotification( } } + await sendPushToUsers( + allowedRecipients.map((recipient) => recipient.userId), + { + title, + message, + type, + redirect: link, + groupId, + createdBy, + }, + ); + return notificationsCreated; } From 2e80cf166262c9798356158c4799ba02bf0ee393 Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 00:19:55 -0700 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E2=9C=A8=20push=20notif=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw-register.js | 6 + public/sw.js | 61 +++++++++ .../profile/notifications-panel.tsx | 2 + .../push/web-push-permission-button.tsx | 119 ++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 src/components/push/web-push-permission-button.tsx diff --git a/public/sw-register.js b/public/sw-register.js index cbedf21f5..f4f339af5 100644 --- a/public/sw-register.js +++ b/public/sw-register.js @@ -28,4 +28,10 @@ .catch((err) => { console.warn("[sw] registration failed", err); }); + + navigator.serviceWorker.addEventListener("message", (event) => { + if (event.data && event.data.type === "notification-click") { + window.location.assign(event.data.redirect || "/summary"); + } + }); })(); diff --git a/public/sw.js b/public/sw.js index 9d38ed0f8..8a45531a8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -153,3 +153,64 @@ self.addEventListener("message", (event) => { self.skipWaiting(); } }); + +function getPushPayload(event) { + if (!event.data) { + return { + title: "ZotMeet", + message: "You have a new notification.", + redirect: "/summary", + }; + } + + try { + return event.data.json(); + } catch (_err) { + return { + title: "ZotMeet", + message: event.data.text(), + redirect: "/summary", + }; + } +} + +self.addEventListener("push", (event) => { + const payload = getPushPayload(event); + const title = payload.title || "ZotMeet"; + const redirect = payload.redirect || payload.url || "/summary"; + + event.waitUntil( + self.registration.showNotification(title, { + body: payload.message || payload.body || "You have a new notification.", + icon: "/icons/icon-192.png", + badge: "/icons/icon-96.png", + data: { redirect }, + }), + ); +}); + +self.addEventListener("notificationclick", (event) => { + event.notification.close(); + + const redirect = event.notification.data?.redirect || "/summary"; + const targetUrl = new URL(redirect, self.location.origin).href; + + event.waitUntil( + (async () => { + const clientList = await clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + + for (const client of clientList) { + if (new URL(client.url).origin === self.location.origin) { + await client.focus(); + client.postMessage({ type: "notification-click", redirect }); + return; + } + } + + await clients.openWindow(targetUrl); + })(), + ); +}); diff --git a/src/components/profile/notifications-panel.tsx b/src/components/profile/notifications-panel.tsx index 0edb9f06a..c2ab2bf8f 100644 --- a/src/components/profile/notifications-panel.tsx +++ b/src/components/profile/notifications-panel.tsx @@ -6,6 +6,7 @@ import Stack from "@mui/material/Stack"; import Switch from "@mui/material/Switch"; import Typography from "@mui/material/Typography"; import { useState, useTransition } from "react"; +import { WebPushPermissionButton } from "@/components/push/web-push-permission-button"; import { useSnackbar } from "@/components/ui/snackbar-provider"; import { NOTIFICATION_PREF_OPTIONS, @@ -56,6 +57,7 @@ export function NotificationsPanel({ Notifications + {NOTIFICATION_PREF_OPTIONS.map((option) => ( (getInitialPermission); + const [isStandalone, setIsStandalone] = useState(false); + const [isNative, setIsNative] = useState(false); + const [isRequesting, setIsRequesting] = useState(false); + const { showSuccess, showError } = useSnackbar(); + + useEffect(() => { + setPermission(getInitialPermission()); + setIsStandalone(isStandaloneWebApp()); + setIsNative(isNativeIosApp()); + }, []); + + if (isNative || permission === "unsupported") return null; + + const handleEnable = async () => { + if (!browserSupportsNotifications()) return; + + setIsRequesting(true); + try { + await navigator.serviceWorker.ready; + const nextPermission = await Notification.requestPermission(); + setPermission(nextPermission); + + if (nextPermission === "granted") { + showSuccess("Notification permission enabled"); + } else { + showError("Notification permission was not enabled"); + } + } catch { + showError("Failed to request notification permission"); + } finally { + setIsRequesting(false); + } + }; + + const statusText = + permission === "granted" + ? "Browser notification permission is enabled on this device." + : permission === "denied" + ? "Browser notification permission is blocked in system settings." + : isStandalone + ? "Enable browser notification permission for this home-screen app." + : "Install ZotMeet to your home screen to enable iOS browser notifications."; + + return ( + + + + Device Notifications + + + {statusText} + + + + + ); +} From cad10ba224d8028e7512d3b0d8de6a35f382db11 Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 08:41:13 -0700 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=F0=9F=90=9B=20cubic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/src/ZotMeet/ViewController.swift | 3 - ios/src/ZotMeet/WebView.swift | 1 - public/sw-register.js | 14 +- public/sw.js | 16 +- src/app/api/push-subscriptions/route.ts | 86 + src/app/api/push-tokens/route.ts | 78 + .../push/native-ios-push-bridge.tsx | 40 +- .../push/web-push-permission-button.tsx | 109 +- src/db/migrations/0018_push_subscriptions.sql | 19 + src/db/migrations/0019_native_push_tokens.sql | 18 + src/db/migrations/0020_nice_synch.sql | 37 + src/db/migrations/meta/0020_snapshot.json | 1480 +++++++++++++++++ src/db/migrations/meta/_journal.json | 23 +- src/db/schema.ts | 63 + src/lib/push/send-push.ts | 65 +- src/lib/push/web-push.ts | 126 ++ src/server/data/user/queries.ts | 26 +- 17 files changed, 2143 insertions(+), 61 deletions(-) create mode 100644 src/app/api/push-subscriptions/route.ts create mode 100644 src/app/api/push-tokens/route.ts create mode 100644 src/db/migrations/0018_push_subscriptions.sql create mode 100644 src/db/migrations/0019_native_push_tokens.sql create mode 100644 src/db/migrations/0020_nice_synch.sql create mode 100644 src/db/migrations/meta/0020_snapshot.json create mode 100644 src/lib/push/web-push.ts diff --git a/ios/src/ZotMeet/ViewController.swift b/ios/src/ZotMeet/ViewController.swift index 04ce9f615..6ecbf5cb5 100644 --- a/ios/src/ZotMeet/ViewController.swift +++ b/ios/src/ZotMeet/ViewController.swift @@ -228,9 +228,6 @@ extension ViewController: WKScriptMessageHandler { if message.name == "print" { printView(webView: ZotMeet.webView) } - if message.name == "push-subscribe" { - handleSubscribeTouch(message: message) - } if message.name == "push-permission-request" { handlePushPermission() } diff --git a/ios/src/ZotMeet/WebView.swift b/ios/src/ZotMeet/WebView.swift index b3c7cf12b..d1b929813 100644 --- a/ios/src/ZotMeet/WebView.swift +++ b/ios/src/ZotMeet/WebView.swift @@ -10,7 +10,6 @@ func createWebView(container: UIView, WKSMH: WKScriptMessageHandler, WKND: WKNav let userContentController = WKUserContentController() userContentController.add(WKSMH, name: "print") - userContentController.add(WKSMH, name: "push-subscribe") userContentController.add(WKSMH, name: "push-permission-request") userContentController.add(WKSMH, name: "push-permission-state") userContentController.add(WKSMH, name: "push-token") diff --git a/public/sw-register.js b/public/sw-register.js index f4f339af5..87d5d2122 100644 --- a/public/sw-register.js +++ b/public/sw-register.js @@ -29,9 +29,21 @@ console.warn("[sw] registration failed", err); }); + function getSameOriginRedirect(value) { + if (typeof value !== "string") return "/summary"; + + try { + const url = new URL(value, window.location.origin); + if (url.origin !== window.location.origin) return "/summary"; + return url.pathname + url.search + url.hash; + } catch (_err) { + return "/summary"; + } + } + navigator.serviceWorker.addEventListener("message", (event) => { if (event.data && event.data.type === "notification-click") { - window.location.assign(event.data.redirect || "/summary"); + window.location.assign(getSameOriginRedirect(event.data.redirect)); } }); })(); diff --git a/public/sw.js b/public/sw.js index 8a45531a8..4c3e54f91 100644 --- a/public/sw.js +++ b/public/sw.js @@ -174,10 +174,22 @@ function getPushPayload(event) { } } +function getSameOriginRedirect(value) { + if (typeof value !== "string") return "/summary"; + + try { + const url = new URL(value, self.location.origin); + if (url.origin !== self.location.origin) return "/summary"; + return `${url.pathname}${url.search}${url.hash}`; + } catch (_err) { + return "/summary"; + } +} + self.addEventListener("push", (event) => { const payload = getPushPayload(event); const title = payload.title || "ZotMeet"; - const redirect = payload.redirect || payload.url || "/summary"; + const redirect = getSameOriginRedirect(payload.redirect || payload.url); event.waitUntil( self.registration.showNotification(title, { @@ -192,7 +204,7 @@ self.addEventListener("push", (event) => { self.addEventListener("notificationclick", (event) => { event.notification.close(); - const redirect = event.notification.data?.redirect || "/summary"; + const redirect = getSameOriginRedirect(event.notification.data?.redirect); const targetUrl = new URL(redirect, self.location.origin).href; event.waitUntil( diff --git a/src/app/api/push-subscriptions/route.ts b/src/app/api/push-subscriptions/route.ts new file mode 100644 index 000000000..1fcd798ea --- /dev/null +++ b/src/app/api/push-subscriptions/route.ts @@ -0,0 +1,86 @@ +import { and, eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { pushSubscriptions } from "@/db/schema"; +import { getCurrentSession } from "@/lib/auth"; + +type PushSubscriptionPayload = { + endpoint?: unknown; + keys?: { + p256dh?: unknown; + auth?: unknown; + }; +}; + +function isValidSubscriptionPayload( + payload: PushSubscriptionPayload, +): payload is { + endpoint: string; + keys: { p256dh: string; auth: string }; +} { + return ( + typeof payload.endpoint === "string" && + typeof payload.keys?.p256dh === "string" && + typeof payload.keys.auth === "string" + ); +} + +export async function POST(request: Request) { + const { user } = await getCurrentSession(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const payload = (await request.json()) as PushSubscriptionPayload; + if (!isValidSubscriptionPayload(payload)) { + return NextResponse.json( + { error: "Invalid push subscription" }, + { status: 400 }, + ); + } + + await db + .insert(pushSubscriptions) + .values({ + userId: user.id, + endpoint: payload.endpoint, + p256dh: payload.keys.p256dh, + auth: payload.keys.auth, + userAgent: request.headers.get("user-agent"), + }) + .onConflictDoUpdate({ + target: pushSubscriptions.endpoint, + set: { + userId: user.id, + p256dh: payload.keys.p256dh, + auth: payload.keys.auth, + userAgent: request.headers.get("user-agent"), + updatedAt: new Date(), + }, + }); + + return NextResponse.json({ success: true }); +} + +export async function DELETE(request: Request) { + const { user } = await getCurrentSession(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const payload = (await request.json()) as { endpoint?: unknown }; + if (typeof payload.endpoint !== "string") { + return NextResponse.json({ error: "Invalid endpoint" }, { status: 400 }); + } + + await db + .delete(pushSubscriptions) + .where( + and( + eq(pushSubscriptions.endpoint, payload.endpoint), + eq(pushSubscriptions.userId, user.id), + ), + ); + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/push-tokens/route.ts b/src/app/api/push-tokens/route.ts new file mode 100644 index 000000000..86323734d --- /dev/null +++ b/src/app/api/push-tokens/route.ts @@ -0,0 +1,78 @@ +import { and, eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { nativePushTokens } from "@/db/schema"; +import { getCurrentSession } from "@/lib/auth"; + +type PushTokenPayload = { + token?: unknown; + platform?: unknown; +}; + +function getValidToken(payload: PushTokenPayload) { + if (typeof payload.token !== "string") return null; + + const token = payload.token.trim(); + if (!token || token === "ERROR GET TOKEN") return null; + + return token; +} + +export async function POST(request: Request) { + const { user } = await getCurrentSession(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const payload = (await request.json()) as PushTokenPayload; + const token = getValidToken(payload); + if (!token) { + return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); + } + + const platform = payload.platform === "ios" ? "ios" : "unknown"; + + await db + .insert(nativePushTokens) + .values({ + userId: user.id, + token, + platform, + userAgent: request.headers.get("user-agent"), + }) + .onConflictDoUpdate({ + target: nativePushTokens.token, + set: { + userId: user.id, + platform, + userAgent: request.headers.get("user-agent"), + updatedAt: new Date(), + }, + }); + + return NextResponse.json({ success: true }); +} + +export async function DELETE(request: Request) { + const { user } = await getCurrentSession(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const payload = (await request.json()) as PushTokenPayload; + const token = getValidToken(payload); + if (!token) { + return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); + } + + await db + .delete(nativePushTokens) + .where( + and( + eq(nativePushTokens.token, token), + eq(nativePushTokens.userId, user.id), + ), + ); + + return NextResponse.json({ success: true }); +} diff --git a/src/components/push/native-ios-push-bridge.tsx b/src/components/push/native-ios-push-bridge.tsx index e544cacfa..9e9e24336 100644 --- a/src/components/push/native-ios-push-bridge.tsx +++ b/src/components/push/native-ios-push-bridge.tsx @@ -20,10 +20,6 @@ function getBridgeHandler(name: string) { return window.webkit?.messageHandlers?.[name]; } -function getUserTopic(userId: string) { - return `user_${userId}`; -} - function postMessage(name: string, payload?: unknown) { const handler = getBridgeHandler(name); if (!handler) return; @@ -34,14 +30,28 @@ type NativeIosPushBridgeProps = { userId: string; }; -export function NativeIosPushBridge({ userId }: NativeIosPushBridgeProps) { +async function savePushToken(token: string) { + const trimmedToken = token.trim(); + if (!trimmedToken || trimmedToken === "ERROR GET TOKEN") return; + + try { + await fetch("/api/push-tokens", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token: trimmedToken, platform: "ios" }), + }); + } catch (error) { + console.error("Failed to save native push token", error); + } +} + +export function NativeIosPushBridge({ + userId: _userId, +}: NativeIosPushBridgeProps) { useEffect(() => { if (!isNativeIosApp()) return; if (!window.webkit?.messageHandlers) return; - const topic = getUserTopic(userId); - const subscribe = () => postMessage("push-subscribe", { topic }); - const handlePermissionState = (event: Event) => { const detail = (event as CustomEvent).detail; if ( @@ -49,7 +59,6 @@ export function NativeIosPushBridge({ userId }: NativeIosPushBridgeProps) { detail === "ephemeral" || detail === "provisional" ) { - subscribe(); postMessage("push-token"); return; } @@ -62,16 +71,23 @@ export function NativeIosPushBridge({ userId }: NativeIosPushBridgeProps) { const handlePermissionRequestResult = (event: Event) => { const detail = (event as CustomEvent).detail; if (detail === "granted") { - subscribe(); postMessage("push-token"); } }; + const handlePushToken = (event: Event) => { + const detail = (event as CustomEvent).detail; + if (typeof detail === "string") { + void savePushToken(detail); + } + }; + window.addEventListener("push-permission-state", handlePermissionState); window.addEventListener( "push-permission-request", handlePermissionRequestResult, ); + window.addEventListener("push-token", handlePushToken); postMessage("push-permission-state"); @@ -84,9 +100,9 @@ export function NativeIosPushBridge({ userId }: NativeIosPushBridgeProps) { "push-permission-request", handlePermissionRequestResult, ); - postMessage("push-subscribe", { topic, unsubscribe: true }); + window.removeEventListener("push-token", handlePushToken); }; - }, [userId]); + }, []); return null; } diff --git a/src/components/push/web-push-permission-button.tsx b/src/components/push/web-push-permission-button.tsx index ce530c738..9944cdc41 100644 --- a/src/components/push/web-push-permission-button.tsx +++ b/src/components/push/web-push-permission-button.tsx @@ -4,11 +4,14 @@ import NotificationsActiveOutlinedIcon from "@mui/icons-material/NotificationsAc import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useSnackbar } from "@/components/ui/snackbar-provider"; import { isNativeIosApp } from "@/lib/platform"; type PermissionState = NotificationPermission | "unsupported"; +type SubscriptionState = "idle" | "subscribing" | "subscribed" | "error"; + +const WEB_PUSH_PUBLIC_KEY = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY; function browserSupportsNotifications() { return ( @@ -33,20 +36,80 @@ function getInitialPermission(): PermissionState { return Notification.permission; } +function base64UrlToUint8Array(value: string) { + const padding = "=".repeat((4 - (value.length % 4)) % 4); + const base64 = (value + padding).replace(/-/g, "+").replace(/_/g, "/"); + const raw = window.atob(base64); + const bytes = new Uint8Array(raw.length); + + for (let i = 0; i < raw.length; i += 1) { + bytes[i] = raw.charCodeAt(i); + } + + return bytes; +} + export function WebPushPermissionButton() { const [permission, setPermission] = useState(getInitialPermission); const [isStandalone, setIsStandalone] = useState(false); const [isNative, setIsNative] = useState(false); const [isRequesting, setIsRequesting] = useState(false); + const [subscriptionState, setSubscriptionState] = + useState("idle"); const { showSuccess, showError } = useSnackbar(); + const ensureSubscription = useCallback(async () => { + if (!browserSupportsNotifications()) return false; + if (!WEB_PUSH_PUBLIC_KEY) { + setSubscriptionState("error"); + showError("Push notifications are not configured for this deployment"); + return false; + } + + setSubscriptionState("subscribing"); + + try { + const registration = await navigator.serviceWorker.ready; + const existing = await registration.pushManager.getSubscription(); + const subscription = + existing ?? + (await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: base64UrlToUint8Array(WEB_PUSH_PUBLIC_KEY), + })); + + const response = await fetch("/api/push-subscriptions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(subscription.toJSON()), + }); + + if (!response.ok) { + throw new Error("Failed to save push subscription"); + } + + setSubscriptionState("subscribed"); + return true; + } catch { + setSubscriptionState("error"); + showError("Failed to enable push notifications"); + return false; + } + }, [showError]); + useEffect(() => { setPermission(getInitialPermission()); setIsStandalone(isStandaloneWebApp()); setIsNative(isNativeIosApp()); }, []); + useEffect(() => { + if (permission === "granted" && isStandalone && !isNative) { + void ensureSubscription(); + } + }, [permission, isStandalone, isNative, ensureSubscription]); + if (isNative || permission === "unsupported") return null; const handleEnable = async () => { @@ -54,12 +117,12 @@ export function WebPushPermissionButton() { setIsRequesting(true); try { - await navigator.serviceWorker.ready; const nextPermission = await Notification.requestPermission(); setPermission(nextPermission); if (nextPermission === "granted") { - showSuccess("Notification permission enabled"); + const subscribed = await ensureSubscription(); + if (subscribed) showSuccess("Notifications enabled"); } else { showError("Notification permission was not enabled"); } @@ -70,14 +133,22 @@ export function WebPushPermissionButton() { } }; - const statusText = - permission === "granted" - ? "Browser notification permission is enabled on this device." - : permission === "denied" - ? "Browser notification permission is blocked in system settings." - : isStandalone - ? "Enable browser notification permission for this home-screen app." - : "Install ZotMeet to your home screen to enable iOS browser notifications."; + let statusText = + "Install ZotMeet to your home screen to enable iOS browser notifications."; + if (permission === "granted" && subscriptionState === "subscribed") { + statusText = "Device notifications are enabled on this home-screen app."; + } else if (permission === "granted" && subscriptionState === "error") { + statusText = + "Permission is enabled, but this device could not be subscribed."; + } else if (permission === "granted") { + statusText = "Finishing device notification setup."; + } else if (permission === "denied") { + statusText = + "Browser notification permission is blocked in system settings."; + } else if (isStandalone) { + statusText = + "Enable browser notification permission for this home-screen app."; + } return ( - {permission === "granted" - ? "Enabled" - : permission === "denied" - ? "Blocked" - : isRequesting - ? "Enabling..." - : "Enable"} + {subscriptionState === "subscribing" + ? "Enabling..." + : permission === "granted" + ? "Enabled" + : permission === "denied" + ? "Blocked" + : isRequesting + ? "Enabling..." + : "Enable"} ); diff --git a/src/db/migrations/0018_push_subscriptions.sql b/src/db/migrations/0018_push_subscriptions.sql new file mode 100644 index 000000000..33ec2263a --- /dev/null +++ b/src/db/migrations/0018_push_subscriptions.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "push_subscriptions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "endpoint" text NOT NULL, + "p256dh" text NOT NULL, + "auth" text NOT NULL, + "user_agent" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "push_subscriptions_endpoint_unique" UNIQUE("endpoint") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "push_subscriptions_user_id_idx" ON "push_subscriptions" USING btree ("user_id"); diff --git a/src/db/migrations/0019_native_push_tokens.sql b/src/db/migrations/0019_native_push_tokens.sql new file mode 100644 index 000000000..a504fe547 --- /dev/null +++ b/src/db/migrations/0019_native_push_tokens.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS "native_push_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "token" text NOT NULL, + "platform" text DEFAULT 'ios' NOT NULL, + "user_agent" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "native_push_tokens_token_unique" UNIQUE("token") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "native_push_tokens" ADD CONSTRAINT "native_push_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "native_push_tokens_user_id_idx" ON "native_push_tokens" USING btree ("user_id"); diff --git a/src/db/migrations/0020_nice_synch.sql b/src/db/migrations/0020_nice_synch.sql new file mode 100644 index 000000000..8a0fb092a --- /dev/null +++ b/src/db/migrations/0020_nice_synch.sql @@ -0,0 +1,37 @@ +CREATE TABLE IF NOT EXISTS "native_push_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "token" text NOT NULL, + "platform" text DEFAULT 'ios' NOT NULL, + "user_agent" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "native_push_tokens_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "push_subscriptions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "endpoint" text NOT NULL, + "p256dh" text NOT NULL, + "auth" text NOT NULL, + "user_agent" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "push_subscriptions_endpoint_unique" UNIQUE("endpoint") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "native_push_tokens" ADD CONSTRAINT "native_push_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "native_push_tokens_user_id_idx" ON "native_push_tokens" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "push_subscriptions_user_id_idx" ON "push_subscriptions" USING btree ("user_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0020_snapshot.json b/src/db/migrations/meta/0020_snapshot.json new file mode 100644 index 000000000..cf94b5070 --- /dev/null +++ b/src/db/migrations/meta/0020_snapshot.json @@ -0,0 +1,1480 @@ +{ + "id": "75e01db7-4aab-4fb6-908a-a1ac6f22e258", + "prevId": "994d6447-5510-4538-867f-4b641182950b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.availabilities": { + "name": "availabilities", + "schema": "", + "columns": { + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "meeting_id": { + "name": "meeting_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "attendance", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "meeting_availabilities": { + "name": "meeting_availabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ifNeeded_availabilities": { + "name": "ifNeeded_availabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "availabilities_member_id_members_id_fk": { + "name": "availabilities_member_id_members_id_fk", + "tableFrom": "availabilities", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "availabilities_meeting_id_meetings_id_fk": { + "name": "availabilities_meeting_id_meetings_id_fk", + "tableFrom": "availabilities", + "tableTo": "meetings", + "columnsFrom": [ + "meeting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "availabilities_member_id_meeting_id_pk": { + "name": "availabilities_member_id_meeting_id_pk", + "columns": [ + "member_id", + "meeting_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_invite_responses": { + "name": "group_invite_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invite_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_invite_responses_invite_id_group_invites_id_fk": { + "name": "group_invite_responses_invite_id_group_invites_id_fk", + "tableFrom": "group_invite_responses", + "tableTo": "group_invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_invite_responses_user_id_users_id_fk": { + "name": "group_invite_responses_user_id_users_id_fk", + "tableFrom": "group_invite_responses", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "group_invite_responses_invite_user_unique": { + "name": "group_invite_responses_invite_user_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_invites": { + "name": "group_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invite_token": { + "name": "invite_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitee_email": { + "name": "invitee_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_invites_group_id_groups_id_fk": { + "name": "group_invites_group_id_groups_id_fk", + "tableFrom": "group_invites", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_invites_inviter_id_users_id_fk": { + "name": "group_invites_inviter_id_users_id_fk", + "tableFrom": "group_invites", + "tableTo": "users", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "group_invites_invite_token_unique": { + "name": "group_invites_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "groups_user_id_users_id_fk": { + "name": "groups_user_id_users_id_fk", + "tableFrom": "groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meeting_invite_responses": { + "name": "meeting_invite_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invite_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "meeting_invite_responses_invite_id_meeting_invites_id_fk": { + "name": "meeting_invite_responses_invite_id_meeting_invites_id_fk", + "tableFrom": "meeting_invite_responses", + "tableTo": "meeting_invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meeting_invite_responses_user_id_users_id_fk": { + "name": "meeting_invite_responses_user_id_users_id_fk", + "tableFrom": "meeting_invite_responses", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "meeting_invite_responses_invite_user_unique": { + "name": "meeting_invite_responses_invite_user_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meeting_invites": { + "name": "meeting_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "meeting_id": { + "name": "meeting_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invite_token": { + "name": "invite_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "meeting_invites_meeting_id_meetings_id_fk": { + "name": "meeting_invites_meeting_id_meetings_id_fk", + "tableFrom": "meeting_invites", + "tableTo": "meetings", + "columnsFrom": [ + "meeting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meeting_invites_inviter_id_users_id_fk": { + "name": "meeting_invites_inviter_id_users_id_fk", + "tableFrom": "meeting_invites", + "tableTo": "users", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "meeting_invites_invite_token_unique": { + "name": "meeting_invites_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meetings": { + "name": "meetings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled": { + "name": "scheduled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "from_time": { + "name": "from_time", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "to_time": { + "name": "to_time", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "host_id": { + "name": "host_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dates": { + "name": "dates", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "meeting_type": { + "name": "meeting_type", + "type": "meeting_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dates'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "members_can_invite": { + "name": "members_can_invite", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "meetings_group_id_groups_id_fk": { + "name": "meetings_group_id_groups_id_fk", + "tableFrom": "meetings", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meetings_host_id_members_id_fk": { + "name": "meetings_host_id_members_id_fk", + "tableFrom": "meetings", + "tableTo": "members", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_name": { + "name": "google_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "school": { + "name": "school", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_picture": { + "name": "profile_picture", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "members_username_unique": { + "name": "members_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_push_tokens": { + "name": "native_push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ios'" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "native_push_tokens_user_id_idx": { + "name": "native_push_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "native_push_tokens_user_id_users_id_fk": { + "name": "native_push_tokens_user_id_users_id_fk", + "tableFrom": "native_push_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "native_push_tokens_token_unique": { + "name": "native_push_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_preferences": { + "name": "notification_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "meeting_invites": { + "name": "meeting_invites", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "group_invites": { + "name": "group_invites", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "nudges": { + "name": "nudges", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_preferences_member_id_members_id_fk": { + "name": "notification_preferences_member_id_members_id_fk", + "tableFrom": "notification_preferences", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_preferences_member_id_unique": { + "name": "notification_preferences_member_id_unique", + "nullsNotDistinct": false, + "columns": [ + "member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect": { + "name": "redirect", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_members_id_fk": { + "name": "notifications_user_id_members_id_fk", + "tableFrom": "notifications", + "tableTo": "members", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_group_id_groups_id_fk": { + "name": "notifications_group_id_groups_id_fk", + "tableFrom": "notifications", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_accounts": { + "name": "oauth_accounts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_accounts_user_id_users_id_fk": { + "name": "oauth_accounts_user_id_users_id_fk", + "tableFrom": "oauth_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oauth_accounts_provider_id_provider_user_id_pk": { + "name": "oauth_accounts_provider_id_provider_user_id_pk", + "columns": [ + "provider_id", + "provider_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_subscriptions": { + "name": "push_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_subscriptions_user_id_idx": { + "name": "push_subscriptions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscriptions_user_id_users_id_fk": { + "name": "push_subscriptions_user_id_users_id_fk", + "tableFrom": "push_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "push_subscriptions_endpoint_unique": { + "name": "push_subscriptions_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "endpoint" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_meetings": { + "name": "scheduled_meetings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "meeting_id": { + "name": "meeting_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scheduled_date": { + "name": "scheduled_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scheduled_from_time": { + "name": "scheduled_from_time", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "scheduled_to_time": { + "name": "scheduled_to_time", + "type": "time", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "scheduled_meetings_meeting_id_meetings_id_fk": { + "name": "scheduled_meetings_meeting_id_meetings_id_fk", + "tableFrom": "scheduled_meetings", + "tableTo": "meetings", + "columnsFrom": [ + "meeting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_access_token": { + "name": "oidc_access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_refresh_token": { + "name": "oidc_refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_access_token": { + "name": "google_access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_refresh_token": { + "name": "google_refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_access_token_expires_at": { + "name": "google_access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_idx_sessions": { + "name": "user_idx_sessions", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "theme_mode": { + "name": "theme_mode", + "type": "theme_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + } + }, + "indexes": {}, + "foreignKeys": { + "users_member_id_members_id_fk": { + "name": "users_member_id_members_id_fk", + "tableFrom": "users", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_in_group": { + "name": "users_in_group", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "group_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": { + "users_in_group_user_id_users_id_fk": { + "name": "users_in_group_user_id_users_id_fk", + "tableFrom": "users_in_group", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_in_group_group_id_groups_id_fk": { + "name": "users_in_group_group_id_groups_id_fk", + "tableFrom": "users_in_group", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_in_group_group_id_user_id_pk": { + "name": "users_in_group_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.attendance": { + "name": "attendance", + "schema": "public", + "values": [ + "accepted", + "maybe", + "declined" + ] + }, + "public.group_role": { + "name": "group_role", + "schema": "public", + "values": [ + "member", + "admin" + ] + }, + "public.invite_status": { + "name": "invite_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "declined", + "expired" + ] + }, + "public.meeting_type": { + "name": "meeting_type", + "schema": "public", + "values": [ + "dates", + "days" + ] + }, + "public.theme_mode": { + "name": "theme_mode", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 054d925e6..b55d98f4b 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -127,6 +127,27 @@ "when": 1779145500000, "tag": "0017_notification_preferences", "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1779993600000, + "tag": "0018_push_subscriptions", + "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1780080000000, + "tag": "0019_native_push_tokens", + "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1779982875335, + "tag": "0020_nice_synch", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index a6f0bbbff..86ffdddbf 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -347,6 +347,69 @@ export const notificationPreferencesRelations = relations( }), ); +export const nativePushTokens = pgTable( + "native_push_tokens", + { + id: uuid("id").defaultRandom().primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + token: text("token").notNull().unique(), + platform: text("platform").notNull().default("ios"), + userAgent: text("user_agent"), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "date", + }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + mode: "date", + }) + .defaultNow() + .notNull(), + }, + (table) => ({ + userIdIdx: index("native_push_tokens_user_id_idx").on(table.userId), + }), +); + +export type SelectNativePushToken = InferSelectModel; +export type InsertNativePushToken = InferInsertModel; + +export const pushSubscriptions = pgTable( + "push_subscriptions", + { + id: uuid("id").defaultRandom().primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + endpoint: text("endpoint").notNull().unique(), + p256dh: text("p256dh").notNull(), + auth: text("auth").notNull(), + userAgent: text("user_agent"), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "date", + }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + mode: "date", + }) + .defaultNow() + .notNull(), + }, + (table) => ({ + userIdIdx: index("push_subscriptions_user_id_idx").on(table.userId), + }), +); + +export type SelectPushSubscription = InferSelectModel; +export type InsertPushSubscription = InferInsertModel; + export const availabilities = pgTable( "availabilities", { diff --git a/src/lib/push/send-push.ts b/src/lib/push/send-push.ts index 3e71f83f4..50f98f130 100644 --- a/src/lib/push/send-push.ts +++ b/src/lib/push/send-push.ts @@ -1,7 +1,11 @@ import "server-only"; +import { inArray } from "drizzle-orm"; import { cert, getApps, initializeApp } from "firebase-admin/app"; import { getMessaging } from "firebase-admin/messaging"; +import { db } from "@/db"; +import { nativePushTokens } from "@/db/schema"; +import { sendWebPushToUsers } from "@/lib/push/web-push"; type PushPayload = { title: string; @@ -76,20 +80,43 @@ function getOrInitFirebaseMessaging() { return getMessaging(app); } -function topicForUser(userId: string) { - return `user_${userId}`; +function chunkArray(items: T[], size: number) { + const chunks: T[][] = []; + for (let index = 0; index < items.length; index += size) { + chunks.push(items.slice(index, index + size)); + } + return chunks; +} + +function isStaleTokenError(code: string | undefined) { + return ( + code === "messaging/invalid-registration-token" || + code === "messaging/registration-token-not-registered" + ); } export async function sendPushToUsers(userIds: string[], payload: PushPayload) { if (userIds.length === 0) return; + await sendWebPushToUsers(userIds); + const messaging = getOrInitFirebaseMessaging(); if (!messaging) return; - const sendResults = await Promise.allSettled( - userIds.map((userId) => - messaging.send({ - topic: topicForUser(userId), + const tokenRows = await db + .select({ token: nativePushTokens.token }) + .from(nativePushTokens) + .where(inArray(nativePushTokens.userId, userIds)); + + const tokens = [...new Set(tokenRows.map((row) => row.token))]; + if (tokens.length === 0) return; + + const staleTokens = new Set(); + + for (const tokenChunk of chunkArray(tokens, 500)) { + try { + const response = await messaging.sendEachForMulticast({ + tokens: tokenChunk, notification: { title: payload.title, body: payload.message, @@ -109,13 +136,27 @@ export async function sendPushToUsers(userIds: string[], payload: PushPayload) { }, }, }, - }), - ), - ); + }); + + response.responses.forEach((sendResponse, index) => { + if (sendResponse.success) return; - for (const result of sendResults) { - if (result.status === "rejected") { - console.error("Failed to send push notification:", result.reason); + const errorCode = sendResponse.error?.code; + if (isStaleTokenError(errorCode)) { + staleTokens.add(tokenChunk[index]); + return; + } + + console.error("Failed to send push notification:", sendResponse.error); + }); + } catch (error) { + console.error("Failed to send push notification batch:", error); } } + + if (staleTokens.size > 0) { + await db + .delete(nativePushTokens) + .where(inArray(nativePushTokens.token, [...staleTokens])); + } } diff --git a/src/lib/push/web-push.ts b/src/lib/push/web-push.ts new file mode 100644 index 000000000..250743d48 --- /dev/null +++ b/src/lib/push/web-push.ts @@ -0,0 +1,126 @@ +import "server-only"; + +import { webcrypto } from "node:crypto"; +import { inArray } from "drizzle-orm"; +import { db } from "@/db"; +import { pushSubscriptions } from "@/db/schema"; + +type StoredPushSubscription = typeof pushSubscriptions.$inferSelect; + +const WEB_PUSH_PUBLIC_KEY = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY; +const WEB_PUSH_PRIVATE_KEY = process.env.WEB_PUSH_PRIVATE_KEY; +const WEB_PUSH_SUBJECT = + process.env.WEB_PUSH_SUBJECT ?? "mailto:support@zotmeet.com"; + +let warnedMissingConfig = false; + +function base64UrlToBuffer(value: string) { + const base64 = value.replace(/-/g, "+").replace(/_/g, "/"); + const padding = "=".repeat((4 - (base64.length % 4)) % 4); + return Buffer.from(base64 + padding, "base64"); +} + +function bufferToBase64Url(value: Buffer | ArrayBuffer) { + const buffer = value instanceof ArrayBuffer ? Buffer.from(value) : value; + return buffer + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); +} + +async function createVapidAuthorization(endpoint: string) { + if (!WEB_PUSH_PUBLIC_KEY || !WEB_PUSH_PRIVATE_KEY) { + if (!warnedMissingConfig) { + console.warn( + "Web Push notifications are disabled: missing VAPID key configuration.", + ); + warnedMissingConfig = true; + } + return null; + } + + const publicKey = base64UrlToBuffer(WEB_PUSH_PUBLIC_KEY); + const privateKey = base64UrlToBuffer(WEB_PUSH_PRIVATE_KEY); + + if (publicKey.length !== 65 || privateKey.length !== 32) { + console.warn("Web Push notifications are disabled: invalid VAPID keys."); + return null; + } + + const aud = new URL(endpoint).origin; + const exp = Math.floor(Date.now() / 1000) + 12 * 60 * 60; + const tokenHeader = bufferToBase64Url( + Buffer.from(JSON.stringify({ typ: "JWT", alg: "ES256" })), + ); + const tokenBody = bufferToBase64Url( + Buffer.from(JSON.stringify({ aud, exp, sub: WEB_PUSH_SUBJECT })), + ); + const tokenInput = `${tokenHeader}.${tokenBody}`; + + const key = await webcrypto.subtle.importKey( + "jwk", + { + kty: "EC", + crv: "P-256", + x: bufferToBase64Url(publicKey.subarray(1, 33)), + y: bufferToBase64Url(publicKey.subarray(33, 65)), + d: bufferToBase64Url(privateKey), + ext: false, + }, + { name: "ECDSA", namedCurve: "P-256" }, + false, + ["sign"], + ); + + const signature = await webcrypto.subtle.sign( + { name: "ECDSA", hash: "SHA-256" }, + key, + Buffer.from(tokenInput), + ); + const jwt = `${tokenInput}.${bufferToBase64Url(signature)}`; + + return `vapid t=${jwt}, k=${WEB_PUSH_PUBLIC_KEY}`; +} + +async function sendWebPush(subscription: StoredPushSubscription) { + const authorization = await createVapidAuthorization(subscription.endpoint); + if (!authorization) return; + + const response = await fetch(subscription.endpoint, { + method: "POST", + headers: { + Authorization: authorization, + TTL: "2419200", + Urgency: "normal", + }, + }); + + if (response.status === 404 || response.status === 410) { + await db + .delete(pushSubscriptions) + .where(inArray(pushSubscriptions.endpoint, [subscription.endpoint])); + return; + } + + if (!response.ok) { + console.error( + "Failed to send Web Push notification:", + response.status, + await response.text(), + ); + } +} + +export async function sendWebPushToUsers(userIds: string[]) { + if (userIds.length === 0) return; + + const subscriptions = await db + .select() + .from(pushSubscriptions) + .where(inArray(pushSubscriptions.userId, userIds)); + + await Promise.allSettled( + subscriptions.map((subscription) => sendWebPush(subscription)), + ); +} diff --git a/src/server/data/user/queries.ts b/src/server/data/user/queries.ts index 57ca27f05..8a3eeb58d 100644 --- a/src/server/data/user/queries.ts +++ b/src/server/data/user/queries.ts @@ -276,17 +276,21 @@ export async function createNewNotification( } } - await sendPushToUsers( - allowedRecipients.map((recipient) => recipient.userId), - { - title, - message, - type, - redirect: link, - groupId, - createdBy, - }, - ); + try { + await sendPushToUsers( + allowedRecipients.map((recipient) => recipient.userId), + { + title, + message, + type, + redirect: link, + groupId, + createdBy, + }, + ); + } catch (error) { + console.error("Failed to send push notification:", error); + } return notificationsCreated; } From 96b6de589e82ff3db4c58970301ecdd20e24cdee Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 09:00:48 -0700 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=F0=9F=90=9B=20rm=20web=20push=20not?= =?UTF-8?q?if=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/src/ZotMeet/AppDelegate.swift | 22 +- ios/src/ZotMeet/PushNotifications.swift | 60 +++--- public/sw-register.js | 18 -- public/sw.js | 73 ------- src/app/api/push-subscriptions/route.ts | 86 -------- src/app/api/push-tokens/route.ts | 20 +- src/components/nav/mui-app-shell.tsx | 2 +- .../profile/notifications-panel.tsx | 2 - .../push/native-ios-push-bridge.tsx | 72 ++++++- .../push/web-push-permission-button.tsx | 192 ------------------ src/db/migrations/0018_push_subscriptions.sql | 19 -- src/db/migrations/0020_nice_synch.sql | 37 ---- ...{0020_snapshot.json => 0019_snapshot.json} | 104 +--------- src/db/migrations/meta/_journal.json | 16 +- src/db/schema.ts | 32 --- src/lib/push/send-push.ts | 3 - src/lib/push/web-push.ts | 126 ------------ 17 files changed, 136 insertions(+), 748 deletions(-) delete mode 100644 src/app/api/push-subscriptions/route.ts delete mode 100644 src/components/push/web-push-permission-button.tsx delete mode 100644 src/db/migrations/0018_push_subscriptions.sql delete mode 100644 src/db/migrations/0020_nice_synch.sql rename src/db/migrations/meta/{0020_snapshot.json => 0019_snapshot.json} (93%) delete mode 100644 src/lib/push/web-push.ts diff --git a/ios/src/ZotMeet/AppDelegate.swift b/ios/src/ZotMeet/AppDelegate.swift index 7be1e42bf..6ced388f2 100644 --- a/ios/src/ZotMeet/AppDelegate.swift +++ b/ios/src/ZotMeet/AppDelegate.swift @@ -21,6 +21,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // [START register_for_notifications] UNUserNotificationCenter.current().delegate = self + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .authorized, .ephemeral, .provisional: + registerForPushNotifications() + default: + return + } + } // let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] // UNUserNotificationCenter.current().requestAuthorization( @@ -45,8 +53,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Message ID 1: \(messageID)") } - // Print full message. +#if DEBUG print("push userInfo 1:", userInfo) +#endif sendPushToWebView(userInfo: userInfo) } @@ -61,8 +70,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Message ID 2: \(messageID)") } - // Print full message. ** +#if DEBUG print("push userInfo 2:", userInfo) +#endif sendPushToWebView(userInfo: userInfo) completionHandler(UIBackgroundFetchResult.newData) @@ -99,8 +109,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Message ID: 3 \(messageID)") } - // Print full message. +#if DEBUG print("push userInfo 3:", userInfo) +#endif sendPushToWebView(userInfo: userInfo) // Change this to your preferred presentation option @@ -118,8 +129,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // With swizzling disabled you must let Messaging know about the message, for Analytics // Messaging.messaging().appDidReceiveMessage(userInfo) - // Print full message. +#if DEBUG print("push userInfo 4:", userInfo) +#endif sendPushClickToWebView(userInfo: userInfo) completionHandler() @@ -130,7 +142,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension AppDelegate : MessagingDelegate { // [START refresh_token] func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { +#if DEBUG print("Firebase registration token: \(String(describing: fcmToken))") +#endif let dataDict:[String: String] = ["token": fcmToken ?? ""] NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict) diff --git a/ios/src/ZotMeet/PushNotifications.swift b/ios/src/ZotMeet/PushNotifications.swift index 1bed14b4a..311d46d71 100644 --- a/ios/src/ZotMeet/PushNotifications.swift +++ b/ios/src/ZotMeet/PushNotifications.swift @@ -1,6 +1,13 @@ +import UIKit import WebKit import FirebaseMessaging +func registerForPushNotifications() { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } +} + class SubscribeMessage { var topic = "" var eventValue = "" @@ -82,36 +89,34 @@ func returnPermissionState(state: String){ func handlePushPermission() { UNUserNotificationCenter.current().getNotificationSettings () { settings in - switch settings.authorizationStatus { - case .notDetermined: - let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] - UNUserNotificationCenter.current().requestAuthorization( - options: authOptions, - completionHandler: { (success, error) in - if error == nil { - if success == true { - returnPermissionResult(isGranted: true) - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - else { - returnPermissionResult(isGranted: false) - } - } - else { + switch settings.authorizationStatus { + case .notDetermined: + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: { (success, error) in + if error == nil { + if success == true { + registerForPushNotifications() + returnPermissionResult(isGranted: true) + } else { returnPermissionResult(isGranted: false) } } - ) - case .denied: - returnPermissionResult(isGranted: false) - case .authorized, .ephemeral, .provisional: - returnPermissionResult(isGranted: true) - @unknown default: - return; - } + else { + returnPermissionResult(isGranted: false) + } + } + ) + case .denied: + returnPermissionResult(isGranted: false) + case .authorized, .ephemeral, .provisional: + registerForPushNotifications() + returnPermissionResult(isGranted: true) + @unknown default: + return; } + } } func handlePushState() { UNUserNotificationCenter.current().getNotificationSettings () { settings in @@ -121,10 +126,13 @@ func handlePushState() { case .denied: returnPermissionState(state: "denied") case .authorized: + registerForPushNotifications() returnPermissionState(state: "authorized") case .ephemeral: + registerForPushNotifications() returnPermissionState(state: "ephemeral") case .provisional: + registerForPushNotifications() returnPermissionState(state: "provisional") @unknown default: returnPermissionState(state: "unknown") diff --git a/public/sw-register.js b/public/sw-register.js index 87d5d2122..cbedf21f5 100644 --- a/public/sw-register.js +++ b/public/sw-register.js @@ -28,22 +28,4 @@ .catch((err) => { console.warn("[sw] registration failed", err); }); - - function getSameOriginRedirect(value) { - if (typeof value !== "string") return "/summary"; - - try { - const url = new URL(value, window.location.origin); - if (url.origin !== window.location.origin) return "/summary"; - return url.pathname + url.search + url.hash; - } catch (_err) { - return "/summary"; - } - } - - navigator.serviceWorker.addEventListener("message", (event) => { - if (event.data && event.data.type === "notification-click") { - window.location.assign(getSameOriginRedirect(event.data.redirect)); - } - }); })(); diff --git a/public/sw.js b/public/sw.js index 4c3e54f91..9d38ed0f8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -153,76 +153,3 @@ self.addEventListener("message", (event) => { self.skipWaiting(); } }); - -function getPushPayload(event) { - if (!event.data) { - return { - title: "ZotMeet", - message: "You have a new notification.", - redirect: "/summary", - }; - } - - try { - return event.data.json(); - } catch (_err) { - return { - title: "ZotMeet", - message: event.data.text(), - redirect: "/summary", - }; - } -} - -function getSameOriginRedirect(value) { - if (typeof value !== "string") return "/summary"; - - try { - const url = new URL(value, self.location.origin); - if (url.origin !== self.location.origin) return "/summary"; - return `${url.pathname}${url.search}${url.hash}`; - } catch (_err) { - return "/summary"; - } -} - -self.addEventListener("push", (event) => { - const payload = getPushPayload(event); - const title = payload.title || "ZotMeet"; - const redirect = getSameOriginRedirect(payload.redirect || payload.url); - - event.waitUntil( - self.registration.showNotification(title, { - body: payload.message || payload.body || "You have a new notification.", - icon: "/icons/icon-192.png", - badge: "/icons/icon-96.png", - data: { redirect }, - }), - ); -}); - -self.addEventListener("notificationclick", (event) => { - event.notification.close(); - - const redirect = getSameOriginRedirect(event.notification.data?.redirect); - const targetUrl = new URL(redirect, self.location.origin).href; - - event.waitUntil( - (async () => { - const clientList = await clients.matchAll({ - type: "window", - includeUncontrolled: true, - }); - - for (const client of clientList) { - if (new URL(client.url).origin === self.location.origin) { - await client.focus(); - client.postMessage({ type: "notification-click", redirect }); - return; - } - } - - await clients.openWindow(targetUrl); - })(), - ); -}); diff --git a/src/app/api/push-subscriptions/route.ts b/src/app/api/push-subscriptions/route.ts deleted file mode 100644 index 1fcd798ea..000000000 --- a/src/app/api/push-subscriptions/route.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { and, eq } from "drizzle-orm"; -import { NextResponse } from "next/server"; -import { db } from "@/db"; -import { pushSubscriptions } from "@/db/schema"; -import { getCurrentSession } from "@/lib/auth"; - -type PushSubscriptionPayload = { - endpoint?: unknown; - keys?: { - p256dh?: unknown; - auth?: unknown; - }; -}; - -function isValidSubscriptionPayload( - payload: PushSubscriptionPayload, -): payload is { - endpoint: string; - keys: { p256dh: string; auth: string }; -} { - return ( - typeof payload.endpoint === "string" && - typeof payload.keys?.p256dh === "string" && - typeof payload.keys.auth === "string" - ); -} - -export async function POST(request: Request) { - const { user } = await getCurrentSession(); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const payload = (await request.json()) as PushSubscriptionPayload; - if (!isValidSubscriptionPayload(payload)) { - return NextResponse.json( - { error: "Invalid push subscription" }, - { status: 400 }, - ); - } - - await db - .insert(pushSubscriptions) - .values({ - userId: user.id, - endpoint: payload.endpoint, - p256dh: payload.keys.p256dh, - auth: payload.keys.auth, - userAgent: request.headers.get("user-agent"), - }) - .onConflictDoUpdate({ - target: pushSubscriptions.endpoint, - set: { - userId: user.id, - p256dh: payload.keys.p256dh, - auth: payload.keys.auth, - userAgent: request.headers.get("user-agent"), - updatedAt: new Date(), - }, - }); - - return NextResponse.json({ success: true }); -} - -export async function DELETE(request: Request) { - const { user } = await getCurrentSession(); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const payload = (await request.json()) as { endpoint?: unknown }; - if (typeof payload.endpoint !== "string") { - return NextResponse.json({ error: "Invalid endpoint" }, { status: 400 }); - } - - await db - .delete(pushSubscriptions) - .where( - and( - eq(pushSubscriptions.endpoint, payload.endpoint), - eq(pushSubscriptions.userId, user.id), - ), - ); - - return NextResponse.json({ success: true }); -} diff --git a/src/app/api/push-tokens/route.ts b/src/app/api/push-tokens/route.ts index 86323734d..bc2e05d48 100644 --- a/src/app/api/push-tokens/route.ts +++ b/src/app/api/push-tokens/route.ts @@ -9,6 +9,14 @@ type PushTokenPayload = { platform?: unknown; }; +async function readPayload(request: Request) { + try { + return (await request.json()) as PushTokenPayload; + } catch { + return null; + } +} + function getValidToken(payload: PushTokenPayload) { if (typeof payload.token !== "string") return null; @@ -24,7 +32,11 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const payload = (await request.json()) as PushTokenPayload; + const payload = await readPayload(request); + if (!payload) { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + const token = getValidToken(payload); if (!token) { return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); @@ -59,7 +71,11 @@ export async function DELETE(request: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const payload = (await request.json()) as PushTokenPayload; + const payload = await readPayload(request); + if (!payload) { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + const token = getValidToken(payload); if (!token) { return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); diff --git a/src/components/nav/mui-app-shell.tsx b/src/components/nav/mui-app-shell.tsx index 73e14de6a..1bb030383 100644 --- a/src/components/nav/mui-app-shell.tsx +++ b/src/components/nav/mui-app-shell.tsx @@ -38,7 +38,7 @@ export function MuiAppShell({ minHeight: "100vh", }} > - {user ? : null} + {user ? : null} {!isMobile && } Notifications - {NOTIFICATION_PREF_OPTIONS.map((option) => ( { if (!isNativeIosApp()) return; if (!window.webkit?.messageHandlers) return; @@ -82,12 +126,18 @@ export function NativeIosPushBridge({ } }; + const handleNotificationClick = (event: Event) => { + const payload = parseNativePushPayload((event as CustomEvent).detail); + router.push(getNotificationRedirect(payload)); + }; + window.addEventListener("push-permission-state", handlePermissionState); window.addEventListener( "push-permission-request", handlePermissionRequestResult, ); window.addEventListener("push-token", handlePushToken); + window.addEventListener("push-notification-click", handleNotificationClick); postMessage("push-permission-state"); @@ -101,8 +151,12 @@ export function NativeIosPushBridge({ handlePermissionRequestResult, ); window.removeEventListener("push-token", handlePushToken); + window.removeEventListener( + "push-notification-click", + handleNotificationClick, + ); }; - }, []); + }, [router]); return null; } diff --git a/src/components/push/web-push-permission-button.tsx b/src/components/push/web-push-permission-button.tsx deleted file mode 100644 index 9944cdc41..000000000 --- a/src/components/push/web-push-permission-button.tsx +++ /dev/null @@ -1,192 +0,0 @@ -"use client"; - -import NotificationsActiveOutlinedIcon from "@mui/icons-material/NotificationsActiveOutlined"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { useCallback, useEffect, useState } from "react"; -import { useSnackbar } from "@/components/ui/snackbar-provider"; -import { isNativeIosApp } from "@/lib/platform"; - -type PermissionState = NotificationPermission | "unsupported"; -type SubscriptionState = "idle" | "subscribing" | "subscribed" | "error"; - -const WEB_PUSH_PUBLIC_KEY = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY; - -function browserSupportsNotifications() { - return ( - typeof window !== "undefined" && - "Notification" in window && - "serviceWorker" in navigator - ); -} - -function isStandaloneWebApp() { - if (typeof window === "undefined") return false; - - return ( - window.matchMedia("(display-mode: standalone)").matches || - ("standalone" in navigator && - (navigator as Navigator & { standalone?: boolean }).standalone === true) - ); -} - -function getInitialPermission(): PermissionState { - if (!browserSupportsNotifications()) return "unsupported"; - return Notification.permission; -} - -function base64UrlToUint8Array(value: string) { - const padding = "=".repeat((4 - (value.length % 4)) % 4); - const base64 = (value + padding).replace(/-/g, "+").replace(/_/g, "/"); - const raw = window.atob(base64); - const bytes = new Uint8Array(raw.length); - - for (let i = 0; i < raw.length; i += 1) { - bytes[i] = raw.charCodeAt(i); - } - - return bytes; -} - -export function WebPushPermissionButton() { - const [permission, setPermission] = - useState(getInitialPermission); - const [isStandalone, setIsStandalone] = useState(false); - const [isNative, setIsNative] = useState(false); - const [isRequesting, setIsRequesting] = useState(false); - const [subscriptionState, setSubscriptionState] = - useState("idle"); - const { showSuccess, showError } = useSnackbar(); - - const ensureSubscription = useCallback(async () => { - if (!browserSupportsNotifications()) return false; - if (!WEB_PUSH_PUBLIC_KEY) { - setSubscriptionState("error"); - showError("Push notifications are not configured for this deployment"); - return false; - } - - setSubscriptionState("subscribing"); - - try { - const registration = await navigator.serviceWorker.ready; - const existing = await registration.pushManager.getSubscription(); - const subscription = - existing ?? - (await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: base64UrlToUint8Array(WEB_PUSH_PUBLIC_KEY), - })); - - const response = await fetch("/api/push-subscriptions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(subscription.toJSON()), - }); - - if (!response.ok) { - throw new Error("Failed to save push subscription"); - } - - setSubscriptionState("subscribed"); - return true; - } catch { - setSubscriptionState("error"); - showError("Failed to enable push notifications"); - return false; - } - }, [showError]); - - useEffect(() => { - setPermission(getInitialPermission()); - setIsStandalone(isStandaloneWebApp()); - setIsNative(isNativeIosApp()); - }, []); - - useEffect(() => { - if (permission === "granted" && isStandalone && !isNative) { - void ensureSubscription(); - } - }, [permission, isStandalone, isNative, ensureSubscription]); - - if (isNative || permission === "unsupported") return null; - - const handleEnable = async () => { - if (!browserSupportsNotifications()) return; - - setIsRequesting(true); - try { - const nextPermission = await Notification.requestPermission(); - setPermission(nextPermission); - - if (nextPermission === "granted") { - const subscribed = await ensureSubscription(); - if (subscribed) showSuccess("Notifications enabled"); - } else { - showError("Notification permission was not enabled"); - } - } catch { - showError("Failed to request notification permission"); - } finally { - setIsRequesting(false); - } - }; - - let statusText = - "Install ZotMeet to your home screen to enable iOS browser notifications."; - if (permission === "granted" && subscriptionState === "subscribed") { - statusText = "Device notifications are enabled on this home-screen app."; - } else if (permission === "granted" && subscriptionState === "error") { - statusText = - "Permission is enabled, but this device could not be subscribed."; - } else if (permission === "granted") { - statusText = "Finishing device notification setup."; - } else if (permission === "denied") { - statusText = - "Browser notification permission is blocked in system settings."; - } else if (isStandalone) { - statusText = - "Enable browser notification permission for this home-screen app."; - } - - return ( - - - - Device Notifications - - - {statusText} - - - - - ); -} diff --git a/src/db/migrations/0018_push_subscriptions.sql b/src/db/migrations/0018_push_subscriptions.sql deleted file mode 100644 index 33ec2263a..000000000 --- a/src/db/migrations/0018_push_subscriptions.sql +++ /dev/null @@ -1,19 +0,0 @@ -CREATE TABLE IF NOT EXISTS "push_subscriptions" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" text NOT NULL, - "endpoint" text NOT NULL, - "p256dh" text NOT NULL, - "auth" text NOT NULL, - "user_agent" text, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "push_subscriptions_endpoint_unique" UNIQUE("endpoint") -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE INDEX IF NOT EXISTS "push_subscriptions_user_id_idx" ON "push_subscriptions" USING btree ("user_id"); diff --git a/src/db/migrations/0020_nice_synch.sql b/src/db/migrations/0020_nice_synch.sql deleted file mode 100644 index 8a0fb092a..000000000 --- a/src/db/migrations/0020_nice_synch.sql +++ /dev/null @@ -1,37 +0,0 @@ -CREATE TABLE IF NOT EXISTS "native_push_tokens" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" text NOT NULL, - "token" text NOT NULL, - "platform" text DEFAULT 'ios' NOT NULL, - "user_agent" text, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "native_push_tokens_token_unique" UNIQUE("token") -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "push_subscriptions" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" text NOT NULL, - "endpoint" text NOT NULL, - "p256dh" text NOT NULL, - "auth" text NOT NULL, - "user_agent" text, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "push_subscriptions_endpoint_unique" UNIQUE("endpoint") -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "native_push_tokens" ADD CONSTRAINT "native_push_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE INDEX IF NOT EXISTS "native_push_tokens_user_id_idx" ON "native_push_tokens" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "push_subscriptions_user_id_idx" ON "push_subscriptions" USING btree ("user_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0020_snapshot.json b/src/db/migrations/meta/0019_snapshot.json similarity index 93% rename from src/db/migrations/meta/0020_snapshot.json rename to src/db/migrations/meta/0019_snapshot.json index cf94b5070..d49efb5be 100644 --- a/src/db/migrations/meta/0020_snapshot.json +++ b/src/db/migrations/meta/0019_snapshot.json @@ -1033,108 +1033,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.push_subscriptions": { - "name": "push_subscriptions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "endpoint": { - "name": "endpoint", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "p256dh": { - "name": "p256dh", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "auth": { - "name": "auth", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "push_subscriptions_user_id_idx": { - "name": "push_subscriptions_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "push_subscriptions_user_id_users_id_fk": { - "name": "push_subscriptions_user_id_users_id_fk", - "tableFrom": "push_subscriptions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "push_subscriptions_endpoint_unique": { - "name": "push_subscriptions_endpoint_unique", - "nullsNotDistinct": false, - "columns": [ - "endpoint" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.scheduled_meetings": { "name": "scheduled_meetings", "schema": "", @@ -1477,4 +1375,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index b55d98f4b..1988fabd9 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -131,23 +131,9 @@ { "idx": 18, "version": "7", - "when": 1779993600000, - "tag": "0018_push_subscriptions", - "breakpoints": true - }, - { - "idx": 19, - "version": "7", "when": 1780080000000, "tag": "0019_native_push_tokens", "breakpoints": true - }, - { - "idx": 20, - "version": "7", - "when": 1779982875335, - "tag": "0020_nice_synch", - "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 86ffdddbf..f2553b0dd 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -378,38 +378,6 @@ export const nativePushTokens = pgTable( export type SelectNativePushToken = InferSelectModel; export type InsertNativePushToken = InferInsertModel; -export const pushSubscriptions = pgTable( - "push_subscriptions", - { - id: uuid("id").defaultRandom().primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - endpoint: text("endpoint").notNull().unique(), - p256dh: text("p256dh").notNull(), - auth: text("auth").notNull(), - userAgent: text("user_agent"), - createdAt: timestamp("created_at", { - withTimezone: true, - mode: "date", - }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { - withTimezone: true, - mode: "date", - }) - .defaultNow() - .notNull(), - }, - (table) => ({ - userIdIdx: index("push_subscriptions_user_id_idx").on(table.userId), - }), -); - -export type SelectPushSubscription = InferSelectModel; -export type InsertPushSubscription = InferInsertModel; - export const availabilities = pgTable( "availabilities", { diff --git a/src/lib/push/send-push.ts b/src/lib/push/send-push.ts index 50f98f130..2bd26cbde 100644 --- a/src/lib/push/send-push.ts +++ b/src/lib/push/send-push.ts @@ -5,7 +5,6 @@ import { cert, getApps, initializeApp } from "firebase-admin/app"; import { getMessaging } from "firebase-admin/messaging"; import { db } from "@/db"; import { nativePushTokens } from "@/db/schema"; -import { sendWebPushToUsers } from "@/lib/push/web-push"; type PushPayload = { title: string; @@ -98,8 +97,6 @@ function isStaleTokenError(code: string | undefined) { export async function sendPushToUsers(userIds: string[], payload: PushPayload) { if (userIds.length === 0) return; - await sendWebPushToUsers(userIds); - const messaging = getOrInitFirebaseMessaging(); if (!messaging) return; diff --git a/src/lib/push/web-push.ts b/src/lib/push/web-push.ts deleted file mode 100644 index 250743d48..000000000 --- a/src/lib/push/web-push.ts +++ /dev/null @@ -1,126 +0,0 @@ -import "server-only"; - -import { webcrypto } from "node:crypto"; -import { inArray } from "drizzle-orm"; -import { db } from "@/db"; -import { pushSubscriptions } from "@/db/schema"; - -type StoredPushSubscription = typeof pushSubscriptions.$inferSelect; - -const WEB_PUSH_PUBLIC_KEY = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY; -const WEB_PUSH_PRIVATE_KEY = process.env.WEB_PUSH_PRIVATE_KEY; -const WEB_PUSH_SUBJECT = - process.env.WEB_PUSH_SUBJECT ?? "mailto:support@zotmeet.com"; - -let warnedMissingConfig = false; - -function base64UrlToBuffer(value: string) { - const base64 = value.replace(/-/g, "+").replace(/_/g, "/"); - const padding = "=".repeat((4 - (base64.length % 4)) % 4); - return Buffer.from(base64 + padding, "base64"); -} - -function bufferToBase64Url(value: Buffer | ArrayBuffer) { - const buffer = value instanceof ArrayBuffer ? Buffer.from(value) : value; - return buffer - .toString("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, ""); -} - -async function createVapidAuthorization(endpoint: string) { - if (!WEB_PUSH_PUBLIC_KEY || !WEB_PUSH_PRIVATE_KEY) { - if (!warnedMissingConfig) { - console.warn( - "Web Push notifications are disabled: missing VAPID key configuration.", - ); - warnedMissingConfig = true; - } - return null; - } - - const publicKey = base64UrlToBuffer(WEB_PUSH_PUBLIC_KEY); - const privateKey = base64UrlToBuffer(WEB_PUSH_PRIVATE_KEY); - - if (publicKey.length !== 65 || privateKey.length !== 32) { - console.warn("Web Push notifications are disabled: invalid VAPID keys."); - return null; - } - - const aud = new URL(endpoint).origin; - const exp = Math.floor(Date.now() / 1000) + 12 * 60 * 60; - const tokenHeader = bufferToBase64Url( - Buffer.from(JSON.stringify({ typ: "JWT", alg: "ES256" })), - ); - const tokenBody = bufferToBase64Url( - Buffer.from(JSON.stringify({ aud, exp, sub: WEB_PUSH_SUBJECT })), - ); - const tokenInput = `${tokenHeader}.${tokenBody}`; - - const key = await webcrypto.subtle.importKey( - "jwk", - { - kty: "EC", - crv: "P-256", - x: bufferToBase64Url(publicKey.subarray(1, 33)), - y: bufferToBase64Url(publicKey.subarray(33, 65)), - d: bufferToBase64Url(privateKey), - ext: false, - }, - { name: "ECDSA", namedCurve: "P-256" }, - false, - ["sign"], - ); - - const signature = await webcrypto.subtle.sign( - { name: "ECDSA", hash: "SHA-256" }, - key, - Buffer.from(tokenInput), - ); - const jwt = `${tokenInput}.${bufferToBase64Url(signature)}`; - - return `vapid t=${jwt}, k=${WEB_PUSH_PUBLIC_KEY}`; -} - -async function sendWebPush(subscription: StoredPushSubscription) { - const authorization = await createVapidAuthorization(subscription.endpoint); - if (!authorization) return; - - const response = await fetch(subscription.endpoint, { - method: "POST", - headers: { - Authorization: authorization, - TTL: "2419200", - Urgency: "normal", - }, - }); - - if (response.status === 404 || response.status === 410) { - await db - .delete(pushSubscriptions) - .where(inArray(pushSubscriptions.endpoint, [subscription.endpoint])); - return; - } - - if (!response.ok) { - console.error( - "Failed to send Web Push notification:", - response.status, - await response.text(), - ); - } -} - -export async function sendWebPushToUsers(userIds: string[]) { - if (userIds.length === 0) return; - - const subscriptions = await db - .select() - .from(pushSubscriptions) - .where(inArray(pushSubscriptions.userId, userIds)); - - await Promise.allSettled( - subscriptions.map((subscription) => sendWebPush(subscription)), - ); -} From e94594357b8409d7e5a4d8c0b514cc1e405106ca Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 09:07:58 -0700 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=F0=9F=90=9B=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/src/ZotMeet/PushNotifications.swift | 77 ++++--------------- src/app/api/push-tokens/route.ts | 19 ++++- .../push/native-ios-push-bridge.tsx | 51 +++--------- src/lib/notification/types.ts | 6 +- src/lib/push/parse-payload.ts | 73 ++++++++++++++++++ src/lib/push/redirect.ts | 42 ++++++++++ src/lib/push/send-push.ts | 37 ++++++--- .../data/meeting/send-meeting-invites.ts | 5 +- 8 files changed, 190 insertions(+), 120 deletions(-) create mode 100644 src/lib/push/parse-payload.ts create mode 100644 src/lib/push/redirect.ts diff --git a/ios/src/ZotMeet/PushNotifications.swift b/ios/src/ZotMeet/PushNotifications.swift index 311d46d71..a33c98e1f 100644 --- a/ios/src/ZotMeet/PushNotifications.swift +++ b/ios/src/ZotMeet/PushNotifications.swift @@ -8,67 +8,16 @@ func registerForPushNotifications() { } } -class SubscribeMessage { - var topic = "" - var eventValue = "" - var unsubscribe = false - struct Keys { - static var TOPIC = "topic" - static var UNSUBSCRIBE = "unsubscribe" - static var EVENTVALUE = "eventValue" - } - convenience init(dict: Dictionary) { - self.init() - if let topic = dict[Keys.TOPIC] as? String { - self.topic = topic - } - if let unsubscribe = dict[Keys.UNSUBSCRIBE] as? Bool { - self.unsubscribe = unsubscribe - } - if let eventValue = dict[Keys.EVENTVALUE] as? String { - self.eventValue = eventValue - } - } -} - -func handleSubscribeTouch(message: WKScriptMessage) { - // [START subscribe_topic] - let subscribeMessages = parseSubscribeMessage(message: message) - if (subscribeMessages.count > 0){ - let _message = subscribeMessages[0] - if (_message.unsubscribe) { - Messaging.messaging().unsubscribe(fromTopic: _message.topic) { error in } - } - else { - Messaging.messaging().subscribe(toTopic: _message.topic) { error in } - } - } - - - // [END subscribe_topic] -} - -func parseSubscribeMessage(message: WKScriptMessage) -> [SubscribeMessage] { - var subscribeMessages = [SubscribeMessage]() - if let objStr = message.body as? String { +private let pushPayloadKeys = ["type", "redirect", "title", "message", "groupId", "createdBy"] - let data: Data = objStr.data(using: .utf8)! - do { - let jsObj = try JSONSerialization.jsonObject(with: data, options: .init(rawValue: 0)) - if let jsonObjDict = jsObj as? Dictionary { - let subscribeMessage = SubscribeMessage(dict: jsonObjDict) - subscribeMessages.append(subscribeMessage) - } else if let jsonArr = jsObj as? [Dictionary] { - for jsonObj in jsonArr { - let sMessage = SubscribeMessage(dict: jsonObj) - subscribeMessages.append(sMessage) - } - } - } catch _ { - +func pushPayloadForWebView(userInfo: [AnyHashable: Any]) -> [String: String] { + var payload = [String: String]() + for key in pushPayloadKeys { + if let value = userInfo[key] as? String, !value.isEmpty { + payload[key] = value } } - return subscribeMessages + return payload } func returnPermissionResult(isGranted: Bool){ @@ -169,24 +118,28 @@ func handleFCMToken(){ } func sendPushToWebView(userInfo: [AnyHashable: Any]){ + let payload = pushPayloadForWebView(userInfo: userInfo) + guard !payload.isEmpty else { return } var json = ""; do { - let jsonData = try JSONSerialization.data(withJSONObject: userInfo) + let jsonData = try JSONSerialization.data(withJSONObject: payload) json = String(data: jsonData, encoding: .utf8)! } catch { - print("ERROR: userInfo parsing problem") + print("ERROR: push payload parsing problem") return } checkViewAndEvaluate(event: "push-notification", detail: json) } func sendPushClickToWebView(userInfo: [AnyHashable: Any]){ + let payload = pushPayloadForWebView(userInfo: userInfo) + guard !payload.isEmpty else { return } var json = ""; do { - let jsonData = try JSONSerialization.data(withJSONObject: userInfo) + let jsonData = try JSONSerialization.data(withJSONObject: payload) json = String(data: jsonData, encoding: .utf8)! } catch { - print("ERROR: userInfo parsing problem") + print("ERROR: push payload parsing problem") return } checkViewAndEvaluate(event: "push-notification-click", detail: json) diff --git a/src/app/api/push-tokens/route.ts b/src/app/api/push-tokens/route.ts index bc2e05d48..bc1bcdb6c 100644 --- a/src/app/api/push-tokens/route.ts +++ b/src/app/api/push-tokens/route.ts @@ -1,8 +1,10 @@ import { and, eq } from "drizzle-orm"; +import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import { db } from "@/db"; import { nativePushTokens } from "@/db/schema"; import { getCurrentSession } from "@/lib/auth"; +import { isNativeIosAppFromCookies } from "@/lib/platform"; type PushTokenPayload = { token?: unknown; @@ -42,7 +44,22 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); } - const platform = payload.platform === "ios" ? "ios" : "unknown"; + if (payload.platform !== "ios") { + return NextResponse.json( + { error: "Only native iOS push tokens are supported" }, + { status: 400 }, + ); + } + + const cookieStore = await cookies(); + if (!isNativeIosAppFromCookies(cookieStore)) { + return NextResponse.json( + { error: "Push tokens can only be registered from the iOS app" }, + { status: 403 }, + ); + } + + const platform = "ios"; await db .insert(nativePushTokens) diff --git a/src/components/push/native-ios-push-bridge.tsx b/src/components/push/native-ios-push-bridge.tsx index ef28c1d56..90a5c26da 100644 --- a/src/components/push/native-ios-push-bridge.tsx +++ b/src/components/push/native-ios-push-bridge.tsx @@ -2,8 +2,9 @@ import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { NOTIFICATION_TYPES } from "@/lib/notification/types"; import { isNativeIosApp } from "@/lib/platform"; +import { parseNativePushPayload } from "@/lib/push/parse-payload"; +import { normalizePushRedirect } from "@/lib/push/redirect"; declare global { interface Window { @@ -47,46 +48,12 @@ async function savePushToken(token: string) { } } -type NativePushPayload = { - type?: unknown; - redirect?: unknown; -}; - -function parseNativePushPayload(detail: unknown): NativePushPayload { - if (typeof detail !== "string") { - return typeof detail === "object" && detail !== null - ? (detail as NativePushPayload) - : {}; - } - - try { - const parsed = JSON.parse(detail) as unknown; - return typeof parsed === "object" && parsed !== null - ? (parsed as NativePushPayload) - : {}; - } catch { - return {}; - } -} - -function getNotificationRedirect(payload: NativePushPayload) { - const redirect = typeof payload.redirect === "string" ? payload.redirect : ""; - - if (payload.type === NOTIFICATION_TYPES.GROUP_INVITE && redirect) { - return redirect.startsWith("/") - ? redirect - : `/invite/${encodeURIComponent(redirect)}`; - } - - if (!redirect) return "/summary"; - - try { - const url = new URL(redirect, window.location.origin); - if (url.origin !== window.location.origin) return "/summary"; - return `${url.pathname}${url.search}${url.hash}`; - } catch { - return redirect.startsWith("/") ? redirect : "/summary"; - } +function getNotificationRedirect( + type: string | undefined, + redirect: string | undefined, +) { + if (!type || !redirect) return "/summary"; + return normalizePushRedirect(type, redirect); } export function NativeIosPushBridge() { @@ -128,7 +95,7 @@ export function NativeIosPushBridge() { const handleNotificationClick = (event: Event) => { const payload = parseNativePushPayload((event as CustomEvent).detail); - router.push(getNotificationRedirect(payload)); + router.push(getNotificationRedirect(payload.type, payload.redirect)); }; window.addEventListener("push-permission-state", handlePermissionState); diff --git a/src/lib/notification/types.ts b/src/lib/notification/types.ts index 9c36f39ff..b2584d114 100644 --- a/src/lib/notification/types.ts +++ b/src/lib/notification/types.ts @@ -62,18 +62,18 @@ export const NOTIFICATION_PREF_OPTIONS: { key: "meetingInvites", label: "Meeting Invites", description: - "Receive in-app, email, and push notifications when you're invited to a meeting.", + "Receive in-app, email, and iOS push notifications when you're invited to a meeting.", }, { key: "groupInvites", label: "Group Invites", description: - "Receive in-app, email, and push notifications when you're invited to a group.", + "Receive in-app, email, and iOS push notifications when you're invited to a group.", }, { key: "nudges", label: "Nudges", description: - "Receive in-app, email, and push reminders to add your availability.", + "Receive in-app, email, and iOS push reminders to add your availability.", }, ]; diff --git a/src/lib/push/parse-payload.ts b/src/lib/push/parse-payload.ts new file mode 100644 index 000000000..e38aa4826 --- /dev/null +++ b/src/lib/push/parse-payload.ts @@ -0,0 +1,73 @@ +const PUSH_DATA_KEYS = [ + "type", + "redirect", + "title", + "message", + "groupId", + "createdBy", +] as const; + +export type NativePushPayload = { + type?: string; + redirect?: string; + title?: string; + message?: string; + groupId?: string; + createdBy?: string; +}; + +function stringField(value: unknown): string | undefined { + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return undefined; +} + +function pickPushFields(record: Record): NativePushPayload { + const payload: NativePushPayload = {}; + + for (const key of PUSH_DATA_KEYS) { + const value = stringField(record[key]); + if (value !== undefined) { + payload[key] = value; + } + } + + return payload; +} + +/** + * Extracts FCM custom data from the JSON blob iOS forwards from `userInfo`. + */ +export function parseNativePushPayload(detail: unknown): NativePushPayload { + if (typeof detail === "string") { + try { + return parseNativePushPayload(JSON.parse(detail) as unknown); + } catch { + return {}; + } + } + + if (!detail || typeof detail !== "object") { + return {}; + } + + const record = detail as Record; + const direct = pickPushFields(record); + if (direct.type || direct.redirect) { + return direct; + } + + if (record.data && typeof record.data === "object") { + const nested = pickPushFields(record.data as Record); + if (nested.type || nested.redirect) { + return nested; + } + } + + return direct; +} diff --git a/src/lib/push/redirect.ts b/src/lib/push/redirect.ts new file mode 100644 index 000000000..89d4185f2 --- /dev/null +++ b/src/lib/push/redirect.ts @@ -0,0 +1,42 @@ +import { NOTIFICATION_TYPES } from "@/lib/notification/types"; + +/** + * App-relative path used in FCM data payloads and native tap handling. + * In-app notification rows may still store full URLs or invite tokens. + */ +export function normalizePushRedirect(type: string, redirect: string): string { + const trimmed = redirect.trim(); + if (!trimmed) return "/summary"; + + if (type === NOTIFICATION_TYPES.GROUP_INVITE) { + if (trimmed.startsWith("/invite/")) return trimmed; + if (trimmed.startsWith("/")) return trimmed; + return `/invite/${encodeURIComponent(trimmed)}`; + } + + if ( + type === NOTIFICATION_TYPES.MEETING_INVITE || + type === NOTIFICATION_TYPES.NUDGE + ) { + if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { + try { + const url = new URL(trimmed); + return `${url.pathname}${url.search}${url.hash}` || "/summary"; + } catch { + return "/summary"; + } + } + return trimmed.startsWith("/") ? trimmed : "/summary"; + } + + if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { + try { + const url = new URL(trimmed); + return `${url.pathname}${url.search}${url.hash}` || "/summary"; + } catch { + return "/summary"; + } + } + + return trimmed.startsWith("/") ? trimmed : "/summary"; +} diff --git a/src/lib/push/send-push.ts b/src/lib/push/send-push.ts index 2bd26cbde..6b279b640 100644 --- a/src/lib/push/send-push.ts +++ b/src/lib/push/send-push.ts @@ -1,10 +1,11 @@ import "server-only"; -import { inArray } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { cert, getApps, initializeApp } from "firebase-admin/app"; import { getMessaging } from "firebase-admin/messaging"; import { db } from "@/db"; import { nativePushTokens } from "@/db/schema"; +import { normalizePushRedirect } from "@/lib/push/redirect"; type PushPayload = { title: string; @@ -103,11 +104,26 @@ export async function sendPushToUsers(userIds: string[], payload: PushPayload) { const tokenRows = await db .select({ token: nativePushTokens.token }) .from(nativePushTokens) - .where(inArray(nativePushTokens.userId, userIds)); + .where( + and( + inArray(nativePushTokens.userId, userIds), + eq(nativePushTokens.platform, "ios"), + ), + ); const tokens = [...new Set(tokenRows.map((row) => row.token))]; if (tokens.length === 0) return; + const redirect = normalizePushRedirect(payload.type, payload.redirect); + const data = { + type: payload.type, + redirect, + title: payload.title, + message: payload.message, + groupId: payload.groupId ?? "", + createdBy: payload.createdBy ?? "", + }; + const staleTokens = new Set(); for (const tokenChunk of chunkArray(tokens, 500)) { @@ -118,19 +134,20 @@ export async function sendPushToUsers(userIds: string[], payload: PushPayload) { title: payload.title, body: payload.message, }, - data: { - type: payload.type, - redirect: payload.redirect, - title: payload.title, - message: payload.message, - groupId: payload.groupId ?? "", - createdBy: payload.createdBy ?? "", - }, + data, apns: { + headers: { + "apns-priority": "10", + }, payload: { aps: { + alert: { + title: payload.title, + body: payload.message, + }, sound: "default", }, + ...data, }, }, }); diff --git a/src/server/data/meeting/send-meeting-invites.ts b/src/server/data/meeting/send-meeting-invites.ts index bb9f3a0db..c02d9dfae 100644 --- a/src/server/data/meeting/send-meeting-invites.ts +++ b/src/server/data/meeting/send-meeting-invites.ts @@ -58,7 +58,8 @@ export async function sendMeetingInvitesToUsers(params: { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const origin = baseUrl.replace(/\/$/, ""); - const meetingLink = `${origin}/availability/${meetingId}`; + const meetingPath = `/availability/${meetingId}`; + const meetingLink = `${origin}${meetingPath}`; const inviterName = inviter.displayName?.trim() || "Someone"; await createNewNotification( @@ -66,7 +67,7 @@ export async function sendMeetingInvitesToUsers(params: { meetingTitle, `You've been invited to join "${meetingTitle}". Click to view the meeting.`, NOTIFICATION_TYPES.MEETING_INVITE, - meetingLink, + meetingPath, null, inviter.id, { From 5ac9f6014f023f236b8d4b40e444f57a4d044d60 Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 09:29:12 -0700 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=F0=9F=90=9B=20edge=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/src/ZotMeet/PushNotifications.swift | 60 ++++++++++++++-- src/app/api/push-tokens/route.ts | 13 ++-- .../push/native-ios-push-bridge.tsx | 12 +--- ...tokens.sql => 0018_native_push_tokens.sql} | 2 +- ...{0019_snapshot.json => 0018_snapshot.json} | 4 +- src/db/migrations/meta/_journal.json | 6 +- src/lib/push/redirect.ts | 69 +++++++++++++------ 7 files changed, 114 insertions(+), 52 deletions(-) rename src/db/migrations/{0019_native_push_tokens.sql => 0018_native_push_tokens.sql} (94%) rename src/db/migrations/meta/{0019_snapshot.json => 0018_snapshot.json} (99%) diff --git a/ios/src/ZotMeet/PushNotifications.swift b/ios/src/ZotMeet/PushNotifications.swift index a33c98e1f..39c4958c7 100644 --- a/ios/src/ZotMeet/PushNotifications.swift +++ b/ios/src/ZotMeet/PushNotifications.swift @@ -8,15 +8,65 @@ func registerForPushNotifications() { } } -private let pushPayloadKeys = ["type", "redirect", "title", "message", "groupId", "createdBy"] +private let pushPayloadKeys: Set = [ + "type", "redirect", "title", "message", "groupId", "createdBy", +] + +func pushFieldString(_ value: Any?) -> String? { + guard let value = value else { return nil } + + if let string = value as? String { + let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + if let string = value as? NSString { + let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + if let number = value as? NSNumber { + return number.stringValue + } + if let bool = value as? Bool { + return bool ? "true" : "false" + } + return nil +} + +private func pushPayloadKeyName(_ key: AnyHashable) -> String? { + if let key = key as? String { + return key + } + if let key = key as? NSString { + return key as String + } + return nil +} + +private func mergePushFields( + into payload: inout [String: String], + from record: [AnyHashable: Any] +) { + for (rawKey, value) in record { + guard let key = pushPayloadKeyName(rawKey), pushPayloadKeys.contains(key) else { + continue + } + if payload[key] != nil { + continue + } + if let string = pushFieldString(value) { + payload[key] = string + } + } +} func pushPayloadForWebView(userInfo: [AnyHashable: Any]) -> [String: String] { var payload = [String: String]() - for key in pushPayloadKeys { - if let value = userInfo[key] as? String, !value.isEmpty { - payload[key] = value - } + mergePushFields(into: &payload, from: userInfo) + + if let nested = userInfo[AnyHashable("data")] as? [AnyHashable: Any] { + mergePushFields(into: &payload, from: nested) } + return payload } diff --git a/src/app/api/push-tokens/route.ts b/src/app/api/push-tokens/route.ts index bc1bcdb6c..6c21dc99b 100644 --- a/src/app/api/push-tokens/route.ts +++ b/src/app/api/push-tokens/route.ts @@ -1,10 +1,8 @@ import { and, eq } from "drizzle-orm"; -import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import { db } from "@/db"; import { nativePushTokens } from "@/db/schema"; import { getCurrentSession } from "@/lib/auth"; -import { isNativeIosAppFromCookies } from "@/lib/platform"; type PushTokenPayload = { token?: unknown; @@ -51,13 +49,10 @@ export async function POST(request: Request) { ); } - const cookieStore = await cookies(); - if (!isNativeIosAppFromCookies(cookieStore)) { - return NextResponse.json( - { error: "Push tokens can only be registered from the iOS app" }, - { status: 403 }, - ); - } + // Registration requires a signed-in user. The `app-platform` cookie is set by + // the WKWebView for client UX only and must not be used as an auth gate + // (it is unsigned and trivially spoofed). Invalid or non-iOS FCM tokens are + // rejected by Firebase on send and pruned in sendPushToUsers. const platform = "ios"; diff --git a/src/components/push/native-ios-push-bridge.tsx b/src/components/push/native-ios-push-bridge.tsx index 90a5c26da..e2d9ac496 100644 --- a/src/components/push/native-ios-push-bridge.tsx +++ b/src/components/push/native-ios-push-bridge.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { isNativeIosApp } from "@/lib/platform"; import { parseNativePushPayload } from "@/lib/push/parse-payload"; -import { normalizePushRedirect } from "@/lib/push/redirect"; +import { resolvePushNotificationPath } from "@/lib/push/redirect"; declare global { interface Window { @@ -48,14 +48,6 @@ async function savePushToken(token: string) { } } -function getNotificationRedirect( - type: string | undefined, - redirect: string | undefined, -) { - if (!type || !redirect) return "/summary"; - return normalizePushRedirect(type, redirect); -} - export function NativeIosPushBridge() { const router = useRouter(); @@ -95,7 +87,7 @@ export function NativeIosPushBridge() { const handleNotificationClick = (event: Event) => { const payload = parseNativePushPayload((event as CustomEvent).detail); - router.push(getNotificationRedirect(payload.type, payload.redirect)); + router.push(resolvePushNotificationPath(payload.type, payload.redirect)); }; window.addEventListener("push-permission-state", handlePermissionState); diff --git a/src/db/migrations/0019_native_push_tokens.sql b/src/db/migrations/0018_native_push_tokens.sql similarity index 94% rename from src/db/migrations/0019_native_push_tokens.sql rename to src/db/migrations/0018_native_push_tokens.sql index a504fe547..3d95e3fd2 100644 --- a/src/db/migrations/0019_native_push_tokens.sql +++ b/src/db/migrations/0018_native_push_tokens.sql @@ -15,4 +15,4 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint -CREATE INDEX IF NOT EXISTS "native_push_tokens_user_id_idx" ON "native_push_tokens" USING btree ("user_id"); +CREATE INDEX IF NOT EXISTS "native_push_tokens_user_id_idx" ON "native_push_tokens" USING btree ("user_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0019_snapshot.json b/src/db/migrations/meta/0018_snapshot.json similarity index 99% rename from src/db/migrations/meta/0019_snapshot.json rename to src/db/migrations/meta/0018_snapshot.json index d49efb5be..78924d154 100644 --- a/src/db/migrations/meta/0019_snapshot.json +++ b/src/db/migrations/meta/0018_snapshot.json @@ -1,5 +1,5 @@ { - "id": "75e01db7-4aab-4fb6-908a-a1ac6f22e258", + "id": "b14ad417-4e8b-4bde-8702-5c666f68c39a", "prevId": "994d6447-5510-4538-867f-4b641182950b", "version": "7", "dialect": "postgresql", @@ -1375,4 +1375,4 @@ "schemas": {}, "tables": {} } -} +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 1988fabd9..d60ef8258 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -131,9 +131,9 @@ { "idx": 18, "version": "7", - "when": 1780080000000, - "tag": "0019_native_push_tokens", + "when": 1779985305821, + "tag": "0018_native_push_tokens", "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/lib/push/redirect.ts b/src/lib/push/redirect.ts index 89d4185f2..066c55637 100644 --- a/src/lib/push/redirect.ts +++ b/src/lib/push/redirect.ts @@ -1,10 +1,37 @@ import { NOTIFICATION_TYPES } from "@/lib/notification/types"; +function isHttpUrl(redirect: string): boolean { + return redirect.startsWith("http://") || redirect.startsWith("https://"); +} + +function toAppPath(redirect: string): string { + if (isHttpUrl(redirect)) { + try { + const url = new URL(redirect); + return `${url.pathname}${url.search}${url.hash}` || "/summary"; + } catch { + return "/summary"; + } + } + return redirect.startsWith("/") ? redirect : "/summary"; +} + +/** Bare invite tokens are stored without a leading slash. */ +function looksLikeInviteToken(redirect: string): boolean { + return !redirect.startsWith("/") && !isHttpUrl(redirect); +} + /** * App-relative path used in FCM data payloads and native tap handling. * In-app notification rows may still store full URLs or invite tokens. + * + * When `type` is omitted (legacy or partial FCM payloads), infers the path from + * `redirect` shape (e.g. `/availability/…`, `/invite/…`, or a bare invite token). */ -export function normalizePushRedirect(type: string, redirect: string): string { +export function normalizePushRedirect( + type: string | undefined, + redirect: string, +): string { const trimmed = redirect.trim(); if (!trimmed) return "/summary"; @@ -14,29 +41,27 @@ export function normalizePushRedirect(type: string, redirect: string): string { return `/invite/${encodeURIComponent(trimmed)}`; } - if ( - type === NOTIFICATION_TYPES.MEETING_INVITE || - type === NOTIFICATION_TYPES.NUDGE - ) { - if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { - try { - const url = new URL(trimmed); - return `${url.pathname}${url.search}${url.hash}` || "/summary"; - } catch { - return "/summary"; - } + if (!type) { + if ( + trimmed.startsWith("/invite/") || + trimmed.startsWith("/availability/") + ) { + return trimmed; } - return trimmed.startsWith("/") ? trimmed : "/summary"; - } - - if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { - try { - const url = new URL(trimmed); - return `${url.pathname}${url.search}${url.hash}` || "/summary"; - } catch { - return "/summary"; + if (looksLikeInviteToken(trimmed)) { + return `/invite/${encodeURIComponent(trimmed)}`; } } - return trimmed.startsWith("/") ? trimmed : "/summary"; + // Meeting invites, nudges, unknown types, and legacy redirects without type. + return toAppPath(trimmed); +} + +/** Resolves a tap target from native push payload fields. */ +export function resolvePushNotificationPath( + type: string | undefined, + redirect: string | undefined, +): string { + if (!redirect?.trim()) return "/summary"; + return normalizePushRedirect(type, redirect); } From 7e546d3a0b5cc3c6a3add68b5223e6691d1aedcc Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 09:43:13 -0700 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E2=9C=A8=20switch=20firebase=20to?= =?UTF-8?q?=20aws=20sns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 + README.md | 20 +- ios/src/Podfile | 5 +- ios/src/ZotMeet/AppDelegate.swift | 106 +- ios/src/ZotMeet/PushNotifications.swift | 36 +- ios/src/ZotMeet/ViewController.swift | 2 +- package.json | 2 +- pnpm-lock.yaml | 1182 ++++++----------------- src/app/api/push-tokens/route.ts | 40 +- src/lib/push/parse-payload.ts | 2 +- src/lib/push/redirect.ts | 2 +- src/lib/push/send-push.ts | 204 ++-- src/lib/push/sns-client.ts | 13 + src/lib/push/sns-config.ts | 31 + src/lib/push/sns-register.ts | 82 ++ sst.config.ts | 16 + 16 files changed, 630 insertions(+), 1120 deletions(-) create mode 100644 src/lib/push/sns-client.ts create mode 100644 src/lib/push/sns-config.ts create mode 100644 src/lib/push/sns-register.ts diff --git a/.env.example b/.env.example index 7ac230429..befce8a55 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,10 @@ NEXT_PUBLIC_IOS_BUNDLE_ID=com.zotmeet.app # Register the service worker in non-production builds (e.g. when testing with PWABuilder # against a preview URL). Leave unset in local dev unless you need SW debugging. # NEXT_PUBLIC_ENABLE_SW=true + +# iOS push via AWS SNS (APNs platform application in AWS Console / IaC). +# Create an Apple platform application, upload your APNs key (.p8), then set: +# SNS_IOS_PLATFORM_APPLICATION_ARN=arn:aws:sns:us-west-1:ACCOUNT:app/APNS/ZotMeet +# SNS_REGION=us-west-1 +# Use SNS_IOS_APNS_ENV=sandbox for Xcode debug builds; production for TestFlight/App Store. +# SNS_IOS_APNS_ENV=production diff --git a/README.md b/README.md index 51a36aa59..8d839395b 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,27 @@ Graph, robots, sitemap, and invite/email links. 4. Click `Package for stores` → `iOS` → `Generate Package`. 5. Take note of the `Bundle ID` and download the package. 6. Open the generated `.xcworkspace` in Xcode (≥ iOS 17 SDK), run - `pod install` in `src/`, build, and archive for distribution following the + `pod install` in `ios/src/`, build, and archive for distribution following the [PWA Builder iOS guide](https://docs.pwabuilder.com/#/builder/app-store). +### iOS push notifications (AWS SNS) + +Native push uses **AWS SNS → APNs** (same AWS account pattern as SES email). + +1. In [Apple Developer](https://developer.apple.com/account/resources/authkeys/list), + create an APNs key (`.p8`) and note Key ID, Team ID, and your app Bundle ID. +2. In AWS Console → SNS → Mobile → Push notifications → Platform applications, + create an **Apple iOS/VoIP/Mac** platform application (token-based `.p8` auth). +3. Set environment variables (see `.env.example`): + - `SNS_IOS_PLATFORM_APPLICATION_ARN` — platform application ARN + - `SNS_REGION` — region where the platform app was created (default `us-west-1` in `sst.config.ts`) + - `SNS_IOS_APNS_ENV` — `sandbox` for Xcode debug builds, `production` for TestFlight/App Store +4. Deploy so the Next.js workload has SNS IAM permissions (`sst.config.ts` includes + `sns:Publish`, `CreatePlatformEndpoint`, etc.). + +After login in the native app, iOS prompts for notification permission, registers an +APNs device token with the server, and the server stores an SNS platform endpoint ARN. + ### Regenerating PWA assets ```bash diff --git a/ios/src/Podfile b/ios/src/Podfile index f78f5a98e..f110119b3 100644 --- a/ios/src/Podfile +++ b/ios/src/Podfile @@ -5,9 +5,6 @@ target 'ZotMeet' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - # Add the pod for Firebase Cloud Messaging - pod 'Firebase/Messaging' - end post_install do |installer| @@ -16,4 +13,4 @@ post_install do |installer| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.4' end end -end \ No newline at end of file +end diff --git a/ios/src/ZotMeet/AppDelegate.swift b/ios/src/ZotMeet/AppDelegate.swift index 6ced388f2..9359e7cac 100644 --- a/ios/src/ZotMeet/AppDelegate.swift +++ b/ios/src/ZotMeet/AppDelegate.swift @@ -1,25 +1,13 @@ import UIKit -import FirebaseCore -import FirebaseMessaging - @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - + var window : UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - FirebaseApp.configure() - - // [START set_messaging_delegate] - Messaging.messaging().delegate = self - // [END set_messaging_delegate] - // Register for remote notifications. This shows a permission dialog on first run, to - // show the dialog at a more appropriate time move this registration accordingly. - // [START register_for_notifications] - UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().getNotificationSettings { settings in switch settings.authorizationStatus { @@ -30,91 +18,37 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - // let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] - // UNUserNotificationCenter.current().requestAuthorization( - // options: authOptions, - // completionHandler: {_, _ in }) - -// TODO: if we're using Firebase, uncomment next string - // application.registerForRemoteNotifications() - - // [END register_for_notifications] return true } - // [START receive_message] func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { - // If you are receiving a notification message while your app is in the background, - // this callback will not be fired till the user taps on the notification launching the application. - // With swizzling disabled you must let Messaging know about the message, for Analytics - // Messaging.messaging().appDidReceiveMessage(userInfo) - // Print message ID. - if let messageID = userInfo[gcmMessageIDKey] { - print("Message ID 1: \(messageID)") - } - -#if DEBUG - print("push userInfo 1:", userInfo) -#endif sendPushToWebView(userInfo: userInfo) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // If you are receiving a notification message while your app is in the background, - // this callback will not be fired till the user taps on the notification launching the application. - // With swizzling disabled you must let Messaging know about the message, for Analytics - // Messaging.messaging().appDidReceiveMessage(userInfo) - // Print message ID. - if let messageID = userInfo[gcmMessageIDKey] { - print("Message ID 2: \(messageID)") - } - -#if DEBUG - print("push userInfo 2:", userInfo) -#endif sendPushToWebView(userInfo: userInfo) - completionHandler(UIBackgroundFetchResult.newData) } - // [END receive_message] func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Unable to register for remote notifications: \(error.localizedDescription)") } - // This function is added here only for debugging purposes, and can be removed if swizzling is enabled. - // If swizzling is disabled then this function must be implemented so that the APNs token can be paired to - // the FCM registration token. -// func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { -// print("APNs token retrieved: \(deviceToken)") -// -// // With swizzling disabled you must set the APNs token here. -// // Messaging.messaging().apnsToken = deviceToken -// } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() + setApnsDeviceTokenHex(token) + sendApnsTokenToWebView(token: token) + } } - // [START ios_10_message_handling] extension AppDelegate : UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let userInfo = notification.request.content.userInfo - - // With swizzling disabled you must let Messaging know about the message, for Analytics - // Messaging.messaging().appDidReceiveMessage(userInfo) - // Print message ID. - if let messageID = userInfo[gcmMessageIDKey] { - print("Message ID: 3 \(messageID)") - } - -#if DEBUG - print("push userInfo 3:", userInfo) -#endif sendPushToWebView(userInfo: userInfo) - - // Change this to your preferred presentation option completionHandler([[.banner, .list, .sound]]) } @@ -122,35 +56,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo - // Print message ID. - if let messageID = userInfo[gcmMessageIDKey] { - print("Message ID 4: \(messageID)") - } - - // With swizzling disabled you must let Messaging know about the message, for Analytics - // Messaging.messaging().appDidReceiveMessage(userInfo) -#if DEBUG - print("push userInfo 4:", userInfo) -#endif sendPushClickToWebView(userInfo: userInfo) - completionHandler() } } - // [END ios_10_message_handling] - - extension AppDelegate : MessagingDelegate { - // [START refresh_token] - func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { -#if DEBUG - print("Firebase registration token: \(String(describing: fcmToken))") -#endif - - let dataDict:[String: String] = ["token": fcmToken ?? ""] - NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict) - handleFCMToken() - // TODO: If necessary send token to application server. - // Note: This callback is fired at each app startup and whenever a new token is generated. - } - // [END refresh_token] - } diff --git a/ios/src/ZotMeet/PushNotifications.swift b/ios/src/ZotMeet/PushNotifications.swift index 39c4958c7..7a565fd9e 100644 --- a/ios/src/ZotMeet/PushNotifications.swift +++ b/ios/src/ZotMeet/PushNotifications.swift @@ -1,6 +1,11 @@ import UIKit import WebKit -import FirebaseMessaging + +var apnsDeviceTokenHex: String? + +func setApnsDeviceTokenHex(_ token: String) { + apnsDeviceTokenHex = token +} func registerForPushNotifications() { DispatchQueue.main.async { @@ -8,6 +13,16 @@ func registerForPushNotifications() { } } +func apnsTokenHexString(from deviceToken: Data) -> String { + return deviceToken.map { String(format: "%02.2hhx", $0) }.joined() +} + +func sendApnsTokenToWebView(token: String) { + let escaped = token.replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "'", with: "\\'") + checkViewAndEvaluate(event: "push-token", detail: "'\(escaped)'") +} + private let pushPayloadKeys: Set = [ "type", "redirect", "title", "message", "groupId", "createdBy", ] @@ -153,17 +168,20 @@ func checkViewAndEvaluate(event: String, detail: String) { } } -func handleFCMToken(){ +func handleApnsToken(){ DispatchQueue.main.async(execute: { - Messaging.messaging().token { token, error in - if let error = error { - print("Error fetching FCM registration token: \(error)") + if let token = apnsDeviceTokenHex, !token.isEmpty { + sendApnsTokenToWebView(token: token) + return + } + registerForPushNotifications() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + if let token = apnsDeviceTokenHex, !token.isEmpty { + sendApnsTokenToWebView(token: token) + } else { checkViewAndEvaluate(event: "push-token", detail: "ERROR GET TOKEN") - } else if let token = token { - print("FCM registration token: \(token)") - checkViewAndEvaluate(event: "push-token", detail: "'\(token)'") } - } + } }) } diff --git a/ios/src/ZotMeet/ViewController.swift b/ios/src/ZotMeet/ViewController.swift index 6ecbf5cb5..57d8ff312 100644 --- a/ios/src/ZotMeet/ViewController.swift +++ b/ios/src/ZotMeet/ViewController.swift @@ -235,7 +235,7 @@ extension ViewController: WKScriptMessageHandler { handlePushState() } if message.name == "push-token" { - handleFCMToken() + handleApnsToken() } } } diff --git a/package.json b/package.json index 41137f19d..547c21396 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@aws-sdk/client-sesv2": "^3.1039.0", + "@aws-sdk/client-sns": "^3.1039.0", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -51,7 +52,6 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^16.4.5", - "firebase-admin": "^13.10.0", "googleapis": "^148.0.0", "lucide-react": "^0.453.0", "next": "16.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c25470ef..2162c3b02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@aws-sdk/client-sesv2': specifier: ^3.1039.0 version: 3.1039.0 + '@aws-sdk/client-sns': + specifier: ^3.1039.0 + version: 3.1054.0 '@emotion/cache': specifier: ^11.14.0 version: 11.14.0 @@ -102,9 +105,6 @@ importers: dotenv: specifier: ^16.4.5 version: 16.6.1 - firebase-admin: - specifier: ^13.10.0 - version: 13.10.0 googleapis: specifier: ^148.0.0 version: 148.0.0 @@ -221,6 +221,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -238,6 +242,14 @@ packages: resolution: {integrity: sha512-I7k/0cW98TuIZFbeMNdrsoflVKDYAgiLstwy3wgW1Ss7t/hVdVXMrkISD+3o+TB1RR2pU/ZX11CIBTT+H74feQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-sns@3.1054.0': + resolution: {integrity: sha512-8Befe/r+dyKneJkqLBcO9dEwSNsrf1FRPUHGaVN535gq8jGTSv45ybNT4SWSWU76mxR8oGTbvNGWoJGvlMGjcQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.14': + resolution: {integrity: sha512-ppamm04uoj3hhNO5IlQSs5D6rWX1fWkzcn6a4pZrojk8Y6ObY9wzLDdT/Eq3gv6O9hOebi9tYTNB8b8fQj9XJw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/core@3.974.7': resolution: {integrity: sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw==} engines: {node: '>=20.0.0'} @@ -246,34 +258,66 @@ packages: resolution: {integrity: sha512-bJV7eViSJV6GSuuN+VIdNVPdwPsNSf75BiC2v5alPrjR/OCcqgKwSZInKbDFz9mNeizldsyf67jt6YSIiv53Cw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.40': + resolution: {integrity: sha512-jjT0p0Y7KZtcvExYiPCLJnqM9lkXDV1KBEg/13OE2DXv/9batzlyJHVKUEnRNJccY0O2Sul17E1su38CgdBhGQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.35': resolution: {integrity: sha512-x/BQGEIdq0oI+4WxLjKmnQvT7CnF9r8ezdGt7wXwxb7ckHXQz0Zmgxt8v3Ne0JaT3R5YefmuybHX6E8EnsDXyA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.42': + resolution: {integrity: sha512-+3fsKtWybe5BjKEUA3/07oh7Ayfd82IED2+gyyaVfS/4PU78E3TaOQxSGOJ1t7Imefoidw/ne9QA7apX8wEnJg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.37': resolution: {integrity: sha512-eUTpmWfd/BKsq9medhCRcu+GRAhFP2Zrn7/2jKDHHOOjCkhrMoTp/t4cEthqFoG7gE0VGp5wUxrXTdvBCmSmJg==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.44': + resolution: {integrity: sha512-gZFw5wBefCIPg9vpT+gV5FdhfNKhYTVDZa1IsZCcn3SRoYUOJ/E05vwIogkJoonqBL0ttBGi5vhthX7xceekRg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.37': resolution: {integrity: sha512-Ty68y8ISSC+g5Q3D0K8uAaoINwvfaOslnNpsF/LgVUxyosYXHawcK2yV4HLXDVugiTTYLQfJfcw0ce5meAGkKw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.44': + resolution: {integrity: sha512-QqEGHfQeZgUDqh7zpqHufrZ8T644ELEWvB+4gUdewLyRw4IRF+6CJqeQuRWqucZdQzoQeMh7fNAD9BWxFAdNig==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.38': resolution: {integrity: sha512-BQ9XYnBDVxR2HuV5huXYQYF/PZMTsY+EnwfGnCU2cA8Zw63XpkOtPY8WqiMIZMQCrKPQQEiFURS/o9CIolRLqg==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.45': + resolution: {integrity: sha512-3YCv52ExXIRz3LAVNysevd+s7akSpg9dl39v9LJ7dOQH+s5rHi3jMZYQyxwMmglxQGMuzYRfQ0o1VSP2UOlIRw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.33': resolution: {integrity: sha512-yfjGksI9WQbdMObb0VeLXqzTLI+a0qXLJT9gCDiv0+X/xjPpI3mTz6a5FibrhpuEKIe0gSgvs3MaoFZy5cx4WA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.40': + resolution: {integrity: sha512-cXaozlgJCOwmE6D7x4npcPdyk7kiFZdrGjN3D6tXXtItJJMNGPafDfAJn4YQmciMooG/X+b0Y6RTqdVVMx26jg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.37': resolution: {integrity: sha512-fpwE+20ntpp3i9Xb9vUuQfXLDKYHH+5I2V+ZG96SX1nBzrruhy10RXDgmN7t1etOz3c55stlA3TeQASUA451NQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.44': + resolution: {integrity: sha512-YePoj5kQuPmE0MHnyftXCfsO8ZSBd2kDr50XEIUrdejSbGFlayYvUuCohdb8drhGhPm6b65o7H1eC26EZhwUvA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.37': resolution: {integrity: sha512-aryawqyebf+3WhAFNHfF62rekFpYtVcVN7dQ89qnAWsa4n5hJst8qBG6gXC24WHtW7Nnhkf9ScYnjwo0Brn3bw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.44': + resolution: {integrity: sha512-Ys/JJe++8Z2Y5meR1taMBaVcrGBA0/XsVTQR+qOKZbdNyg+8Jlv5rYZSwh8SqEHY00goSOZy7PHzZ2rLNQxDLg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-host-header@3.972.10': resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} engines: {node: '>=20.0.0'} @@ -294,6 +338,10 @@ packages: resolution: {integrity: sha512-N1oNpdiLoVAWYD3WFBnUi3LlfoDA06ZHo4ozyjbsJNLvILzvt//0CnR8N+CZ0NWeYgVB/5V59ivixHCWCx2ALw==} engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.997.12': + resolution: {integrity: sha512-Js2VYaCM269feB0cs0cGmlIhdOgT9aMqzdBx68lCy6kVCYfzr0T36ovUFDvfUmatkuBeyBJhCwaLBh7P8meH5Q==} + engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.997.5': resolution: {integrity: sha512-jGFr6DxtcMTmzOkG/a0jCZYv4BBDmeNYVeO+/memSoDkYCJu4Y58xviYmzwJfYyIVSts+X/BVjJm1uGBnwHEMg==} engines: {node: '>=20.0.0'} @@ -306,14 +354,26 @@ packages: resolution: {integrity: sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw==} engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.29': + resolution: {integrity: sha512-Few9FoQqOt/0KSvZYP+qdW0dfOhfQ9N+gl2UUDvCPW6mkPKHli9LMbKxWj+wZ5zKPaOoqxuR3Hhy3OTpndkfSw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1039.0': resolution: {integrity: sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1054.0': + resolution: {integrity: sha512-hG9YKApmZOw+drJ9Nuoaf/OvC8e5W1+3eoLeN5p2uVCZRWsv27teIS0b4kiH6Sfv3WMmamqYJxmE2WMwyp/L/A==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.8': resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.9': + resolution: {integrity: sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-arn-parser@3.972.3': resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} engines: {node: '>=20.0.0'} @@ -342,6 +402,10 @@ packages: resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.26': + resolution: {integrity: sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.2.4': resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} @@ -1017,41 +1081,6 @@ packages: cpu: [x64] os: [win32] - '@fastify/busboy@3.2.0': - resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} - - '@firebase/app-check-interop-types@0.3.4': - resolution: {integrity: sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==} - - '@firebase/app-types@0.9.5': - resolution: {integrity: sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==} - - '@firebase/auth-interop-types@0.2.5': - resolution: {integrity: sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==} - - '@firebase/component@0.7.3': - resolution: {integrity: sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==} - engines: {node: '>=20.0.0'} - - '@firebase/database-compat@2.1.4': - resolution: {integrity: sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==} - engines: {node: '>=20.0.0'} - - '@firebase/database-types@1.0.20': - resolution: {integrity: sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==} - - '@firebase/database@1.1.3': - resolution: {integrity: sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==} - engines: {node: '>=20.0.0'} - - '@firebase/logger@0.5.1': - resolution: {integrity: sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==} - engines: {node: '>=20.0.0'} - - '@firebase/util@1.15.1': - resolution: {integrity: sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==} - engines: {node: '>=20.0.0'} - '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -1067,40 +1096,6 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@google-cloud/firestore@7.11.6': - resolution: {integrity: sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==} - engines: {node: '>=14.0.0'} - - '@google-cloud/paginator@5.0.2': - resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} - engines: {node: '>=14.0.0'} - - '@google-cloud/projectify@4.0.0': - resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} - engines: {node: '>=14.0.0'} - - '@google-cloud/promisify@4.0.0': - resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} - engines: {node: '>=14'} - - '@google-cloud/storage@7.19.0': - resolution: {integrity: sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==} - engines: {node: '>=14'} - - '@grpc/grpc-js@1.14.4': - resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.7.15': - resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} - engines: {node: '>=6'} - hasBin: true - - '@grpc/proto-loader@0.8.1': - resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} - engines: {node: '>=6'} - hasBin: true - '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -1272,9 +1267,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@modelcontextprotocol/sdk@1.6.1': resolution: {integrity: sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==} engines: {node: '>=18'} @@ -1620,36 +1612,6 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.2': - resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2129,14 +2091,26 @@ packages: resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} engines: {node: '>=18.0.0'} + '@smithy/core@3.24.4': + resolution: {integrity: sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.14': resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.3.4': + resolution: {integrity: sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.3.17': resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.4.4': + resolution: {integrity: sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==} + engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.14': resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} engines: {node: '>=18.0.0'} @@ -2181,6 +2155,10 @@ packages: resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.7.4': + resolution: {integrity: sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==} + engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.2.14': resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} engines: {node: '>=18.0.0'} @@ -2209,6 +2187,10 @@ packages: resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.4.4': + resolution: {integrity: sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==} + engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.12.13': resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} engines: {node: '>=18.0.0'} @@ -2217,6 +2199,10 @@ packages: resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} engines: {node: '>=18.0.0'} + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.14': resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} engines: {node: '>=18.0.0'} @@ -2295,31 +2281,15 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tootallnate/once@2.0.1': - resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} - engines: {node: '>= 10'} - '@tsconfig/bun@1.0.7': resolution: {integrity: sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/caseless@0.12.5': - resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} - '@types/conventional-commits-parser@5.0.2': resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} - '@types/jsonwebtoken@9.0.10': - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - - '@types/long@4.0.2': - resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.31': resolution: {integrity: sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==} @@ -2345,31 +2315,17 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - '@types/request@2.48.13': - resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} - '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2424,16 +2380,6 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - arrify@2.0.1: - resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} - engines: {node: '>=8'} - - async-retry@1.3.3: - resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2567,10 +2513,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -2655,10 +2597,6 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - date-fns-tz@3.2.0: resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} peerDependencies: @@ -2680,10 +2618,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2816,9 +2750,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexify@4.1.3: - resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} - ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -2835,9 +2766,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2861,10 +2789,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -2900,10 +2824,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -2932,10 +2852,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - farmhash-modern@1.1.0: - resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} - engines: {node: '>=18.0.0'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2949,17 +2865,20 @@ packages: fast-xml-builder@1.1.5: resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + fast-xml-parser@5.7.2: resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} hasBin: true + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - faye-websocket@0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2969,10 +2888,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2988,22 +2903,10 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} - firebase-admin@13.10.0: - resolution: {integrity: sha512-rbuCrJvYRwqBqvbccMS8fj/x2zsaMisdf5RQbRzQzr14Rbq9r2UlpuBHqWAwrO6c9dIRF56xF/xoepXsD5yDuQ==} - engines: {node: '>=18'} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - form-data@2.5.5: - resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} - engines: {node: '>= 0.12'} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3020,25 +2923,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - functional-red-black-tree@1.0.1: - resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - gaxios@6.7.1: resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} engines: {node: '>=14'} - gaxios@7.1.4: - resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} - engines: {node: '>=18'} - gcp-metadata@6.1.1: resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} engines: {node: '>=14'} - gcp-metadata@8.1.2: - resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} - engines: {node: '>=18'} - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -3083,26 +2975,14 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - google-auth-library@10.6.2: - resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} - engines: {node: '>=18'} - google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} - google-gax@4.6.1: - resolution: {integrity: sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==} - engines: {node: '>=14'} - google-logging-utils@0.0.2: resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} engines: {node: '>=14'} - google-logging-utils@1.1.3: - resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} - engines: {node: '>=14'} - googleapis-common@7.2.0: resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} engines: {node: '>=14.0.0'} @@ -3145,24 +3025,10 @@ packages: resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} engines: {node: '>=16.9.0'} - html-entities@2.6.0: - resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-parser-js@0.5.10: - resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} - - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3317,17 +3183,9 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - jsonwebtoken@9.0.3: - resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} - engines: {node: '>=12', npm: '>=6'} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jwks-rsa@3.2.2: - resolution: {integrity: sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==} - engines: {node: '>=14'} - jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -3335,9 +3193,6 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} - limiter@1.1.5: - resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} - lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3357,27 +3212,9 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -3387,9 +3224,6 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -3406,9 +3240,6 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3417,9 +3248,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-memoizer@2.3.0: - resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} - lucide-react@0.453.0: resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} peerDependencies: @@ -3449,27 +3277,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3527,11 +3342,6 @@ packages: sass: optional: true - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3541,10 +3351,6 @@ packages: encoding: optional: true - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3614,10 +3420,6 @@ packages: openid-client@5.6.4: resolution: {integrity: sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3800,14 +3602,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proto3-json-serializer@2.0.2: - resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} - engines: {node: '>=14.0.0'} - - protobufjs@7.6.1: - resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==} - engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3895,10 +3689,6 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3934,14 +3724,6 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - retry-request@7.0.2: - resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} - engines: {node: '>=14'} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4096,12 +3878,6 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - stream-events@1.0.5: - resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} - - stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -4118,9 +3894,6 @@ packages: resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==} engines: {node: '>=20'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4132,9 +3905,6 @@ packages: strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} - stubs@3.0.0: - resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -4181,10 +3951,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - teeny-request@9.0.0: - resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} - engines: {node: '>=14'} - text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -4308,21 +4074,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - websocket-driver@0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} - - websocket-extensions@0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4341,6 +4095,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -4380,10 +4138,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -4421,6 +4175,12 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + tslib: 2.8.1 + '@aws-crypto/sha256-browser@5.2.0': dependencies: '@aws-crypto/sha256-js': 5.2.0 @@ -4492,6 +4252,30 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-sns@3.1054.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.14 + '@aws-sdk/credential-provider-node': 3.972.45 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/fetch-http-handler': 5.4.4 + '@smithy/node-http-handler': 4.7.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.14': + dependencies: + '@aws-sdk/types': 3.973.9 + '@aws-sdk/xml-builder': 3.972.26 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.4 + '@smithy/signature-v4': 5.4.4 + '@smithy/types': 4.14.2 + bowser: 2.14.1 + tslib: 2.8.1 + '@aws-sdk/core@3.974.7': dependencies: '@aws-sdk/types': 3.973.8 @@ -4517,6 +4301,14 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.35': dependencies: '@aws-sdk/core': 3.974.7 @@ -4530,6 +4322,16 @@ snapshots: '@smithy/util-stream': 4.5.25 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/fetch-http-handler': 5.4.4 + '@smithy/node-http-handler': 4.7.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.972.37': dependencies: '@aws-sdk/core': 3.974.7 @@ -4549,6 +4351,22 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-ini@3.972.44': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/credential-provider-env': 3.972.40 + '@aws-sdk/credential-provider-http': 3.972.42 + '@aws-sdk/credential-provider-login': 3.972.44 + '@aws-sdk/credential-provider-process': 3.972.40 + '@aws-sdk/credential-provider-sso': 3.972.44 + '@aws-sdk/credential-provider-web-identity': 3.972.44 + '@aws-sdk/nested-clients': 3.997.12 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/credential-provider-imds': 4.3.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.37': dependencies: '@aws-sdk/core': 3.974.7 @@ -4562,6 +4380,15 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-login@3.972.44': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/nested-clients': 3.997.12 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.38': dependencies: '@aws-sdk/credential-provider-env': 3.972.33 @@ -4579,6 +4406,20 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.972.45': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.40 + '@aws-sdk/credential-provider-http': 3.972.42 + '@aws-sdk/credential-provider-ini': 3.972.44 + '@aws-sdk/credential-provider-process': 3.972.40 + '@aws-sdk/credential-provider-sso': 3.972.44 + '@aws-sdk/credential-provider-web-identity': 3.972.44 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/credential-provider-imds': 4.3.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.33': dependencies: '@aws-sdk/core': 3.974.7 @@ -4588,6 +4429,14 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.972.37': dependencies: '@aws-sdk/core': 3.974.7 @@ -4601,6 +4450,16 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-sso@3.972.44': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/nested-clients': 3.997.12 + '@aws-sdk/token-providers': 3.1054.0 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-web-identity@3.972.37': dependencies: '@aws-sdk/core': 3.974.7 @@ -4613,6 +4472,15 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-web-identity@3.972.44': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/nested-clients': 3.997.12 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.972.10': dependencies: '@aws-sdk/types': 3.973.8 @@ -4662,6 +4530,19 @@ snapshots: '@smithy/util-retry': 4.3.6 tslib: 2.8.1 + '@aws-sdk/nested-clients@3.997.12': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.14 + '@aws-sdk/signature-v4-multi-region': 3.996.29 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/fetch-http-handler': 5.4.4 + '@smithy/node-http-handler': 4.7.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/nested-clients@3.997.5': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -4723,6 +4604,13 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.29': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/signature-v4': 5.4.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1039.0': dependencies: '@aws-sdk/core': 3.974.7 @@ -4735,11 +4623,25 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/token-providers@3.1054.0': + dependencies: + '@aws-sdk/core': 3.974.14 + '@aws-sdk/nested-clients': 3.997.12 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/types@3.973.8': dependencies: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/types@3.973.9': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/util-arn-parser@3.972.3': dependencies: tslib: 2.8.1 @@ -4779,6 +4681,12 @@ snapshots: fast-xml-parser: 5.7.2 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.26': + dependencies: + '@smithy/types': 4.14.2 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.2.4': {} '@babel/code-frame@7.29.0': @@ -5306,141 +5214,26 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@fastify/busboy@3.2.0': {} + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 - '@firebase/app-check-interop-types@0.3.4': {} + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 - '@firebase/app-types@0.9.5': + '@floating-ui/react-dom@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@firebase/logger': 0.5.1 + '@floating-ui/dom': 1.7.5 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - '@firebase/auth-interop-types@0.2.5': {} + '@floating-ui/utils@0.2.10': {} - '@firebase/component@0.7.3': + '@hookform/resolvers@3.10.0(react-hook-form@7.71.1(react@19.2.3))': dependencies: - '@firebase/util': 1.15.1 - tslib: 2.8.1 - - '@firebase/database-compat@2.1.4': - dependencies: - '@firebase/component': 0.7.3 - '@firebase/database': 1.1.3 - '@firebase/database-types': 1.0.20 - '@firebase/logger': 0.5.1 - '@firebase/util': 1.15.1 - tslib: 2.8.1 - - '@firebase/database-types@1.0.20': - dependencies: - '@firebase/app-types': 0.9.5 - '@firebase/util': 1.15.1 - - '@firebase/database@1.1.3': - dependencies: - '@firebase/app-check-interop-types': 0.3.4 - '@firebase/auth-interop-types': 0.2.5 - '@firebase/component': 0.7.3 - '@firebase/logger': 0.5.1 - '@firebase/util': 1.15.1 - faye-websocket: 0.11.4 - tslib: 2.8.1 - - '@firebase/logger@0.5.1': - dependencies: - tslib: 2.8.1 - - '@firebase/util@1.15.1': - dependencies: - tslib: 2.8.1 - - '@floating-ui/core@1.7.4': - dependencies: - '@floating-ui/utils': 0.2.10 - - '@floating-ui/dom@1.7.5': - dependencies: - '@floating-ui/core': 1.7.4 - '@floating-ui/utils': 0.2.10 - - '@floating-ui/react-dom@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@floating-ui/dom': 1.7.5 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - '@floating-ui/utils@0.2.10': {} - - '@google-cloud/firestore@7.11.6': - dependencies: - '@opentelemetry/api': 1.9.1 - fast-deep-equal: 3.1.3 - functional-red-black-tree: 1.0.1 - google-gax: 4.6.1 - protobufjs: 7.6.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - '@google-cloud/paginator@5.0.2': - dependencies: - arrify: 2.0.1 - extend: 3.0.2 - optional: true - - '@google-cloud/projectify@4.0.0': - optional: true - - '@google-cloud/promisify@4.0.0': - optional: true - - '@google-cloud/storage@7.19.0': - dependencies: - '@google-cloud/paginator': 5.0.2 - '@google-cloud/projectify': 4.0.0 - '@google-cloud/promisify': 4.0.0 - abort-controller: 3.0.0 - async-retry: 1.3.3 - duplexify: 4.1.3 - fast-xml-parser: 5.7.2 - gaxios: 6.7.1 - google-auth-library: 9.15.1 - html-entities: 2.6.0 - mime: 3.0.0 - p-limit: 3.1.0 - retry-request: 7.0.2 - teeny-request: 9.0.0 - uuid: 8.0.0 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - '@grpc/grpc-js@1.14.4': - dependencies: - '@grpc/proto-loader': 0.8.1 - '@js-sdsl/ordered-map': 4.4.2 - optional: true - - '@grpc/proto-loader@0.7.15': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.6.1 - yargs: 17.7.2 - optional: true - - '@grpc/proto-loader@0.8.1': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.6.1 - yargs: 17.7.2 - optional: true - - '@hookform/resolvers@3.10.0(react-hook-form@7.71.1(react@19.2.3))': - dependencies: - react-hook-form: 7.71.1(react@19.2.3) + react-hook-form: 7.71.1(react@19.2.3) '@img/colour@1.0.0': {} @@ -5552,9 +5345,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@js-sdsl/ordered-map@4.4.2': - optional: true - '@modelcontextprotocol/sdk@1.6.1': dependencies: content-type: 1.0.5 @@ -5828,38 +5618,6 @@ snapshots: '@popperjs/core@2.11.8': {} - '@protobufjs/aspromise@1.1.2': - optional: true - - '@protobufjs/base64@1.1.2': - optional: true - - '@protobufjs/codegen@2.0.5': - optional: true - - '@protobufjs/eventemitter@1.1.1': - optional: true - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - optional: true - - '@protobufjs/float@1.0.2': - optional: true - - '@protobufjs/inquire@1.1.2': - optional: true - - '@protobufjs/path@1.1.2': - optional: true - - '@protobufjs/pool@1.1.0': - optional: true - - '@protobufjs/utf8@1.1.1': - optional: true - '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -6356,6 +6114,12 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 + '@smithy/core@3.24.4': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.14': dependencies: '@smithy/node-config-provider': 4.3.14 @@ -6364,6 +6128,12 @@ snapshots: '@smithy/url-parser': 4.2.14 tslib: 2.8.1 + '@smithy/credential-provider-imds@4.3.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.3.17': dependencies: '@smithy/protocol-http': 5.3.14 @@ -6372,6 +6142,12 @@ snapshots: '@smithy/util-base64': 4.3.2 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.4.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/hash-node@4.2.14': dependencies: '@smithy/types': 4.14.1 @@ -6448,6 +6224,12 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@smithy/node-http-handler@4.7.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/property-provider@4.2.14': dependencies: '@smithy/types': 4.14.1 @@ -6489,6 +6271,12 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/signature-v4@5.4.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/smithy-client@4.12.13': dependencies: '@smithy/core': 3.23.17 @@ -6503,6 +6291,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/types@4.14.2': + dependencies: + tslib: 2.8.1 + '@smithy/url-parser@4.2.14': dependencies: '@smithy/querystring-parser': 4.2.14 @@ -6610,9 +6402,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@tootallnate/once@2.0.1': - optional: true - '@tsconfig/bun@1.0.7': {} '@tybys/wasm-util@0.10.1': @@ -6620,23 +6409,10 @@ snapshots: tslib: 2.8.1 optional: true - '@types/caseless@0.12.5': - optional: true - '@types/conventional-commits-parser@5.0.2': dependencies: '@types/node': 20.19.31 - '@types/jsonwebtoken@9.0.10': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 20.19.31 - - '@types/long@4.0.2': - optional: true - - '@types/ms@2.1.0': {} - '@types/node@20.19.31': dependencies: undici-types: 6.21.0 @@ -6663,41 +6439,18 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/request@2.48.13': - dependencies: - '@types/caseless': 0.12.5 - '@types/node': 20.19.31 - '@types/tough-cookie': 4.0.5 - form-data: 2.5.5 - optional: true - '@types/tinycolor2@1.4.6': {} - '@types/tough-cookie@4.0.5': - optional: true - JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 through: 2.3.8 - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - optional: true - accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 - agent-base@6.0.2: - dependencies: - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - optional: true - agent-base@7.1.4: {} ajv@8.17.1: @@ -6746,17 +6499,6 @@ snapshots: array-ify@1.0.0: {} - arrify@2.0.1: - optional: true - - async-retry@1.3.3: - dependencies: - retry: 0.13.1 - optional: true - - asynckit@0.4.0: - optional: true - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -6905,11 +6647,6 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - optional: true - commander@14.0.3: {} commander@4.1.1: {} @@ -6981,8 +6718,6 @@ snapshots: dargs@8.1.0: {} - data-uri-to-buffer@4.0.1: {} - date-fns-tz@3.2.0(date-fns@4.1.0): dependencies: date-fns: 4.1.0 @@ -7001,9 +6736,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - delayed-stream@1.0.0: - optional: true - depd@2.0.0: {} detect-libc@2.1.2: {} @@ -7054,14 +6786,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexify@4.1.3: - dependencies: - end-of-stream: 1.4.5 - inherits: 2.0.4 - readable-stream: 3.6.2 - stream-shift: 1.0.3 - optional: true - ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -7074,11 +6798,6 @@ snapshots: encodeurl@2.0.0: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - optional: true - env-paths@2.2.1: {} environment@1.1.0: {} @@ -7095,14 +6814,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - optional: true - esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -7198,9 +6909,6 @@ snapshots: etag@1.8.1: {} - event-target-shim@5.0.1: - optional: true - eventemitter3@5.0.4: {} events@1.1.1: {} @@ -7250,8 +6958,6 @@ snapshots: extend@3.0.2: {} - farmhash-modern@1.1.0: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -7268,6 +6974,11 @@ snapshots: dependencies: path-expression-matcher: 1.5.0 + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + fast-xml-parser@5.7.2: dependencies: '@nodable/entities': 2.1.0 @@ -7275,23 +6986,21 @@ snapshots: path-expression-matcher: 1.5.0 strnum: 2.2.3 - fastq@1.20.1: + fast-xml-parser@5.7.3: dependencies: - reusify: 1.1.0 + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 - faye-websocket@0.11.4: + fastq@1.20.1: dependencies: - websocket-driver: 0.7.4 + reusify: 1.1.0 fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -7315,41 +7024,10 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 - firebase-admin@13.10.0: - dependencies: - '@fastify/busboy': 3.2.0 - '@firebase/database-compat': 2.1.4 - '@firebase/database-types': 1.0.20 - farmhash-modern: 1.1.0 - fast-deep-equal: 3.1.3 - google-auth-library: 10.6.2 - jsonwebtoken: 9.0.3 - jwks-rsa: 3.2.2 - optionalDependencies: - '@google-cloud/firestore': 7.11.6 - '@google-cloud/storage': 7.19.0 - transitivePeerDependencies: - - encoding - - supports-color - for-each@0.3.5: dependencies: is-callable: 1.2.7 - form-data@2.5.5: - 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 - optional: true - - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fresh@2.0.0: {} @@ -7359,9 +7037,6 @@ snapshots: function-bind@1.1.2: {} - functional-red-black-tree@1.0.1: - optional: true - gaxios@6.7.1: dependencies: extend: 3.0.2 @@ -7373,14 +7048,6 @@ snapshots: - encoding - supports-color - gaxios@7.1.4: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6(supports-color@10.2.2) - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - gcp-metadata@6.1.1: dependencies: gaxios: 6.7.1 @@ -7390,14 +7057,6 @@ snapshots: - encoding - supports-color - gcp-metadata@8.1.2: - dependencies: - gaxios: 7.1.4 - google-logging-utils: 1.1.3 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - generator-function@2.0.1: {} get-caller-file@2.0.5: {} @@ -7446,17 +7105,6 @@ snapshots: dependencies: ini: 4.1.1 - google-auth-library@10.6.2: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.4 - gcp-metadata: 8.1.2 - google-logging-utils: 1.1.3 - jws: 4.0.1 - transitivePeerDependencies: - - supports-color - google-auth-library@9.15.1: dependencies: base64-js: 1.5.1 @@ -7469,29 +7117,8 @@ snapshots: - encoding - supports-color - google-gax@4.6.1: - dependencies: - '@grpc/grpc-js': 1.14.4 - '@grpc/proto-loader': 0.7.15 - '@types/long': 4.0.2 - abort-controller: 3.0.0 - duplexify: 4.1.3 - google-auth-library: 9.15.1 - node-fetch: 2.7.0 - object-hash: 3.0.0 - proto3-json-serializer: 2.0.2 - protobufjs: 7.6.1 - retry-request: 7.0.2 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - google-logging-utils@0.0.2: {} - google-logging-utils@1.1.3: {} - googleapis-common@7.2.0: dependencies: extend: 3.0.2 @@ -7544,9 +7171,6 @@ snapshots: hono@4.7.4: {} - html-entities@2.6.0: - optional: true - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -7555,25 +7179,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-parser-js@0.5.10: {} - - http-proxy-agent@5.0.0: - dependencies: - '@tootallnate/once': 2.0.1 - agent-base: 6.0.2 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - optional: true - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - optional: true - https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: agent-base: 7.1.4 @@ -7696,35 +7301,12 @@ snapshots: jsonparse@1.3.1: {} - jsonwebtoken@9.0.3: - 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.1.1 - ms: 2.1.3 - semver: 7.7.3 - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jwks-rsa@3.2.2: - dependencies: - '@types/jsonwebtoken': 9.0.10 - debug: 4.4.3(supports-color@10.2.2) - jose: 4.15.9 - limiter: 1.1.5 - lru-memoizer: 2.3.0 - transitivePeerDependencies: - - supports-color - jws@4.0.1: dependencies: jwa: 2.0.1 @@ -7732,8 +7314,6 @@ snapshots: lilconfig@3.1.3: {} - limiter@1.1.5: {} - lines-and-columns@1.2.4: {} lint-staged@16.2.7: @@ -7761,28 +7341,14 @@ snapshots: lodash.camelcase@4.3.0: {} - lodash.clonedeep@4.5.0: {} - - 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.kebabcase@4.1.1: {} lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} - lodash.once@4.1.1: {} - lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -7799,9 +7365,6 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 - long@5.3.2: - optional: true - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -7810,11 +7373,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-memoizer@2.3.0: - dependencies: - lodash.clonedeep: 4.5.0 - lru-cache: 6.0.0 - lucide-react@0.453.0(react@19.2.3): dependencies: react: 19.2.3 @@ -7834,23 +7392,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: - optional: true - mime-db@1.54.0: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - optional: true - mime-types@3.0.2: dependencies: mime-db: 1.54.0 - mime@3.0.0: - optional: true - mimic-function@5.0.1: {} minimatch@5.1.6: @@ -7903,18 +7450,10 @@ snapshots: - '@babel/core' - babel-plugin-macros - node-domexception@1.0.0: {} - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - normalize-path@3.0.0: {} nuqs@2.8.8(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): @@ -7973,11 +7512,6 @@ snapshots: object-hash: 2.2.0 oidc-token-hash: 5.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - optional: true - p-limit@4.0.0: dependencies: yocto-queue: 1.2.2 @@ -8129,27 +7663,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - proto3-json-serializer@2.0.2: - dependencies: - protobufjs: 7.6.1 - optional: true - - protobufjs@7.6.1: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 20.19.31 - long: 5.3.2 - optional: true - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8229,13 +7742,6 @@ snapshots: dependencies: pify: 2.3.0 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - optional: true - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -8263,19 +7769,6 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - retry-request@7.0.2: - dependencies: - '@types/request': 2.48.13 - extend: 3.0.2 - teeny-request: 9.0.0 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - retry@0.13.1: - optional: true - reusify@1.1.0: {} rfdc@1.4.1: {} @@ -8477,14 +7970,6 @@ snapshots: statuses@2.0.2: {} - stream-events@1.0.5: - dependencies: - stubs: 3.0.0 - optional: true - - stream-shift@1.0.3: - optional: true - string-argv@0.3.2: {} string-width@4.2.3: @@ -8504,11 +7989,6 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - optional: true - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8519,9 +7999,6 @@ snapshots: strnum@2.2.3: {} - stubs@3.0.0: - optional: true - styled-jsx@5.1.6(react@19.2.3): dependencies: client-only: 0.0.1 @@ -8581,18 +8058,6 @@ snapshots: - tsx - yaml - teeny-request@9.0.0: - dependencies: - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - node-fetch: 2.7.0 - stream-events: 1.0.5 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - text-extensions@2.4.0: {} thenify-all@1.6.0: @@ -8693,18 +8158,8 @@ snapshots: vary@1.1.2: {} - web-streams-polyfill@3.3.3: {} - webidl-conversions@3.0.1: {} - websocket-driver@0.7.4: - dependencies: - http-parser-js: 0.5.10 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 - - websocket-extensions@0.1.4: {} - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -8734,6 +8189,8 @@ snapshots: wrappy@1.0.2: {} + xml-naming@0.1.0: {} + xml2js@0.6.2: dependencies: sax: 1.2.1 @@ -8765,9 +8222,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yocto-queue@0.1.0: - optional: true - yocto-queue@1.2.2: {} zod-to-json-schema@3.24.3(zod@3.24.2): diff --git a/src/app/api/push-tokens/route.ts b/src/app/api/push-tokens/route.ts index 6c21dc99b..e6c8836ab 100644 --- a/src/app/api/push-tokens/route.ts +++ b/src/app/api/push-tokens/route.ts @@ -3,6 +3,11 @@ import { NextResponse } from "next/server"; import { db } from "@/db"; import { nativePushTokens } from "@/db/schema"; import { getCurrentSession } from "@/lib/auth"; +import { isApnsDeviceToken, isSnsEndpointArn } from "@/lib/push/sns-config"; +import { + deleteIosPushEndpoint, + registerIosPushEndpoint, +} from "@/lib/push/sns-register"; type PushTokenPayload = { token?: unknown; @@ -17,11 +22,12 @@ async function readPayload(request: Request) { } } -function getValidToken(payload: PushTokenPayload) { +function getValidApnsToken(payload: PushTokenPayload) { if (typeof payload.token !== "string") return null; const token = payload.token.trim(); if (!token || token === "ERROR GET TOKEN") return null; + if (!isApnsDeviceToken(token)) return null; return token; } @@ -37,9 +43,12 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } - const token = getValidToken(payload); - if (!token) { - return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); + const apnsToken = getValidApnsToken(payload); + if (!apnsToken) { + return NextResponse.json( + { error: "Invalid APNs device token" }, + { status: 400 }, + ); } if (payload.platform !== "ios") { @@ -49,10 +58,13 @@ export async function POST(request: Request) { ); } - // Registration requires a signed-in user. The `app-platform` cookie is set by - // the WKWebView for client UX only and must not be used as an auth gate - // (it is unsigned and trivially spoofed). Invalid or non-iOS FCM tokens are - // rejected by Firebase on send and pruned in sendPushToUsers. + const endpointArn = await registerIosPushEndpoint(apnsToken, user.id); + if (!endpointArn) { + return NextResponse.json( + { error: "Push registration is not configured" }, + { status: 503 }, + ); + } const platform = "ios"; @@ -60,7 +72,7 @@ export async function POST(request: Request) { .insert(nativePushTokens) .values({ userId: user.id, - token, + token: endpointArn, platform, userAgent: request.headers.get("user-agent"), }) @@ -88,17 +100,21 @@ export async function DELETE(request: Request) { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } - const token = getValidToken(payload); + const token = typeof payload.token === "string" ? payload.token.trim() : ""; if (!token) { - return NextResponse.json({ error: "Invalid push token" }, { status: 400 }); + return NextResponse.json({ error: "Invalid token" }, { status: 400 }); + } + + if (isSnsEndpointArn(token)) { + await deleteIosPushEndpoint(token); } await db .delete(nativePushTokens) .where( and( - eq(nativePushTokens.token, token), eq(nativePushTokens.userId, user.id), + eq(nativePushTokens.token, token), ), ); diff --git a/src/lib/push/parse-payload.ts b/src/lib/push/parse-payload.ts index e38aa4826..c4455562d 100644 --- a/src/lib/push/parse-payload.ts +++ b/src/lib/push/parse-payload.ts @@ -41,7 +41,7 @@ function pickPushFields(record: Record): NativePushPayload { } /** - * Extracts FCM custom data from the JSON blob iOS forwards from `userInfo`. + * Extracts APNs custom data from the JSON blob iOS forwards from `userInfo`. */ export function parseNativePushPayload(detail: unknown): NativePushPayload { if (typeof detail === "string") { diff --git a/src/lib/push/redirect.ts b/src/lib/push/redirect.ts index 066c55637..451b898ed 100644 --- a/src/lib/push/redirect.ts +++ b/src/lib/push/redirect.ts @@ -22,7 +22,7 @@ function looksLikeInviteToken(redirect: string): boolean { } /** - * App-relative path used in FCM data payloads and native tap handling. + * App-relative path used in APNs/SNS payloads and native tap handling. * In-app notification rows may still store full URLs or invite tokens. * * When `type` is omitted (legacy or partial FCM payloads), infers the path from diff --git a/src/lib/push/send-push.ts b/src/lib/push/send-push.ts index 6b279b640..aa042f108 100644 --- a/src/lib/push/send-push.ts +++ b/src/lib/push/send-push.ts @@ -1,11 +1,17 @@ import "server-only"; +import { PublishCommand } from "@aws-sdk/client-sns"; import { and, eq, inArray } from "drizzle-orm"; -import { cert, getApps, initializeApp } from "firebase-admin/app"; -import { getMessaging } from "firebase-admin/messaging"; import { db } from "@/db"; import { nativePushTokens } from "@/db/schema"; import { normalizePushRedirect } from "@/lib/push/redirect"; +import { getSnsClient } from "@/lib/push/sns-client"; +import { + getSnsIosApnsEnv, + getSnsIosPlatformApplicationArn, + isSnsEndpointArn, +} from "@/lib/push/sns-config"; +import { deleteIosPushEndpoint } from "@/lib/push/sns-register"; type PushPayload = { title: string; @@ -16,70 +22,6 @@ type PushPayload = { createdBy?: string | null; }; -const FIREBASE_SERVICE_ACCOUNT_JSON = process.env.FIREBASE_SERVICE_ACCOUNT_JSON; -const FIREBASE_SERVICE_ACCOUNT_BASE64 = - process.env.FIREBASE_SERVICE_ACCOUNT_BASE64; - -let warnedMissingConfig = false; - -function parseServiceAccount(): { - projectId: string; - clientEmail: string; - privateKey: string; -} | null { - let raw = FIREBASE_SERVICE_ACCOUNT_JSON?.trim(); - - if (!raw && FIREBASE_SERVICE_ACCOUNT_BASE64) { - raw = Buffer.from(FIREBASE_SERVICE_ACCOUNT_BASE64, "base64").toString( - "utf8", - ); - } - - if (!raw) return null; - - try { - const parsed = JSON.parse(raw) as { - project_id?: string; - client_email?: string; - private_key?: string; - }; - - if (!parsed.project_id || !parsed.client_email || !parsed.private_key) { - return null; - } - - return { - projectId: parsed.project_id, - clientEmail: parsed.client_email, - privateKey: parsed.private_key, - }; - } catch { - return null; - } -} - -function getOrInitFirebaseMessaging() { - if (getApps().length > 0) { - return getMessaging(); - } - - const serviceAccount = parseServiceAccount(); - if (!serviceAccount) { - if (!warnedMissingConfig) { - console.warn( - "Push notifications are disabled: missing Firebase service account configuration.", - ); - warnedMissingConfig = true; - } - return null; - } - - const app = initializeApp({ - credential: cert(serviceAccount), - }); - return getMessaging(app); -} - function chunkArray(items: T[], size: number) { const chunks: T[][] = []; for (let index = 0; index < items.length; index += size) { @@ -88,18 +30,47 @@ function chunkArray(items: T[], size: number) { return chunks; } -function isStaleTokenError(code: string | undefined) { +function buildSnsApnsMessage(payload: PushPayload, redirect: string): string { + const apnsPayload = { + aps: { + alert: { + title: payload.title, + body: payload.message, + }, + sound: "default", + }, + type: payload.type, + redirect, + title: payload.title, + message: payload.message, + groupId: payload.groupId ?? "", + createdBy: payload.createdBy ?? "", + }; + + const apns = JSON.stringify(apnsPayload); + const apnsKey = getSnsIosApnsEnv() === "sandbox" ? "APNS_SANDBOX" : "APNS"; + + return JSON.stringify({ + default: payload.message, + [apnsKey]: apns, + }); +} + +function isStaleEndpointError(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + const name = "name" in error ? String(error.name) : ""; + const message = "message" in error ? String(error.message) : ""; return ( - code === "messaging/invalid-registration-token" || - code === "messaging/registration-token-not-registered" + name === "EndpointDisabledException" || + name === "InvalidParameterException" || + message.includes("Endpoint is disabled") || + message.includes("Invalid parameter: TargetArn") ); } export async function sendPushToUsers(userIds: string[], payload: PushPayload) { if (userIds.length === 0) return; - - const messaging = getOrInitFirebaseMessaging(); - if (!messaging) return; + if (!getSnsIosPlatformApplicationArn()) return; const tokenRows = await db .select({ token: nativePushTokens.token }) @@ -111,66 +82,47 @@ export async function sendPushToUsers(userIds: string[], payload: PushPayload) { ), ); - const tokens = [...new Set(tokenRows.map((row) => row.token))]; - if (tokens.length === 0) return; + const endpointArns = [ + ...new Set( + tokenRows + .map((row) => row.token) + .filter((token) => isSnsEndpointArn(token)), + ), + ]; + if (endpointArns.length === 0) return; const redirect = normalizePushRedirect(payload.type, payload.redirect); - const data = { - type: payload.type, - redirect, - title: payload.title, - message: payload.message, - groupId: payload.groupId ?? "", - createdBy: payload.createdBy ?? "", - }; - - const staleTokens = new Set(); - - for (const tokenChunk of chunkArray(tokens, 500)) { - try { - const response = await messaging.sendEachForMulticast({ - tokens: tokenChunk, - notification: { - title: payload.title, - body: payload.message, - }, - data, - apns: { - headers: { - "apns-priority": "10", - }, - payload: { - aps: { - alert: { - title: payload.title, - body: payload.message, - }, - sound: "default", - }, - ...data, - }, - }, - }); - - response.responses.forEach((sendResponse, index) => { - if (sendResponse.success) return; - - const errorCode = sendResponse.error?.code; - if (isStaleTokenError(errorCode)) { - staleTokens.add(tokenChunk[index]); - return; + const message = buildSnsApnsMessage(payload, redirect); + const client = getSnsClient(); + const staleEndpoints = new Set(); + + for (const endpointChunk of chunkArray(endpointArns, 10)) { + await Promise.all( + endpointChunk.map(async (endpointArn) => { + try { + await client.send( + new PublishCommand({ + TargetArn: endpointArn, + MessageStructure: "json", + Message: message, + }), + ); + } catch (error) { + if (isStaleEndpointError(error)) { + staleEndpoints.add(endpointArn); + return; + } + console.error("Failed to send push notification:", error); } - - console.error("Failed to send push notification:", sendResponse.error); - }); - } catch (error) { - console.error("Failed to send push notification batch:", error); - } + }), + ); } - if (staleTokens.size > 0) { + if (staleEndpoints.size > 0) { + const staleList = [...staleEndpoints]; + await Promise.all(staleList.map((arn) => deleteIosPushEndpoint(arn))); await db .delete(nativePushTokens) - .where(inArray(nativePushTokens.token, [...staleTokens])); + .where(inArray(nativePushTokens.token, staleList)); } } diff --git a/src/lib/push/sns-client.ts b/src/lib/push/sns-client.ts new file mode 100644 index 000000000..5ac800cb7 --- /dev/null +++ b/src/lib/push/sns-client.ts @@ -0,0 +1,13 @@ +import "server-only"; + +import { SNSClient } from "@aws-sdk/client-sns"; +import { getSnsRegion } from "@/lib/push/sns-config"; + +let client: SNSClient | null = null; + +export function getSnsClient(): SNSClient { + if (!client) { + client = new SNSClient({ region: getSnsRegion() }); + } + return client; +} diff --git a/src/lib/push/sns-config.ts b/src/lib/push/sns-config.ts new file mode 100644 index 000000000..4415a0dda --- /dev/null +++ b/src/lib/push/sns-config.ts @@ -0,0 +1,31 @@ +import "server-only"; + +const DEFAULT_SNS_REGION = "us-west-1"; + +export function getSnsRegion(): string { + return ( + process.env.SNS_REGION?.trim() || + process.env.AWS_REGION?.trim() || + DEFAULT_SNS_REGION + ); +} + +/** ARN of the SNS platform application for Apple Push (APNs). */ +export function getSnsIosPlatformApplicationArn(): string | null { + const arn = process.env.SNS_IOS_PLATFORM_APPLICATION_ARN?.trim(); + return arn || null; +} + +/** `sandbox` for dev/TestFlight debug builds; `production` for App Store. */ +export function getSnsIosApnsEnv(): "sandbox" | "production" { + return process.env.SNS_IOS_APNS_ENV === "sandbox" ? "sandbox" : "production"; +} + +export function isSnsEndpointArn(value: string): boolean { + return value.startsWith("arn:aws:sns:"); +} + +/** APNs device token from `UIApplication` (hex-encoded bytes). */ +export function isApnsDeviceToken(value: string): boolean { + return /^[0-9a-f]{32,}$/i.test(value); +} diff --git a/src/lib/push/sns-register.ts b/src/lib/push/sns-register.ts new file mode 100644 index 000000000..f571a651f --- /dev/null +++ b/src/lib/push/sns-register.ts @@ -0,0 +1,82 @@ +import "server-only"; + +import { + CreatePlatformEndpointCommand, + DeleteEndpointCommand, + SetEndpointAttributesCommand, +} from "@aws-sdk/client-sns"; +import { getSnsClient } from "@/lib/push/sns-client"; +import { + getSnsIosPlatformApplicationArn, + isSnsEndpointArn, +} from "@/lib/push/sns-config"; + +let warnedMissingPlatformArn = false; + +export async function registerIosPushEndpoint( + apnsDeviceToken: string, + userId: string, +): Promise { + const platformApplicationArn = getSnsIosPlatformApplicationArn(); + if (!platformApplicationArn) { + if (!warnedMissingPlatformArn) { + console.warn( + "Push notifications are disabled: missing SNS_IOS_PLATFORM_APPLICATION_ARN.", + ); + warnedMissingPlatformArn = true; + } + return null; + } + + const client = getSnsClient(); + + let endpointArn: string | undefined; + try { + const created = await client.send( + new CreatePlatformEndpointCommand({ + PlatformApplicationArn: platformApplicationArn, + Token: apnsDeviceToken, + CustomUserData: userId, + }), + ); + endpointArn = created.EndpointArn; + } catch (error) { + console.error("Failed to create SNS platform endpoint:", error); + return null; + } + + if (!endpointArn) return null; + + try { + await client.send( + new SetEndpointAttributesCommand({ + EndpointArn: endpointArn, + Attributes: { + Token: apnsDeviceToken, + Enabled: "true", + CustomUserData: userId, + }, + }), + ); + } catch (error) { + console.error("Failed to update SNS endpoint attributes:", error); + } + + return endpointArn; +} + +export async function deleteIosPushEndpoint( + endpointArn: string, +): Promise { + if (!isSnsEndpointArn(endpointArn)) return; + + try { + await getSnsClient().send( + new DeleteEndpointCommand({ + EndpointArn: endpointArn, + }), + ); + } catch (error) { + console.error("Failed to delete SNS endpoint:", error); + } +} diff --git a/sst.config.ts b/sst.config.ts index 06b2eda00..ceb20c63f 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -27,12 +27,28 @@ export default $config({ provider: sesProvider, }), ], + permissions: [ + { + actions: [ + "sns:Publish", + "sns:CreatePlatformEndpoint", + "sns:SetEndpointAttributes", + "sns:GetEndpointAttributes", + "sns:DeleteEndpoint", + ], + resources: ["*"], + }, + ], environment: { DATABASE_URL: process.env.DATABASE_URL ?? "localhost:3000", OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID!, OIDC_ISSUER_URL: process.env.OIDC_ISSUER_URL!, GOOGLE_OAUTH_REDIRECT_URI: `${baseUrl}/auth/login/google/callback`, NEXT_PUBLIC_BASE_URL: baseUrl, + SNS_REGION: "us-west-1", + SNS_IOS_PLATFORM_APPLICATION_ARN: + process.env.SNS_IOS_PLATFORM_APPLICATION_ARN ?? "", + SNS_IOS_APNS_ENV: process.env.SNS_IOS_APNS_ENV ?? "production", }, cachePolicy: "e6e88864-aee5-41aa-b393-c48f78e33d2d", domain: { From 567061f6a8c6e9cc25ec434405dd7af73bcb8f8f Mon Sep 17 00:00:00 2001 From: ethancha0 Date: Thu, 28 May 2026 09:50:59 -0700 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20=F0=9F=94=A7=20rm=20dead=20firebas?= =?UTF-8?q?e=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/src/Podfile.lock | 79 +---------------------- ios/src/ZotMeet.xcodeproj/project.pbxproj | 4 -- ios/src/ZotMeet/GoogleService-Info.plist | 34 ---------- ios/src/ZotMeet/Settings.swift | 2 - src/lib/push/redirect.ts | 2 +- 5 files changed, 2 insertions(+), 119 deletions(-) delete mode 100644 ios/src/ZotMeet/GoogleService-Info.plist diff --git a/ios/src/Podfile.lock b/ios/src/Podfile.lock index b773de8b4..4388eb8e1 100644 --- a/ios/src/Podfile.lock +++ b/ios/src/Podfile.lock @@ -1,89 +1,12 @@ PODS: - - Firebase/CoreOnly (12.12.1): - - FirebaseCore (~> 12.12.1) - - Firebase/Messaging (12.12.1): - - Firebase/CoreOnly - - FirebaseMessaging (~> 12.12.0) - - FirebaseCore (12.12.1): - - FirebaseCoreInternal (~> 12.12.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Logger (~> 8.1) - - FirebaseCoreInternal (12.12.0): - - "GoogleUtilities/NSData+zlib (~> 8.1)" - - FirebaseInstallations (12.12.0): - - FirebaseCore (~> 12.12.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) - - PromisesObjC (~> 2.4) - - FirebaseMessaging (12.12.0): - - FirebaseCore (~> 12.12.0) - - FirebaseInstallations (~> 12.12.0) - - GoogleDataTransport (~> 10.1) - - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Reachability (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) - - nanopb (~> 3.30910.0) - - GoogleDataTransport (10.1.0): - - nanopb (~> 3.30910.0) - - PromisesObjC (~> 2.4) - - GoogleUtilities/AppDelegateSwizzler (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Privacy - - GoogleUtilities/Environment (8.1.0): - - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Privacy - - GoogleUtilities/Network (8.1.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Privacy - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (8.1.0)": - - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.1.0) - - GoogleUtilities/Reachability (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - nanopb (3.30910.0): - - nanopb/decode (= 3.30910.0) - - nanopb/encode (= 3.30910.0) - - nanopb/decode (3.30910.0) - - nanopb/encode (3.30910.0) - - PromisesObjC (2.4.0) DEPENDENCIES: - - Firebase/Messaging SPEC REPOS: trunk: - - Firebase - - FirebaseCore - - FirebaseCoreInternal - - FirebaseInstallations - - FirebaseMessaging - - GoogleDataTransport - - GoogleUtilities - - nanopb - - PromisesObjC SPEC CHECKSUMS: - Firebase: 14f11e91129d246a8a6166b4c1c2ea61b56806ec - FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284 - FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9 - FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e - FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31 - GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 -PODFILE CHECKSUM: 5cd187e394ea190fb18c0c57d79f58afdc0a7b86 +PODFILE CHECKSUM: 613fd0f1fe9f534e781d74b06ed8e753449a3335 COCOAPODS: 1.16.2 diff --git a/ios/src/ZotMeet.xcodeproj/project.pbxproj b/ios/src/ZotMeet.xcodeproj/project.pbxproj index 1a3c5cf29..5b585f5df 100644 --- a/ios/src/ZotMeet.xcodeproj/project.pbxproj +++ b/ios/src/ZotMeet.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 25D3517F267A48E0002E5DC0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25D3517E267A48E0002E5DC0 /* GoogleService-Info.plist */; }; 595F23A525CEFBFE0053416C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595F239A25CEFBFD0053416C /* Settings.swift */; }; 595F23A625CEFBFE0053416C /* Entitlements in Resources */ = {isa = PBXBuildFile; fileRef = 595F239B25CEFBFD0053416C /* Entitlements */; }; 595F23A725CEFBFE0053416C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 595F239C25CEFBFD0053416C /* Assets.xcassets */; }; @@ -23,7 +22,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 25D3517E267A48E0002E5DC0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "ZotMeet/GoogleService-Info.plist"; sourceTree = ""; }; 4C1C2162A8048FDF52B2A9D0 /* Pods-ZotMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ZotMeet.release.xcconfig"; path = "Target Support Files/Pods-ZotMeet/Pods-ZotMeet.release.xcconfig"; sourceTree = ""; }; 59333BAA25CFF706003392A4 /* ZotMeet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ZotMeet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 595F239A25CEFBFD0053416C /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Settings.swift; path = ZotMeet/Settings.swift; sourceTree = ""; }; @@ -78,7 +76,6 @@ 595F239C25CEFBFD0053416C /* Assets.xcassets */, 595F239B25CEFBFD0053416C /* Entitlements */, 595F23A025CEFBFE0053416C /* Info.plist */, - 25D3517E267A48E0002E5DC0 /* GoogleService-Info.plist */, 595F23A325CEFBFE0053416C /* Printer.swift */, 595F239F25CEFBFE0053416C /* PushNotifications.swift */, 595F239D25CEFBFD0053416C /* SceneDelegate.swift */, @@ -158,7 +155,6 @@ CDC0FE292388222C002C8D56 /* Main.storyboard in Resources */, CDC0FE2A2388222C002C8D56 /* LaunchScreen.storyboard in Resources */, 595F23A725CEFBFE0053416C /* Assets.xcassets in Resources */, - 25D3517F267A48E0002E5DC0 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/src/ZotMeet/GoogleService-Info.plist b/ios/src/ZotMeet/GoogleService-Info.plist deleted file mode 100644 index a4d8f651d..000000000 --- a/ios/src/ZotMeet/GoogleService-Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CLIENT_ID - 000000000000-000000000000000000000000000000.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.0000000000-00000000000000000000000 - API_KEY - 0000000000000000000000000 - GCM_SENDER_ID - 000000000000 - PLIST_VERSION - 1 - BUNDLE_ID - com.microsoft.pwabuilder-ios - PROJECT_ID - pwabuilder-ios-template - STORAGE_BUCKET - pwabuilder-ios-template.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:619930292029:ios:f6737372189b8ee9123f54 - - diff --git a/ios/src/ZotMeet/Settings.swift b/ios/src/ZotMeet/Settings.swift index 049206776..bbf8c460a 100644 --- a/ios/src/ZotMeet/Settings.swift +++ b/ios/src/ZotMeet/Settings.swift @@ -5,8 +5,6 @@ struct Cookie { var value: String } -let gcmMessageIDKey = "00000000000" // update this with actual ID if using Firebase - // URL for first launch let rootUrl = URL(string: "https://zotmeet.com")! diff --git a/src/lib/push/redirect.ts b/src/lib/push/redirect.ts index 451b898ed..fc2594f85 100644 --- a/src/lib/push/redirect.ts +++ b/src/lib/push/redirect.ts @@ -25,7 +25,7 @@ function looksLikeInviteToken(redirect: string): boolean { * App-relative path used in APNs/SNS payloads and native tap handling. * In-app notification rows may still store full URLs or invite tokens. * - * When `type` is omitted (legacy or partial FCM payloads), infers the path from + * When `type` is omitted (legacy or partial APNs payloads), infers the path from * `redirect` shape (e.g. `/availability/…`, `/invite/…`, or a bare invite token). */ export function normalizePushRedirect(