From 67058fcd1c221467b2da1e7a48f20e1f079084a9 Mon Sep 17 00:00:00 2001 From: TotoCode Date: Fri, 4 Sep 2026 18:23:39 +0200 Subject: [PATCH 01/11] feat: add web server uses fastify, right now it's just an API and nothing more. has a websocket server too, and makes it easy to add new APIs and API versions as well as websocket message types. also has optional sentry for error handling and drizzle + any postgresql compatible database for production --- .gitignore | 3 + package.json | 10 +- pnpm-lock.yaml | 2064 ++++++++++++++++- pnpm-workspace.yaml | 4 + src/Waypoint/package.json | 32 + src/Waypoint/src/api/loader.ts | 37 + src/Waypoint/src/external/.gitignore | 1 + src/Waypoint/src/external/db.ts | 23 + src/Waypoint/src/external/drizzle.config.ts | 26 + src/Waypoint/src/external/schema.ts | 4 + src/Waypoint/src/external/schema/sessions.ts | 9 + src/Waypoint/src/external/sentry.ts | 23 + src/Waypoint/src/main.ts | 26 + src/Waypoint/src/services/auth/index.ts | 9 + src/Waypoint/src/services/auth/v1.ts | 67 + src/Waypoint/src/services/meta/index.ts | 8 + src/Waypoint/src/services/meta/v1.ts | 15 + .../src/services/ws/datatypehandler.ts | 31 + .../src/services/ws/datatypes/whoami.ts | 23 + src/Waypoint/src/services/ws/index.ts | 9 + src/Waypoint/src/services/ws/v1.ts | 86 + src/Waypoint/src/ws.d.ts | 10 + src/Waypoint/tsconfig.json | 24 + 23 files changed, 2527 insertions(+), 17 deletions(-) create mode 100644 pnpm-workspace.yaml create mode 100644 src/Waypoint/package.json create mode 100644 src/Waypoint/src/api/loader.ts create mode 100644 src/Waypoint/src/external/.gitignore create mode 100644 src/Waypoint/src/external/db.ts create mode 100644 src/Waypoint/src/external/drizzle.config.ts create mode 100644 src/Waypoint/src/external/schema.ts create mode 100644 src/Waypoint/src/external/schema/sessions.ts create mode 100644 src/Waypoint/src/external/sentry.ts create mode 100644 src/Waypoint/src/main.ts create mode 100644 src/Waypoint/src/services/auth/index.ts create mode 100644 src/Waypoint/src/services/auth/v1.ts create mode 100644 src/Waypoint/src/services/meta/index.ts create mode 100644 src/Waypoint/src/services/meta/v1.ts create mode 100644 src/Waypoint/src/services/ws/datatypehandler.ts create mode 100644 src/Waypoint/src/services/ws/datatypes/whoami.ts create mode 100644 src/Waypoint/src/services/ws/index.ts create mode 100644 src/Waypoint/src/services/ws/v1.ts create mode 100644 src/Waypoint/src/ws.d.ts create mode 100644 src/Waypoint/tsconfig.json diff --git a/.gitignore b/.gitignore index ca76e37..42f2ade 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ node_modules .env.development .env.production .env.test + +# Build files +dist diff --git a/package.json b/package.json index 8268070..db902b7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,14 @@ "scripts": { "format": "stylua src && prettier --write .", "format:check": "pnpm lint", - "lint": "selene src" + "lint": "selene src", + "dev": "concurrently \"pnpm -F waypoint dev\" \"rojo serve\"", + "build": "rojo build && pnpm -F waypoint build", + "start": "pnpm -F waypoint start", + "db:generate": "pnpm -F waypoint db:generate", + "db:migrate": "pnpm -F waypoint db:migrate", + "db:push": "pnpm -F waypoint db:push", + "db:studio": "pnpm -F waypoint db:studio" }, "keywords": [], "author": "", @@ -14,6 +21,7 @@ "packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee", "type": "module", "devDependencies": { + "concurrently": "^10.0.5", "prettier": "^3.9.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 306c2da..21585c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,24 +1,2056 @@ -lockfileVersion: "9.0" +lockfileVersion: '9.0' settings: - autoInstallPeers: true - excludeLinksFromLockfile: false + autoInstallPeers: true + excludeLinksFromLockfile: false importers: - .: - devDependencies: - prettier: - specifier: ^3.9.6 - version: 3.9.6 + + .: + devDependencies: + concurrently: + specifier: ^10.0.5 + version: 10.0.5 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + + src/Waypoint: + dependencies: + '@fastify/websocket': + specifier: ^11.3.0 + version: 11.3.0 + '@sentry/node': + specifier: ^10.71.0 + version: 10.71.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@10.2.2) + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(pg@8.23.0) + fastify: + specifier: ^5.12.1 + version: 5.12.1 + pg: + specifier: ^8.23.0 + version: 8.23.0 + ulid: + specifier: ^3.0.2 + version: 3.0.2 + devDependencies: + '@types/node': + specifier: ^26.2.0 + version: 26.2.0 + '@types/pg': + specifier: ^8.23.1 + version: 8.23.1 + drizzle-kit: + specifier: ^0.31.10 + version: 0.31.10 + tsx: + specifier: ^4.23.12 + version: 4.23.12 packages: - prettier@3.9.6: - resolution: - { - integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==, - } - engines: { node: ">=14" } - hasBin: true + + '@apm-js-collab/code-transformer-bundler-plugins@0.7.4': + resolution: {integrity: sha512-nAfOeZPSUAQvJa1iFT/5oCrTm5YQhMMrfCNthNnaXHZiOQhu1KGuLoIx7HtbAi3wfwaBYLaICPIeenIaEwcXIg==} + engines: {node: '>=18.0.0'} + + '@apm-js-collab/code-transformer@0.18.1': + resolution: {integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==} + hasBin: true + + '@apm-js-collab/tracing-hooks@0.13.0': + resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fastify/ajv-compiler@4.0.6': + resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.1.0': + resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==} + + '@fastify/forwarded@3.0.2': + resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/websocket@11.3.0': + resolution: {integrity: sha512-g89ag4BCcD9YP5wBZXixzoLnuf5j89p/sXFcfpCiv2pdEkYYukBEoK3heVzqsp0EAtszVDc2BBZG0KZqeAShIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.10.0': + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.220.0': + resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.10.0': + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.10.0': + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.10.0': + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.71.0': + resolution: {integrity: sha512-OIjT7rzcWJjUC6r3eBT3Td1j0afDBMkbbx9jTocSD+ZSfc25eEU7hoIPS0WvfeIOTIN3y8bfQnXavwMReaNVHQ==} + engines: {node: '>=18'} + + '@sentry/node-core@10.71.0': + resolution: {integrity: sha512-sxd0/ZW+Uda/17H0R7lB2Othm37VYcdwKdWdyHRizg872TfCX4UwTTPaoS2tMJAUjV2tVh83ZA0fsoC2q9SpjA==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + + '@sentry/node@10.71.0': + resolution: {integrity: sha512-bw2M/xkMu2+ATo6QWFmtTZTYp5LV1krt9/DTtYqtt4GmhXmXgdFPXCv6783AJI3HUoEMx2BcehhNbKzGrFXo/g==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.71.0': + resolution: {integrity: sha512-YgeL0xTObKma3MuOrt+/6M/f6mo/Z08LHh3OxPomzQpgpCgCLGyJ/739cDSSH1LRjqWdPQtoia5asl5ZRdhWmw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + + '@sentry/server-utils@10.71.0': + resolution: {integrity: sha512-zdyShKNsghzPGVWVRzc7oybYTsRZdKgdPtq+dsksImo1b6n7ILlD7eYx1Q0eAOZoWkDqDmm+Ml6MQb76IT1eTA==} + engines: {node: '>=18'} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.3.0: + resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + concurrently@10.0.5: + resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==} + engines: {node: '>=22'} + hasBin: true + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + hasBin: true + + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stringify@7.0.1: + resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + + fast-uri@4.1.3: + resolution: {integrity: sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw==} + + fastify-plugin@6.0.0: + resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} + + fastify@5.12.1: + resolution: {integrity: sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + find-my-way@9.9.0: + resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} + engines: {node: '>=20'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + import-in-the-middle@3.3.3: + resolution: {integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@2.5.0: + resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} + engines: {node: '>= 10'} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + meriyah@6.1.4: + resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} + engines: {node: '>=18.0.0'} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semifies@1.0.0: + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shell-quote@1.9.0: + resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + engines: {node: '>= 0.4'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + ulid@3.0.2: + resolution: {integrity: sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} snapshots: - prettier@3.9.6: {} + + '@apm-js-collab/code-transformer-bundler-plugins@0.7.4': + dependencies: + '@apm-js-collab/code-transformer': 0.18.1 + es-module-lexer: 2.3.2 + magic-string: 0.30.21 + module-details-from-path: 1.0.4 + + '@apm-js-collab/code-transformer@0.18.1': + dependencies: + '@types/estree': 1.0.9 + astring: 1.9.0 + esquery: 1.7.0 + meriyah: 6.1.4 + semifies: 1.0.0 + source-map: 0.6.1 + + '@apm-js-collab/tracing-hooks@0.13.0(supports-color@10.2.2)': + dependencies: + '@apm-js-collab/code-transformer': 0.18.1 + debug: 4.4.3(supports-color@10.2.2) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.3 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@fastify/ajv-compiler@4.0.6': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.3 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.1.0': + dependencies: + fast-json-stringify: 7.0.1 + + '@fastify/forwarded@3.0.2': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.2 + ipaddr.js: 2.5.0 + + '@fastify/websocket@11.3.0': + dependencies: + duplexify: 4.1.3 + fastify-plugin: 6.0.0 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + import-in-the-middle: 3.3.3 + require-in-the-middle: 8.0.1(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@pinojs/redact@0.4.0': {} + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.71.0': + dependencies: + '@sentry/conventions': 0.16.0 + + '@sentry/node-core@10.71.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.71.0 + '@sentry/opentelemetry': 10.71.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.3.3 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + + '@sentry/node@10.71.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@10.2.2)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.71.0 + '@sentry/node-core': 10.71.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.71.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.71.0(supports-color@10.2.2) + import-in-the-middle: 3.3.3 + transitivePeerDependencies: + - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.71.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.71.0 + + '@sentry/server-utils@10.71.0(supports-color@10.2.2)': + dependencies: + '@apm-js-collab/code-transformer-bundler-plugins': 0.7.4 + '@apm-js-collab/tracing-hooks': 0.13.0(supports-color@10.2.2) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.71.0 + meriyah: 6.1.4 + transitivePeerDependencies: + - supports-color + + '@types/estree@1.0.9': {} + + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + + '@types/pg@8.23.1': + dependencies: + '@types/node': 26.2.0 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + + abstract-logging@2.0.1: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@6.3.0: {} + + ansi-styles@6.2.3: {} + + astring@1.9.0: {} + + atomic-sleep@1.0.0: {} + + avvio@9.3.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + buffer-from@1.1.2: {} + + chalk@5.6.2: {} + + cjs-module-lexer@2.2.1: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + concurrently@10.0.5: + dependencies: + chalk: 5.6.2 + rxjs: 7.8.2 + shell-quote: 1.9.0 + supports-color: 10.2.2 + tree-kill: 1.2.2 + yargs: 18.0.0 + + cookie@1.1.1: {} + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + dequal@2.0.3: {} + + dotenv@17.4.2: {} + + drizzle-kit@0.31.10: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.23.12 + + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(pg@8.23.0): + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/pg': 8.23.1 + pg: 8.23.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 + + emoji-regex@10.6.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-module-lexer@2.3.2: {} + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stringify@7.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.3 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.6: {} + + fast-uri@4.1.3: {} + + fastify-plugin@6.0.0: {} + + fastify@5.12.1: + dependencies: + '@fastify/ajv-compiler': 4.0.6 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.1.0 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.3.0 + fast-json-stringify: 7.0.1 + find-my-way: 9.9.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.1.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + find-my-way@9.9.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + import-in-the-middle@3.3.3: + dependencies: + cjs-module-lexer: 2.2.1 + es-module-lexer: 2.3.2 + module-details-from-path: 1.0.4 + + inherits@2.0.4: {} + + ipaddr.js@2.5.0: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@1.0.0: {} + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + meriyah@6.1.4: {} + + module-details-from-path@1.0.4: {} + + ms@2.1.3: {} + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prettier@3.9.6: {} + + process-warning@4.0.1: {} + + process-warning@5.1.0: {} + + quick-format-unescaped@4.0.4: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + require-from-string@2.0.2: {} + + require-in-the-middle@8.0.1(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + resolve-pkg-maps@1.0.0: {} + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + secure-json-parse@4.1.0: {} + + semifies@1.0.0: {} + + semver@7.8.5: {} + + set-cookie-parser@2.7.2: {} + + shell-quote@1.9.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + stream-shift@1.0.3: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + supports-color@10.2.2: {} + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + toad-cache@3.7.4: {} + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + ulid@3.0.2: {} + + undici-types@8.3.0: {} + + util-deprecate@1.0.2: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.21.3: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..31370f9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - "src/Waypoint" +allowBuilds: + esbuild: true diff --git a/src/Waypoint/package.json b/src/Waypoint/package.json new file mode 100644 index 0000000..02ad50b --- /dev/null +++ b/src/Waypoint/package.json @@ -0,0 +1,32 @@ +{ + "name": "waypoint", + "main": "src/main.ts", + "type": "module", + "private": true, + "version": "0.0.0", + "description": "The Spawnpad backend", + "scripts": { + "dev": "tsx --watch src/main.ts", + "build": "tsc", + "start": "node dist/main.js", + "db:generate": "drizzle-kit generate --config src/external/drizzle.config.ts", + "db:migrate": "drizzle-kit migrate --config src/external/drizzle.config.ts", + "db:push": "drizzle-kit push --config src/external/drizzle.config.ts", + "db:studio": "drizzle-kit studio --config src/external/drizzle.config.ts" + }, + "devDependencies": { + "@types/node": "^26.2.0", + "@types/pg": "^8.23.1", + "drizzle-kit": "^0.31.10", + "tsx": "^4.23.12" + }, + "dependencies": { + "@fastify/websocket": "^11.3.0", + "@sentry/node": "^10.71.0", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "fastify": "^5.12.1", + "pg": "^8.23.0", + "ulid": "^3.0.2" + } +} diff --git a/src/Waypoint/src/api/loader.ts b/src/Waypoint/src/api/loader.ts new file mode 100644 index 0000000..b7f04eb --- /dev/null +++ b/src/Waypoint/src/api/loader.ts @@ -0,0 +1,37 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { app } from "../main.js"; +import { FastifyPluginAsync } from "fastify"; +import { pathToFileURL } from "node:url"; + +export async function loadApiRoutes() { + try { + const routes = await fs.readdir( + path.join(import.meta.dirname, "../services"), + ); + for (const route of routes) { + let routePath = path.join( + import.meta.dirname, + "../services", + route, + ); + + routePath = pathToFileURL(routePath).href; + + try { + const { default: routeModule } = await import(routePath); + for (const [version, router] of Object.entries( + routeModule.versions, + ) as [string, FastifyPluginAsync][]) { + app.register(router, { + prefix: `/api/${version}/${routeModule.name}`, + }); + } + } catch (error) { + throw new Error(`Failed to load route ${route}: ${error}`); + } + } + } catch (error) { + throw new Error(`Failed to load routes: ${error}`); + } +} diff --git a/src/Waypoint/src/external/.gitignore b/src/Waypoint/src/external/.gitignore new file mode 100644 index 0000000..836bbf4 --- /dev/null +++ b/src/Waypoint/src/external/.gitignore @@ -0,0 +1 @@ +drizzle diff --git a/src/Waypoint/src/external/db.ts b/src/Waypoint/src/external/db.ts new file mode 100644 index 0000000..76874e6 --- /dev/null +++ b/src/Waypoint/src/external/db.ts @@ -0,0 +1,23 @@ +import dotenv from "dotenv"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Pool } from "pg"; +import * as schema from "./schema.js"; + +const currentDirectory = dirname(fileURLToPath(import.meta.url)); + +dotenv.config({ + path: resolve(currentDirectory, "../../../../.env"), + quiet: true, +}); + +const connectionString = process.env.DATABASE_URL; + +if (!connectionString) { + throw new Error("DATABASE_URL is not set"); +} + +const pool = new Pool({ connectionString }); + +export const db = drizzle({ client: pool, schema }); diff --git a/src/Waypoint/src/external/drizzle.config.ts b/src/Waypoint/src/external/drizzle.config.ts new file mode 100644 index 0000000..2e949a7 --- /dev/null +++ b/src/Waypoint/src/external/drizzle.config.ts @@ -0,0 +1,26 @@ +import { config } from "dotenv"; +import { defineConfig } from "drizzle-kit"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const currentDirectory = dirname(fileURLToPath(import.meta.url)); + +config({ + quiet: true, + path: resolve(currentDirectory, "../../../../.env"), +}); + +const connectionString = process.env.DATABASE_URL; + +if (!connectionString) { + throw new Error("DATABASE_URL is required to run Drizzle commands"); +} + +export default defineConfig({ + dialect: "postgresql", + schema: "./src/external/schema.ts", + out: "./src/external/drizzle", + dbCredentials: { + url: connectionString, + }, +}); diff --git a/src/Waypoint/src/external/schema.ts b/src/Waypoint/src/external/schema.ts new file mode 100644 index 0000000..1032336 --- /dev/null +++ b/src/Waypoint/src/external/schema.ts @@ -0,0 +1,4 @@ +import { sessions } from "./schema/sessions.js"; + +// Define Drizzle tables in this module. Migration generation reads this file. +export { sessions }; diff --git a/src/Waypoint/src/external/schema/sessions.ts b/src/Waypoint/src/external/schema/sessions.ts new file mode 100644 index 0000000..89d4330 --- /dev/null +++ b/src/Waypoint/src/external/schema/sessions.ts @@ -0,0 +1,9 @@ +import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +export const sessions = pgTable("sessions", { + id: text("id").primaryKey(), + userId: text("user_id").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").notNull(), + hashedToken: text("hashed_token").notNull(), +}); diff --git a/src/Waypoint/src/external/sentry.ts b/src/Waypoint/src/external/sentry.ts new file mode 100644 index 0000000..c4c5f48 --- /dev/null +++ b/src/Waypoint/src/external/sentry.ts @@ -0,0 +1,23 @@ +import * as Sentry from "@sentry/node"; + +if (process.env.SENTRY_ENABLED === "true") { + if (!process.env.SENTRY_DSN) { + throw new Error("SENTRY_ENABLED is true but SENTRY_DSN is not set"); + } + + Sentry.init({ + dsn: process.env.SENTRY_DSN, + dataCollection: { + userInfo: false, + genAI: { + inputs: false, + outputs: false, + }, + cookies: false, + }, + + enableLogs: process.env.SENTRY_ENABLE_LOGS === "true", + + tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE) || 0.1, + }); +} diff --git a/src/Waypoint/src/main.ts b/src/Waypoint/src/main.ts new file mode 100644 index 0000000..dd139e3 --- /dev/null +++ b/src/Waypoint/src/main.ts @@ -0,0 +1,26 @@ +import dotenv from "dotenv"; +import path from "node:path"; +dotenv.config({ + quiet: true, + path: path.join(import.meta.dirname, "../../../.env"), +}); +await import("./external/sentry.js"); +import fastify from "fastify"; +import * as Sentry from "@sentry/node"; +import { loadApiRoutes } from "./api/loader.js"; + +export const app = fastify(); + +Sentry.setupFastifyErrorHandler(app); + +await loadApiRoutes(); + +app.get("/", async (req, res) => { + res.send( + `Hey user! +You might be wondering what this is. This is Waypoint, Spawnpad's external server. It has yet to have a user-facing interface, so it's nearly blank here. +This is just the API, so you can't see much yet. If you're a developer, you can poke around by looking through the Spawnpad source code.`.trim(), + ); +}); + +app.listen({ port: 3000 }); diff --git a/src/Waypoint/src/services/auth/index.ts b/src/Waypoint/src/services/auth/index.ts new file mode 100644 index 0000000..96d105b --- /dev/null +++ b/src/Waypoint/src/services/auth/index.ts @@ -0,0 +1,9 @@ +import { authPlugin } from "./v1.js"; + +export default { + name: "auth", + + versions: { + v1: authPlugin, + }, +}; diff --git a/src/Waypoint/src/services/auth/v1.ts b/src/Waypoint/src/services/auth/v1.ts new file mode 100644 index 0000000..e5ed5e5 --- /dev/null +++ b/src/Waypoint/src/services/auth/v1.ts @@ -0,0 +1,67 @@ +import { FastifyPluginAsync } from "fastify"; +import { randomBytes, createHash } from "node:crypto"; +import { ulid } from "ulid"; +import { and, eq } from "drizzle-orm"; +import { db } from "../../external/db.js"; +import { sessions } from "../../external/schema.js"; + +export interface Session { + id: string; + userId: number; + hashedToken: string; + createdAt: Date; + expiresAt: Date; +} + +export const authPlugin: FastifyPluginAsync = async (fastify, opts) => { + fastify.post("/roblox", async (req, res) => { + const body = req.body as { userId: string }; + const userId = body.userId; + + if (!userId) { + return res.status(400).send({ error: "userId is required" }); + } + + const token = randomBytes(32).toString("base64url"); + const hashedToken = createHash("sha256").update(token).digest("hex"); + + await db.insert(sessions).values({ + id: ulid(), + userId, + hashedToken, + expiresAt: new Date(new Date().getTime() + 1000 * 60 * 60 * 24), + createdAt: new Date(), + }); + + res.send({ token }); + }); + + fastify.post("/check", async (req, res) => { + const body = req.body as { userId: string }; + const userId = body.userId; + const token = req.headers.authorization?.slice(7); + + if (!token || !userId) { + return res + .status(400) + .send({ error: "token and userId are required" }); + } + + const hashedToken = createHash("sha256").update(token).digest("hex"); + const session = await db + .select() + .from(sessions) + .where( + and( + eq(sessions.hashedToken, hashedToken), + eq(sessions.userId, userId), + ), + ); + + if (session.length === 0) { + return res.status(404).send({ error: "session not found" }); + } + + res.send({ success: true }); + }); +}; diff --git a/src/Waypoint/src/services/meta/index.ts b/src/Waypoint/src/services/meta/index.ts new file mode 100644 index 0000000..79d0d71 --- /dev/null +++ b/src/Waypoint/src/services/meta/index.ts @@ -0,0 +1,8 @@ +import { metaV1 } from "./v1.js"; + +export default { + name: "meta", + versions: { + v1: metaV1, + }, +}; diff --git a/src/Waypoint/src/services/meta/v1.ts b/src/Waypoint/src/services/meta/v1.ts new file mode 100644 index 0000000..0b613ca --- /dev/null +++ b/src/Waypoint/src/services/meta/v1.ts @@ -0,0 +1,15 @@ +import { FastifyPluginAsync } from "fastify"; +import packageJson from "../../../package.json" with { type: "json" }; + +export const metaV1: FastifyPluginAsync = async (fastify, opts) => { + fastify.get("/", async (request, reply) => { + reply.send({ + version: packageJson.version, + apiVersion: 1, + sdkVersions: { + min: "0.0.0", + max: "0.0.0", + }, + }); + }); +}; diff --git a/src/Waypoint/src/services/ws/datatypehandler.ts b/src/Waypoint/src/services/ws/datatypehandler.ts new file mode 100644 index 0000000..25d302a --- /dev/null +++ b/src/Waypoint/src/services/ws/datatypehandler.ts @@ -0,0 +1,31 @@ +import type { WebSocket } from "@fastify/websocket"; +import * as Sentry from "@sentry/node"; + +const dataTypeHandlers: Record void> = {}; + +export function registerDataTypeHandler( + type: string, + handler: (socket: WebSocket, data: any) => void, +) { + dataTypeHandlers[type] = handler; +} + +export function handleDataType(socket: WebSocket, type: string, data: any) { + const handler = dataTypeHandlers[type]; + if (handler) { + handler(socket, data); + } else { + console.warn(`No handler for data type "${type}"`); + socket.send( + JSON.stringify({ + type: "error", + data: { + message: `No handler for data type "${type}"`, + }, + }), + ); + Sentry.captureMessage(`No handler for data type "${type}"`, { + level: "warning", + }); + } +} diff --git a/src/Waypoint/src/services/ws/datatypes/whoami.ts b/src/Waypoint/src/services/ws/datatypes/whoami.ts new file mode 100644 index 0000000..eafa926 --- /dev/null +++ b/src/Waypoint/src/services/ws/datatypes/whoami.ts @@ -0,0 +1,23 @@ +import { registerDataTypeHandler } from "../datatypehandler.js"; + +registerDataTypeHandler("whoami", (socket, data) => { + if (!socket.state) { + socket.send( + JSON.stringify({ + type: "error", + message: "Not authenticated", + }), + ); + return; + } + + socket.send( + JSON.stringify({ + type: "whoami", + data: { + userId: socket.state.userId, + id: socket.state.id, + }, + }), + ); +}); diff --git a/src/Waypoint/src/services/ws/index.ts b/src/Waypoint/src/services/ws/index.ts new file mode 100644 index 0000000..976d5c6 --- /dev/null +++ b/src/Waypoint/src/services/ws/index.ts @@ -0,0 +1,9 @@ +import { wsV1 } from "./v1.js"; + +export default { + name: "ws", + + versions: { + v1: wsV1, + }, +}; diff --git a/src/Waypoint/src/services/ws/v1.ts b/src/Waypoint/src/services/ws/v1.ts new file mode 100644 index 0000000..9bbf2af --- /dev/null +++ b/src/Waypoint/src/services/ws/v1.ts @@ -0,0 +1,86 @@ +import { FastifyPluginAsync } from "fastify"; +import websocket from "@fastify/websocket"; +import { handleDataType } from "./datatypehandler.js"; +import fs from "fs/promises"; +import path from "path"; +import { pathToFileURL } from "url"; +import { db } from "../../external/db.js"; +import { sessions } from "../../external/schema.js"; +import { eq } from "drizzle-orm"; +import { createHash } from "node:crypto"; + +type WebsocketResponse = { + type: string; + data: any; +}; + +async function loadDatatypes() { + try { + const datatypes = await fs.readdir( + path.join(import.meta.dirname, "./datatypes"), + ); + datatypes.forEach(async (route) => { + let datatypePath = path.join( + import.meta.dirname, + "./datatypes", + route, + ); + + datatypePath = pathToFileURL(datatypePath).href; + + try { + await import(datatypePath); + } catch (error) { + throw new Error(`Failed to load datatype ${route}: ${error}`); + } + }); + } catch (error) { + throw new Error(`Failed to load datatypes: ${error}`); + } +} + +await loadDatatypes(); + +export const wsV1: FastifyPluginAsync = async (fastify, opts) => { + await fastify.register(websocket); + + fastify.get("/ws", { websocket: true }, async (socket, req) => { + const token = req.headers.authorization?.slice(7); + + if (!token) { + socket.close(1008, "Unauthorized"); + return; + } + + const hashedToken = createHash("sha256").update(token).digest("hex"); + + const session = await db.query.sessions.findFirst({ + where: eq(sessions.hashedToken, hashedToken), + }); + + if (!session) { + socket.close(1008, "Unauthorized"); + return; + } + + const { userId, id } = session; + + socket.state = { userId, id }; + + socket.on("message", (message: Buffer) => { + try { + const { data, type }: WebsocketResponse = JSON.parse( + message.toString(), + ); + + handleDataType(socket, type, data); + } catch (e) { + throw e; + } + }); + + socket.on("close", () => { + console.log("socket closed"); + }); + }); +}; diff --git a/src/Waypoint/src/ws.d.ts b/src/Waypoint/src/ws.d.ts new file mode 100644 index 0000000..47170b8 --- /dev/null +++ b/src/Waypoint/src/ws.d.ts @@ -0,0 +1,10 @@ +import type { WebSocket } from "@fastify/websocket"; + +declare module "@fastify/websocket" { + interface WebSocket { + state: { + userId: number; + id: string; + }; + } +} diff --git a/src/Waypoint/tsconfig.json b/src/Waypoint/tsconfig.json new file mode 100644 index 0000000..1f9853d --- /dev/null +++ b/src/Waypoint/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + "rootDir": "src", + "outDir": "dist", + + "declaration": true, + "sourceMap": true, + + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "types": ["node"], + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"], +} From 5f980fe90620489102bb165bad94d94f0ac1bf06 Mon Sep 17 00:00:00 2001 From: TotoCode Date: Fri, 4 Sep 2026 18:23:59 +0200 Subject: [PATCH 02/11] feat: add starting log to web process --- src/Waypoint/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Waypoint/src/main.ts b/src/Waypoint/src/main.ts index dd139e3..0886d52 100644 --- a/src/Waypoint/src/main.ts +++ b/src/Waypoint/src/main.ts @@ -23,4 +23,6 @@ This is just the API, so you can't see much yet. If you're a developer, you can ); }); -app.listen({ port: 3000 }); +app.listen({ port: Number(process.env.PORT) || 3000 }, () => { + console.log(`Listening on port ${process.env.PORT || 3000}`); +}); From 793e2b665c24fba61126e5ec1290920b46c8bc7e Mon Sep 17 00:00:00 2001 From: TotoCode Date: Fri, 4 Sep 2026 19:02:40 +0200 Subject: [PATCH 03/11] feat: expire tokens that hit their expiration time --- src/Waypoint/src/services/auth/v1.ts | 12 +++- src/Waypoint/src/services/ws/v1.ts | 103 ++++++++++++++++++++++++++- src/Waypoint/src/ws.d.ts | 2 +- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/Waypoint/src/services/auth/v1.ts b/src/Waypoint/src/services/auth/v1.ts index e5ed5e5..d907d15 100644 --- a/src/Waypoint/src/services/auth/v1.ts +++ b/src/Waypoint/src/services/auth/v1.ts @@ -1,7 +1,7 @@ import { FastifyPluginAsync } from "fastify"; import { randomBytes, createHash } from "node:crypto"; import { ulid } from "ulid"; -import { and, eq } from "drizzle-orm"; +import { and, eq, gt, lte } from "drizzle-orm"; import { db } from "../../external/db.js"; import { sessions } from "../../external/schema.js"; @@ -55,10 +55,20 @@ export const authPlugin: FastifyPluginAsync = async (fastify, opts) => { and( eq(sessions.hashedToken, hashedToken), eq(sessions.userId, userId), + gt(sessions.expiresAt, new Date()), ), ); if (session.length === 0) { + await db + .delete(sessions) + .where( + and( + eq(sessions.hashedToken, hashedToken), + lte(sessions.expiresAt, new Date()), + ), + ); + return res.status(404).send({ error: "session not found" }); } diff --git a/src/Waypoint/src/services/ws/v1.ts b/src/Waypoint/src/services/ws/v1.ts index 9bbf2af..dd57898 100644 --- a/src/Waypoint/src/services/ws/v1.ts +++ b/src/Waypoint/src/services/ws/v1.ts @@ -6,14 +6,75 @@ import path from "path"; import { pathToFileURL } from "url"; import { db } from "../../external/db.js"; import { sessions } from "../../external/schema.js"; -import { eq } from "drizzle-orm"; +import { and, eq, gt, lte } from "drizzle-orm"; import { createHash } from "node:crypto"; +import type { WebSocket } from "@fastify/websocket"; type WebsocketResponse = { type: string; data: any; }; +const socketsBySessionId = new Map>(); +const expirationTimers = new Map(); + +function closeSessionSockets(sessionId: string) { + const sockets = socketsBySessionId.get(sessionId); + + for (const socket of sockets ?? []) { + socket.close(1008, "Token expired"); + } +} + +async function deleteExpiredSession(sessionId: string) { + const timer = expirationTimers.get(sessionId); + if (timer) { + clearTimeout(timer); + } + + try { + await db + .delete(sessions) + .where( + and( + eq(sessions.id, sessionId), + lte(sessions.expiresAt, new Date()), + ), + ); + } finally { + closeSessionSockets(sessionId); + expirationTimers.delete(sessionId); + } +} + +function scheduleSessionExpiration( + sessionId: string, + expiresAt: Date, + onError: (error: unknown) => void, +) { + if (expirationTimers.has(sessionId)) { + return; + } + + const delay = Math.max(0, expiresAt.getTime() - Date.now()); + const timer = setTimeout(() => { + void deleteExpiredSession(sessionId).catch(onError); + }, delay); + + expirationTimers.set(sessionId, timer); +} + +async function deleteAllExpiredSessions() { + const expiredSessions = await db + .select({ id: sessions.id }) + .from(sessions) + .where(lte(sessions.expiresAt, new Date())); + + for (const { id } of expiredSessions) { + await deleteExpiredSession(id); + } +} + async function loadDatatypes() { try { const datatypes = await fs.readdir( @@ -44,6 +105,22 @@ await loadDatatypes(); export const wsV1: FastifyPluginAsync = async (fastify, opts) => { await fastify.register(websocket); + await deleteAllExpiredSessions(); + const cleanupInterval = setInterval(() => { + void deleteAllExpiredSessions().catch((error) => { + fastify.log.error(error, "Failed to delete expired sessions"); + }); + }, 60_000); + + fastify.addHook("onClose", async () => { + clearInterval(cleanupInterval); + + for (const timer of expirationTimers.values()) { + clearTimeout(timer); + } + expirationTimers.clear(); + }); + fastify.get("/ws", { websocket: true }, async (socket, req) => { const token = req.headers.authorization?.slice(7); @@ -55,10 +132,22 @@ export const wsV1: FastifyPluginAsync = async (fastify, opts) => { const hashedToken = createHash("sha256").update(token).digest("hex"); const session = await db.query.sessions.findFirst({ - where: eq(sessions.hashedToken, hashedToken), + where: and( + eq(sessions.hashedToken, hashedToken), + gt(sessions.expiresAt, new Date()), + ), }); if (!session) { + await db + .delete(sessions) + .where( + and( + eq(sessions.hashedToken, hashedToken), + lte(sessions.expiresAt, new Date()), + ), + ); + socket.close(1008, "Unauthorized"); return; } @@ -66,6 +155,12 @@ export const wsV1: FastifyPluginAsync = async (fastify, opts) => { const { userId, id } = session; socket.state = { userId, id }; + const sockets = socketsBySessionId.get(id) ?? new Set(); + sockets.add(socket); + socketsBySessionId.set(id, sockets); + scheduleSessionExpiration(id, session.expiresAt, (error) => { + fastify.log.error(error, "Failed to expire session"); + }); socket.on("message", (message: Buffer) => { try { @@ -80,6 +175,10 @@ export const wsV1: FastifyPluginAsync = async (fastify, opts) => { }); socket.on("close", () => { + sockets.delete(socket); + if (sockets.size === 0) { + socketsBySessionId.delete(id); + } console.log("socket closed"); }); }); diff --git a/src/Waypoint/src/ws.d.ts b/src/Waypoint/src/ws.d.ts index 47170b8..e8598ff 100644 --- a/src/Waypoint/src/ws.d.ts +++ b/src/Waypoint/src/ws.d.ts @@ -3,7 +3,7 @@ import type { WebSocket } from "@fastify/websocket"; declare module "@fastify/websocket" { interface WebSocket { state: { - userId: number; + userId: string; id: string; }; } From 6db60436e350b8e0c33bbfb32348a0532f16a5be Mon Sep 17 00:00:00 2001 From: TotoCode Date: Fri, 4 Sep 2026 19:09:09 +0200 Subject: [PATCH 04/11] feat: token limit (3) + discard oldest token when limit is hit --- src/Waypoint/src/services/auth/v1.ts | 25 +++++++++++++++++-- .../src/services/ws/session-sockets.ts | 24 ++++++++++++++++++ src/Waypoint/src/services/ws/v1.ts | 25 ++++++------------- 3 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 src/Waypoint/src/services/ws/session-sockets.ts diff --git a/src/Waypoint/src/services/auth/v1.ts b/src/Waypoint/src/services/auth/v1.ts index d907d15..490e19e 100644 --- a/src/Waypoint/src/services/auth/v1.ts +++ b/src/Waypoint/src/services/auth/v1.ts @@ -1,13 +1,14 @@ import { FastifyPluginAsync } from "fastify"; import { randomBytes, createHash } from "node:crypto"; import { ulid } from "ulid"; -import { and, eq, gt, lte } from "drizzle-orm"; +import { and, asc, eq, gt, lte } from "drizzle-orm"; import { db } from "../../external/db.js"; import { sessions } from "../../external/schema.js"; +import { closeSessionSockets } from "../ws/session-sockets.js"; export interface Session { id: string; - userId: number; + userId: string; hashedToken: string; createdAt: Date; expiresAt: Date; @@ -22,6 +23,26 @@ export const authPlugin: FastifyPluginAsync = async (fastify, opts) => { return res.status(400).send({ error: "userId is required" }); } + const now = new Date(); + await db + .delete(sessions) + .where( + and(eq(sessions.userId, userId), lte(sessions.expiresAt, now)), + ); + + const activeSessions = await db + .select({ id: sessions.id }) + .from(sessions) + .where( + and(eq(sessions.userId, userId), gt(sessions.expiresAt, now)), + ) + .orderBy(asc(sessions.createdAt)); + + for (const session of activeSessions.slice(0, -2)) { + await db.delete(sessions).where(eq(sessions.id, session.id)); + closeSessionSockets(session.id, "Token revoked"); + } + const token = randomBytes(32).toString("base64url"); const hashedToken = createHash("sha256").update(token).digest("hex"); diff --git a/src/Waypoint/src/services/ws/session-sockets.ts b/src/Waypoint/src/services/ws/session-sockets.ts new file mode 100644 index 0000000..dc3c5ee --- /dev/null +++ b/src/Waypoint/src/services/ws/session-sockets.ts @@ -0,0 +1,24 @@ +import type { WebSocket } from "@fastify/websocket"; + +const socketsBySessionId = new Map>(); + +export function addSessionSocket(sessionId: string, socket: WebSocket) { + const sockets = socketsBySessionId.get(sessionId) ?? new Set(); + sockets.add(socket); + socketsBySessionId.set(sessionId, sockets); + + return () => { + sockets.delete(socket); + if (sockets.size === 0) { + socketsBySessionId.delete(sessionId); + } + }; +} + +export function closeSessionSockets(sessionId: string, reason: string) { + const sockets = socketsBySessionId.get(sessionId); + + for (const socket of sockets ?? []) { + socket.close(1008, reason); + } +} diff --git a/src/Waypoint/src/services/ws/v1.ts b/src/Waypoint/src/services/ws/v1.ts index dd57898..8163932 100644 --- a/src/Waypoint/src/services/ws/v1.ts +++ b/src/Waypoint/src/services/ws/v1.ts @@ -8,24 +8,18 @@ import { db } from "../../external/db.js"; import { sessions } from "../../external/schema.js"; import { and, eq, gt, lte } from "drizzle-orm"; import { createHash } from "node:crypto"; -import type { WebSocket } from "@fastify/websocket"; +import { + addSessionSocket, + closeSessionSockets, +} from "./session-sockets.js"; type WebsocketResponse = { type: string; data: any; }; -const socketsBySessionId = new Map>(); const expirationTimers = new Map(); -function closeSessionSockets(sessionId: string) { - const sockets = socketsBySessionId.get(sessionId); - - for (const socket of sockets ?? []) { - socket.close(1008, "Token expired"); - } -} - async function deleteExpiredSession(sessionId: string) { const timer = expirationTimers.get(sessionId); if (timer) { @@ -42,7 +36,7 @@ async function deleteExpiredSession(sessionId: string) { ), ); } finally { - closeSessionSockets(sessionId); + closeSessionSockets(sessionId, "Token expired"); expirationTimers.delete(sessionId); } } @@ -155,9 +149,7 @@ export const wsV1: FastifyPluginAsync = async (fastify, opts) => { const { userId, id } = session; socket.state = { userId, id }; - const sockets = socketsBySessionId.get(id) ?? new Set(); - sockets.add(socket); - socketsBySessionId.set(id, sockets); + const removeSocket = addSessionSocket(id, socket); scheduleSessionExpiration(id, session.expiresAt, (error) => { fastify.log.error(error, "Failed to expire session"); }); @@ -175,10 +167,7 @@ export const wsV1: FastifyPluginAsync = async (fastify, opts) => { }); socket.on("close", () => { - sockets.delete(socket); - if (sockets.size === 0) { - socketsBySessionId.delete(id); - } + removeSocket(); console.log("socket closed"); }); }); From f5bb764f75efd8e94c4f5c1f64a08190ae98e7d0 Mon Sep 17 00:00:00 2001 From: TotoCode Date: Sat, 5 Sep 2026 09:36:57 +0200 Subject: [PATCH 05/11] docs: add environment variables template and update setup docs --- .env.example | 7 +++++++ README.md | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fe4bf85 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Sentry +SENTRY_ENABLED=true # Whether to enable Sentry for error logging +SENTRY_DSN=your-sentry-dsn +SENTRY_ENABLE_LOGS=true # Whether to also include logs in Sentry + +# Database +DATABASE_URL=postgresql-database-connection-string # Connection string to your PostgreSQL database diff --git a/README.md b/README.md index 4016683..08599c5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Roblox game that acts as a launcher, alongside features such as music, party, ## Requirements This project uses [rokit](https://github.com/rojo-rbx/rokit) to manage the Roblox development tools, [pnpm](https://pnpm.io/) to manage Node.js dependencies and scripts and [corepack](https://nodejs.org/api/corepack.html) to manage the pnpm installation. -You will need to have rokit and Node.js installed and added to your PATH alongside the `corepack` CLI installed globally to npm. +You will need to have rokit and Node.js installed and added to your PATH (`corepack` isn't required but recommended). ## Installation @@ -19,6 +19,14 @@ corepack enable pnpm install rokit install +# Copy the .env.example file to .env +cp .env.example .env + +# Edit the .env file to set your environment variables + +# Push the database schema +pnpm db:push + # To serve the game through Rojo rojo serve From 08d8d8b3311641bf408a90bfe202ac68e1ec9693 Mon Sep 17 00:00:00 2001 From: TotoCode Date: Sat, 5 Sep 2026 09:39:43 +0200 Subject: [PATCH 06/11] fix: remove ReplicatedFirst folder since it has yet to be a part of the source code, most people who serve on rojo will get an error --- default.project.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/default.project.json b/default.project.json index 009f33e..2ce992e 100644 --- a/default.project.json +++ b/default.project.json @@ -21,12 +21,6 @@ "$path": "src/Spawnpad.Client" } } - }, - - "ReplicatedFirst": { - "Spawnpad": { - "$path": "src/Spawnpad.ReplicatedFirst" - } } } } From 9e4a4a4174a8187e0cb7db3e69b55a45f36645ad Mon Sep 17 00:00:00 2001 From: TotoCode Date: Sat, 5 Sep 2026 11:47:56 +0200 Subject: [PATCH 07/11] feat: handle graceful shutdown for active ws sessions not tested --- src/Waypoint/src/services/ws/v1.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Waypoint/src/services/ws/v1.ts b/src/Waypoint/src/services/ws/v1.ts index 8163932..4f8e9ee 100644 --- a/src/Waypoint/src/services/ws/v1.ts +++ b/src/Waypoint/src/services/ws/v1.ts @@ -8,10 +8,7 @@ import { db } from "../../external/db.js"; import { sessions } from "../../external/schema.js"; import { and, eq, gt, lte } from "drizzle-orm"; import { createHash } from "node:crypto"; -import { - addSessionSocket, - closeSessionSockets, -} from "./session-sockets.js"; +import { addSessionSocket, closeSessionSockets } from "./session-sockets.js"; type WebsocketResponse = { type: string; @@ -172,3 +169,17 @@ export const wsV1: FastifyPluginAsync = async (fastify, opts) => { }); }); }; + +async function onServerShutdown() { + const sessions = await db.query.sessions.findMany(); + console.log("Closing", sessions.length, "sessions..."); + for (const session of sessions) { + await closeSessionSockets( + session.id, + "Server is undergoing maintenance, please stand by...", + ); + } +} + +process.on("SIGTERM", onServerShutdown); +process.on("SIGINT", onServerShutdown); From 99582e1403642717cf5b39ae41f79ae9ebc0e4f0 Mon Sep 17 00:00:00 2001 From: TotoCode Date: Wed, 9 Sep 2026 16:59:00 +0200 Subject: [PATCH 08/11] chore: formatting smth smth --- pnpm-workspace.yaml | 4 ++-- src/Waypoint/tsconfig.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 31370f9..7a68f93 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,4 @@ packages: - - "src/Waypoint" + - "src/Waypoint" allowBuilds: - esbuild: true + esbuild: true diff --git a/src/Waypoint/tsconfig.json b/src/Waypoint/tsconfig.json index 1f9853d..b3a9abc 100644 --- a/src/Waypoint/tsconfig.json +++ b/src/Waypoint/tsconfig.json @@ -17,8 +17,8 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - "types": ["node"], + "types": ["node"] }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"], + "exclude": ["node_modules", "dist"] } From 8a5c9b0b81b29b147e45f9d425dc826b8fff75e8 Mon Sep 17 00:00:00 2001 From: TotoCode Date: Wed, 9 Sep 2026 17:57:10 +0200 Subject: [PATCH 09/11] feat: client connection server-side --- .../Waypoint/DataTypeLoader.luau | 20 +++++ .../Waypoint/DataTypes/whoami.luau | 5 ++ src/Spawnpad.Shared/Waypoint/init.luau | 80 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau create mode 100644 src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau create mode 100644 src/Spawnpad.Shared/Waypoint/init.luau diff --git a/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau b/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau new file mode 100644 index 0000000..691487b --- /dev/null +++ b/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau @@ -0,0 +1,20 @@ +local DataTypeLoader = {} + +local DataTypes = {} + +function DataTypeLoader:LoadDataTypes() + local dataTypesFolder = script.Parent:WaitForChild("DataTypes") + for _, dataType in ipairs(dataTypesFolder:GetChildren()) do + DataTypes[dataType.Name] = require(dataType :: any) :: any + end +end + +function DataTypeLoader:CallDataType(dataType: string, ...) + local dataTypeFunc = DataTypes[dataType] + if dataTypeFunc then + return dataTypeFunc(...) + end + return nil +end + +return DataTypeLoader diff --git a/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau b/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau new file mode 100644 index 0000000..41e3e7e --- /dev/null +++ b/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau @@ -0,0 +1,5 @@ +function whoami(data) + print(data.userId) +end + +return whoami diff --git a/src/Spawnpad.Shared/Waypoint/init.luau b/src/Spawnpad.Shared/Waypoint/init.luau new file mode 100644 index 0000000..10ed108 --- /dev/null +++ b/src/Spawnpad.Shared/Waypoint/init.luau @@ -0,0 +1,80 @@ +local HttpService = game:GetService("HttpService") +local DataTypeLoader = require("./Waypoint/DataTypeLoader") +local Waypoint = {} +Waypoint.__index = Waypoint + +local SPAWNPAD_WAYPOINT_URL_EXISTS, SPAWNPAD_WAYPOINT_URL = pcall(function(...) + return HttpService:GetSecret("SPAWNPAD_WAYPOINT_URL") +end) +local SPAWNPAD_WAYPOINT_INSECURE_EXISTS, SPAWNPAD_WAYPOINT_INSECURE = pcall(function(...) + return HttpService:GetSecret("SPAWNPAD_WAYPOINT_INSECURE") +end) + +Waypoint.BASE_URL = ((SPAWNPAD_WAYPOINT_URL_EXISTS and SPAWNPAD_WAYPOINT_URL) or "localhost:3000") +Waypoint.API_URL = ((SPAWNPAD_WAYPOINT_INSECURE_EXISTS) and "http://" or "https://" ) .. Waypoint.BASE_URL +Waypoint.WS_URL = ((SPAWNPAD_WAYPOINT_INSECURE_EXISTS) and "ws://" or "wss://") .. Waypoint.BASE_URL + +DataTypeLoader:LoadDataTypes() + +function Waypoint.new(userId: number) + local self = setmetatable({}, Waypoint) + self.userId = userId + + local tokenSuccess, token = pcall(function() + return HttpService:RequestAsync({ + Url = Waypoint.API_URL .. "/api/v1/auth/roblox", + Method = "POST", + Headers = { + ["Content-Type"] = "application/json", + }, + Body = HttpService:JSONEncode({ + userId = userId, + }), + }) + end) + + if not tokenSuccess then + error(token) + return token + end + + local tokenData = HttpService:JSONDecode(token.Body) + + self.token = tokenData.token + + self.wsError = false + + self.ws = HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, { + Url = Waypoint.WS_URL .. "/api/v1/ws/ws", + Headers = { + ["Authorization"] = "Bearer " .. self.token, + }, + }) + + self.ws.Error:Connect(function(code: number, message: string) + self.wsError = true + error("WS Error: " .. tostring(code) .. ": " .. message) + end) + + self.ws.Closed:Connect(function() + if self.wsError then + return + end + error("WS Connection closed.") + end) + + self.ws.MessageReceived:Connect(function(message: string) + local dataTable = HttpService:JSONDecode(message) + local data, type = dataTable.data, dataTable.type + + DataTypeLoader:CallDataType(type, data) + end) + + self.ws:Send(HttpService:JSONEncode({ + type = "whoami", + })) + + return self +end + +return Waypoint From cc413a8ea11c3212c6ae474b353de84fa5e2559c Mon Sep 17 00:00:00 2001 From: TotoCode Date: Wed, 9 Sep 2026 17:59:04 +0200 Subject: [PATCH 10/11] fix: GamesOpen button isn't selected correctly --- src/Spawnpad.Client/UI/Apps/Open/GamesOpen.client.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spawnpad.Client/UI/Apps/Open/GamesOpen.client.luau b/src/Spawnpad.Client/UI/Apps/Open/GamesOpen.client.luau index 63dcc25..0c2ba68 100644 --- a/src/Spawnpad.Client/UI/Apps/Open/GamesOpen.client.luau +++ b/src/Spawnpad.Client/UI/Apps/Open/GamesOpen.client.luau @@ -6,7 +6,7 @@ local gamesGui = playerGui:WaitForChild("Games"):WaitForChild("Main") -- local viewportFrame = script.Parent.ViewportFrame:WaitForChild("ViewportFrame") -- local model = viewportFrame:WaitForChild("controller") -script.Parent.MouseButton1Click:Connect(function() +mainGui:WaitForChild("Frame"):WaitForChild("Apps"):WaitForChild("Games").MouseButton1Click:Connect(function() gamesGui.Visible = true mainGui.Visible = false end) From 52f34e6e010d28e8d90f0fe1d62238afabbdfb1b Mon Sep 17 00:00:00 2001 From: TotoCode Date: Wed, 9 Sep 2026 18:54:02 +0200 Subject: [PATCH 11/11] fix: wait for server to be ready + correct url detection --- .../Waypoint/DataTypeLoader.luau | 18 +++++------ .../Waypoint/DataTypes/whoami.luau | 2 +- src/Spawnpad.Shared/Waypoint/init.luau | 32 ++++++++++++------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau b/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau index 691487b..b7218b9 100644 --- a/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau +++ b/src/Spawnpad.Shared/Waypoint/DataTypeLoader.luau @@ -3,18 +3,18 @@ local DataTypeLoader = {} local DataTypes = {} function DataTypeLoader:LoadDataTypes() - local dataTypesFolder = script.Parent:WaitForChild("DataTypes") - for _, dataType in ipairs(dataTypesFolder:GetChildren()) do - DataTypes[dataType.Name] = require(dataType :: any) :: any - end + local dataTypesFolder = script.Parent:WaitForChild("DataTypes") + for _, dataType in ipairs(dataTypesFolder:GetChildren()) do + DataTypes[dataType.Name] = require(dataType :: any) :: any + end end function DataTypeLoader:CallDataType(dataType: string, ...) - local dataTypeFunc = DataTypes[dataType] - if dataTypeFunc then - return dataTypeFunc(...) - end - return nil + local dataTypeFunc = DataTypes[dataType] + if dataTypeFunc then + return dataTypeFunc(...) + end + return nil end return DataTypeLoader diff --git a/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau b/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau index 41e3e7e..f5cde49 100644 --- a/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau +++ b/src/Spawnpad.Shared/Waypoint/DataTypes/whoami.luau @@ -1,5 +1,5 @@ function whoami(data) - print(data.userId) + print(data) end return whoami diff --git a/src/Spawnpad.Shared/Waypoint/init.luau b/src/Spawnpad.Shared/Waypoint/init.luau index 10ed108..69ffb0a 100644 --- a/src/Spawnpad.Shared/Waypoint/init.luau +++ b/src/Spawnpad.Shared/Waypoint/init.luau @@ -3,16 +3,17 @@ local DataTypeLoader = require("./Waypoint/DataTypeLoader") local Waypoint = {} Waypoint.__index = Waypoint -local SPAWNPAD_WAYPOINT_URL_EXISTS, SPAWNPAD_WAYPOINT_URL = pcall(function(...) +local SPAWNPAD_WAYPOINT_URL_EXISTS, SPAWNPAD_WAYPOINT_URL = pcall(function() return HttpService:GetSecret("SPAWNPAD_WAYPOINT_URL") end) -local SPAWNPAD_WAYPOINT_INSECURE_EXISTS, SPAWNPAD_WAYPOINT_INSECURE = pcall(function(...) +local SPAWNPAD_WAYPOINT_INSECURE_EXISTS = pcall(function() return HttpService:GetSecret("SPAWNPAD_WAYPOINT_INSECURE") end) Waypoint.BASE_URL = ((SPAWNPAD_WAYPOINT_URL_EXISTS and SPAWNPAD_WAYPOINT_URL) or "localhost:3000") -Waypoint.API_URL = ((SPAWNPAD_WAYPOINT_INSECURE_EXISTS) and "http://" or "https://" ) .. Waypoint.BASE_URL -Waypoint.WS_URL = ((SPAWNPAD_WAYPOINT_INSECURE_EXISTS) and "ws://" or "wss://") .. Waypoint.BASE_URL +Waypoint.API_URL = (SPAWNPAD_WAYPOINT_INSECURE_EXISTS and "http://" or "https://") .. Waypoint.BASE_URL + +Waypoint.WS_URL = (SPAWNPAD_WAYPOINT_INSECURE_EXISTS and "ws://" or "wss://") .. Waypoint.BASE_URL DataTypeLoader:LoadDataTypes() @@ -51,29 +52,38 @@ function Waypoint.new(userId: number) }, }) + self.ws.Opened:Connect(function() + -- wait 3 seconds before sending whoami because of server-side checks + task.wait(3) + local success = pcall(function() + return self.ws:Send(HttpService:JSONEncode({ + type = "whoami", + })) + end) + + print(success) + end) + self.ws.Error:Connect(function(code: number, message: string) self.wsError = true - error("WS Error: " .. tostring(code) .. ": " .. message) + warn("WS Error: " .. tostring(code) .. ": " .. message) end) self.ws.Closed:Connect(function() - if self.wsError then + if self.wsError then return end - error("WS Connection closed.") + warn("WS Connection closed.") end) self.ws.MessageReceived:Connect(function(message: string) + print(message) local dataTable = HttpService:JSONDecode(message) local data, type = dataTable.data, dataTable.type DataTypeLoader:CallDataType(type, data) end) - self.ws:Send(HttpService:JSONEncode({ - type = "whoami", - })) - return self end