diff --git a/Dockerfile b/Dockerfile index 9af85358..5b316802 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,13 +42,11 @@ COPY package.json yarn.lock* .yarnrc.yml ./ COPY .yarn ./.yarn COPY ./packages/db ./packages/db COPY ./packages/schemas ./packages/schemas -COPY ./packages/crypto ./packages/crypto COPY ./packages/error ./packages/error COPY ./packages/shared ./packages/shared RUN yarn workspace @sourcebot/db install RUN yarn workspace @sourcebot/schemas install -RUN yarn workspace @sourcebot/crypto install RUN yarn workspace @sourcebot/error install RUN yarn workspace @sourcebot/shared install # ------------------------------------ @@ -95,7 +93,6 @@ COPY ./packages/web ./packages/web COPY --from=shared-libs-builder /app/node_modules ./node_modules COPY --from=shared-libs-builder /app/packages/db ./packages/db COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas -COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto COPY --from=shared-libs-builder /app/packages/error ./packages/error COPY --from=shared-libs-builder /app/packages/shared ./packages/shared @@ -135,7 +132,6 @@ COPY ./packages/backend ./packages/backend COPY --from=shared-libs-builder /app/node_modules ./node_modules COPY --from=shared-libs-builder /app/packages/db ./packages/db COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas -COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto COPY --from=shared-libs-builder /app/packages/error ./packages/error COPY --from=shared-libs-builder /app/packages/shared ./packages/shared RUN yarn workspace @sourcebot/backend install @@ -221,7 +217,6 @@ COPY --from=backend-builder /app/packages/backend ./packages/backend COPY --from=shared-libs-builder /app/node_modules ./node_modules COPY --from=shared-libs-builder /app/packages/db ./packages/db COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas -COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto COPY --from=shared-libs-builder /app/packages/error ./packages/error COPY --from=shared-libs-builder /app/packages/shared ./packages/shared diff --git a/package.json b/package.json index 7c726c7e..06b07c90 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio", "dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset", "dev:prisma:db:push": "yarn with-env yarn workspace @sourcebot/db prisma:db:push", - "build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build" + "build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/db,@sourcebot/shared}' run build" }, "devDependencies": { "concurrently": "^9.2.1", diff --git a/packages/backend/package.json b/packages/backend/package.json index 99b11588..d3105606 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -29,7 +29,6 @@ "@sentry/cli": "^2.42.2", "@sentry/node": "^9.3.0", "@sentry/profiling-node": "^9.3.0", - "@sourcebot/crypto": "workspace:*", "@sourcebot/db": "workspace:*", "@sourcebot/error": "workspace:*", "@sourcebot/schemas": "workspace:*", diff --git a/packages/backend/src/azuredevops.ts b/packages/backend/src/azuredevops.ts index 779ee82d..31b543bb 100644 --- a/packages/backend/src/azuredevops.ts +++ b/packages/backend/src/azuredevops.ts @@ -7,7 +7,7 @@ import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js"; import * as Sentry from "@sentry/node"; import * as azdev from "azure-devops-node-api"; import { GitRepository } from "azure-devops-node-api/interfaces/GitInterfaces.js"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; const logger = createLogger('azuredevops'); const AZUREDEVOPS_CLOUD_HOSTNAME = "dev.azure.com"; diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index c1069b61..3f0e18ff 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -11,7 +11,7 @@ import { import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi"; import { processPromiseResults } from "./connectionUtils.js"; import { throwIfAnyFailed } from "./connectionUtils.js"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; const logger = createLogger('bitbucket'); const BITBUCKET_CLOUD_GIT = 'https://bitbucket.org'; diff --git a/packages/backend/src/ee/githubAppManager.ts b/packages/backend/src/ee/githubAppManager.ts index dc6e1cde..0ee50acf 100644 --- a/packages/backend/src/ee/githubAppManager.ts +++ b/packages/backend/src/ee/githubAppManager.ts @@ -1,5 +1,5 @@ import { App } from "@octokit/app"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; import { PrismaClient } from "@sourcebot/db"; import { createLogger } from "@sourcebot/shared"; import { GitHubAppConfig } from "@sourcebot/schemas/v3/index.type"; diff --git a/packages/backend/src/gitea.ts b/packages/backend/src/gitea.ts index 53bf55bf..23f5c520 100644 --- a/packages/backend/src/gitea.ts +++ b/packages/backend/src/gitea.ts @@ -1,5 +1,5 @@ import * as Sentry from "@sentry/node"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; import { createLogger } from '@sourcebot/shared'; import { GiteaConnectionConfig } from '@sourcebot/schemas/v3/gitea.type'; import { env } from "@sourcebot/shared"; diff --git a/packages/backend/src/github.ts b/packages/backend/src/github.ts index e913a128..49ce1d37 100644 --- a/packages/backend/src/github.ts +++ b/packages/backend/src/github.ts @@ -1,6 +1,6 @@ import { Octokit } from "@octokit/rest"; import * as Sentry from "@sentry/node"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; import { createLogger } from "@sourcebot/shared"; import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; import { env, hasEntitlement } from "@sourcebot/shared"; diff --git a/packages/backend/src/gitlab.ts b/packages/backend/src/gitlab.ts index b7f2dc91..b461d59b 100644 --- a/packages/backend/src/gitlab.ts +++ b/packages/backend/src/gitlab.ts @@ -1,6 +1,6 @@ import { Gitlab, ProjectSchema } from "@gitbeaker/rest"; import * as Sentry from "@sentry/node"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; import { createLogger } from "@sourcebot/shared"; import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; import { env } from "@sourcebot/shared"; diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index dd1e7dfa..e85ce766 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -2,7 +2,7 @@ import { Logger } from "winston"; import { RepoAuthCredentials, RepoWithConnections } from "./types.js"; import path from 'path'; import { Repo } from "@sourcebot/db"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; import * as Sentry from "@sentry/node"; import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; import { GithubAppManager } from "./ee/githubAppManager.js"; diff --git a/packages/crypto/.gitignore b/packages/crypto/.gitignore deleted file mode 100644 index 3a8fe5ed..00000000 --- a/packages/crypto/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env.local \ No newline at end of file diff --git a/packages/crypto/package.json b/packages/crypto/package.json deleted file mode 100644 index d25e412f..00000000 --- a/packages/crypto/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@sourcebot/crypto", - "version": "0.1.0", - "main": "dist/index.js", - "private": true, - "scripts": { - "build": "tsc", - "postinstall": "yarn build" - }, - "dependencies": { - "@google-cloud/secret-manager": "^6.1.1", - "@sourcebot/db": "*", - "@sourcebot/schemas": "*", - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^22.7.5", - "typescript": "^5.7.3" - } -} diff --git a/packages/crypto/src/environment.ts b/packages/crypto/src/environment.ts deleted file mode 100644 index 8efe296d..00000000 --- a/packages/crypto/src/environment.ts +++ /dev/null @@ -1,13 +0,0 @@ -import dotenv from 'dotenv'; - -export const getEnv = (env: string | undefined, defaultValue?: string) => { - return env ?? defaultValue; -} - -dotenv.config({ - path: './.env.local', - override: true -}); - -// @note: You can use https://generate-random.org/encryption-key-generator to create a new 32 byte key -export const SOURCEBOT_ENCRYPTION_KEY = getEnv(process.env.SOURCEBOT_ENCRYPTION_KEY); \ No newline at end of file diff --git a/packages/crypto/src/tokenUtils.ts b/packages/crypto/src/tokenUtils.ts deleted file mode 100644 index abefa7ef..00000000 --- a/packages/crypto/src/tokenUtils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SecretManagerServiceClient } from "@google-cloud/secret-manager"; -import { Token } from "@sourcebot/schemas/v3/shared.type"; - -export const getTokenFromConfig = async (token: Token): Promise => { - if ('env' in token) { - const envToken = process.env[token.env]; - if (!envToken) { - throw new Error(`Environment variable ${token.env} not found.`); - } - - return envToken; - } else if ('googleCloudSecret' in token) { - try { - const client = new SecretManagerServiceClient(); - const [response] = await client.accessSecretVersion({ - name: token.googleCloudSecret, - }); - - if (!response.payload?.data) { - throw new Error(`Secret ${token.googleCloudSecret} not found.`); - } - - return response.payload.data.toString(); - } catch (error) { - throw new Error(`Failed to access Google Cloud secret ${token.googleCloudSecret}: ${error instanceof Error ? error.message : String(error)}`); - } - } else { - throw new Error('Invalid token configuration'); - } -}; \ No newline at end of file diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json deleted file mode 100644 index b364feca..00000000 --- a/packages/crypto/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "lib": ["ES2023"], - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "moduleResolution": "Node16", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "isolatedModules": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] - } - \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json index a921526e..e8e6b3fa 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -9,9 +9,9 @@ "postinstall": "yarn build" }, "dependencies": { + "@google-cloud/secret-manager": "^6.1.1", "@logtail/node": "^0.5.2", "@logtail/winston": "^0.5.2", - "@sourcebot/crypto": "workspace:*", "@sourcebot/db": "workspace:*", "@sourcebot/schemas": "workspace:*", "@t3-oss/env-core": "^0.12.0", diff --git a/packages/crypto/src/index.ts b/packages/shared/src/crypto.ts similarity index 62% rename from packages/crypto/src/index.ts rename to packages/shared/src/crypto.ts index 8f6ca211..0d446129 100644 --- a/packages/crypto/src/index.ts +++ b/packages/shared/src/crypto.ts @@ -1,6 +1,8 @@ import crypto from 'crypto'; import fs from 'fs'; -import { SOURCEBOT_ENCRYPTION_KEY } from './environment'; +import { env } from './env.js'; +import { Token } from '@sourcebot/schemas/v3/shared.type'; +import { SecretManagerServiceClient } from "@google-cloud/secret-manager"; const algorithm = 'aes-256-cbc'; const ivLength = 16; // 16 bytes for CBC @@ -12,11 +14,7 @@ const generateIV = (): Buffer => { }; export function encrypt(text: string): { iv: string; encryptedData: string } { - if (!SOURCEBOT_ENCRYPTION_KEY) { - throw new Error('Encryption key is not set'); - } - - const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii'); + const encryptionKey = Buffer.from(env.SOURCEBOT_ENCRYPTION_KEY, 'ascii'); const iv = generateIV(); const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv); @@ -28,18 +26,10 @@ export function encrypt(text: string): { iv: string; encryptedData: string } { } export function hashSecret(text: string): string { - if (!SOURCEBOT_ENCRYPTION_KEY) { - throw new Error('Encryption key is not set'); - } - - return crypto.createHmac('sha256', SOURCEBOT_ENCRYPTION_KEY).update(text).digest('hex'); + return crypto.createHmac('sha256', env.SOURCEBOT_ENCRYPTION_KEY).update(text).digest('hex'); } export function generateApiKey(): { key: string; hash: string } { - if (!SOURCEBOT_ENCRYPTION_KEY) { - throw new Error('Encryption key is not set'); - } - const secret = crypto.randomBytes(32).toString('hex'); const hash = hashSecret(secret); @@ -50,11 +40,7 @@ export function generateApiKey(): { key: string; hash: string } { } export function decrypt(iv: string, encryptedText: string): string { - if (!SOURCEBOT_ENCRYPTION_KEY) { - throw new Error('Encryption key is not set'); - } - - const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii'); + const encryptionKey = Buffer.from(env.SOURCEBOT_ENCRYPTION_KEY, 'ascii'); const ivBuffer = Buffer.from(iv, 'hex'); const encryptedBuffer = Buffer.from(encryptedText, 'hex'); @@ -92,4 +78,30 @@ export function verifySignature(data: string, signature: string, publicKeyPath: } } -export { getTokenFromConfig } from './tokenUtils.js'; \ No newline at end of file +export const getTokenFromConfig = async (token: Token): Promise => { + if ('env' in token) { + const envToken = process.env[token.env]; + if (!envToken) { + throw new Error(`Environment variable ${token.env} not found.`); + } + + return envToken; + } else if ('googleCloudSecret' in token) { + try { + const client = new SecretManagerServiceClient(); + const [response] = await client.accessSecretVersion({ + name: token.googleCloudSecret, + }); + + if (!response.payload?.data) { + throw new Error(`Secret ${token.googleCloudSecret} not found.`); + } + + return response.payload.data.toString(); + } catch (error) { + throw new Error(`Failed to access Google Cloud secret ${token.googleCloudSecret}: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + throw new Error('Invalid token configuration'); + } +}; \ No newline at end of file diff --git a/packages/shared/src/entitlements.ts b/packages/shared/src/entitlements.ts index 184a2cb6..dfb829ee 100644 --- a/packages/shared/src/entitlements.ts +++ b/packages/shared/src/entitlements.ts @@ -1,9 +1,9 @@ import { base64Decode } from "./utils.js"; import { z } from "zod"; import { createLogger } from "./logger.js"; -import { verifySignature } from "@sourcebot/crypto"; import { env } from "./env.js"; import { SOURCEBOT_SUPPORT_EMAIL, SOURCEBOT_UNLIMITED_SEATS } from "./constants.js"; +import { verifySignature } from "./crypto.js"; const logger = createLogger('entitlements'); diff --git a/packages/shared/src/env.ts b/packages/shared/src/env.ts index 604315b5..9b15cad0 100644 --- a/packages/shared/src/env.ts +++ b/packages/shared/src/env.ts @@ -2,7 +2,7 @@ import { createEnv } from "@t3-oss/env-core"; import { z } from "zod"; import { SOURCEBOT_CLOUD_ENVIRONMENT } from "./constants.js"; import { SourcebotConfig } from "@sourcebot/schemas/v3/index.type"; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "./crypto.js"; import { loadConfig } from "./utils.js"; // Booleans are specified as 'true' or 'false' strings. diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index 585f4e53..85086eda 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -33,4 +33,12 @@ export { } from "./logger.js"; export type { Logger, -} from "./logger.js"; \ No newline at end of file +} from "./logger.js"; +export { + getTokenFromConfig, + encrypt, + decrypt, + hashSecret, + generateApiKey, + verifySignature, +} from "./crypto.js"; \ No newline at end of file diff --git a/packages/web/package.json b/packages/web/package.json index 62ede328..0580c931 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -90,7 +90,6 @@ "@sentry/nextjs": "^9", "@shopify/lang-jsonc": "^1.0.0", "@sourcebot/codemirror-lang-tcl": "^1.0.12", - "@sourcebot/crypto": "workspace:*", "@sourcebot/db": "workspace:*", "@sourcebot/error": "workspace:*", "@sourcebot/schemas": "workspace:*", diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 112d7c28..c3568be9 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -9,7 +9,7 @@ import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils"; import { prisma } from "@/prisma"; import { render } from "@react-email/components"; import * as Sentry from '@sentry/nextjs'; -import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto"; +import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/shared"; import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db"; import { createLogger } from "@sourcebot/shared"; import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; diff --git a/packages/web/src/features/chat/actions.ts b/packages/web/src/features/chat/actions.ts index 19ec9abd..577812e5 100644 --- a/packages/web/src/features/chat/actions.ts +++ b/packages/web/src/features/chat/actions.ts @@ -20,7 +20,7 @@ import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider"; import { createXai } from '@ai-sdk/xai'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; -import { getTokenFromConfig } from "@sourcebot/crypto"; +import { getTokenFromConfig } from "@sourcebot/shared"; import { ChatVisibility, OrgRole, Prisma } from "@sourcebot/db"; import { createLogger } from "@sourcebot/logger"; import { LanguageModel } from "@sourcebot/schemas/v3/languageModel.type"; diff --git a/packages/web/src/withAuthV2.test.ts b/packages/web/src/withAuthV2.test.ts index 5a1dd343..0463a152 100644 --- a/packages/web/src/withAuthV2.test.ts +++ b/packages/web/src/withAuthV2.test.ts @@ -38,16 +38,13 @@ vi.mock('@/prisma', async () => { }; }); -vi.mock('@sourcebot/crypto', () => ({ - hashSecret: vi.fn((secret: string) => secret), -})); - vi.mock('server-only', () => ({ default: vi.fn(), })); vi.mock('@sourcebot/shared', () => ({ hasEntitlement: mocks.hasEntitlement, + hashSecret: vi.fn((secret: string) => secret), })); // Test utility to set the mock session diff --git a/packages/web/src/withAuthV2.ts b/packages/web/src/withAuthV2.ts index c4bf8095..65ebb054 100644 --- a/packages/web/src/withAuthV2.ts +++ b/packages/web/src/withAuthV2.ts @@ -1,5 +1,5 @@ import { prisma as __unsafePrisma, userScopedPrismaClientExtension } from "@/prisma"; -import { hashSecret } from "@sourcebot/crypto"; +import { hashSecret } from "@sourcebot/shared"; import { ApiKey, Org, OrgRole, PrismaClient, User } from "@sourcebot/db"; import { headers } from "next/headers"; import { auth } from "./auth"; diff --git a/yarn.lock b/yarn.lock index fd3f54f0..6b26f1ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7893,7 +7893,6 @@ __metadata: "@sentry/cli": "npm:^2.42.2" "@sentry/node": "npm:^9.3.0" "@sentry/profiling-node": "npm:^9.3.0" - "@sourcebot/crypto": "workspace:*" "@sourcebot/db": "workspace:*" "@sourcebot/error": "workspace:*" "@sourcebot/schemas": "workspace:*" @@ -7945,20 +7944,7 @@ __metadata: languageName: node linkType: hard -"@sourcebot/crypto@workspace:*, @sourcebot/crypto@workspace:packages/crypto": - version: 0.0.0-use.local - resolution: "@sourcebot/crypto@workspace:packages/crypto" - dependencies: - "@google-cloud/secret-manager": "npm:^6.1.1" - "@sourcebot/db": "npm:*" - "@sourcebot/schemas": "npm:*" - "@types/node": "npm:^22.7.5" - dotenv: "npm:^16.4.5" - typescript: "npm:^5.7.3" - languageName: unknown - linkType: soft - -"@sourcebot/db@npm:*, @sourcebot/db@workspace:*, @sourcebot/db@workspace:packages/db": +"@sourcebot/db@workspace:*, @sourcebot/db@workspace:packages/db": version: 0.0.0-use.local resolution: "@sourcebot/db@workspace:packages/db" dependencies: @@ -8001,7 +7987,7 @@ __metadata: languageName: unknown linkType: soft -"@sourcebot/schemas@npm:*, @sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:packages/schemas": +"@sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:packages/schemas": version: 0.0.0-use.local resolution: "@sourcebot/schemas@workspace:packages/schemas" dependencies: @@ -8018,9 +8004,9 @@ __metadata: version: 0.0.0-use.local resolution: "@sourcebot/shared@workspace:packages/shared" dependencies: + "@google-cloud/secret-manager": "npm:^6.1.1" "@logtail/node": "npm:^0.5.2" "@logtail/winston": "npm:^0.5.2" - "@sourcebot/crypto": "workspace:*" "@sourcebot/db": "workspace:*" "@sourcebot/schemas": "workspace:*" "@t3-oss/env-core": "npm:^0.12.0" @@ -8120,7 +8106,6 @@ __metadata: "@sentry/nextjs": "npm:^9" "@shopify/lang-jsonc": "npm:^1.0.0" "@sourcebot/codemirror-lang-tcl": "npm:^1.0.12" - "@sourcebot/crypto": "workspace:*" "@sourcebot/db": "workspace:*" "@sourcebot/error": "workspace:*" "@sourcebot/schemas": "workspace:*"