From e356a6d33baa86632cc9aa172ce9d5a43de0ebbd Mon Sep 17 00:00:00 2001 From: Lokowitz Date: Tue, 27 Jan 2026 11:06:26 +0000 Subject: [PATCH 001/247] fix lable error and make dockerfile readable --- Dockerfile | 57 +++++++++++++++++----------------------------------- package.json | 4 ++-- 2 files changed, 20 insertions(+), 41 deletions(-) diff --git a/Dockerfile b/Dockerfile index 07371f77..487fa033 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,11 @@ FROM node:24-alpine AS builder -# OCI Image Labels - Build Args for dynamic values -ARG VERSION="dev" -ARG REVISION="" -ARG CREATED="" -ARG LICENSE="AGPL-3.0" - WORKDIR /app ARG BUILD=oss ARG DATABASE=sqlite -# Derive title and description based on BUILD type -ARG IMAGE_TITLE="Pangolin" -ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" - -RUN apk add --no-cache curl tzdata python3 make g++ +RUN apk add --no-cache python3 make g++ # COPY package.json package-lock.json ./ COPY package*.json ./ @@ -23,41 +13,31 @@ RUN npm ci COPY . . -RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts -RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts - -RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts - -# Copy the appropriate TypeScript configuration based on build type -RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \ - elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \ - elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \ - fi - -# if the build is oss then remove the server/private directory -RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi - -RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi - -RUN mkdir -p dist -RUN npm run next:build -RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD -RUN if [ "$DATABASE" = "pg" ]; then \ - node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \ - else \ - node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \ - fi +RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \ + npm run set:$DATABASE && \ + npm run set:$BUILD && \ + npm run db:$DATABASE:generate && \ + npm run build:$DATABASE && \ + npm run build:cli # test to make sure the build output is there and error if not RUN test -f dist/server.mjs -RUN npm run build:cli - # Prune dev dependencies and clean up to prepare for copy to runner RUN npm prune --omit=dev && npm cache clean --force FROM node:24-alpine AS runner +# OCI Image Labels - Build Args for dynamic values +ARG VERSION="dev" +ARG REVISION="" +ARG CREATED="" +ARG LICENSE="AGPL-3.0" + +# Derive title and description based on BUILD type +ARG IMAGE_TITLE="Pangolin" +ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" + WORKDIR /app # Only curl and tzdata needed at runtime - no build tools! @@ -66,11 +46,10 @@ RUN apk add --no-cache curl tzdata # Copy pre-built node_modules from builder (already pruned to production only) # This includes the compiled native modules like better-sqlite3 COPY --from=builder /app/node_modules ./node_modules - COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/dist ./dist -COPY --from=builder /app/init ./dist/init +COPY --from=builder /app/server/migrations ./dist/init COPY --from=builder /app/package.json ./package.json COPY ./cli/wrapper.sh /usr/local/bin/pangctl diff --git a/package.json b/package.json index 25d94c4d..1d40b67f 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,13 @@ "set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", "set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts", - "next:build": "next build", + "build:next": "next build", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3005", "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs", - "format": "prettier --write ." + "format:write": "prettier --write ." }, "dependencies": { "@asteasolutions/zod-to-openapi": "8.4.0", From 14974690165e455af8defbcdd3fbbe1683d5fb51 Mon Sep 17 00:00:00 2001 From: Lokowitz Date: Tue, 27 Jan 2026 11:52:23 +0000 Subject: [PATCH 002/247] revert format:write --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d40b67f..fa89274d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3005", "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs", - "format:write": "prettier --write ." + "format": "prettier --write ." }, "dependencies": { "@asteasolutions/zod-to-openapi": "8.4.0", From cb569ff14d02bd07ef30fff57a2aac5772f6b500 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 28 Jan 2026 15:03:31 -0800 Subject: [PATCH 003/247] Properly insert PANGOLIN_SETUP_TOKEN into db Fixes #2361 --- .github/workflows/cicd.yml | 49 ++++++++++++++++++++++++++++++-- server/setup/ensureSetupToken.ts | 20 +++++++------ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 715b74c7..0e4d9bc6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -504,10 +504,55 @@ jobs: } echo "==> cosign verify (public key) ${REF}" - retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text" + if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then + VERIFIED_INDEX=true + else + VERIFIED_INDEX=false + fi echo "==> cosign verify (keyless policy) ${REF}" - retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text" + if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then + VERIFIED_INDEX_KEYLESS=true + else + VERIFIED_INDEX_KEYLESS=false + fi + + # If index verification fails, attempt to verify child platform manifests + if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then + echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}" + CHILD_VERIFIED=false + + for ARCH in arm64 amd64; do + CHILD_TAG="${IMAGE_TAG}-${ARCH}" + echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}" + CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)" + if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then + CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}" + echo "==> cosign verify (public key) child ${CHILD_REF}" + if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then + CHILD_VERIFIED=true + echo "Public key verification succeeded for child ${CHILD_REF}" + else + echo "Public key verification failed for child ${CHILD_REF}" + fi + + echo "==> cosign verify (keyless policy) child ${CHILD_REF}" + if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then + CHILD_VERIFIED=true + echo "Keyless verification succeeded for child ${CHILD_REF}" + else + echo "Keyless verification failed for child ${CHILD_REF}" + fi + else + echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping" + fi + done + + if [ "${CHILD_VERIFIED}" != "true" ]; then + echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}" + exit 10 + fi + fi echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}" done diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 5ea9542a..ff6387f0 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -64,16 +64,20 @@ export async function ensureSetupToken() { ); } - if (existingToken?.token !== envSetupToken) { - console.warn( - "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" - ); + if (existingToken) { + // Token exists in DB - update it if different + if (existingToken.token !== envSetupToken) { + console.warn( + "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" + ); - await db - .update(setupTokens) - .set({ token: envSetupToken }) - .where(eq(setupTokens.tokenId, existingToken.tokenId)); + await db + .update(setupTokens) + .set({ token: envSetupToken }) + .where(eq(setupTokens.tokenId, existingToken.tokenId)); + } } else { + // No existing token - insert new one const tokenId = generateId(15); await db.insert(setupTokens).values({ From 141c846fe2f09888684423a7c1155554606044c7 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 28 Jan 2026 15:03:31 -0800 Subject: [PATCH 004/247] Properly insert PANGOLIN_SETUP_TOKEN into db Fixes #2361 --- .github/workflows/cicd.yml | 49 ++++++++++++++++++++++++++++++-- server/setup/ensureSetupToken.ts | 20 +++++++------ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 715b74c7..0e4d9bc6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -504,10 +504,55 @@ jobs: } echo "==> cosign verify (public key) ${REF}" - retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text" + if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then + VERIFIED_INDEX=true + else + VERIFIED_INDEX=false + fi echo "==> cosign verify (keyless policy) ${REF}" - retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text" + if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then + VERIFIED_INDEX_KEYLESS=true + else + VERIFIED_INDEX_KEYLESS=false + fi + + # If index verification fails, attempt to verify child platform manifests + if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then + echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}" + CHILD_VERIFIED=false + + for ARCH in arm64 amd64; do + CHILD_TAG="${IMAGE_TAG}-${ARCH}" + echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}" + CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)" + if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then + CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}" + echo "==> cosign verify (public key) child ${CHILD_REF}" + if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then + CHILD_VERIFIED=true + echo "Public key verification succeeded for child ${CHILD_REF}" + else + echo "Public key verification failed for child ${CHILD_REF}" + fi + + echo "==> cosign verify (keyless policy) child ${CHILD_REF}" + if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then + CHILD_VERIFIED=true + echo "Keyless verification succeeded for child ${CHILD_REF}" + else + echo "Keyless verification failed for child ${CHILD_REF}" + fi + else + echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping" + fi + done + + if [ "${CHILD_VERIFIED}" != "true" ]; then + echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}" + exit 10 + fi + fi echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}" done diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 5ea9542a..ff6387f0 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -64,16 +64,20 @@ export async function ensureSetupToken() { ); } - if (existingToken?.token !== envSetupToken) { - console.warn( - "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" - ); + if (existingToken) { + // Token exists in DB - update it if different + if (existingToken.token !== envSetupToken) { + console.warn( + "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" + ); - await db - .update(setupTokens) - .set({ token: envSetupToken }) - .where(eq(setupTokens.tokenId, existingToken.tokenId)); + await db + .update(setupTokens) + .set({ token: envSetupToken }) + .where(eq(setupTokens.tokenId, existingToken.tokenId)); + } } else { + // No existing token - insert new one const tokenId = generateId(15); await db.insert(setupTokens).values({ From 8fec8f35bcaa8f3066223f91d1eed359366df37c Mon Sep 17 00:00:00 2001 From: Lokowitz Date: Tue, 27 Jan 2026 12:20:50 +0000 Subject: [PATCH 005/247] removed unused code --- package-lock.json | 2194 +--------------------------------- package.json | 10 +- src/components/Toploader.tsx | 44 +- 3 files changed, 41 insertions(+), 2207 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a01c8c5..d5f7c924 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,9 +51,7 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "cookie": "1.1.1", "cookie-parser": "1.4.7", - "cookies": "0.9.1", "cors": "2.8.5", "crypto-js": "4.2.0", "d3": "7.9.0", @@ -66,7 +64,6 @@ "glob": "13.0.0", "helmet": "8.1.0", "http-errors": "2.0.1", - "i": "0.3.7", "input-otp": "1.4.2", "ioredis": "5.9.2", "jmespath": "0.16.0", @@ -80,10 +77,7 @@ "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "node-fetch": "3.3.2", "nodemailer": "7.0.11", - "npm": "11.7.0", - "nprogress": "0.2.0", "oslo": "1.2.1", "pg": "8.17.1", "posthog-node": "5.23.0", @@ -94,7 +88,6 @@ "react-easy-sort": "1.8.0", "react-hook-form": "7.71.1", "react-icons": "5.5.0", - "rebuild": "0.1.2", "recharts": "2.15.4", "reodotdev": "1.0.0", "resend": "6.8.0", @@ -1820,6 +1813,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4206,6 +4200,7 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -7895,6 +7890,7 @@ "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9341,6 +9337,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.12" }, @@ -9446,6 +9443,7 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -9785,6 +9783,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -9878,6 +9877,7 @@ "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -9906,6 +9906,7 @@ "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -9932,6 +9933,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -9942,6 +9944,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -10017,8 +10020,7 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -10089,6 +10091,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -10558,6 +10561,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10988,6 +10992,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -11043,6 +11048,7 @@ "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -11168,6 +11174,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11696,19 +11703,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -11737,19 +11731,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookies": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -12136,6 +12117,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -12226,15 +12208,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "license": "BSD-2-Clause" }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -12565,7 +12538,6 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -13671,6 +13643,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -13767,6 +13740,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -14249,6 +14223,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -14458,29 +14433,6 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -14684,18 +14636,6 @@ "node": ">= 0.6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15198,14 +15138,6 @@ "node": ">=10.17.0" } }, - "node_modules/i": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", - "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15981,19 +15913,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -16707,7 +16626,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -16718,7 +16636,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -16805,6 +16722,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -16984,44 +16902,6 @@ "node": ">= 8.0.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -17047,162 +16927,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.7.0.tgz", - "integrity": "sha512-wiCZpv/41bIobCoJ31NStIWKfAxxYyD1iYnWCtiyns8s5v3+l8y0HCP/sScuH6B5+GhIfda4HQKiqeGZwJWhFw==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/metavuln-calculator", - "@npmcli/package-json", - "@npmcli/promise-spawn", - "@npmcli/redact", - "@npmcli/run-script", - "@sigstore/tuf", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "semver", - "spdx-expression-parse", - "ssri", - "supports-color", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which" - ], - "license": "Artistic-2.0", - "workspaces": [ - "docs", - "smoke-tests", - "mock-globals", - "mock-registry", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.9", - "@npmcli/config": "^10.4.5", - "@npmcli/fs": "^5.0.0", - "@npmcli/map-workspaces": "^5.0.3", - "@npmcli/metavuln-calculator": "^9.0.3", - "@npmcli/package-json": "^7.0.4", - "@npmcli/promise-spawn": "^9.0.1", - "@npmcli/redact": "^4.0.0", - "@npmcli/run-script": "^10.0.3", - "@sigstore/tuf": "^4.0.0", - "abbrev": "^4.0.0", - "archy": "~1.0.0", - "cacache": "^20.0.3", - "chalk": "^5.6.2", - "ci-info": "^4.3.1", - "cli-columns": "^4.0.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.3", - "glob": "^13.0.0", - "graceful-fs": "^4.2.11", - "hosted-git-info": "^9.0.2", - "ini": "^6.0.0", - "init-package-json": "^8.2.4", - "is-cidr": "^6.0.1", - "json-parse-even-better-errors": "^5.0.0", - "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.0.12", - "libnpmexec": "^10.1.11", - "libnpmfund": "^7.0.12", - "libnpmorg": "^8.0.1", - "libnpmpack": "^9.0.12", - "libnpmpublish": "^11.1.3", - "libnpmsearch": "^9.0.1", - "libnpmteam": "^8.0.2", - "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.3", - "minimatch": "^10.1.1", - "minipass": "^7.1.1", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^12.1.0", - "nopt": "^9.0.0", - "npm-audit-report": "^7.0.0", - "npm-install-checks": "^8.0.0", - "npm-package-arg": "^13.0.2", - "npm-pick-manifest": "^11.0.3", - "npm-profile": "^12.0.1", - "npm-registry-fetch": "^19.1.1", - "npm-user-validate": "^4.0.0", - "p-map": "^7.0.4", - "pacote": "^21.0.4", - "parse-conflict-json": "^5.0.1", - "proc-log": "^6.1.0", - "qrcode-terminal": "^0.12.0", - "read": "^5.0.1", - "semver": "^7.7.3", - "spdx-expression-parse": "^4.0.0", - "ssri": "^13.0.0", - "supports-color": "^10.2.2", - "tar": "^7.5.2", - "text-table": "~0.2.0", - "tiny-relative-date": "^2.0.2", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^7.0.0", - "which": "^6.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -17216,1822 +16940,6 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/agent": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^11.2.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.9", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^5.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/map-workspaces": "^5.0.0", - "@npmcli/metavuln-calculator": "^9.0.2", - "@npmcli/name-from-folder": "^4.0.0", - "@npmcli/node-gyp": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/query": "^5.0.0", - "@npmcli/redact": "^4.0.0", - "@npmcli/run-script": "^10.0.0", - "bin-links": "^6.0.0", - "cacache": "^20.0.1", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^9.0.0", - "json-stringify-nice": "^1.1.4", - "lru-cache": "^11.2.1", - "minimatch": "^10.0.3", - "nopt": "^9.0.0", - "npm-install-checks": "^8.0.0", - "npm-package-arg": "^13.0.0", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "pacote": "^21.0.2", - "parse-conflict-json": "^5.0.1", - "proc-log": "^6.0.0", - "proggy": "^4.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^3.0.1", - "semver": "^7.3.7", - "ssri": "^13.0.0", - "treeverse": "^3.0.0", - "walk-up-path": "^4.0.0" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "ci-info": "^4.0.0", - "ini": "^6.0.0", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "7.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^9.0.0", - "ini": "^6.0.0", - "lru-cache": "^11.2.1", - "npm-pick-manifest": "^11.0.1", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^5.0.0", - "npm-normalize-package-bin": "^5.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "5.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "glob": "^13.0.0", - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "9.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^20.0.0", - "json-parse-even-better-errors": "^5.0.0", - "pacote": "^21.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "7.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "glob": "^13.0.0", - "hosted-git-info": "^9.0.0", - "json-parse-even-better-errors": "^5.0.0", - "proc-log": "^6.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "10.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "4.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.0.1", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.2", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "7.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "proc-log": "^6.0.0", - "read-cmd-shim": "^6.0.0", - "write-file-atomic": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "20.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^5.0.0", - "fs-minipass": "^3.0.0", - "glob": "^13.0.0", - "lru-cache": "^11.1.0", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "5.6.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "4.3.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "5.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.4.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/diff": { - "version": "8.0.2", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.3", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "13.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "9.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.2.0", - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/ini": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "8.2.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^7.0.0", - "npm-package-arg": "^13.0.0", - "promzard": "^3.0.1", - "read": "^5.0.1", - "semver": "^7.7.2", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/ip-address": { - "version": "10.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "5.0.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "6.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "10.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^13.0.0", - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.12", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.9", - "@npmcli/installed-package-contents": "^4.0.0", - "binary-extensions": "^3.0.0", - "diff": "^8.0.2", - "minimatch": "^10.0.3", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2", - "tar": "^7.5.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.11", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.9", - "@npmcli/package-json": "^7.0.0", - "@npmcli/run-script": "^10.0.0", - "ci-info": "^4.0.0", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "read": "^5.0.1", - "semver": "^7.3.7", - "signal-exit": "^4.1.0", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.12", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.9" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "8.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.12", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.9", - "@npmcli/run-script": "^10.0.0", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.1.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^7.0.0", - "ci-info": "^4.0.0", - "npm-package-arg": "^13.0.0", - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.7", - "sigstore": "^4.0.0", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "9.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/run-script": "^10.0.0", - "json-parse-even-better-errors": "^5.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "15.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^4.0.0", - "cacache": "^20.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "10.1.1", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "2.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/negotiator": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "12.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.2", - "tinyglobby": "^0.2.12", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "8.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "13.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^8.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "11.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "npm-package-arg": "^13.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "12.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "19.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^4.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^15.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^13.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "4.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "7.0.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/pacote": { - "version": "21.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "@npmcli/run-script": "^10.0.0", - "cacache": "^20.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^13.0.0", - "npm-packlist": "^10.0.1", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^4.0.0", - "ssri": "^13.0.0", - "tar": "^7.4.3" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "5.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^5.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "6.1.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/proggy": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "3.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "5.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "^3.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.7.3", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/sigstore": { - "version": "4.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.0.0", - "@sigstore/tuf": "^4.0.0", - "@sigstore/verify": "^3.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.8.7", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.5.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.22", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/ssri": { - "version": "13.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "10.2.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "7.5.2", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.15", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "4.0.0", - "debug": "^4.4.1", - "make-fetch-happen": "^15.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/which": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, "node_modules/nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", @@ -19247,15 +17155,6 @@ "yaml": "^2.8.0" } }, - "node_modules/optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", - "license": "MIT/X11", - "dependencies": { - "wordwrap": "~0.0.2" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -19744,6 +17643,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", "integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.10.0", "pg-pool": "^3.11.0", @@ -20229,6 +18129,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -20259,6 +18160,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -21075,6 +18977,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -21229,20 +19132,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/rebuild": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/rebuild/-/rebuild-0.1.2.tgz", - "integrity": "sha512-EtDZ5IapND57htCrOOcfH7MzXCQKivzSZUIZIuc8H0xDHfmi9HDBZIyjT7Neh5GcUoxQ6hfsXluC+UrYLgGbZg==", - "dependencies": { - "optimist": "0.3.x" - }, - "bin": { - "rebuild": "cli.js" - }, - "engines": { - "node": ">=0.8.8" - } - }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -22600,7 +20489,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -22879,15 +20769,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "license": "MIT", - "engines": { - "node": ">=0.6.x" - } - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -23068,6 +20949,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23354,15 +21236,6 @@ "integrity": "sha512-jHl/NQgASfw5ZML3cnbjdfr/gXK5zO8a2xKSoCVe+5+EsIaO9tMTh7SsnfhESnCpZ+Xb6XBeU91wiuyERUPshQ==", "license": "BSD-3-Clause" }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -23476,6 +21349,7 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -23534,15 +21408,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -23783,6 +21648,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index fa89274d..70c9ab32 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "license": "SEE LICENSE IN LICENSE AND README.md", "scripts": { "dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts", + "dev:check": "npx tsc --noEmit && npm run format:check", + "dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push", "db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts", "db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts", "db:pg:push": "npx tsx server/db/pg/migrate.ts", @@ -30,6 +32,7 @@ "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3005", "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs", + "format:check": "prettier --check .", "format": "prettier --write ." }, "dependencies": { @@ -75,9 +78,7 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "cookie": "1.1.1", "cookie-parser": "1.4.7", - "cookies": "0.9.1", "cors": "2.8.5", "crypto-js": "4.2.0", "d3": "7.9.0", @@ -90,7 +91,6 @@ "glob": "13.0.0", "helmet": "8.1.0", "http-errors": "2.0.1", - "i": "0.3.7", "input-otp": "1.4.2", "ioredis": "5.9.2", "jmespath": "0.16.0", @@ -104,10 +104,7 @@ "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "node-fetch": "3.3.2", "nodemailer": "7.0.11", - "npm": "11.7.0", - "nprogress": "0.2.0", "oslo": "1.2.1", "pg": "8.17.1", "posthog-node": "5.23.0", @@ -118,7 +115,6 @@ "react-easy-sort": "1.8.0", "react-hook-form": "7.71.1", "react-icons": "5.5.0", - "rebuild": "0.1.2", "recharts": "2.15.4", "reodotdev": "1.0.0", "resend": "6.8.0", diff --git a/src/components/Toploader.tsx b/src/components/Toploader.tsx index 17b32bad..53e04b73 100644 --- a/src/components/Toploader.tsx +++ b/src/components/Toploader.tsx @@ -1,41 +1,13 @@ "use client"; -import * as React from "react"; -import * as NProgress from "nprogress"; + import NextTopLoader from "nextjs-toploader"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; export function TopLoader() { - return ( - <> - - - - ); -} - -function FinishingLoader() { - const pathname = usePathname(); - const router = useRouter(); - const searchParams = useSearchParams(); - React.useEffect(() => { - NProgress.done(); - }, [pathname, router, searchParams]); - React.useEffect(() => { - const linkClickListener = (ev: MouseEvent) => { - const element = ev.target as HTMLElement; - const closestlink = element.closest("a"); - const isOpenToNewTabClick = - ev.ctrlKey || - ev.shiftKey || - ev.metaKey || // apple - (ev.button && ev.button == 1); // middle click, >IE9 + everyone else - - if (closestlink && isOpenToNewTabClick) { - NProgress.done(); - } - }; - window.addEventListener("click", linkClickListener); - return () => window.removeEventListener("click", linkClickListener); - }, []); - return null; + return ( + + ); } From 873408270e02a5b9e792a705125e1dbd9c339377 Mon Sep 17 00:00:00 2001 From: Lokowitz Date: Tue, 27 Jan 2026 12:23:12 +0000 Subject: [PATCH 006/247] removed unused gomod code --- .github/dependabot.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 196676e9..685be384 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,19 +44,9 @@ updates: schedule: interval: "daily" groups: - dev-patch-updates: - dependency-type: "development" + patch-updates: update-types: - "patch" - dev-minor-updates: - dependency-type: "development" + minor-updates: update-types: - "minor" - prod-patch-updates: - dependency-type: "production" - update-types: - - "patch" - prod-minor-updates: - dependency-type: "production" - update-types: - - "minor" \ No newline at end of file From 5dda8c384fcd7274d51ebc2f5d4a430d51e4d7ac Mon Sep 17 00:00:00 2001 From: MoweME Date: Thu, 29 Jan 2026 14:28:06 +0100 Subject: [PATCH 007/247] fix(i18n): correct German translation strings Corrects mistranslation of device timestamp labels and fixes product name reference in site tunnel settings. --- messages/de-DE.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index e0c85879..489519d2 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -97,7 +97,7 @@ "siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren", "siteSettingDescription": "Standorteinstellungen konfigurieren", "siteSetting": "{siteName} Einstellungen", - "siteNewtTunnel": "Neuer Standort (empfohlen)", + "siteNewtTunnel": "Newt Standort (empfohlen)", "siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.", "siteWg": "Einfacher WireGuard Tunnel", "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", @@ -2503,7 +2503,7 @@ "deviceModel": "Gerätemodell", "serialNumber": "Seriennummer", "hostname": "Hostname", - "firstSeen": "Erster Blick", + "firstSeen": "Zuerst gesehen", "lastSeen": "Zuletzt gesehen", "biometricsEnabled": "Biometrie aktiviert", "diskEncrypted": "Festplatte verschlüsselt", From b0566d3c6fc0e2a993c8606acc128a68c4e27247 Mon Sep 17 00:00:00 2001 From: MoweME Date: Thu, 29 Jan 2026 14:36:59 +0100 Subject: [PATCH 008/247] fix(i18n): correct German site terminology Updates the German translation to use "Standort" (site) instead of "Seite" (page) for consistency with the site context. --- messages/de-DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 489519d2..cbf8b4d3 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -107,7 +107,7 @@ "siteSeeAll": "Alle Standorte anzeigen", "siteTunnelDescription": "Legen Sie fest, wie Sie sich mit dem Standort verbinden möchten", "siteNewtCredentials": "Zugangsdaten", - "siteNewtCredentialsDescription": "So wird sich die Seite mit dem Server authentifizieren", + "siteNewtCredentialsDescription": "So wird sich der Standort mit dem Server authentifizieren", "remoteNodeCredentialsDescription": "So wird sich der entfernte Node mit dem Server authentifizieren", "siteCredentialsSave": "Anmeldedaten speichern", "siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.", From f2ba4b270f96bb49b0aa01e4e89b642abc186635 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 29 Jan 2026 20:56:46 -0800 Subject: [PATCH 009/247] Dont write stripe to files anymore --- server/integrationApiServer.ts | 12 +- server/lib/billing/usageService.ts | 378 +++++---------------------- server/private/lib/config.ts | 5 +- server/private/lib/readConfigFile.ts | 2 +- 4 files changed, 71 insertions(+), 326 deletions(-) diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index 0ef0c0af..6d513cf6 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -105,11 +105,13 @@ function getOpenApiDocumentation() { servers: [{ url: "/v1" }] }); - // convert to yaml and save to file - const outputPath = path.join(APP_PATH, "openapi.yaml"); - const yamlOutput = yaml.dump(generated); - fs.writeFileSync(outputPath, yamlOutput, "utf8"); - logger.info(`OpenAPI documentation saved to ${outputPath}`); + if (!process.env.DISABLE_GEN_OPENAPI) { + // convert to yaml and save to file + const outputPath = path.join(APP_PATH, "openapi.yaml"); + const yamlOutput = yaml.dump(generated); + fs.writeFileSync(outputPath, yamlOutput, "utf8"); + logger.info(`OpenAPI documentation saved to ${outputPath}`); + } return generated; } diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts index 0fde8eba..4d52ee69 100644 --- a/server/lib/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -1,8 +1,6 @@ import { eq, sql, and } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { PutObjectCommand } from "@aws-sdk/client-s3"; -import * as fs from "fs/promises"; -import * as path from "path"; import { db, usage, @@ -34,8 +32,7 @@ interface StripeEvent { export function noop() { if ( build !== "saas" || - !process.env.S3_BUCKET || - !process.env.LOCAL_FILE_PATH + !process.env.S3_BUCKET ) { return true; } @@ -44,31 +41,37 @@ export function noop() { export class UsageService { private bucketName: string | undefined; - private currentEventFile: string | null = null; - private currentFileStartTime: number = 0; - private eventsDir: string | undefined; - private uploadingFiles: Set = new Set(); + private events: StripeEvent[] = []; + private lastUploadTime: number = Date.now(); + private isUploading: boolean = false; constructor() { if (noop()) { return; } - // this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket; - // this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath; this.bucketName = process.env.S3_BUCKET || undefined; - this.eventsDir = process.env.LOCAL_FILE_PATH || undefined; - // Ensure events directory exists - this.initializeEventsDirectory().then(() => { - this.uploadPendingEventFilesOnStartup(); - }); - - // Periodically check for old event files to upload + // Periodically check and upload events setInterval(() => { - this.uploadOldEventFiles().catch((err) => { - logger.error("Error in periodic event file upload:", err); + this.checkAndUploadEvents().catch((err) => { + logger.error("Error in periodic event upload:", err); }); }, 30000); // every 30 seconds + + // Handle graceful shutdown on SIGTERM + process.on("SIGTERM", async () => { + logger.info("SIGTERM received, uploading events before shutdown..."); + await this.forceUpload(); + logger.info("Events uploaded, proceeding with shutdown"); + }); + + // Handle SIGINT as well (Ctrl+C) + process.on("SIGINT", async () => { + logger.info("SIGINT received, uploading events before shutdown..."); + await this.forceUpload(); + logger.info("Events uploaded, proceeding with shutdown"); + process.exit(0); + }); } /** @@ -78,85 +81,6 @@ export class UsageService { return Math.round(value * 100000000000) / 100000000000; // 11 decimal places } - private async initializeEventsDirectory(): Promise { - if (!this.eventsDir) { - logger.warn( - "Stripe local file path is not configured, skipping events directory initialization." - ); - return; - } - try { - await fs.mkdir(this.eventsDir, { recursive: true }); - } catch (error) { - logger.error("Failed to create events directory:", error); - } - } - - private async uploadPendingEventFilesOnStartup(): Promise { - if (!this.eventsDir || !this.bucketName) { - logger.warn( - "Stripe local file path or bucket name is not configured, skipping leftover event file upload." - ); - return; - } - try { - const files = await fs.readdir(this.eventsDir); - for (const file of files) { - if (file.endsWith(".json")) { - const filePath = path.join(this.eventsDir, file); - try { - const fileContent = await fs.readFile( - filePath, - "utf-8" - ); - const events = JSON.parse(fileContent); - if (Array.isArray(events) && events.length > 0) { - // Upload to S3 - const uploadCommand = new PutObjectCommand({ - Bucket: this.bucketName, - Key: file, - Body: fileContent, - ContentType: "application/json" - }); - await s3Client.send(uploadCommand); - - // Check if file still exists before unlinking - try { - await fs.access(filePath); - await fs.unlink(filePath); - } catch (unlinkError) { - logger.debug( - `Startup file ${file} was already deleted` - ); - } - - logger.info( - `Uploaded leftover event file ${file} to S3 with ${events.length} events` - ); - } else { - // Remove empty file - try { - await fs.access(filePath); - await fs.unlink(filePath); - } catch (unlinkError) { - logger.debug( - `Empty startup file ${file} was already deleted` - ); - } - } - } catch (err) { - logger.error( - `Error processing leftover event file ${file}:`, - err - ); - } - } - } - } catch (error) { - logger.error("Failed to scan for leftover event files"); - } - } - public async add( orgId: string, featureId: FeatureId, @@ -450,121 +374,58 @@ export class UsageService { } }; - await this.writeEventToFile(event); - await this.checkAndUploadFile(); + this.addEventToMemory(event); + await this.checkAndUploadEvents(); } - private async writeEventToFile(event: StripeEvent): Promise { - if (!this.eventsDir || !this.bucketName) { + private addEventToMemory(event: StripeEvent): void { + if (!this.bucketName) { logger.warn( - "Stripe local file path or bucket name is not configured, skipping event file write." + "S3 bucket name is not configured, skipping event storage." ); return; } - if (!this.currentEventFile) { - this.currentEventFile = this.generateEventFileName(); - this.currentFileStartTime = Date.now(); - } - - const filePath = path.join(this.eventsDir, this.currentEventFile); - - try { - let events: StripeEvent[] = []; - - // Try to read existing file - try { - const fileContent = await fs.readFile(filePath, "utf-8"); - events = JSON.parse(fileContent); - } catch (error) { - // File doesn't exist or is empty, start with empty array - events = []; - } - - // Add new event - events.push(event); - - // Write back to file - await fs.writeFile(filePath, JSON.stringify(events, null, 2)); - } catch (error) { - logger.error("Failed to write event to file:", error); - } + this.events.push(event); } - private async checkAndUploadFile(): Promise { - if (!this.currentEventFile) { - return; - } - + private async checkAndUploadEvents(): Promise { const now = Date.now(); - const fileAge = now - this.currentFileStartTime; + const timeSinceLastUpload = now - this.lastUploadTime; - // Check if file is at least 1 minute old - if (fileAge >= 60000) { - // 60 seconds - await this.uploadFileToS3(); + // Check if at least 1 minute has passed since last upload + if (timeSinceLastUpload >= 60000 && this.events.length > 0) { + await this.uploadEventsToS3(); } } - private async uploadFileToS3(): Promise { - if (!this.bucketName || !this.eventsDir) { + private async uploadEventsToS3(): Promise { + if (!this.bucketName) { logger.warn( - "Stripe local file path or bucket name is not configured, skipping S3 upload." - ); - return; - } - if (!this.currentEventFile) { - return; - } - - const fileName = this.currentEventFile; - const filePath = path.join(this.eventsDir, fileName); - - // Check if this file is already being uploaded - if (this.uploadingFiles.has(fileName)) { - logger.debug( - `File ${fileName} is already being uploaded, skipping` + "S3 bucket name is not configured, skipping S3 upload." ); return; } - // Mark file as being uploaded - this.uploadingFiles.add(fileName); + if (this.events.length === 0) { + return; + } + + // Check if already uploading + if (this.isUploading) { + logger.debug("Already uploading events, skipping"); + return; + } + + this.isUploading = true; try { - // Check if file exists before trying to read it - try { - await fs.access(filePath); - } catch (error) { - logger.debug( - `File ${fileName} does not exist, may have been already processed` - ); - this.uploadingFiles.delete(fileName); - // Reset current file if it was this file - if (this.currentEventFile === fileName) { - this.currentEventFile = null; - this.currentFileStartTime = 0; - } - return; - } + // Take a snapshot of current events and clear the array + const eventsToUpload = [...this.events]; + this.events = []; + this.lastUploadTime = Date.now(); - // Check if file exists and has content - const fileContent = await fs.readFile(filePath, "utf-8"); - const events = JSON.parse(fileContent); - - if (events.length === 0) { - // No events to upload, just clean up - try { - await fs.unlink(filePath); - } catch (unlinkError) { - // File may have been already deleted - logger.debug( - `File ${fileName} was already deleted during cleanup` - ); - } - this.currentEventFile = null; - this.uploadingFiles.delete(fileName); - return; - } + const fileName = this.generateEventFileName(); + const fileContent = JSON.stringify(eventsToUpload, null, 2); // Upload to S3 const uploadCommand = new PutObjectCommand({ @@ -576,29 +437,15 @@ export class UsageService { await s3Client.send(uploadCommand); - // Clean up local file - check if it still exists before unlinking - try { - await fs.access(filePath); - await fs.unlink(filePath); - } catch (unlinkError) { - // File may have been already deleted by another process - logger.debug( - `File ${fileName} was already deleted during upload` - ); - } - logger.info( - `Uploaded ${fileName} to S3 with ${events.length} events` + `Uploaded ${fileName} to S3 with ${eventsToUpload.length} events` ); - - // Reset for next file - this.currentEventFile = null; - this.currentFileStartTime = 0; } catch (error) { - logger.error(`Failed to upload ${fileName} to S3:`, error); + logger.error("Failed to upload events to S3:", error); + // Note: Events are lost if upload fails. In a production system, + // you might want to add the events back to the array or implement retry logic } finally { - // Always remove from uploading set - this.uploadingFiles.delete(fileName); + this.isUploading = false; } } @@ -695,111 +542,10 @@ export class UsageService { } public async forceUpload(): Promise { - await this.uploadFileToS3(); - } - - /** - * Scan the events directory for files older than 1 minute and upload them if not empty. - */ - private async uploadOldEventFiles(): Promise { - if (!this.eventsDir || !this.bucketName) { - logger.warn( - "Stripe local file path or bucket name is not configured, skipping old event file upload." - ); - return; - } - try { - const files = await fs.readdir(this.eventsDir); - const now = Date.now(); - for (const file of files) { - if (!file.endsWith(".json")) continue; - - // Skip files that are already being uploaded - if (this.uploadingFiles.has(file)) { - logger.debug( - `Skipping file ${file} as it's already being uploaded` - ); - continue; - } - - const filePath = path.join(this.eventsDir, file); - - try { - // Check if file still exists before processing - try { - await fs.access(filePath); - } catch (accessError) { - logger.debug(`File ${file} does not exist, skipping`); - continue; - } - - const stat = await fs.stat(filePath); - const age = now - stat.mtimeMs; - if (age >= 90000) { - // 1.5 minutes - Mark as being uploaded - this.uploadingFiles.add(file); - - try { - const fileContent = await fs.readFile( - filePath, - "utf-8" - ); - const events = JSON.parse(fileContent); - if (Array.isArray(events) && events.length > 0) { - // Upload to S3 - const uploadCommand = new PutObjectCommand({ - Bucket: this.bucketName, - Key: file, - Body: fileContent, - ContentType: "application/json" - }); - await s3Client.send(uploadCommand); - - // Check if file still exists before unlinking - try { - await fs.access(filePath); - await fs.unlink(filePath); - } catch (unlinkError) { - logger.debug( - `File ${file} was already deleted during interval upload` - ); - } - - logger.info( - `Interval: Uploaded event file ${file} to S3 with ${events.length} events` - ); - // If this was the current event file, reset it - if (this.currentEventFile === file) { - this.currentEventFile = null; - this.currentFileStartTime = 0; - } - } else { - // Remove empty file - try { - await fs.access(filePath); - await fs.unlink(filePath); - } catch (unlinkError) { - logger.debug( - `Empty file ${file} was already deleted` - ); - } - } - } finally { - // Always remove from uploading set - this.uploadingFiles.delete(file); - } - } - } catch (err) { - logger.error( - `Interval: Error processing event file ${file}:`, - err - ); - // Remove from uploading set on error - this.uploadingFiles.delete(file); - } - } - } catch (err) { - logger.error("Interval: Failed to scan for event files:", err); + if (this.events.length > 0) { + // Force upload regardless of time + this.lastUploadTime = 0; // Reset to force upload + await this.uploadEventsToS3(); } } diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts index ae9ca5c7..f37ba2c1 100644 --- a/server/private/lib/config.ts +++ b/server/private/lib/config.ts @@ -128,10 +128,7 @@ export class PrivateConfig { if (this.rawPrivateConfig.stripe?.s3Bucket) { process.env.S3_BUCKET = this.rawPrivateConfig.stripe.s3Bucket; } - if (this.rawPrivateConfig.stripe?.localFilePath) { - process.env.LOCAL_FILE_PATH = - this.rawPrivateConfig.stripe.localFilePath; - } + if (this.rawPrivateConfig.stripe?.s3Region) { process.env.S3_REGION = this.rawPrivateConfig.stripe.s3Region; } diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index 374dee7c..34eccf29 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -161,7 +161,7 @@ export const privateConfigSchema = z.object({ webhook_secret: z.string(), s3Bucket: z.string(), s3Region: z.string().default("us-east-1"), - localFilePath: z.string() + localFilePath: z.string().optional() }) .optional() }); From 7a72d209ea74d82c419ef150ad902b45e36ad410 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 1 Feb 2026 17:24:01 -0800 Subject: [PATCH 010/247] add --network host to newt install command for docker run --- src/components/newt-install-commands.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/newt-install-commands.tsx b/src/components/newt-install-commands.tsx index c06b2047..b814fdae 100644 --- a/src/components/newt-install-commands.tsx +++ b/src/components/newt-install-commands.tsx @@ -91,7 +91,7 @@ export function NewtSiteInstallCommands({ - NEWT_SECRET=${secret}${acceptClientsEnv}` ], "Docker Run": [ - `docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + `docker run -dit --network host fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] }, kubernetes: { From 37695827aa79dd038f2af2b95c268350f2b86091 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 1 Feb 2026 17:30:05 -0800 Subject: [PATCH 011/247] show user display name on device page --- server/routers/client/getClient.ts | 10 +++++++++- src/components/ClientInfoCard.tsx | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 138a286c..66a6432f 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, olms } from "@server/db"; +import { db, olms, users } from "@server/db"; import { clients, currentFingerprint } from "@server/db"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; @@ -36,6 +36,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) { currentFingerprint, eq(olms.olmId, currentFingerprint.olmId) ) + .leftJoin(users, eq(clients.userId, users.userId)) .limit(1); return res; } else if (niceId && orgId) { @@ -48,6 +49,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) { currentFingerprint, eq(olms.olmId, currentFingerprint.olmId) ) + .leftJoin(users, eq(clients.userId, users.userId)) .limit(1); return res; } @@ -207,6 +209,9 @@ export type GetClientResponse = NonNullable< olmId: string | null; agent: string | null; olmVersion: string | null; + userEmail: string | null; + userName: string | null; + userUsername: string | null; fingerprint: { username: string | null; hostname: string | null; @@ -322,6 +327,9 @@ export async function getClient( olmId: client.olms ? client.olms.olmId : null, agent: client.olms?.agent || null, olmVersion: client.olms?.version || null, + userEmail: client.user?.email ?? null, + userName: client.user?.name ?? null, + userUsername: client.user?.username ?? null, fingerprint: fingerprintData, posture: postureData }; diff --git a/src/components/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx index a50b6039..1a4cbd54 100644 --- a/src/components/ClientInfoCard.tsx +++ b/src/components/ClientInfoCard.tsx @@ -8,6 +8,7 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { useTranslations } from "next-intl"; type ClientInfoCardProps = {}; @@ -16,6 +17,12 @@ export default function SiteInfoCard({}: ClientInfoCardProps) { const { client, updateClient } = useClientContext(); const t = useTranslations(); + const userDisplayName = getUserDisplayName({ + email: client.userEmail, + name: client.userName, + username: client.userUsername + }); + return ( @@ -25,8 +32,12 @@ export default function SiteInfoCard({}: ClientInfoCardProps) { {client.name} - {t("identifier")} - {client.niceId} + + {userDisplayName ? t("user") : t("identifier")} + + + {userDisplayName || client.niceId} + {t("status")} From 8f8775cb93db6979bff388047e78f2c92ac74b67 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 1 Feb 2026 17:37:18 -0800 Subject: [PATCH 012/247] override device name with computed device name on register --- server/routers/olm/handleOlmRegisterMessage.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index db156c2c..e4bb6f4f 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -13,6 +13,7 @@ import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { validateSessionToken } from "@server/auth/sessions/app"; import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; +import { getUserDeviceName } from "@server/db/names"; import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { OlmErrorCodes, sendOlmError } from "./error"; import { handleFingerprintInsertion } from "./fingerprintingUtils"; @@ -97,6 +98,21 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } + const deviceModel = fingerprint?.deviceModel ?? null; + const computedName = getUserDeviceName(deviceModel, client.name); + if (computedName && computedName !== client.name) { + await db + .update(clients) + .set({ name: computedName }) + .where(eq(clients.clientId, client.clientId)); + } + if (computedName && computedName !== olm.name) { + await db + .update(olms) + .set({ name: computedName }) + .where(eq(olms.olmId, olm.olmId)); + } + const [org] = await db .select() .from(orgs) From 5ad564d21bc13de7030adc3f33d248d4d18aaf54 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 2 Feb 2026 10:25:14 -0800 Subject: [PATCH 013/247] Use rand --- install/main.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/install/main.go b/install/main.go index 3ea6af22..242af741 100644 --- a/install/main.go +++ b/install/main.go @@ -6,7 +6,8 @@ import ( "fmt" "io" "io/fs" - "math/rand" + "crypto/rand" + "encoding/base64" "net" "net/http" "net/url" @@ -592,17 +593,12 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai } func generateRandomSecretKey() string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - const length = 32 - - var seededRand *rand.Rand = rand.New( - rand.NewSource(time.Now().UnixNano())) - - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] + secret := make([]byte, 32) + _, err := rand.Read(secret) + if err != nil { + panic(fmt.Sprintf("Failed to generate random secret key: %v", err)) } - return string(b) + return base64.StdEncoding.EncodeToString(secret) } func getPublicIP() string { From f5f757e4bd9f6b69d780c67872f3a20cb94a680f Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 2 Feb 2026 16:45:54 -0800 Subject: [PATCH 014/247] Subscribed limits for domains is higher --- server/lib/billing/limitSet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/billing/limitSet.ts b/server/lib/billing/limitSet.ts index 820b121a..fdd077d9 100644 --- a/server/lib/billing/limitSet.ts +++ b/server/lib/billing/limitSet.ts @@ -40,7 +40,7 @@ export const subscribedLimitSet: LimitSet = { description: "Contact us to increase soft limit." }, // 12000 GB [FeatureId.DOMAINS]: { - value: 25, + value: 250, description: "Contact us to increase soft limit." }, [FeatureId.REMOTE_EXIT_NODES]: { From 20ae903d7fc559c98f56658c3d4d112a3033643e Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 2 Feb 2026 16:45:54 -0800 Subject: [PATCH 015/247] Subscribed limits for domains is higher --- server/lib/billing/limitSet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/billing/limitSet.ts b/server/lib/billing/limitSet.ts index 820b121a..fdd077d9 100644 --- a/server/lib/billing/limitSet.ts +++ b/server/lib/billing/limitSet.ts @@ -40,7 +40,7 @@ export const subscribedLimitSet: LimitSet = { description: "Contact us to increase soft limit." }, // 12000 GB [FeatureId.DOMAINS]: { - value: 25, + value: 250, description: "Contact us to increase soft limit." }, [FeatureId.REMOTE_EXIT_NODES]: { From e4d4c62833eb309ffb2fd9db05d1dbee6b6761f6 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 2 Feb 2026 18:19:13 -0800 Subject: [PATCH 016/247] Dont create newt sites with exit node or subnet --- server/routers/site/createSite.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index c798ea30..4f1bba06 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -17,7 +17,6 @@ import { hashPassword } from "@server/auth/password"; import { isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; -import { build } from "@server/build"; const createSiteParamsSchema = z.strictObject({ orgId: z.string() @@ -259,7 +258,19 @@ export async function createSite( let newSite: Site; await db.transaction(async (trx) => { - if (type == "wireguard" || type == "newt") { + if (type == "newt") { + [newSite] = await trx + .insert(sites) + .values({ + orgId, + name, + niceId, + address: updatedAddress || null, + type, + dockerSocketEnabled: true + }) + .returning(); + } else if (type == "wireguard") { // we are creating a site with an exit node (tunneled) if (!subnet) { return next( @@ -311,11 +322,9 @@ export async function createSite( exitNodeId, name, niceId, - address: updatedAddress || null, subnet, type, - dockerSocketEnabled: type == "newt", - ...(pubKey && type == "wireguard" && { pubKey }) + pubKey: pubKey || null }) .returning(); } else if (type == "local") { From bf5dd3b0a115c9c0030f10f3ec6c2adf68affe3e Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 2 Feb 2026 21:39:18 -0800 Subject: [PATCH 017/247] Pull secrets from env vars --- server/lib/getEnvOrYaml.ts | 3 +++ server/lib/readConfigFile.ts | 10 ++++----- server/private/lib/certificates.ts | 11 +--------- server/private/lib/readConfigFile.ts | 32 ++++++++++++++++++---------- server/private/routers/hybrid.ts | 15 +++---------- 5 files changed, 33 insertions(+), 38 deletions(-) create mode 100644 server/lib/getEnvOrYaml.ts diff --git a/server/lib/getEnvOrYaml.ts b/server/lib/getEnvOrYaml.ts new file mode 100644 index 00000000..62081cef --- /dev/null +++ b/server/lib/getEnvOrYaml.ts @@ -0,0 +1,3 @@ +export const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { + return process.env[envVar] ?? valFromYaml; +}; diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 90ebdc89..362210ae 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -3,13 +3,10 @@ import yaml from "js-yaml"; import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; +import { getEnvOrYaml } from "./getEnvOrYaml"; const portSchema = z.number().positive().gt(0).lte(65535); -const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { - return process.env[envVar] ?? valFromYaml; -}; - export const configSchema = z .object({ app: z @@ -311,7 +308,10 @@ export const configSchema = z .object({ smtp_host: z.string().optional(), smtp_port: portSchema.optional(), - smtp_user: z.string().optional(), + smtp_user: z + .string() + .optional() + .transform(getEnvOrYaml("EMAIL_SMTP_USER")), smtp_pass: z .string() .optional() diff --git a/server/private/lib/certificates.ts b/server/private/lib/certificates.ts index 06571cac..bc1dffcd 100644 --- a/server/private/lib/certificates.ts +++ b/server/private/lib/certificates.ts @@ -19,7 +19,6 @@ import * as fs from "fs"; import logger from "@server/logger"; import cache from "@server/lib/cache"; -let encryptionKeyPath = ""; let encryptionKeyHex = ""; let encryptionKey: Buffer; function loadEncryptData() { @@ -27,15 +26,7 @@ function loadEncryptData() { return; // already loaded } - encryptionKeyPath = config.getRawPrivateConfig().server.encryption_key_path; - - if (!fs.existsSync(encryptionKeyPath)) { - throw new Error( - "Encryption key file not found. Please generate one first." - ); - } - - encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim(); + encryptionKeyHex = config.getRawPrivateConfig().server.encryption_key; encryptionKey = Buffer.from(encryptionKeyHex, "hex"); } diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index 374dee7c..3fa7f060 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -17,6 +17,7 @@ import { privateConfigFilePath1 } from "@server/lib/consts"; import { z } from "zod"; import { colorsSchema } from "@server/lib/colorsSchema"; import { build } from "@server/build"; +import { getEnvOrYaml } from "@server/lib/getEnvOrYaml"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -32,19 +33,25 @@ export const privateConfigSchema = z.object({ }), server: z .object({ - encryption_key_path: z + encryption_key: z .string() .optional() - .default("./config/encryption.pem") - .pipe(z.string().min(8)), - resend_api_key: z.string().optional(), - reo_client_id: z.string().optional(), - fossorial_api_key: z.string().optional() + .transform(getEnvOrYaml("SERVER_ENCRYPTION_KEY")), + resend_api_key: z + .string() + .optional() + .transform(getEnvOrYaml("RESEND_API_KEY")), + reo_client_id: z + .string() + .optional() + .transform(getEnvOrYaml("REO_CLIENT_ID")), + fossorial_api_key: z + .string() + .optional() + .transform(getEnvOrYaml("FOSSORIAL_API_KEY")) }) .optional() - .default({ - encryption_key_path: "./config/encryption.pem" - }), + .prefault({}), redis: z .object({ host: z.string(), @@ -157,8 +164,11 @@ export const privateConfigSchema = z.object({ .optional(), stripe: z .object({ - secret_key: z.string(), - webhook_secret: z.string(), + secret_key: z.string().optional().transform(getEnvOrYaml("STRIPE_SECRET_KEY")), + webhook_secret: z + .string() + .optional() + .transform(getEnvOrYaml("STRIPE_WEBHOOK_SECRET")), s3Bucket: z.string(), s3Region: z.string().default("us-east-1"), localFilePath: z.string() diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index a398dfe6..0e5d1ec2 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -186,7 +186,7 @@ export type ResourceWithAuth = { password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; - org: Org + org: Org; }; export type UserSessionWithUser = { @@ -270,7 +270,6 @@ hybridRouter.get( } ); -let encryptionKeyPath = ""; let encryptionKeyHex = ""; let encryptionKey: Buffer; function loadEncryptData() { @@ -278,16 +277,8 @@ function loadEncryptData() { return; // already loaded } - encryptionKeyPath = - privateConfig.getRawPrivateConfig().server.encryption_key_path; - - if (!fs.existsSync(encryptionKeyPath)) { - throw new Error( - "Encryption key file not found. Please generate one first." - ); - } - - encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim(); + encryptionKeyHex = + privateConfig.getRawPrivateConfig().server.encryption_key; encryptionKey = Buffer.from(encryptionKeyHex, "hex"); } From 158d7b23d89b937fa106923341d87e0c177c1a82 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 14:13:25 -0800 Subject: [PATCH 018/247] Add test button to launch stripe --- server/lib/billing/licenses.ts | 37 ++++++ .../billing/createCheckoutSessionLicense.ts | 113 ++++++++++++++++++ ...ession.ts => createCheckoutSessionSAAS.ts} | 4 +- server/private/routers/billing/index.ts | 3 +- server/private/routers/external.ts | 12 +- .../settings/(private)/billing/page.tsx | 2 +- src/components/GenerateLicenseKeyForm.tsx | 61 ++++++++-- 7 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 server/lib/billing/licenses.ts create mode 100644 server/private/routers/billing/createCheckoutSessionLicense.ts rename server/private/routers/billing/{createCheckoutSession.ts => createCheckoutSessionSAAS.ts} (96%) diff --git a/server/lib/billing/licenses.ts b/server/lib/billing/licenses.ts new file mode 100644 index 00000000..a481527e --- /dev/null +++ b/server/lib/billing/licenses.ts @@ -0,0 +1,37 @@ +export enum LicenseId { + SMALL_LICENSE = "small_license", + BIG_LICENSE = "big_license" +} + +export type LicensePriceSet = { + [key in LicenseId]: string; +}; + +export const licensePriceSet: LicensePriceSet = { + // Free license matches the freeLimitSet + [LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN", + [LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl" +}; + +export const licensePriceSetSandbox: LicensePriceSet = { + // Free license matches the freeLimitSet + // when matching license the keys closer to 0 index are matched first so list the licenses in descending order of value + [LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN", + [LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl" +}; + +export function getLicensePriceSet( + environment?: string, + sandbox_mode?: boolean +): LicensePriceSet { + if ( + (process.env.ENVIRONMENT == "prod" && + process.env.SANDBOX_MODE !== "true") || + (environment === "prod" && sandbox_mode !== true) + ) { + // THIS GETS LOADED CLIENT SIDE AND SERVER SIDE + return licensePriceSet; + } else { + return licensePriceSetSandbox; + } +} diff --git a/server/private/routers/billing/createCheckoutSessionLicense.ts b/server/private/routers/billing/createCheckoutSessionLicense.ts new file mode 100644 index 00000000..045f1797 --- /dev/null +++ b/server/private/routers/billing/createCheckoutSessionLicense.ts @@ -0,0 +1,113 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { customers, db } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import config from "@server/lib/config"; +import { fromError } from "zod-validation-error"; +import stripe from "#private/lib/stripe"; +import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; + +const createCheckoutSessionParamsSchema = z.strictObject({ + orgId: z.string(), +}); + +const createCheckoutSessionBodySchema = z.strictObject({ + tier: z.enum([LicenseId.BIG_LICENSE, LicenseId.SMALL_LICENSE]), +}); + +export async function createCheckoutSessionoLicense( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = createCheckoutSessionParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const parsedBody = createCheckoutSessionBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { tier } = parsedBody.data; + + // check if we already have a customer for this org + const [customer] = await db + .select() + .from(customers) + .where(eq(customers.orgId, orgId)) + .limit(1); + + // If we don't have a customer, create one + if (!customer) { + // error + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "No customer found for this organization" + ) + ); + } + + const tierPrice = getLicensePriceSet()[tier] + + const session = await stripe!.checkout.sessions.create({ + client_reference_id: orgId, // So we can look it up the org later on the webhook + billing_address_collection: "required", + line_items: [ + { + price: tierPrice, // Use the standard tier + quantity: 1 + }, + ], // Start with the standard feature set that matches the free limits + customer: customer.customerId, + mode: "subscription", + success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?success=true&session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true` + }); + + return response(res, { + data: session.url, + success: true, + error: false, + message: "Checkout session created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSessionSAAS.ts similarity index 96% rename from server/private/routers/billing/createCheckoutSession.ts rename to server/private/routers/billing/createCheckoutSessionSAAS.ts index a2d8080f..0f9b783e 100644 --- a/server/private/routers/billing/createCheckoutSession.ts +++ b/server/private/routers/billing/createCheckoutSessionSAAS.ts @@ -29,7 +29,7 @@ const createCheckoutSessionSchema = z.strictObject({ orgId: z.string() }); -export async function createCheckoutSession( +export async function createCheckoutSessionSAAS( req: Request, res: Response, next: NextFunction @@ -87,7 +87,7 @@ export async function createCheckoutSession( data: session.url, success: true, error: false, - message: "Organization created successfully", + message: "Checkout session created successfully", status: HttpCode.CREATED }); } catch (error) { diff --git a/server/private/routers/billing/index.ts b/server/private/routers/billing/index.ts index 59fce8d6..aef867af 100644 --- a/server/private/routers/billing/index.ts +++ b/server/private/routers/billing/index.ts @@ -11,8 +11,9 @@ * This file is not licensed under the AGPLv3. */ -export * from "./createCheckoutSession"; +export * from "./createCheckoutSessionSAAS"; export * from "./createPortalSession"; export * from "./getOrgSubscription"; export * from "./getOrgUsage"; export * from "./internalGetOrgTier"; +export * from "./createCheckoutSessionLicense"; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index cf6e58bc..9ad0609f 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -159,11 +159,19 @@ if (build === "saas") { ); authenticated.post( - "/org/:orgId/billing/create-checkout-session", + "/org/:orgId/billing/create-checkout-session-saas", verifyOrgAccess, verifyUserHasAction(ActionsEnum.billing), logActionAudit(ActionsEnum.billing), - billing.createCheckoutSession + billing.createCheckoutSessionSAAS + ); + + authenticated.post( + "/org/:orgId/billing/create-checkout-session-license", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + logActionAudit(ActionsEnum.billing), + billing.createCheckoutSessionoLicense ); authenticated.post( diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index 1ed5c094..e63eebcc 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -121,7 +121,7 @@ export default function GeneralPage() { setIsLoading(true); try { const response = await api.post>( - `/org/${org.org.orgId}/billing/create-checkout-session`, + `/org/${org.org.orgId}/billing/create-checkout-session-saas`, {} ); console.log("Checkout session response:", response.data); diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx index 6a380082..6a5aaf54 100644 --- a/src/components/GenerateLicenseKeyForm.tsx +++ b/src/components/GenerateLicenseKeyForm.tsx @@ -345,6 +345,37 @@ export default function GenerateLicenseKeyForm({ resetForm(); }; + const handleTestCheckout = async () => { + setLoading(true); + try { + const response = await api.post>( + `/org/${orgId}/billing/create-checkout-session-license`, + { + tier: "big_license" + } + ); + console.log("Checkout session response:", response.data); + const checkoutUrl = response.data.data; + if (checkoutUrl) { + window.location.href = checkoutUrl; + } else { + toast({ + title: "Failed to get checkout URL", + description: "Please try again later", + variant: "destructive" + }); + setLoading(false); + } + } catch (error) { + toast({ + title: "Checkout error", + description: formatAxiosError(error), + variant: "destructive" + }); + setLoading(false); + } + }; + return ( @@ -1066,16 +1097,26 @@ export default function GenerateLicenseKeyForm({ )} {!generatedKey && useCaseType === "business" && ( - + <> + + + )} From 5a3d75ca12d46853e67de7775631ae304dc33130 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 4 Feb 2026 15:19:53 -0800 Subject: [PATCH 019/247] add quantity check --- server/license/license.ts | 8 +++- server/private/license/license.ts | 51 +++++++++++++++++++++--- server/routers/generatedLicense/types.ts | 1 + 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/server/license/license.ts b/server/license/license.ts index cfa45d7c..7c960984 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -12,6 +12,10 @@ export type LicenseStatus = { isLicenseValid: boolean; // Is the license key valid? hostId: string; // Host ID tier?: LicenseKeyTier; + maxSites?: number; + usedSites?: number; + maxUsers?: number; + usedUsers?: number; }; export type LicenseKeyCache = { @@ -22,12 +26,14 @@ export type LicenseKeyCache = { type?: LicenseKeyType; tier?: LicenseKeyTier; terminateAt?: Date; + quantity?: number; + quantity_2?: number; }; export class License { private serverSecret!: string; - constructor(private hostMeta: HostMeta) {} + constructor(private hostMeta: HostMeta) { } public async check(): Promise { return { diff --git a/server/private/license/license.ts b/server/private/license/license.ts index f8f774c6..972dbc82 100644 --- a/server/private/license/license.ts +++ b/server/private/license/license.ts @@ -11,12 +11,12 @@ * This file is not licensed under the AGPLv3. */ -import { db, HostMeta } from "@server/db"; +import { db, HostMeta, sites, users } from "@server/db"; import { hostMeta, licenseKey } from "@server/db"; import logger from "@server/logger"; import NodeCache from "node-cache"; import { validateJWT } from "./licenseJwt"; -import { eq } from "drizzle-orm"; +import { count, eq } from "drizzle-orm"; import moment from "moment"; import { encrypt, decrypt } from "@server/lib/crypto"; import { @@ -54,6 +54,7 @@ type TokenPayload = { type: LicenseKeyType; tier: LicenseKeyTier; quantity: number; + quantity_2: number; terminateAt: string; // ISO iat: number; // Issued at }; @@ -140,10 +141,20 @@ LQIDAQAB }; } + // Count used sites and users for license comparison + const [siteCountRes] = await db + .select({ value: count() }) + .from(sites); + const [userCountRes] = await db + .select({ value: count() }) + .from(users); + const status: LicenseStatus = { hostId: this.hostMeta.hostMetaId, isHostLicensed: true, - isLicenseValid: false + isLicenseValid: false, + usedSites: siteCountRes?.value ?? 0, + usedUsers: userCountRes?.value ?? 0 }; this.checkInProgress = true; @@ -151,6 +162,8 @@ LQIDAQAB try { if (!this.doRecheck && this.statusCache.has(this.statusKey)) { const res = this.statusCache.get("status") as LicenseStatus; + res.usedSites = status.usedSites; + res.usedUsers = status.usedUsers; return res; } logger.debug("Checking license status..."); @@ -193,7 +206,9 @@ LQIDAQAB type: payload.type, tier: payload.tier, iat: new Date(payload.iat * 1000), - terminateAt: new Date(payload.terminateAt) + terminateAt: new Date(payload.terminateAt), + quantity: payload.quantity, + quantity_2: payload.quantity_2 }); if (payload.type === "host") { @@ -292,6 +307,8 @@ LQIDAQAB cached.tier = payload.tier; cached.iat = new Date(payload.iat * 1000); cached.terminateAt = new Date(payload.terminateAt); + cached.quantity = payload.quantity; + cached.quantity_2 = payload.quantity_2; // Encrypt the updated token before storing const encryptedKey = encrypt( @@ -317,7 +334,7 @@ LQIDAQAB } } - // Compute host status + // Compute host status: quantity = users, quantity_2 = sites for (const key of keys) { const cached = newCache.get(key.licenseKey)!; @@ -329,6 +346,28 @@ LQIDAQAB if (!cached.valid) { continue; } + + // Only consider quantity if defined and >= 0 (quantity = users, quantity_2 = sites) + if ( + cached.quantity_2 !== undefined && + cached.quantity_2 >= 0 + ) { + status.maxSites = + (status.maxSites ?? 0) + cached.quantity_2; + } + if (cached.quantity !== undefined && cached.quantity >= 0) { + status.maxUsers = (status.maxUsers ?? 0) + cached.quantity; + } + } + + // Invalidate license if over user or site limits + if ( + (status.maxSites !== undefined && + (status.usedSites ?? 0) > status.maxSites) || + (status.maxUsers !== undefined && + (status.usedUsers ?? 0) > status.maxUsers) + ) { + status.isLicenseValid = false; } // Invalidate old cache and set new cache @@ -502,7 +541,7 @@ LQIDAQAB // Calculate exponential backoff delay const retryDelay = Math.floor( initialRetryDelay * - Math.pow(exponentialFactor, attempt - 1) + Math.pow(exponentialFactor, attempt - 1) ); logger.debug( diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts index 76e86265..d05da2de 100644 --- a/server/routers/generatedLicense/types.ts +++ b/server/routers/generatedLicense/types.ts @@ -19,6 +19,7 @@ export type NewLicenseKey = { tier: string; type: string; quantity: number; + quantity_2: number; isValid: boolean; updatedAt: string; createdAt: string; From 34b914f509262be2059dcddabfcb550b92acb884 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 4 Feb 2026 15:38:02 -0800 Subject: [PATCH 020/247] add license email --- .../EnterpriseEditionKeyGenerated.tsx | 118 ++++++++++++++++++ .../templates/components/CopyCodeBox.tsx | 14 ++- 2 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 server/emails/templates/EnterpriseEditionKeyGenerated.tsx diff --git a/server/emails/templates/EnterpriseEditionKeyGenerated.tsx b/server/emails/templates/EnterpriseEditionKeyGenerated.tsx new file mode 100644 index 00000000..44472c8a --- /dev/null +++ b/server/emails/templates/EnterpriseEditionKeyGenerated.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { themeColors } from "./lib/theme"; +import { + EmailContainer, + EmailFooter, + EmailGreeting, + EmailHeading, + EmailInfoSection, + EmailLetterHead, + EmailSection, + EmailSignature, + EmailText +} from "./components/Email"; +import CopyCodeBox from "./components/CopyCodeBox"; +import ButtonLink from "./components/ButtonLink"; + +type EnterpriseEditionKeyGeneratedProps = { + keyValue: string; + personalUseOnly: boolean; + users: number; + sites: number; + modifySubscriptionLink?: string; +}; + +export const EnterpriseEditionKeyGenerated = ({ + keyValue, + personalUseOnly, + users, + sites, + modifySubscriptionLink +}: EnterpriseEditionKeyGeneratedProps) => { + const previewText = personalUseOnly + ? "Your Enterprise Edition key for personal use is ready" + : "Thank you for your purchase — your Enterprise Edition key is ready"; + + return ( + + + {previewText} + + + + + + Hi there, + + {personalUseOnly ? ( + + Your Enterprise Edition license key has been + generated. Qualifying users can use the + Enterprise Edition for free for{" "} + personal use only. + + ) : ( + <> + + Thank you for your purchase. Your Enterprise + Edition license key is ready. Below are the + terms of your license. + + + {modifySubscriptionLink && ( + + + Modify subscription + + + )} + + )} + + + Your license key: + + + + + If you need to purchase additional license keys or + modify your existing license, please reach out to + our support team at{" "} + + support@pangolin.net + + . + + + + + + + + + + ); +}; + +export default EnterpriseEditionKeyGenerated; diff --git a/server/emails/templates/components/CopyCodeBox.tsx b/server/emails/templates/components/CopyCodeBox.tsx index 3e4d1d08..497fe7a9 100644 --- a/server/emails/templates/components/CopyCodeBox.tsx +++ b/server/emails/templates/components/CopyCodeBox.tsx @@ -1,6 +1,14 @@ import React from "react"; -export default function CopyCodeBox({ text }: { text: string }) { +const DEFAULT_HINT = "Copy and paste this code when prompted"; + +export default function CopyCodeBox({ + text, + hint +}: { + text: string; + hint?: string; +}) { return (
@@ -8,9 +16,7 @@ export default function CopyCodeBox({ text }: { text: string }) { {text}
-

- Copy and paste this code when prompted -

+

{hint ?? DEFAULT_HINT}

); } From a5c7913e771e91a38ba68c984bdd22dc4f1b5d31 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 15:49:40 -0800 Subject: [PATCH 021/247] Checkout flow works --- server/private/lib/readConfigFile.ts | 9 +- .../routers/billing/hooks/getSubType.ts | 35 +++++++ .../hooks/handleSubscriptionCreated.ts | 92 ++++++++++++++++--- server/private/routers/billing/index.ts | 1 - server/private/routers/external.ts | 16 ++-- .../generateNewEnterpriseLicense.ts} | 80 +++++++++++----- .../generatedLicense/generateNewLicense.ts | 37 +++++++- .../private/routers/generatedLicense/index.ts | 1 + .../generatedLicense/listGeneratedLicenses.ts | 2 +- src/components/GenerateLicenseKeyForm.tsx | 88 +++++++----------- 10 files changed, 256 insertions(+), 105 deletions(-) create mode 100644 server/private/routers/billing/hooks/getSubType.ts rename server/private/routers/{billing/createCheckoutSessionLicense.ts => generatedLicense/generateNewEnterpriseLicense.ts} (57%) diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index 3fa7f060..0211a330 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -45,6 +45,10 @@ export const privateConfigSchema = z.object({ .string() .optional() .transform(getEnvOrYaml("REO_CLIENT_ID")), + fossorial_api: z + .string() + .optional() + .default("https://api.fossorial.io"), fossorial_api_key: z .string() .optional() @@ -164,7 +168,10 @@ export const privateConfigSchema = z.object({ .optional(), stripe: z .object({ - secret_key: z.string().optional().transform(getEnvOrYaml("STRIPE_SECRET_KEY")), + secret_key: z + .string() + .optional() + .transform(getEnvOrYaml("STRIPE_SECRET_KEY")), webhook_secret: z .string() .optional() diff --git a/server/private/routers/billing/hooks/getSubType.ts b/server/private/routers/billing/hooks/getSubType.ts new file mode 100644 index 00000000..8cd07713 --- /dev/null +++ b/server/private/routers/billing/hooks/getSubType.ts @@ -0,0 +1,35 @@ +import { + getLicensePriceSet, +} from "@server/lib/billing/licenses"; +import { + getTierPriceSet, +} from "@server/lib/billing/tiers"; +import Stripe from "stripe"; + +export function getSubType(fullSubscription: Stripe.Response): "saas" | "license" { + // Determine subscription type by checking subscription items + let type: "saas" | "license" = "saas"; + if (Array.isArray(fullSubscription.items?.data)) { + for (const item of fullSubscription.items.data) { + const priceId = item.price.id; + + // Check if price ID matches any license price + const licensePrices = Object.values(getLicensePriceSet()); + + if (licensePrices.includes(priceId)) { + type = "license"; + break; + } + + // Check if price ID matches any tier price (saas) + const tierPrices = Object.values(getTierPriceSet()); + + if (tierPrices.includes(priceId)) { + type = "saas"; + break; + } + } + } + + return type; +} diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 223a2545..431798dd 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -25,6 +25,8 @@ import logger from "@server/logger"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; +import { getSubType } from "./getSubType"; +import privateConfig from "#private/lib/config"; export async function handleSubscriptionCreated( subscription: Stripe.Subscription @@ -123,24 +125,86 @@ export async function handleSubscriptionCreated( return; } - await handleSubscriptionLifesycle(customer.orgId, subscription.status); + const type = getSubType(fullSubscription); + if (type === "saas") { + logger.debug( + `Handling SAAS subscription lifecycle for org ${customer.orgId}` + ); + // we only need to handle the limit lifecycle for saas subscriptions not for the licenses + await handleSubscriptionLifesycle( + customer.orgId, + subscription.status + ); - const [orgUserRes] = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.orgId, customer.orgId), - eq(userOrgs.isOwner, true) + const [orgUserRes] = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.orgId, customer.orgId), + eq(userOrgs.isOwner, true) + ) ) - ) - .innerJoin(users, eq(userOrgs.userId, users.userId)); + .innerJoin(users, eq(userOrgs.userId, users.userId)); - if (orgUserRes) { - const email = orgUserRes.user.email; + if (orgUserRes) { + const email = orgUserRes.user.email; - if (email) { - moveEmailToAudience(email, AudienceIds.Subscribed); + if (email) { + moveEmailToAudience(email, AudienceIds.Subscribed); + } + } + } else if (type === "license") { + logger.debug( + `License subscription created for org ${customer.orgId}, no lifecycle handling needed.` + ); + + // Retrieve the client_reference_id from the checkout session + let licenseId: string | null = null; + + try { + const sessions = await stripe!.checkout.sessions.list({ + subscription: subscription.id, + limit: 1 + }); + if (sessions.data.length > 0) { + licenseId = sessions.data[0].client_reference_id || null; + } + + if (!licenseId) { + logger.error( + `No client_reference_id found for subscription ${subscription.id}` + ); + return; + } + + logger.debug( + `Retrieved licenseId ${licenseId} from checkout session for subscription ${subscription.id}` + ); + + const response = await fetch( + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, // this says enterprise but it does both + { + method: "POST", + headers: { + "api-key": + privateConfig.getRawPrivateConfig().server + .fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + licenseId: parseInt(licenseId) + }) + } + ); + + const data = await response.json(); + + logger.debug("Fossorial API response:", { data }); + return data; + } catch (error) { + console.error("Error creating new license:", error); + throw error; } } } catch (error) { diff --git a/server/private/routers/billing/index.ts b/server/private/routers/billing/index.ts index aef867af..106f3e43 100644 --- a/server/private/routers/billing/index.ts +++ b/server/private/routers/billing/index.ts @@ -16,4 +16,3 @@ export * from "./createPortalSession"; export * from "./getOrgSubscription"; export * from "./getOrgUsage"; export * from "./internalGetOrgTier"; -export * from "./createCheckoutSessionLicense"; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 9ad0609f..3377db46 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -166,14 +166,6 @@ if (build === "saas") { billing.createCheckoutSessionSAAS ); - authenticated.post( - "/org/:orgId/billing/create-checkout-session-license", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.billing), - logActionAudit(ActionsEnum.billing), - billing.createCheckoutSessionoLicense - ); - authenticated.post( "/org/:orgId/billing/create-portal-session", verifyOrgAccess, @@ -208,6 +200,14 @@ if (build === "saas") { generateLicense.generateNewLicense ); + authenticated.put( + "/org/:orgId/license/enterprise", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + logActionAudit(ActionsEnum.billing), + generateLicense.generateNewEnterpriseLicense + ); + authenticated.post( "/send-support-request", rateLimit({ diff --git a/server/private/routers/billing/createCheckoutSessionLicense.ts b/server/private/routers/generatedLicense/generateNewEnterpriseLicense.ts similarity index 57% rename from server/private/routers/billing/createCheckoutSessionLicense.ts rename to server/private/routers/generatedLicense/generateNewEnterpriseLicense.ts index 045f1797..7cffb9d7 100644 --- a/server/private/routers/billing/createCheckoutSessionLicense.ts +++ b/server/private/routers/generatedLicense/generateNewEnterpriseLicense.ts @@ -12,33 +12,33 @@ */ import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { customers, db } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib/response"; +import privateConfig from "#private/lib/config"; +import { createNewLicense } from "./generateNewLicense"; import config from "@server/lib/config"; -import { fromError } from "zod-validation-error"; -import stripe from "#private/lib/stripe"; import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; +import stripe from "#private/lib/stripe"; +import { customers, db } from "@server/db"; +import { fromError } from "zod-validation-error"; +import z from "zod"; +import { eq } from "drizzle-orm"; +import { log } from "winston"; -const createCheckoutSessionParamsSchema = z.strictObject({ - orgId: z.string(), +const generateNewEnterpriseLicenseParamsSchema = z.strictObject({ + orgId: z.string() }); -const createCheckoutSessionBodySchema = z.strictObject({ - tier: z.enum([LicenseId.BIG_LICENSE, LicenseId.SMALL_LICENSE]), -}); - -export async function createCheckoutSessionoLicense( +export async function generateNewEnterpriseLicense( req: Request, res: Response, next: NextFunction ): Promise { try { - const parsedParams = createCheckoutSessionParamsSchema.safeParse(req.params); + + const parsedParams = generateNewEnterpriseLicenseParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -50,17 +50,49 @@ export async function createCheckoutSessionoLicense( const { orgId } = parsedParams.data; - const parsedBody = createCheckoutSessionBodySchema.safeParse(req.body); - if (!parsedBody.success) { + if (!orgId) { return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() + "Organization ID is required" ) ); } - const { tier } = parsedBody.data; + logger.debug(`Generating new license for orgId: ${orgId}`); + + const licenseData = req.body; + + if (licenseData.tier != "big_license" && licenseData.tier != "small_license") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid tier specified. Must be either 'big_license' or 'small_license'." + ) + ); + } + + const apiResponse = await createNewLicense(orgId, licenseData); + + // Check if the API call was successful + if (!apiResponse.success || apiResponse.error) { + return next( + createHttpError( + apiResponse.status || HttpCode.BAD_REQUEST, + apiResponse.message || "Failed to create license from Fossorial API" + ) + ); + } + + const keyId = apiResponse?.data?.licenseKey?.id; + if (!keyId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Fossorial API did not return a valid license key ID" + ) + ); + } // check if we already have a customer for this org const [customer] = await db @@ -80,10 +112,11 @@ export async function createCheckoutSessionoLicense( ); } + const tier = licenseData.tier === "big_license" ? LicenseId.BIG_LICENSE : LicenseId.SMALL_LICENSE; const tierPrice = getLicensePriceSet()[tier] const session = await stripe!.checkout.sessions.create({ - client_reference_id: orgId, // So we can look it up the org later on the webhook + client_reference_id: keyId.toString(), billing_address_collection: "required", line_items: [ { @@ -97,17 +130,20 @@ export async function createCheckoutSessionoLicense( cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true` }); - return response(res, { + return sendResponse(res, { data: session.url, success: true, error: false, - message: "Checkout session created successfully", + message: "License and checkout session created successfully", status: HttpCode.CREATED }); } catch (error) { logger.error(error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while generating new license." + ) ); } } diff --git a/server/private/routers/generatedLicense/generateNewLicense.ts b/server/private/routers/generatedLicense/generateNewLicense.ts index 2c0c4420..9835f40a 100644 --- a/server/private/routers/generatedLicense/generateNewLicense.ts +++ b/server/private/routers/generatedLicense/generateNewLicense.ts @@ -19,10 +19,40 @@ import { response as sendResponse } from "@server/lib/response"; import privateConfig from "#private/lib/config"; import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; -async function createNewLicense(orgId: string, licenseData: any): Promise { +export interface CreateNewLicenseResponse { + data: Data + success: boolean + error: boolean + message: string + status: number +} + +export interface Data { + licenseKey: LicenseKey +} + +export interface LicenseKey { + id: number + instanceName: any + instanceId: string + licenseKey: string + tier: string + type: string + quantity: number + quantity_2: number + isValid: boolean + updatedAt: string + createdAt: string + expiresAt: string + paidFor: boolean + orgId: string + metadata: string +} + +export async function createNewLicense(orgId: string, licenseData: any): Promise { try { const response = await fetch( - `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/create`, + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/create`, // this says enterprise but it does both { method: "PUT", headers: { @@ -35,9 +65,8 @@ async function createNewLicense(orgId: string, licenseData: any): Promise { } ); - const data = await response.json(); + const data: CreateNewLicenseResponse = await response.json(); - logger.debug("Fossorial API response:", { data }); return data; } catch (error) { console.error("Error creating new license:", error); diff --git a/server/private/routers/generatedLicense/index.ts b/server/private/routers/generatedLicense/index.ts index 83d88634..70b9b001 100644 --- a/server/private/routers/generatedLicense/index.ts +++ b/server/private/routers/generatedLicense/index.ts @@ -13,3 +13,4 @@ export * from "./listGeneratedLicenses"; export * from "./generateNewLicense"; +export * from "./generateNewEnterpriseLicense"; diff --git a/server/private/routers/generatedLicense/listGeneratedLicenses.ts b/server/private/routers/generatedLicense/listGeneratedLicenses.ts index fb54c763..cb930882 100644 --- a/server/private/routers/generatedLicense/listGeneratedLicenses.ts +++ b/server/private/routers/generatedLicense/listGeneratedLicenses.ts @@ -25,7 +25,7 @@ import { async function fetchLicenseKeys(orgId: string): Promise { try { const response = await fetch( - `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/list`, + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/list`, { method: "GET", headers: { diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx index 6a5aaf54..7274004c 100644 --- a/src/components/GenerateLicenseKeyForm.tsx +++ b/src/components/GenerateLicenseKeyForm.tsx @@ -250,20 +250,41 @@ export default function GenerateLicenseKeyForm({ const submitLicenseRequest = async (payload: any) => { setLoading(true); try { - const response = await api.put< - AxiosResponse - >(`/org/${orgId}/license`, payload); + // Check if this is a business/enterprise license request + if (payload.useCaseType === "business") { + const response = await api.put< + AxiosResponse + >(`/org/${orgId}/license/enterprise`, { ...payload, tier: "big_license" } ); - if (response.data.data?.licenseKey?.licenseKey) { - setGeneratedKey(response.data.data.licenseKey.licenseKey); - onGenerated?.(); - toast({ - title: t("generateLicenseKeyForm.toasts.success.title"), - description: t( - "generateLicenseKeyForm.toasts.success.description" - ), - variant: "default" - }); + console.log("Checkout session response:", response.data); + const checkoutUrl = response.data.data; + if (checkoutUrl) { + window.location.href = checkoutUrl; + } else { + toast({ + title: "Failed to get checkout URL", + description: "Please try again later", + variant: "destructive" + }); + setLoading(false); + } + } else { + // Personal license flow + const response = await api.put< + AxiosResponse + >(`/org/${orgId}/license`, payload); + + if (response.data.data?.licenseKey?.licenseKey) { + setGeneratedKey(response.data.data.licenseKey.licenseKey); + onGenerated?.(); + toast({ + title: t("generateLicenseKeyForm.toasts.success.title"), + description: t( + "generateLicenseKeyForm.toasts.success.description" + ), + variant: "default" + }); + } } } catch (e) { console.error(e); @@ -345,37 +366,6 @@ export default function GenerateLicenseKeyForm({ resetForm(); }; - const handleTestCheckout = async () => { - setLoading(true); - try { - const response = await api.post>( - `/org/${orgId}/billing/create-checkout-session-license`, - { - tier: "big_license" - } - ); - console.log("Checkout session response:", response.data); - const checkoutUrl = response.data.data; - if (checkoutUrl) { - window.location.href = checkoutUrl; - } else { - toast({ - title: "Failed to get checkout URL", - description: "Please try again later", - variant: "destructive" - }); - setLoading(false); - } - } catch (error) { - toast({ - title: "Checkout error", - description: formatAxiosError(error), - variant: "destructive" - }); - setLoading(false); - } - }; - return ( @@ -1097,15 +1087,6 @@ export default function GenerateLicenseKeyForm({ )} {!generatedKey && useCaseType === "business" && ( - <> - + + + {!generatedKey && personalUseOnly && ( + + )} + + {!generatedKey && !personalUseOnly && ( + + )} + + + + ); +} From 1bc4480d849b138f075b62f6c37249a0da5efaf6 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 16:32:39 -0800 Subject: [PATCH 023/247] Working on complete auth flow --- .../hooks/handleSubscriptionCreated.ts | 33 ++++++++++++- .../hooks/handleSubscriptionDeleted.ts | 46 +++++++++++++------ .../hooks/handleSubscriptionUpdated.ts | 24 +++++++--- server/routers/generatedLicense/types.ts | 2 + src/components/GenerateLicenseKeysTable.tsx | 42 +++++++++++++++++ 5 files changed, 125 insertions(+), 22 deletions(-) diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 431798dd..4370d088 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -27,6 +27,7 @@ import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import privateConfig from "#private/lib/config"; +import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; export async function handleSubscriptionCreated( subscription: Stripe.Subscription @@ -182,6 +183,33 @@ export async function handleSubscriptionCreated( `Retrieved licenseId ${licenseId} from checkout session for subscription ${subscription.id}` ); + // Determine users and sites based on license type + const priceSet = getLicensePriceSet(); + const subscriptionPriceId = + fullSubscription.items.data[0]?.price.id; + + let numUsers: number; + let numSites: number; + + if (subscriptionPriceId === priceSet[LicenseId.SMALL_LICENSE]) { + numUsers = 25; + numSites = 25; + } else if ( + subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE] + ) { + numUsers = 50; + numSites = 50; + } else { + logger.error( + `Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}` + ); + return; + } + + logger.debug( + `License type determined: ${numUsers} users, ${numSites} sites for subscription ${subscription.id}` + ); + const response = await fetch( `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, // this says enterprise but it does both { @@ -193,7 +221,10 @@ export async function handleSubscriptionCreated( "Content-Type": "application/json" }, body: JSON.stringify({ - licenseId: parseInt(licenseId) + licenseId: parseInt(licenseId), + paidFor: true, + users: numUsers, + sites: numSites }) } ); diff --git a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index 7a7d9149..56fca02b 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -24,11 +24,21 @@ import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; +import { getSubType } from "./getSubType"; +import stripe from "#private/lib/stripe"; export async function handleSubscriptionDeleted( subscription: Stripe.Subscription ): Promise { try { + // Fetch the subscription from Stripe with expanded price.tiers + const fullSubscription = await stripe!.subscriptions.retrieve( + subscription.id, + { + expand: ["items.data.price.tiers"] + } + ); + const [existingSubscription] = await db .select() .from(subscriptions) @@ -64,25 +74,33 @@ export async function handleSubscriptionDeleted( return; } - await handleSubscriptionLifesycle(customer.orgId, subscription.status); + const type = getSubType(fullSubscription); + if (type === "saas") { + logger.debug(`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); - const [orgUserRes] = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.orgId, customer.orgId), - eq(userOrgs.isOwner, true) + await handleSubscriptionLifesycle(customer.orgId, subscription.status); + + const [orgUserRes] = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.orgId, customer.orgId), + eq(userOrgs.isOwner, true) + ) ) - ) - .innerJoin(users, eq(userOrgs.userId, users.userId)); + .innerJoin(users, eq(userOrgs.userId, users.userId)); - if (orgUserRes) { - const email = orgUserRes.user.email; + if (orgUserRes) { + const email = orgUserRes.user.email; - if (email) { - moveEmailToAudience(email, AudienceIds.Churned); + if (email) { + moveEmailToAudience(email, AudienceIds.Churned); + } } + } else if (type === "license") { + logger.debug(`Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); + } } catch (error) { logger.error( diff --git a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts index 01086054..8e6f901e 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts @@ -26,6 +26,7 @@ import logger from "@server/logger"; import { getFeatureIdByMetricId } from "@server/lib/billing/features"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; +import { getSubType } from "./getSubType"; export async function handleSubscriptionUpdated( subscription: Stripe.Subscription, @@ -74,11 +75,6 @@ export async function handleSubscriptionUpdated( }) .where(eq(subscriptions.subscriptionId, subscription.id)); - await handleSubscriptionLifesycle( - existingCustomer.orgId, - subscription.status - ); - // Upsert subscription items if (Array.isArray(fullSubscription.items?.data)) { const itemsToUpsert = fullSubscription.items.data.map((item) => ({ @@ -141,14 +137,14 @@ export async function handleSubscriptionUpdated( // This item has cycled const meterId = item.plan.meter; if (!meterId) { - logger.warn( + logger.debug( `No meterId found for subscription item ${item.id}. Skipping usage reset.` ); continue; } const featureId = getFeatureIdByMetricId(meterId); if (!featureId) { - logger.warn( + logger.debug( `No featureId found for meterId ${meterId}. Skipping usage reset.` ); continue; @@ -236,6 +232,20 @@ export async function handleSubscriptionUpdated( } } // --- end usage update --- + + const type = getSubType(fullSubscription); + if (type === "saas") { + logger.debug(`Handling SAAS subscription lifecycle for org ${existingCustomer.orgId}`); + // we only need to handle the limit lifecycle for saas subscriptions not for the licenses + await handleSubscriptionLifesycle( + existingCustomer.orgId, + subscription.status + ); + } else { + logger.debug( + `Subscription ${subscription.id} is of type ${type}. No lifecycle handling needed.` + ); + } } } catch (error) { logger.error( diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts index d05da2de..d78f2332 100644 --- a/server/routers/generatedLicense/types.ts +++ b/server/routers/generatedLicense/types.ts @@ -6,6 +6,8 @@ export type GeneratedLicenseKey = { createdAt: string; tier: string; type: string; + users: number; + sites: number; }; export type ListGeneratedLicenseKeysResponse = GeneratedLicenseKey[]; diff --git a/src/components/GenerateLicenseKeysTable.tsx b/src/components/GenerateLicenseKeysTable.tsx index c6db4e1d..036b2fb5 100644 --- a/src/components/GenerateLicenseKeysTable.tsx +++ b/src/components/GenerateLicenseKeysTable.tsx @@ -158,6 +158,48 @@ export default function GenerateLicenseKeysTable({ : t("licenseTierPersonal"); } }, + { + accessorKey: "users", + friendlyName: t("users"), + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const users = row.original.users; + return users === -1 ? "∞" : users; + } + }, + { + accessorKey: "sites", + friendlyName: t("sites"), + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const sites = row.original.sites; + return sites === -1 ? "∞" : sites; + } + }, { accessorKey: "terminateAt", friendlyName: t("licenseTableValidUntil"), From 4613aae47dca71ceae77fb19cd0d3d4729e8e9d8 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 17:37:31 -0800 Subject: [PATCH 024/247] Handle license lifecycle --- .../hooks/handleSubscriptionCreated.ts | 32 ++++- .../hooks/handleSubscriptionDeleted.ts | 39 +++++- .../hooks/handleSubscriptionUpdated.ts | 40 +++++- src/components/NewPricingLicenseForm.tsx | 122 ++++++++++-------- 4 files changed, 169 insertions(+), 64 deletions(-) diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 4370d088..9f2ee2ad 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -28,6 +28,9 @@ import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import privateConfig from "#private/lib/config"; import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; +import { sendEmail } from "@server/emails"; +import EnterpriseEditionKeyGenerated from "@server/emails/templates/EnterpriseEditionKeyGenerated"; +import config from "@server/lib/config"; export async function handleSubscriptionCreated( subscription: Stripe.Subscription @@ -211,7 +214,7 @@ export async function handleSubscriptionCreated( ); const response = await fetch( - `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, // this says enterprise but it does both + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, { method: "POST", headers: { @@ -231,7 +234,32 @@ export async function handleSubscriptionCreated( const data = await response.json(); - logger.debug("Fossorial API response:", { data }); + logger.debug(`Fossorial API response: ${JSON.stringify(data)}`); + + if (customer.email) { + logger.debug( + `Sending license key email to ${customer.email} for subscription ${subscription.id}` + ); + await sendEmail( + EnterpriseEditionKeyGenerated({ + keyValue: data.data.licenseKey, + personalUseOnly: false, + users: numUsers, + sites: numSites, + }), + { + to: customer.email, + from: config.getNoReplyEmail(), + subject: + "Your Enterprise Edition license key is ready" + } + ); + } else { + logger.error( + `No email found for customer ${customer.customerId} to send license key.` + ); + } + return data; } catch (error) { console.error("Error creating new license:", error); diff --git a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index 56fca02b..003110aa 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -26,6 +26,7 @@ import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import stripe from "#private/lib/stripe"; +import privateConfig from "#private/lib/config"; export async function handleSubscriptionDeleted( subscription: Stripe.Subscription @@ -76,9 +77,14 @@ export async function handleSubscriptionDeleted( const type = getSubType(fullSubscription); if (type === "saas") { - logger.debug(`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); + logger.debug( + `Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}` + ); - await handleSubscriptionLifesycle(customer.orgId, subscription.status); + await handleSubscriptionLifesycle( + customer.orgId, + subscription.status + ); const [orgUserRes] = await db .select() @@ -99,8 +105,33 @@ export async function handleSubscriptionDeleted( } } } else if (type === "license") { - logger.debug(`Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); - + logger.debug( + `Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}` + ); + try { + // WARNING: + // this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId + await fetch( + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`, + { + method: "POST", + headers: { + "api-key": + privateConfig.getRawPrivateConfig().server + .fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + orgId: customer.orgId, + }) + } + ); + } catch (error) { + logger.error( + `Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`, + error + ); + } } } catch (error) { logger.error( diff --git a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts index 8e6f901e..21943354 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts @@ -27,6 +27,7 @@ import { getFeatureIdByMetricId } from "@server/lib/billing/features"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { getSubType } from "./getSubType"; +import privateConfig from "#private/lib/config"; export async function handleSubscriptionUpdated( subscription: Stripe.Subscription, @@ -57,7 +58,7 @@ export async function handleSubscriptionUpdated( } // get the customer - const [existingCustomer] = await db + const [customer] = await db .select() .from(customers) .where(eq(customers.customerId, subscription.customer as string)) @@ -150,7 +151,7 @@ export async function handleSubscriptionUpdated( continue; } - const orgId = existingCustomer.orgId; + const orgId = customer.orgId; if (!orgId) { logger.warn( @@ -235,16 +236,41 @@ export async function handleSubscriptionUpdated( const type = getSubType(fullSubscription); if (type === "saas") { - logger.debug(`Handling SAAS subscription lifecycle for org ${existingCustomer.orgId}`); + logger.debug( + `Handling SAAS subscription lifecycle for org ${customer.orgId}` + ); // we only need to handle the limit lifecycle for saas subscriptions not for the licenses await handleSubscriptionLifesycle( - existingCustomer.orgId, + customer.orgId, subscription.status ); } else { - logger.debug( - `Subscription ${subscription.id} is of type ${type}. No lifecycle handling needed.` - ); + if (subscription.status === "canceled" || subscription.status == "unpaid" || subscription.status == "incomplete_expired") { + try { + // WARNING: + // this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId + await fetch( + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`, + { + method: "POST", + headers: { + "api-key": + privateConfig.getRawPrivateConfig() + .server.fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + orgId: customer.orgId + }) + } + ); + } catch (error) { + logger.error( + `Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`, + error + ); + } + } } } } catch (error) { diff --git a/src/components/NewPricingLicenseForm.tsx b/src/components/NewPricingLicenseForm.tsx index cc2c9286..d4f76bb9 100644 --- a/src/components/NewPricingLicenseForm.tsx +++ b/src/components/NewPricingLicenseForm.tsx @@ -178,20 +178,42 @@ export default function NewPricingLicenseForm({ ): Promise => { setLoading(true); try { - const response = await api.put< - AxiosResponse - >(`/org/${orgId}/license`, payload); + // Check if this is a business/enterprise license request + if (!personalUseOnly) { + const response = await api.put>( + `/org/${orgId}/license/enterprise`, + { ...payload, tier: TIER_TO_LICENSE_ID[selectedTier] } + ); - if (response.data.data?.licenseKey?.licenseKey) { - setGeneratedKey(response.data.data.licenseKey.licenseKey); - onGenerated?.(); - toast({ - title: t("generateLicenseKeyForm.toasts.success.title"), - description: t( - "generateLicenseKeyForm.toasts.success.description" - ), - variant: "default" - }); + console.log("Checkout session response:", response.data); + const checkoutUrl = response.data.data; + if (checkoutUrl) { + window.location.href = checkoutUrl; + } else { + toast({ + title: "Failed to get checkout URL", + description: "Please try again later", + variant: "destructive" + }); + setLoading(false); + } + } else { + // Personal license flow + const response = await api.put< + AxiosResponse + >(`/org/${orgId}/license`, payload); + + if (response.data.data?.licenseKey?.licenseKey) { + setGeneratedKey(response.data.data.licenseKey.licenseKey); + onGenerated?.(); + toast({ + title: t("generateLicenseKeyForm.toasts.success.title"), + description: t( + "generateLicenseKeyForm.toasts.success.description" + ), + variant: "default" + }); + } } } catch (e) { console.error(e); @@ -229,44 +251,38 @@ export default function NewPricingLicenseForm({ }); }; - const handleContinueToCheckout = async () => { - const valid = await businessForm.trigger(); - if (!valid) return; - - const values = businessForm.getValues(); - setLoading(true); - try { - const tier = TIER_TO_LICENSE_ID[selectedTier]; - const response = await api.post>( - `/org/${orgId}/billing/create-checkout-session-license`, - { tier } - ); - const checkoutUrl = response.data.data; - if (checkoutUrl) { - window.location.href = checkoutUrl; - } else { - toast({ - title: t( - "newPricingLicenseForm.toasts.checkoutError.title" - ), - description: t( - "newPricingLicenseForm.toasts.checkoutError.description" - ), - variant: "destructive" - }); - setLoading(false); + const onSubmitBusiness = async (values: BusinessFormData) => { + const payload = { + email: values.email, + useCaseType: "business", + personal: undefined, + business: { + firstName: values.firstName, + lastName: values.lastName, + jobTitle: "N/A", + aboutYou: { + primaryUse: values.primaryUse, + industry: values.industry, + prospectiveUsers: 100, + prospectiveSites: 100 + }, + companyInfo: { + companyName: values.companyName, + countryOfResidence: "N/A", + stateProvinceRegion: "N/A", + postalZipCode: "N/A", + companyWebsite: values.companyWebsite || "", + companyPhoneNumber: values.companyPhoneNumber || "" + } + }, + consent: { + agreedToTerms: values.agreedToTerms, + acknowledgedPrivacyPolicy: values.agreedToTerms, + complianceConfirmed: values.complianceConfirmed } - } catch (error) { - toast({ - title: t("newPricingLicenseForm.toasts.checkoutError.title"), - description: formatAxiosError( - error, - t("newPricingLicenseForm.toasts.checkoutError.description") - ), - variant: "destructive" - }); - setLoading(false); - } + }; + + await submitLicenseRequest(payload); }; const handleClose = () => { @@ -608,6 +624,9 @@ export default function NewPricingLicenseForm({ {!personalUseOnly && (
@@ -877,7 +896,8 @@ export default function NewPricingLicenseForm({ {!generatedKey && !personalUseOnly && ( + + + + )} ); } diff --git a/src/providers/SubscriptionStatusProvider.tsx b/src/providers/SubscriptionStatusProvider.tsx index 85802cfa..eecafce8 100644 --- a/src/providers/SubscriptionStatusProvider.tsx +++ b/src/providers/SubscriptionStatusProvider.tsx @@ -33,8 +33,11 @@ export function SubscriptionStatusProvider({ }; const isActive = () => { - if (subscriptionStatus?.subscription?.status === "active") { - return true; + if (subscriptionStatus?.subscriptions) { + // Check if any subscription is active + return subscriptionStatus.subscriptions.some( + (sub) => sub.subscription?.status === "active" + ); } return false; }; @@ -42,15 +45,20 @@ export function SubscriptionStatusProvider({ const getTier = () => { const tierPriceSet = getTierPriceSet(env, sandbox_mode); - if (subscriptionStatus?.items && subscriptionStatus.items.length > 0) { - // Iterate through tiers in order (earlier keys are higher tiers) - for (const [tierId, priceId] of Object.entries(tierPriceSet)) { - // Check if any subscription item matches this tier's price ID - const matchingItem = subscriptionStatus.items.find( - (item) => item.priceId === priceId - ); - if (matchingItem) { - return tierId; + if (subscriptionStatus?.subscriptions) { + // Iterate through all subscriptions + for (const { subscription, items } of subscriptionStatus.subscriptions) { + if (items && items.length > 0) { + // Iterate through tiers in order (earlier keys are higher tiers) + for (const [tierId, priceId] of Object.entries(tierPriceSet)) { + // Check if any subscription item matches this tier's price ID + const matchingItem = items.find( + (item) => item.priceId === priceId + ); + if (matchingItem) { + return tierId; + } + } } } } @@ -83,4 +91,4 @@ export function SubscriptionStatusProvider({ ); } -export default SubscriptionStatusProvider; +export default SubscriptionStatusProvider; \ No newline at end of file From 26a91cd5e149bc63a65dcdcc8dc823cdcff1612c Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 18:29:35 -0800 Subject: [PATCH 027/247] Add link --- .../private/routers/billing/hooks/handleSubscriptionCreated.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 9f2ee2ad..a51f825f 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -246,6 +246,7 @@ export async function handleSubscriptionCreated( personalUseOnly: false, users: numUsers, sites: numSites, + modifySubscriptionLink: `${config.getRawConfig().app.dashboard_url}/${customer.orgId}/settings/billing` }), { to: customer.email, From 508369a59d0fae97a2319880049ed1b674e14dca Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 4 Feb 2026 20:25:20 -0800 Subject: [PATCH 028/247] adjust language in form --- messages/en-US.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 81e6ae71..e9d8cc37 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2130,16 +2130,16 @@ "tiers": { "starter": { "title": "Starter", - "description": "Ideal for small teams and getting started. Includes core features and support." + "description": "Enterprise features, 25 users, 25 sites, and community support." }, "scale": { "title": "Scale", - "description": "For growing teams and production use. Higher limits and priority support." + "description": "Enterprise features, 50 users, 50 sites, and priority support." } }, "personalUseOnly": "Personal use only (free license — no checkout)", "buttons": { - "continueToCheckout": "Continue to checkout" + "continueToCheckout": "Continue to Checkout" }, "toasts": { "checkoutError": { From 7d4aed88190811578fa4c19b90acbc33d54d496d Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 4 Feb 2026 20:37:05 -0800 Subject: [PATCH 029/247] Add prod price ids --- server/lib/billing/licenses.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/billing/licenses.ts b/server/lib/billing/licenses.ts index a481527e..3fecb32b 100644 --- a/server/lib/billing/licenses.ts +++ b/server/lib/billing/licenses.ts @@ -9,8 +9,8 @@ export type LicensePriceSet = { export const licensePriceSet: LicensePriceSet = { // Free license matches the freeLimitSet - [LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN", - [LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl" + [LicenseId.SMALL_LICENSE]: "price_1SxKHiD3Ee2Ir7WmvtEh17A8", + [LicenseId.BIG_LICENSE]: "price_1SxKHiD3Ee2Ir7WmMUiP0H6Y" }; export const licensePriceSetSandbox: LicensePriceSet = { From 11408c26560dcaf2e7967d8ffc18d6a428595ff3 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 4 Feb 2026 21:16:53 -0800 Subject: [PATCH 030/247] add internal redirect --- src/app/[orgId]/layout.tsx | 3 ++ src/app/layout.tsx | 2 + src/app/page.tsx | 10 ++++- src/components/ApplyInternalRedirect.tsx | 24 ++++++++++++ src/components/RedirectToOrg.tsx | 24 ++++++++++++ src/components/StoreInternalRedirect.tsx | 27 +++++++++++++ src/lib/internalRedirect.ts | 48 ++++++++++++++++++++++++ 7 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/components/ApplyInternalRedirect.tsx create mode 100644 src/components/RedirectToOrg.tsx create mode 100644 src/components/StoreInternalRedirect.tsx create mode 100644 src/lib/internalRedirect.ts diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index 7d99fc0d..3d4b6054 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -18,6 +18,7 @@ import { build } from "@server/build"; import OrgPolicyResult from "@app/components/OrgPolicyResult"; import UserProvider from "@app/providers/UserProvider"; import { Layout } from "@app/components/Layout"; +import ApplyInternalRedirect from "@app/components/ApplyInternalRedirect"; export default async function OrgLayout(props: { children: React.ReactNode; @@ -70,6 +71,7 @@ export default async function OrgLayout(props: { } catch (e) {} return ( + + {props.children} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 203dd778..ed7635e3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -23,6 +23,7 @@ import Script from "next/script"; import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider"; import { TailwindIndicator } from "@app/components/TailwindIndicator"; import { ViewportHeightFix } from "@app/components/ViewportHeightFix"; +import StoreInternalRedirect from "@app/components/StoreInternalRedirect"; export const metadata: Metadata = { title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, @@ -79,6 +80,7 @@ export default async function RootLayout({ return ( + {build === "saas" && (