diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e9dd28..0c0660bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added copy button for filenames. [#328](https://github.com/sourcebot-dev/sourcebot/pull/328) - Added development docker compose file. [#328](https://github.com/sourcebot-dev/sourcebot/pull/328) +- Added GCP IAP JIT provisioning. [#330](https://github.com/sourcebot-dev/sourcebot/pull/330) ### Fixed - Fixed issue with the symbol hover popover clipping at the top of the page. [#326](https://github.com/sourcebot-dev/sourcebot/pull/326) diff --git a/docs/docs/configuration/auth/overview.mdx b/docs/docs/configuration/auth/overview.mdx index c89299ba..19343c8d 100644 --- a/docs/docs/configuration/auth/overview.mdx +++ b/docs/docs/configuration/auth/overview.mdx @@ -80,6 +80,16 @@ Optional environment variables: - `AUTH_EE_GOOGLE_CLIENT_ID` - `AUTH_EE_GOOGLE_CLIENT_SECRET` +### GCP IAP +--- + +Custom provider built to enable automatic Sourcebot account registration/login when using GCP IAP. + +**Required environment variables** +- `AUTH_EE_GCP_IAP_ENABLED` +- `AUTH_EE_GCP_IAP_AUDIENCE` + - This can be found by selecting the ⋮ icon next to the IAP-enabled backend service and pressing `Get JWT audience code` + ### Okta --- diff --git a/docs/docs/configuration/environment-variables.mdx b/docs/docs/configuration/environment-variables.mdx index 199a214d..f59b3fe0 100644 --- a/docs/docs/configuration/environment-variables.mdx +++ b/docs/docs/configuration/environment-variables.mdx @@ -51,6 +51,8 @@ The following environment variables allow you to configure your Sourcebot deploy | `AUTH_EE_OKTA_CLIENT_ID` | `-` |

The client ID for Okta SSO authentication.

| | `AUTH_EE_OKTA_CLIENT_SECRET` | `-` |

The client secret for Okta SSO authentication.

| | `AUTH_EE_OKTA_ISSUER` | `-` |

The issuer URL for Okta SSO authentication.

| +| `AUTH_EE_GCP_IAP_ENABLED` | `false` |

When enabled, allows Sourcebot to automatically register/login from a successful GCP IAP redirect

| +| `AUTH_EE_GCP_IAP_AUDIENCE` | - |

The GCP IAP audience to use when verifying JWT tokens. Must be set to enable GCP IAP JIT provisioning

| ### Review Agent Environment Variables diff --git a/packages/web/package.json b/packages/web/package.json index 986e18dc..4ce59550 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -114,6 +114,7 @@ "embla-carousel-react": "^8.3.0", "escape-string-regexp": "^5.0.0", "fuse.js": "^7.0.0", + "google-auth-library": "^9.15.1", "graphql": "^16.9.0", "http-status-codes": "^2.3.0", "input-otp": "^1.4.2", diff --git a/packages/web/src/app/[domain]/components/gcpIapAuth.tsx b/packages/web/src/app/[domain]/components/gcpIapAuth.tsx new file mode 100644 index 00000000..292161d6 --- /dev/null +++ b/packages/web/src/app/[domain]/components/gcpIapAuth.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { signIn } from "next-auth/react"; +import { useEffect } from "react"; + +interface GcpIapAuthProps { + callbackUrl?: string; +} + +export const GcpIapAuth = ({ callbackUrl }: GcpIapAuthProps) => { + useEffect(() => { + signIn("gcp-iap", { + redirectTo: callbackUrl ?? "/" + }).catch((error) => { + console.error("Error signing in with GCP IAP:", error); + }); + }, [callbackUrl]); + + return ( +
+
+

Signing in with Google Cloud IAP...

+
+
+ ); +}; \ No newline at end of file diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx index 8b1122a3..1bb708d8 100644 --- a/packages/web/src/app/[domain]/layout.tsx +++ b/packages/web/src/app/[domain]/layout.tsx @@ -17,6 +17,7 @@ import { PendingApprovalCard } from "./components/pendingApproval"; import { hasEntitlement } from "@/features/entitlements/server"; import { getPublicAccessStatus } from "@/ee/features/publicAccess/publicAccess"; import { env } from "@/env.mjs"; +import { GcpIapAuth } from "./components/gcpIapAuth"; interface LayoutProps { children: React.ReactNode, @@ -37,7 +38,12 @@ export default async function Layout({ if (!publicAccessEnabled) { const session = await auth(); if (!session) { - redirect('/login'); + const ssoEntitlement = await hasEntitlement("sso"); + if (ssoEntitlement && env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) { + return ; + } else { + redirect('/login'); + } } const membership = await prisma.userToOrg.findUnique({ diff --git a/packages/web/src/auth.ts b/packages/web/src/auth.ts index f4e0aa82..55fb3891 100644 --- a/packages/web/src/auth.ts +++ b/packages/web/src/auth.ts @@ -5,21 +5,17 @@ import EmailProvider from "next-auth/providers/nodemailer"; import { PrismaAdapter } from "@auth/prisma-adapter" import { prisma } from "@/prisma"; import { env } from "@/env.mjs"; -import { OrgRole, User } from '@sourcebot/db'; +import { User } from '@sourcebot/db'; import 'next-auth/jwt'; import type { Provider } from "next-auth/providers"; import { verifyCredentialsRequestSchema } from './lib/schemas'; import { createTransport } from 'nodemailer'; import { render } from '@react-email/render'; import MagicLinkEmail from './emails/magicLinkEmail'; -import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID } from './lib/constants'; import bcrypt from 'bcryptjs'; -import { createAccountRequest } from './actions'; -import { getSSOProviders, handleJITProvisioning } from '@/ee/sso/sso'; +import { getSSOProviders } from '@/ee/sso/sso'; import { hasEntitlement } from '@/features/entitlements/server'; -import { isServiceError } from './lib/utils'; -import { ServiceErrorException } from './lib/serviceError'; -import { createLogger } from "@sourcebot/logger"; +import { onCreateUser } from '@/lib/authUtils'; export const runtime = 'nodejs'; @@ -37,8 +33,6 @@ declare module 'next-auth/jwt' { } } -const logger = createLogger('web-auth'); - export const getProviders = () => { const providers: Provider[] = []; @@ -134,91 +128,6 @@ export const getProviders = () => { return providers; } -const onCreateUser = async ({ user }: { user: AuthJsUser }) => { - // In single-tenant mode, we assign the first user to sign - // up as the owner of the default org. - if ( - env.SOURCEBOT_TENANCY_MODE === 'single' - ) { - const defaultOrg = await prisma.org.findUnique({ - where: { - id: SINGLE_TENANT_ORG_ID, - }, - include: { - members: { - where: { - role: { - not: OrgRole.GUEST, - } - } - }, - } - }); - - if (!defaultOrg) { - throw new Error("Default org not found on single tenant user creation"); - } - - // We can't use the getOrgMembers action here because we're not authed yet - const members = await prisma.userToOrg.findMany({ - where: { - orgId: SINGLE_TENANT_ORG_ID, - role: { - not: OrgRole.GUEST, - } - }, - }); - - // Only the first user to sign up will be an owner of the default org. - const isFirstUser = members.length === 0; - if (isFirstUser) { - await prisma.$transaction(async (tx) => { - await tx.org.update({ - where: { - id: SINGLE_TENANT_ORG_ID, - }, - data: { - members: { - create: { - role: OrgRole.OWNER, - user: { - connect: { - id: user.id, - } - } - } - } - } - }); - - await tx.user.update({ - where: { - id: user.id, - }, - data: { - pendingApproval: false, - } - }); - }); - } else { - // TODO(auth): handle multi tenant case - if (env.AUTH_EE_ENABLE_JIT_PROVISIONING === 'true' && hasEntitlement("sso")) { - const res = await handleJITProvisioning(user.id!, SINGLE_TENANT_ORG_DOMAIN); - if (isServiceError(res)) { - logger.error(`Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`); - throw new ServiceErrorException(res); - } - } else { - const res = await createAccountRequest(user.id!, SINGLE_TENANT_ORG_DOMAIN); - if (isServiceError(res)) { - logger.error(`Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`); - throw new ServiceErrorException(res); - } - } - } - } -} - export const { handlers, signIn, signOut, auth } = NextAuth({ secret: env.AUTH_SECRET, adapter: PrismaAdapter(prisma), diff --git a/packages/web/src/ee/sso/sso.tsx b/packages/web/src/ee/sso/sso.tsx index c3d80333..bb1ec7b9 100644 --- a/packages/web/src/ee/sso/sso.tsx +++ b/packages/web/src/ee/sso/sso.tsx @@ -12,7 +12,14 @@ import { OrgRole } from "@sourcebot/db"; import { getSeats, SOURCEBOT_UNLIMITED_SEATS } from "@/features/entitlements/server"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "@/lib/errorCodes"; +import { OAuth2Client } from "google-auth-library"; import { sew } from "@/actions"; +import Credentials from "next-auth/providers/credentials"; +import type { User as AuthJsUser } from "next-auth"; +import { onCreateUser } from "@/lib/authUtils"; +import { createLogger } from "@sourcebot/logger"; + +const logger = createLogger('web-sso'); export const getSSOProviders = (): Provider[] => { const providers: Provider[] = []; @@ -88,6 +95,82 @@ export const getSSOProviders = (): Provider[] => { })); } + if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) { + providers.push(Credentials({ + id: "gcp-iap", + name: "Google Cloud IAP", + credentials: {}, + authorize: async (credentials, req) => { + try { + const iapAssertion = req.headers?.get("x-goog-iap-jwt-assertion"); + if (!iapAssertion || typeof iapAssertion !== "string") { + logger.warn("No IAP assertion found in headers"); + return null; + } + + const oauth2Client = new OAuth2Client(); + + const { pubkeys } = await oauth2Client.getIapPublicKeys(); + const ticket = await oauth2Client.verifySignedJwtWithCertsAsync( + iapAssertion, + pubkeys, + env.AUTH_EE_GCP_IAP_AUDIENCE, + ['https://cloud.google.com/iap'] + ); + + const payload = ticket.getPayload(); + if (!payload) { + logger.warn("Invalid IAP token payload"); + return null; + } + + const email = payload.email; + const name = payload.name || payload.email; + const image = payload.picture; + + if (!email) { + logger.warn("Missing email in IAP token"); + return null; + } + + const existingUser = await prisma.user.findUnique({ + where: { email } + }); + + if (!existingUser) { + const newUser = await prisma.user.create({ + data: { + email, + name, + image, + } + }); + + const authJsUser: AuthJsUser = { + id: newUser.id, + email: newUser.email, + name: newUser.name, + image: newUser.image, + }; + + await onCreateUser({ user: authJsUser }); + return authJsUser; + } else { + return { + id: existingUser.id, + email: existingUser.email, + name: existingUser.name, + image: existingUser.image, + }; + } + } catch (error) { + logger.error("Error verifying IAP token:", error); + return null; + } + }, + })); + } + return providers; } @@ -129,7 +212,7 @@ export const handleJITProvisioning = async (userId: string, domain: string): Pro }); if (userToOrg) { - console.warn(`JIT provisioning skipped for user ${userId} since they're already a member of org ${domain}`); + logger.warn(`JIT provisioning skipped for user ${userId} since they're already a member of org ${domain}`); return true; } diff --git a/packages/web/src/env.mjs b/packages/web/src/env.mjs index 60996de9..bd4af8c8 100644 --- a/packages/web/src/env.mjs +++ b/packages/web/src/env.mjs @@ -50,6 +50,9 @@ export const env = createEnv({ AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET: z.string().optional(), AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER: z.string().optional(), + AUTH_EE_GCP_IAP_ENABLED: booleanSchema.default('false'), + AUTH_EE_GCP_IAP_AUDIENCE: z.string().optional(), + DATA_CACHE_DIR: z.string(), // Email diff --git a/packages/web/src/lib/authUtils.ts b/packages/web/src/lib/authUtils.ts new file mode 100644 index 00000000..6b7df3eb --- /dev/null +++ b/packages/web/src/lib/authUtils.ts @@ -0,0 +1,88 @@ +import type { User as AuthJsUser } from "next-auth"; +import { env } from "@/env.mjs"; +import { prisma } from "@/prisma"; +import { OrgRole } from "@sourcebot/db"; +import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { hasEntitlement } from "@/features/entitlements/server"; +import { isServiceError } from "@/lib/utils"; +import { ServiceErrorException } from "@/lib/serviceError"; +import { createAccountRequest } from "@/actions"; +import { handleJITProvisioning } from "@/ee/sso/sso"; +import { createLogger } from "@sourcebot/logger"; + +const logger = createLogger('web-auth-utils'); + +export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { + // In single-tenant mode, we assign the first user to sign + // up as the owner of the default org. + if ( + env.SOURCEBOT_TENANCY_MODE === 'single' + ) { + const defaultOrg = await prisma.org.findUnique({ + where: { + id: SINGLE_TENANT_ORG_ID, + }, + include: { + members: { + where: { + role: { + not: OrgRole.GUEST, + } + } + }, + } + }); + + if (!defaultOrg) { + throw new Error("Default org not found on single tenant user creation"); + } + + // Only the first user to sign up will be an owner of the default org. + const isFirstUser = defaultOrg.members.length === 0; + if (isFirstUser) { + await prisma.$transaction(async (tx) => { + await tx.org.update({ + where: { + id: SINGLE_TENANT_ORG_ID, + }, + data: { + members: { + create: { + role: OrgRole.OWNER, + user: { + connect: { + id: user.id, + } + } + } + } + } + }); + + await tx.user.update({ + where: { + id: user.id, + }, + data: { + pendingApproval: false, + } + }); + }); + } else { + // TODO(auth): handle multi tenant case + if (env.AUTH_EE_ENABLE_JIT_PROVISIONING === 'true' && hasEntitlement("sso")) { + const res = await handleJITProvisioning(user.id!, SINGLE_TENANT_ORG_DOMAIN); + if (isServiceError(res)) { + logger.error(`Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`); + throw new ServiceErrorException(res); + } + } else { + const res = await createAccountRequest(user.id!, SINGLE_TENANT_ORG_DOMAIN); + if (isServiceError(res)) { + logger.error(`Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`); + throw new ServiceErrorException(res); + } + } + } + } +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 80a79b2a..b379f320 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6002,6 +6002,7 @@ __metadata: eslint-plugin-react: "npm:^7.35.0" eslint-plugin-react-hooks: "npm:^4.6.2" fuse.js: "npm:^7.0.0" + google-auth-library: "npm:^9.15.1" graphql: "npm:^16.9.0" http-status-codes: "npm:^2.3.0" input-otp: "npm:^1.4.2" @@ -7480,7 +7481,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.3.1": +"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf @@ -7517,6 +7518,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.3.0 + resolution: "bignumber.js@npm:9.3.0" + checksum: 10c0/f54a79cd6fc98552ac0510c1cd9381650870ae443bdb20ba9b98e3548188d941506ac3c22a9f9c69b2cc60da9be5700e87d3f54d2825310a8b2ae999dfd6d99d + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" @@ -7628,6 +7636,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:^1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e + languageName: node + linkType: hard + "buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" @@ -8821,6 +8836,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -9857,6 +9881,13 @@ __metadata: languageName: node linkType: hard +"extend@npm:^3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: 10c0/73bf6e27406e80aa3e85b0d1c4fd987261e628064e170ca781125c0b635a3dabad5e05adbf07595ea0cf1e6c5396cacb214af933da7cbaf24fe75ff14818e8f9 + languageName: node + linkType: hard + "fast-content-type-parse@npm:^2.0.0": version: 2.0.1 resolution: "fast-content-type-parse@npm:2.0.1" @@ -10199,6 +10230,30 @@ __metadata: languageName: node linkType: hard +"gaxios@npm:^6.0.0, gaxios@npm:^6.1.1": + version: 6.7.1 + resolution: "gaxios@npm:6.7.1" + dependencies: + extend: "npm:^3.0.2" + https-proxy-agent: "npm:^7.0.1" + is-stream: "npm:^2.0.0" + node-fetch: "npm:^2.6.9" + uuid: "npm:^9.0.1" + checksum: 10c0/53e92088470661c5bc493a1de29d05aff58b1f0009ec5e7903f730f892c3642a93e264e61904383741ccbab1ce6e519f12a985bba91e13527678b32ee6d7d3fd + languageName: node + linkType: hard + +"gcp-metadata@npm:^6.1.0": + version: 6.1.1 + resolution: "gcp-metadata@npm:6.1.1" + dependencies: + gaxios: "npm:^6.1.1" + google-logging-utils: "npm:^0.0.2" + json-bigint: "npm:^1.0.0" + checksum: 10c0/71f6ad4800aa622c246ceec3955014c0c78cdcfe025971f9558b9379f4019f5e65772763428ee8c3244fa81b8631977316eaa71a823493f82e5c44d7259ffac8 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -10440,6 +10495,27 @@ __metadata: languageName: node linkType: hard +"google-auth-library@npm:^9.15.1": + version: 9.15.1 + resolution: "google-auth-library@npm:9.15.1" + dependencies: + base64-js: "npm:^1.3.0" + ecdsa-sig-formatter: "npm:^1.0.11" + gaxios: "npm:^6.1.1" + gcp-metadata: "npm:^6.1.0" + gtoken: "npm:^7.0.0" + jws: "npm:^4.0.0" + checksum: 10c0/6eef36d9a9cb7decd11e920ee892579261c6390104b3b24d3e0f3889096673189fe2ed0ee43fd563710e2560de98e63ad5aa4967b91e7f4e69074a422d5f7b65 + languageName: node + linkType: hard + +"google-logging-utils@npm:^0.0.2": + version: 0.0.2 + resolution: "google-logging-utils@npm:0.0.2" + checksum: 10c0/9a4bbd470dd101c77405e450fffca8592d1d7114f245a121288d04a957aca08c9dea2dd1a871effe71e41540d1bb0494731a0b0f6fea4358e77f06645e4268c1 + languageName: node + linkType: hard + "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -10483,6 +10559,16 @@ __metadata: languageName: node linkType: hard +"gtoken@npm:^7.0.0": + version: 7.1.0 + resolution: "gtoken@npm:7.1.0" + dependencies: + gaxios: "npm:^6.0.0" + jws: "npm:^4.0.0" + checksum: 10c0/0a3dcacb1a3c4578abe1ee01c7d0bf20bffe8ded3ee73fc58885d53c00f6eb43b4e1372ff179f0da3ed5cfebd5b7c6ab8ae2776f1787e90d943691b4fe57c716 + languageName: node + linkType: hard + "has-bigints@npm:^1.0.2": version: 1.1.0 resolution: "has-bigints@npm:1.1.0" @@ -11323,6 +11409,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: "npm:^9.0.0" + checksum: 10c0/e3f34e43be3284b573ea150a3890c92f06d54d8ded72894556357946aeed9877fd795f62f37fe16509af189fd314ab1104d0fd0f163746ad231b9f378f5b33f4 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -11431,6 +11526,27 @@ __metadata: languageName: node linkType: hard +"jwa@npm:^2.0.0": + version: 2.0.1 + resolution: "jwa@npm:2.0.1" + dependencies: + buffer-equal-constant-time: "npm:^1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ab3ebc6598e10dc11419d4ed675c9ca714a387481466b10e8a6f3f65d8d9c9237e2826f2505280a739cf4cbcf511cb288eeec22b5c9c63286fc5a2e4f97e78cf + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/f1ca77ea5451e8dc5ee219cb7053b8a4f1254a79cb22417a2e1043c1eb8a569ae118c68f24d72a589e8a3dd1824697f47d6bd4fb4bebb93a3bdf53545e721661 + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -12404,7 +12520,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": +"node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -14359,7 +14475,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -16025,7 +16141,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: