Add configurable storage backends (File, S3, GCS, Azure) (#225)

This commit is contained in:
Ylber Gashi
2025-10-11 01:59:30 +02:00
committed by GitHub
parent c64c5a6406
commit 2f8438549e
33 changed files with 1497 additions and 227 deletions

View File

@@ -2,10 +2,8 @@ name: Hosting - Publish Helm chart to static.colanode.com
on:
push:
branches: [main]
paths:
- 'hosting/kubernetes/chart/**'
- '.github/workflows/helm-chart-publish.yml'
tags:
- 'v*'
workflow_dispatch:
jobs:
@@ -22,11 +20,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- uses: azure/setup-helm@v3
with: { version: v3.14.3 }
- run: helm dependency update "$CHART_DIR"
- name: Update Chart.yaml appVersion
run: |
sed -i "s/^appVersion:.*/appVersion: '${{ env.VERSION }}'/" "$CHART_DIR/Chart.yaml"
- name: Configure AWS CLI
run: aws configure set default.s3.addressing_style path

View File

@@ -58,7 +58,7 @@ If you prefer to host your own Colanode server, check out the [`hosting/`](hosti
- **Postgres** with the **pgvector** extension.
- **Redis** (any Redis-compatible service will work, e.g., Valkey).
- **S3-compatible storage** (supporting basic file operations: PUT, GET, DELETE).
- **Storage backend** for user files. Colanode defaults to local filesystem storage, but you can switch to **S3-compatible**, **Google Cloud Storage**, or **Azure Blob Storage** backends by setting `STORAGE_TYPE`.
- **Colanode server API**, provided as a Docker image.
All required environment variables for the Colanode server can be found in the [`hosting/docker/docker-compose.yaml`](hosting/docker/docker-compose.yaml) file or [`hosting/kubernetes/README.md`](hosting/kubernetes/README.md) for Kubernetes deployments.
@@ -93,13 +93,20 @@ To run Colanode locally in development mode:
npm run dev
```
To spin up the local dependencies (Postgres, Redis, Minio & Mail server) with Docker Compose, run this from
To spin up the local dependencies (Postgres, Redis, and Mail server) with Docker Compose—using filesystem storage
by default—run this from
the project root:
```bash
docker compose -f hosting/docker/docker-compose.yaml up -d
```
When you prefer an S3-compatible backend locally, enable the optional MinIO service with the `s3` profile:
```bash
docker compose -f hosting/docker/docker-compose.yaml --profile s3 up -d
```
The compose file includes a `server` service. When you want to run the API locally with `npm run dev`, comment
out (or override) that service so only the supporting services are started.

View File

@@ -66,14 +66,43 @@ REDIS_URL=redis://:your_valkey_password@localhost:6379/0
# REDIS_EVENTS_CHANNEL=events
# ───────────────────────────────────────────────────────────────
# S3 Storage Configuration (MinIO)
# Storage Configuration
# Supported types: 's3', 'file', 'gcs', 'azure'
# ───────────────────────────────────────────────────────────────
STORAGE_S3_ENDPOINT=http://localhost:9000
STORAGE_S3_ACCESS_KEY=minioadmin
STORAGE_S3_SECRET_KEY=your_minio_password
STORAGE_S3_BUCKET=colanode
STORAGE_S3_REGION=us-east-1
STORAGE_S3_FORCE_PATH_STYLE=true
STORAGE_TYPE=file
# ───────────────────────────────────────────────────────────────
# S3 Storage Configuration (MinIO, AWS S3, or S3-compatible)
# Required when STORAGE_TYPE=s3
# ───────────────────────────────────────────────────────────────
# STORAGE_S3_ENDPOINT=http://localhost:9000
# STORAGE_S3_ACCESS_KEY=minioadmin
# STORAGE_S3_SECRET_KEY=your_minio_password
# STORAGE_S3_BUCKET=colanode
# STORAGE_S3_REGION=us-east-1
# STORAGE_S3_FORCE_PATH_STYLE=true
# ───────────────────────────────────────────────────────────────
# File Storage Configuration (Local Disk)
# Required when STORAGE_TYPE=file
# ───────────────────────────────────────────────────────────────
STORAGE_FILE_DIRECTORY=./colanode
# ───────────────────────────────────────────────────────────────
# Google Cloud Storage (GCS) Configuration
# Required when STORAGE_TYPE=gcs
# ───────────────────────────────────────────────────────────────
# STORAGE_GCS_BUCKET=your-gcs-bucket-name
# STORAGE_GCS_PROJECT_ID=your-gcp-project-id
# STORAGE_GCS_CREDENTIALS=/path/to/service-account-key.json
# ───────────────────────────────────────────────────────────────
# Azure Blob Storage Configuration
# Required when STORAGE_TYPE=azure
# ───────────────────────────────────────────────────────────────
# STORAGE_AZURE_ACCOUNT=your-storage-account-name
# STORAGE_AZURE_ACCOUNT_KEY=your-storage-account-key
# STORAGE_AZURE_CONTAINER_NAME=colanode
# ───────────────────────────────────────────────────────────────
# SMTP Configuration

View File

@@ -32,12 +32,16 @@
"@colanode/crdt": "*",
"@fastify/cors": "^11.1.0",
"@fastify/websocket": "^11.2.0",
"@google-cloud/storage": "^7.15.0",
"@langchain/core": "^0.3.78",
"@langchain/google-genai": "^0.2.18",
"@langchain/langgraph": "^0.4.9",
"@langchain/openai": "^0.6.14",
"@node-rs/argon2": "^2.0.2",
"@redis/client": "^5.8.3",
"@tus/azure-store": "^2.0.0",
"@tus/file-store": "^2.0.0",
"@tus/gcs-store": "^2.0.0",
"@tus/s3-store": "^2.0.1",
"@tus/server": "^2.3.0",
"bullmq": "^5.61.0",

View File

@@ -1,4 +1,3 @@
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import ky from 'ky';
import sharp from 'sharp';
@@ -14,12 +13,12 @@ import {
} from '@colanode/core';
import { database } from '@colanode/server/data/database';
import { UpdateAccount } from '@colanode/server/data/schema';
import { s3Client } from '@colanode/server/data/storage';
import {
buildLoginSuccessOutput,
buildLoginVerifyOutput,
} from '@colanode/server/lib/accounts';
import { config } from '@colanode/server/lib/config';
import { storage } from '@colanode/server/lib/storage';
import { AccountAttributes } from '@colanode/server/types/accounts';
const GoogleUserInfoUrl = 'https://www.googleapis.com/oauth2/v1/userinfo';
@@ -113,14 +112,7 @@ const uploadGooglePictureAsAvatar = async (
.toBuffer();
const avatarId = generateId(IdType.Avatar);
const command = new PutObjectCommand({
Bucket: config.storage.bucket,
Key: `avatars/${avatarId}.jpeg`,
Body: jpegBuffer,
ContentType: 'image/jpeg',
});
await s3Client.send(command);
await storage.upload(`avatars/${avatarId}.jpeg`, jpegBuffer, 'image/jpeg');
return avatarId;
} catch {

View File

@@ -1,12 +1,8 @@
import { Readable } from 'stream';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
import { ApiErrorCode } from '@colanode/core';
import { s3Client } from '@colanode/server/data/storage';
import { config } from '@colanode/server/lib/config';
import { storage } from '@colanode/server/lib/storage';
export const avatarDownloadRoute: FastifyPluginCallbackZod = (
instance,
@@ -24,28 +20,10 @@ export const avatarDownloadRoute: FastifyPluginCallbackZod = (
handler: async (request, reply) => {
try {
const avatarId = request.params.avatarId;
const command = new GetObjectCommand({
Bucket: config.storage.bucket,
Key: `avatars/${avatarId}.jpeg`,
});
const { stream } = await storage.download(`avatars/${avatarId}.jpeg`);
const avatarResponse = await s3Client.send(command);
if (!avatarResponse.Body) {
return reply.code(400).send({
code: ApiErrorCode.AvatarNotFound,
message: 'Avatar not found',
});
}
if (avatarResponse.Body instanceof Readable) {
reply.header('Content-Type', 'image/jpeg');
return reply.send(avatarResponse.Body);
}
return reply.code(400).send({
code: ApiErrorCode.AvatarNotFound,
message: 'Avatar not found',
});
reply.header('Content-Type', 'image/jpeg');
return reply.send(stream);
} catch {
return reply.code(500).send({
code: ApiErrorCode.AvatarDownloadFailed,

View File

@@ -1,4 +1,3 @@
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import sharp from 'sharp';
@@ -9,8 +8,7 @@ import {
generateId,
IdType,
} from '@colanode/core';
import { s3Client } from '@colanode/server/data/storage';
import { config } from '@colanode/server/lib/config';
import { storage } from '@colanode/server/lib/storage';
const ALLOWED_MIME_TYPES = [
'image/jpeg',
@@ -70,14 +68,7 @@ export const avatarUploadRoute: FastifyPluginCallbackZod = (
.toBuffer();
const avatarId = generateId(IdType.Avatar);
const command = new PutObjectCommand({
Bucket: config.storage.bucket,
Key: `avatars/${avatarId}.jpeg`,
Body: jpegBuffer,
ContentType: 'image/jpeg',
});
await s3Client.send(command);
await storage.upload(`avatars/${avatarId}.jpeg`, jpegBuffer, 'image/jpeg');
return { success: true, id: avatarId };
} catch {

View File

@@ -1,6 +1,3 @@
import { Readable } from 'stream';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
@@ -11,9 +8,8 @@ import {
FileStatus,
} from '@colanode/core';
import { database } from '@colanode/server/data/database';
import { s3Client } from '@colanode/server/data/storage';
import { config } from '@colanode/server/lib/config';
import { fetchNodeTree, mapNode } from '@colanode/server/lib/nodes';
import { storage } from '@colanode/server/lib/storage';
export const fileDownloadRoute: FastifyPluginCallbackZod = (
instance,
@@ -84,28 +80,20 @@ export const fileDownloadRoute: FastifyPluginCallbackZod = (
});
}
const command = new GetObjectCommand({
Bucket: config.storage.bucket,
Key: upload.path,
});
try {
const { stream, contentType } = await storage.download(upload.path);
const fileResponse = await s3Client.send(command);
if (!fileResponse.Body) {
if (contentType) {
reply.header('Content-Type', contentType);
}
return reply.send(stream);
} catch (error) {
return reply.code(404).send({
code: ApiErrorCode.FileNotFound,
message: 'File not found.',
});
}
if (fileResponse.Body instanceof Readable) {
reply.header('Content-Type', fileResponse.ContentType);
return reply.send(fileResponse.Body);
}
return reply.code(404).send({
code: ApiErrorCode.FileNotFound,
message: 'File not found.',
});
},
});

View File

@@ -1,34 +1,19 @@
import { S3Store } from '@tus/s3-store';
import { Server } from '@tus/server';
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
import {
ApiErrorCode,
FILE_UPLOAD_PART_SIZE,
FileStatus,
generateId,
IdType,
} from '@colanode/core';
import { ApiErrorCode, FileStatus, generateId, IdType } from '@colanode/core';
import { database } from '@colanode/server/data/database';
import { redis } from '@colanode/server/data/redis';
import { s3Config } from '@colanode/server/data/storage';
import { config } from '@colanode/server/lib/config';
import { fetchCounter } from '@colanode/server/lib/counters';
import { generateUrl } from '@colanode/server/lib/fastify';
import { buildFilePath, deleteFile } from '@colanode/server/lib/files';
import { buildFilePath } from '@colanode/server/lib/files';
import { mapNode, updateNode } from '@colanode/server/lib/nodes';
import { RedisKvStore } from '@colanode/server/lib/tus/redis-kv';
import { RedisLocker } from '@colanode/server/lib/tus/redis-locker';
import { storage } from '@colanode/server/lib/storage';
import { RedisLocker } from '@colanode/server/lib/storage/tus/redis-locker';
const s3Store = new S3Store({
partSize: FILE_UPLOAD_PART_SIZE,
cache: new RedisKvStore(redis, config.redis.tus.kvPrefix),
s3ClientConfig: {
...s3Config,
bucket: config.storage.bucket,
},
});
const tusStore = storage.tusStore;
export const fileUploadTusRoute: FastifyPluginCallbackZod = (
instance,
@@ -102,7 +87,7 @@ export const fileUploadTusRoute: FastifyPluginCallbackZod = (
const tusServer = new Server({
path: '/tus',
datastore: s3Store,
datastore: tusStore,
locker: new RedisLocker(redis, config.redis.tus.lockPrefix),
async onUploadCreate() {
const upload = await database
@@ -277,7 +262,7 @@ export const fileUploadTusRoute: FastifyPluginCallbackZod = (
}
const tusInfoPath = `${path}.info`;
await deleteFile(tusInfoPath);
await storage.delete(tusInfoPath);
return {
status_code: 200,

View File

@@ -1,4 +1,3 @@
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
@@ -10,11 +9,10 @@ import {
IdType,
} from '@colanode/core';
import { database } from '@colanode/server/data/database';
import { s3Client } from '@colanode/server/data/storage';
import { config } from '@colanode/server/lib/config';
import { fetchCounter } from '@colanode/server/lib/counters';
import { buildFilePath } from '@colanode/server/lib/files';
import { mapNode, updateNode } from '@colanode/server/lib/nodes';
import { storage } from '@colanode/server/lib/storage';
export const fileUploadRoute: FastifyPluginCallbackZod = (
instance,
@@ -150,16 +148,13 @@ export const fileUploadRoute: FastifyPluginCallbackZod = (
const path = buildFilePath(workspaceId, fileId, file.attributes);
const stream = request.raw;
const uploadCommand = new PutObjectCommand({
Bucket: config.storage.bucket,
Key: path,
Body: stream,
ContentType: file.attributes.mimeType,
ContentLength: file.attributes.size,
});
try {
await s3Client.send(uploadCommand);
await storage.upload(
path,
stream,
file.attributes.mimeType,
BigInt(file.attributes.size)
);
} catch (error) {
console.error(error);
return reply.code(500).send({

View File

@@ -1,15 +0,0 @@
import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3';
import { config } from '@colanode/server/lib/config';
export const s3Config: S3ClientConfig = {
endpoint: config.storage.endpoint,
region: config.storage.region,
credentials: {
accessKeyId: config.storage.accessKey,
secretAccessKey: config.storage.secretKey,
},
forcePathStyle: config.storage.forcePathStyle,
};
export const s3Client = new S3Client(s3Config);

View File

@@ -4,8 +4,8 @@ import { CreateNodeTombstone } from '@colanode/server/data/schema';
import { JobHandler } from '@colanode/server/jobs';
import { updateDocument } from '@colanode/server/lib/documents';
import { eventBus } from '@colanode/server/lib/event-bus';
import { deleteFile } from '@colanode/server/lib/files';
import { createLogger } from '@colanode/server/lib/logger';
import { storage } from '@colanode/server/lib/storage';
const BATCH_SIZE = 100;
const logger = createLogger('server:job:clean-node-data');
@@ -162,7 +162,7 @@ const cleanNodeFiles = async (nodeIds: string[]) => {
if (uploads.length > 0) {
for (const upload of uploads) {
await deleteFile(upload.path);
await storage.delete(upload.path);
}
await database

View File

@@ -4,9 +4,9 @@ import { database } from '@colanode/server/data/database';
import { redis } from '@colanode/server/data/redis';
import { JobHandler } from '@colanode/server/jobs';
import { config } from '@colanode/server/lib/config';
import { deleteFile } from '@colanode/server/lib/files';
import { createLogger } from '@colanode/server/lib/logger';
import { RedisKvStore } from '@colanode/server/lib/tus/redis-kv';
import { storage } from '@colanode/server/lib/storage';
import { RedisKvStore } from '@colanode/server/lib/storage/tus/redis-kv';
const logger = createLogger('server:job:uploads-clean');
@@ -42,11 +42,11 @@ export const uploadsCleanHandler: JobHandler<UploadsCleanInput> = async () => {
const redisKv = new RedisKvStore(redis, config.redis.tus.kvPrefix);
for (const upload of expiredUploads) {
await deleteFile(upload.path);
await storage.delete(upload.path);
await redisKv.delete(upload.path);
const infoPath = `${upload.path}.info`;
await deleteFile(infoPath);
await storage.delete(infoPath);
await database
.deleteFrom('uploads')

View File

@@ -1,6 +1,6 @@
import { database } from '@colanode/server/data/database';
import { JobHandler } from '@colanode/server/jobs';
import { deleteFile } from '@colanode/server/lib/files';
import { storage } from '@colanode/server/lib/storage';
import { createLogger } from '@colanode/server/lib/logger';
const BATCH_SIZE = 500;
@@ -143,7 +143,7 @@ const deleteWorkspaceUploads = async (workspaceId: string) => {
}
for (const upload of uploads) {
await deleteFile(upload.path);
await storage.delete(upload.path);
}
const fileIds = uploads.map((upload) => upload.file_id);

View File

@@ -1,6 +1,6 @@
import { z } from 'zod/v4';
export const storageConfigSchema = z.object({
const s3StorageConfigSchema = z.object({
type: z.literal('s3'),
endpoint: z.string({ error: 'STORAGE_S3_ENDPOINT is required' }),
accessKey: z.string({ error: 'STORAGE_S3_ACCESS_KEY is required' }),
@@ -10,16 +10,80 @@ export const storageConfigSchema = z.object({
forcePathStyle: z.boolean().optional(),
});
export type StorageConfig = z.infer<typeof storageConfigSchema>;
const fileStorageConfigSchema = z.object({
type: z.literal('file'),
directory: z.string({ error: 'STORAGE_FILE_DIRECTORY is required' }),
});
export const readStorageConfigVariables = () => {
return {
type: 's3',
endpoint: process.env.STORAGE_S3_ENDPOINT,
accessKey: process.env.STORAGE_S3_ACCESS_KEY,
secretKey: process.env.STORAGE_S3_SECRET_KEY,
bucket: process.env.STORAGE_S3_BUCKET,
region: process.env.STORAGE_S3_REGION,
forcePathStyle: process.env.STORAGE_S3_FORCE_PATH_STYLE === 'true',
};
const gcsStorageConfigSchema = z.object({
type: z.literal('gcs'),
bucket: z.string({ error: 'STORAGE_GCS_BUCKET is required' }),
projectId: z.string({ error: 'STORAGE_GCS_PROJECT_ID is required' }),
credentials: z.string({ error: 'STORAGE_GCS_CREDENTIALS is required' }),
});
const azureStorageConfigSchema = z.object({
type: z.literal('azure'),
account: z.string({ error: 'STORAGE_AZURE_ACCOUNT is required' }),
accountKey: z.string({ error: 'STORAGE_AZURE_ACCOUNT_KEY is required' }),
containerName: z.string({ error: 'STORAGE_AZURE_CONTAINER_NAME is required' }),
});
export const storageConfigSchema = z.discriminatedUnion('type', [
s3StorageConfigSchema,
fileStorageConfigSchema,
gcsStorageConfigSchema,
azureStorageConfigSchema,
]);
export type StorageConfig = z.infer<typeof storageConfigSchema>;
export type S3StorageConfig = z.infer<typeof s3StorageConfigSchema>;
export type FileStorageConfig = z.infer<typeof fileStorageConfigSchema>;
export type GCSStorageConfig = z.infer<typeof gcsStorageConfigSchema>;
export type AzureStorageConfig = z.infer<typeof azureStorageConfigSchema>;
export const readStorageConfigVariables = (): StorageConfig => {
const storageType = process.env.STORAGE_TYPE || 's3';
switch (storageType) {
case 's3':
return {
type: 's3',
endpoint: process.env.STORAGE_S3_ENDPOINT,
accessKey: process.env.STORAGE_S3_ACCESS_KEY,
secretKey: process.env.STORAGE_S3_SECRET_KEY,
bucket: process.env.STORAGE_S3_BUCKET,
region: process.env.STORAGE_S3_REGION,
forcePathStyle: process.env.STORAGE_S3_FORCE_PATH_STYLE === 'true',
} as z.infer<typeof s3StorageConfigSchema>;
case 'file':
return {
type: 'file',
directory: process.env.STORAGE_FILE_DIRECTORY,
} as z.infer<typeof fileStorageConfigSchema>;
case 'gcs':
return {
type: 'gcs',
bucket: process.env.STORAGE_GCS_BUCKET,
projectId: process.env.STORAGE_GCS_PROJECT_ID,
credentials: process.env.STORAGE_GCS_CREDENTIALS,
} as z.infer<typeof gcsStorageConfigSchema>;
case 'azure':
return {
type: 'azure',
account: process.env.STORAGE_AZURE_ACCOUNT,
accountKey: process.env.STORAGE_AZURE_ACCOUNT_KEY,
containerName: process.env.STORAGE_AZURE_CONTAINER_NAME,
} as z.infer<typeof azureStorageConfigSchema>;
default:
return {
type: 's3',
endpoint: process.env.STORAGE_S3_ENDPOINT,
accessKey: process.env.STORAGE_S3_ACCESS_KEY,
secretKey: process.env.STORAGE_S3_SECRET_KEY,
bucket: process.env.STORAGE_S3_BUCKET,
region: process.env.STORAGE_S3_REGION,
forcePathStyle: process.env.STORAGE_S3_FORCE_PATH_STYLE === 'true',
} as z.infer<typeof s3StorageConfigSchema>;
}
};

View File

@@ -1,8 +1,5 @@
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
import { FileAttributes } from '@colanode/core';
import { s3Client } from '@colanode/server/data/storage';
import { config } from '@colanode/server/lib/config';
import { storage } from '@colanode/server/lib/storage';
export const buildFilePath = (
workspaceId: string,
@@ -11,12 +8,3 @@ export const buildFilePath = (
) => {
return `files/${workspaceId}/${fileId}_${fileAttributes.version}${fileAttributes.extension}`;
};
export const deleteFile = async (path: string) => {
const command = new DeleteObjectCommand({
Bucket: config.storage.bucket,
Key: path,
});
await s3Client.send(command);
};

View File

@@ -30,8 +30,8 @@ import {
checkCollaboratorChanges,
} from '@colanode/server/lib/collaborations';
import { eventBus } from '@colanode/server/lib/event-bus';
import { deleteFile } from '@colanode/server/lib/files';
import { createLogger } from '@colanode/server/lib/logger';
import { storage } from '@colanode/server/lib/storage';
import { jobService } from '@colanode/server/services/job-service';
import {
ConcurrentUpdateResult,
@@ -711,7 +711,7 @@ export const deleteNodeFromMutation = async (
.executeTakeFirst();
if (upload) {
await deleteFile(upload.path);
await storage.delete(upload.path);
await database
.deleteFrom('uploads')

View File

@@ -0,0 +1,103 @@
import { Readable } from 'stream';
import { DataStore } from '@tus/server';
import { AzureStore } from '@tus/azure-store';
import {
BlobServiceClient,
StorageSharedKeyCredential,
BlockBlobClient,
} from '@azure/storage-blob';
import type { AzureStorageConfig } from '@colanode/server/lib/config/storage';
import type { Storage } from './core';
export class AzureBlobStorage implements Storage {
private readonly containerName: string;
private readonly blobServiceClient: BlobServiceClient;
private readonly config: AzureStorageConfig;
public readonly tusStore: DataStore;
constructor(config: AzureStorageConfig) {
this.config = { ...config };
const sharedKeyCredential = new StorageSharedKeyCredential(
this.config.account,
this.config.accountKey
);
this.blobServiceClient = new BlobServiceClient(
`https://${this.config.account}.blob.core.windows.net`,
sharedKeyCredential
);
this.containerName = this.config.containerName;
this.tusStore = new AzureStore({
account: this.config.account,
accountKey: this.config.accountKey,
containerName: this.containerName,
});
}
private getBlockBlobClient(path: string): BlockBlobClient {
const containerClient = this.blobServiceClient.getContainerClient(
this.containerName
);
return containerClient.getBlockBlobClient(path);
}
async download(
path: string
): Promise<{ stream: Readable; contentType?: string }> {
const containerClient = this.blobServiceClient.getContainerClient(
this.containerName
);
const blobClient = containerClient.getBlobClient(path);
const downloadResponse = await blobClient.download();
if (!downloadResponse.readableStreamBody) {
throw new Error('Failed to download blob: no readable stream body');
}
return {
stream: downloadResponse.readableStreamBody as Readable,
contentType: downloadResponse.contentType,
};
}
async delete(path: string): Promise<void> {
const containerClient = this.blobServiceClient.getContainerClient(
this.containerName
);
const blobClient = containerClient.getBlobClient(path);
await blobClient.delete();
}
async upload(
path: string,
data: Buffer | Readable,
contentType: string,
contentLength?: bigint
): Promise<void> {
const blockBlobClient = this.getBlockBlobClient(path);
if (data instanceof Buffer) {
await blockBlobClient.upload(data, data.length, {
blobHTTPHeaders: {
blobContentType: contentType,
},
});
return;
}
if (!contentLength) {
throw new Error(
'Content length is required for stream uploads to Azure Blob Storage'
);
}
await blockBlobClient.uploadStream(data as Readable, undefined, undefined, {
blobHTTPHeaders: {
blobContentType: contentType,
},
});
}
}

View File

@@ -0,0 +1,14 @@
import { Readable } from 'stream';
import { DataStore } from '@tus/server';
export interface Storage {
download(path: string): Promise<{ stream: Readable; contentType?: string }>;
delete(path: string): Promise<void>;
upload(
path: string,
data: Buffer | Readable,
contentType: string,
contentLength?: bigint
): Promise<void>;
readonly tusStore: DataStore;
}

View File

@@ -0,0 +1,58 @@
import { createReadStream, createWriteStream, promises as fs } from 'fs';
import { Readable } from 'stream';
import { DataStore } from '@tus/server';
import { FileStore } from '@tus/file-store';
import type { FileStorageConfig } from '@colanode/server/lib/config/storage';
import type { Storage } from './core';
export class FileSystemStorage implements Storage {
private readonly directory: string;
public readonly tusStore: DataStore;
constructor(config: FileStorageConfig) {
this.directory = config.directory;
this.tusStore = new FileStore({ directory: this.directory });
}
async download(
path: string
): Promise<{ stream: Readable; contentType?: string }> {
const fullPath = `${this.directory}/${path}`;
const stream = createReadStream(fullPath);
return {
stream,
contentType: undefined,
};
}
async delete(path: string): Promise<void> {
const fullPath = `${this.directory}/${path}`;
await fs.unlink(fullPath);
}
async upload(
path: string,
data: Buffer | Readable,
_contentType: string,
_contentLength?: bigint
): Promise<void> {
const fullPath = `${this.directory}/${path}`;
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
await fs.mkdir(dirPath, { recursive: true });
if (data instanceof Buffer) {
await fs.writeFile(fullPath, data);
return;
}
const writeStream = createWriteStream(fullPath);
await new Promise<void>((resolve, reject) => {
(data as Readable).pipe(writeStream);
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
}
}

View File

@@ -0,0 +1,69 @@
import { Readable } from 'stream';
import { Storage, Bucket, File } from '@google-cloud/storage';
import { DataStore } from '@tus/server';
import { GCSStore } from '@tus/gcs-store';
import type { GCSStorageConfig } from '@colanode/server/lib/config/storage';
import type { Storage as StorageInterface } from './core';
export class GCSStorage implements StorageInterface {
private readonly bucket: Bucket;
public readonly tusStore: DataStore;
constructor(config: GCSStorageConfig) {
const storage = new Storage({
projectId: config.projectId,
keyFilename: config.credentials,
});
this.bucket = storage.bucket(config.bucket);
this.tusStore = new GCSStore({ bucket: this.bucket });
}
private getFile(path: string): File {
return this.bucket.file(path);
}
async download(
path: string
): Promise<{ stream: Readable; contentType?: string }> {
const file = this.getFile(path);
const [metadata] = await file.getMetadata();
const stream = file.createReadStream();
return {
stream,
contentType: metadata.contentType,
};
}
async delete(path: string): Promise<void> {
const file = this.getFile(path);
await file.delete();
}
async upload(
path: string,
data: Buffer | Readable,
contentType: string,
_contentLength?: bigint
): Promise<void> {
const file = this.getFile(path);
if (data instanceof Buffer) {
await file.save(data, { contentType });
return;
}
const writeStream = file.createWriteStream({
metadata: { contentType },
});
await new Promise<void>((resolve, reject) => {
(data as Readable).pipe(writeStream);
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
}
}

View File

@@ -0,0 +1,30 @@
import { config } from '@colanode/server/lib/config';
import type { StorageConfig } from '../config/storage';
import type { Storage } from './core';
import { AzureBlobStorage } from './azure';
import { FileSystemStorage } from './fs';
import { GCSStorage } from './gcs';
import { S3Storage } from './s3';
const buildStorage = (storageConfig: StorageConfig): Storage => {
switch (storageConfig.type) {
case 'file':
return new FileSystemStorage(storageConfig);
case 's3':
return new S3Storage(storageConfig);
case 'gcs':
return new GCSStorage(storageConfig);
case 'azure':
return new AzureBlobStorage(storageConfig);
default:
throw new Error(
`Unsupported storage type: ${(storageConfig as any).type}`
);
}
};
export const storage = buildStorage(config.storage);
export type { Storage } from './core';

View File

@@ -0,0 +1,93 @@
import { Readable } from 'stream';
import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { RedisClientType } from '@redis/client';
import { FILE_UPLOAD_PART_SIZE } from '@colanode/core';
import { DataStore } from '@tus/server';
import { S3Store } from '@tus/s3-store';
import { config } from '@colanode/server/lib/config';
import { RedisKvStore } from '@colanode/server/lib/storage/tus/redis-kv';
import type { S3StorageConfig } from '@colanode/server/lib/config/storage';
import { redis } from '@colanode/server/data/redis';
import type { Storage } from './core';
export class S3Storage implements Storage {
private readonly client: S3Client;
private readonly bucket: string;
private readonly s3Config: S3StorageConfig;
public readonly tusStore: DataStore;
constructor(s3Config: S3StorageConfig) {
this.s3Config = { ...s3Config };
this.client = new S3Client({
endpoint: this.s3Config.endpoint,
region: this.s3Config.region,
credentials: {
accessKeyId: this.s3Config.accessKey,
secretAccessKey: this.s3Config.secretKey,
},
forcePathStyle: this.s3Config.forcePathStyle,
});
this.bucket = this.s3Config.bucket;
this.tusStore = new S3Store({
partSize: FILE_UPLOAD_PART_SIZE,
cache: new RedisKvStore(redis, config.redis.tus.kvPrefix),
s3ClientConfig: {
bucket: this.bucket,
endpoint: this.s3Config.endpoint,
region: this.s3Config.region,
forcePathStyle: this.s3Config.forcePathStyle,
credentials: {
accessKeyId: this.s3Config.accessKey,
secretAccessKey: this.s3Config.secretKey,
},
},
});
}
async download(
path: string
): Promise<{ stream: Readable; contentType?: string }> {
const command = new GetObjectCommand({ Bucket: this.bucket, Key: path });
const response = await this.client.send(command);
if (!response.Body || !(response.Body instanceof Readable)) {
throw new Error('File not found or invalid response body');
}
return {
stream: response.Body,
contentType: response.ContentType,
};
}
async delete(path: string): Promise<void> {
const command = new DeleteObjectCommand({ Bucket: this.bucket, Key: path });
await this.client.send(command);
}
async upload(
path: string,
data: Buffer | Readable,
contentType: string,
contentLength?: bigint
): Promise<void> {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: path,
Body: data,
ContentType: contentType,
ContentLength: contentLength ? Number(contentLength) : undefined,
});
await this.client.send(command);
}
}

View File

@@ -28,7 +28,12 @@ services:
networks:
- colanode_network
# ---------------------------------------------------------------
# Optional MinIO Object Storage (Enable when using S3 storage)
# ---------------------------------------------------------------
minio:
profiles:
- s3
image: minio/minio:RELEASE.2025-04-08T15-41-24Z
container_name: colanode_minio
restart: always
@@ -77,7 +82,6 @@ services:
depends_on:
- postgres
- valkey
- minio
# - smtp # Optional
environment:
# ───────────────────────────────────────────────────────────────
@@ -158,15 +162,20 @@ services:
# REDIS_EVENTS_CHANNEL: 'events'
# ───────────────────────────────────────────────────────────────
# S3 configuration for files.
# In the future we will support other storage providers.
# Storage configuration
# Supported storage types: 'file', 's3', 'gcs', 'azure'
# By default we store files on the local filesystem.
# ───────────────────────────────────────────────────────────────
STORAGE_S3_ENDPOINT: 'http://minio:9000'
STORAGE_S3_ACCESS_KEY: 'minioadmin'
STORAGE_S3_SECRET_KEY: 'your_minio_password'
STORAGE_S3_BUCKET: 'colanode'
STORAGE_S3_REGION: 'us-east-1'
STORAGE_S3_FORCE_PATH_STYLE: 'true'
STORAGE_TYPE: 'file'
STORAGE_FILE_DIRECTORY: '/var/lib/colanode/storage'
# To use the optional MinIO service (or any S3-compatible storage):
# STORAGE_TYPE: 's3'
# STORAGE_S3_ENDPOINT: 'http://minio:9000'
# STORAGE_S3_ACCESS_KEY: 'minioadmin'
# STORAGE_S3_SECRET_KEY: 'your_minio_password'
# STORAGE_S3_BUCKET: 'colanode'
# STORAGE_S3_REGION: 'us-east-1'
# STORAGE_S3_FORCE_PATH_STYLE: 'true'
# ───────────────────────────────────────────────────────────────
# SMTP configuration
@@ -196,6 +205,8 @@ services:
ports:
- '3000:3000'
volumes:
- server_storage:/var/lib/colanode/storage
networks:
- colanode_network
@@ -212,6 +223,7 @@ volumes:
postgres_data:
valkey_data:
minio_data:
server_storage:
networks:
colanode_network:

View File

@@ -9,7 +9,8 @@ This chart deploys a complete Colanode instance with all required dependencies:
- **Colanode Server**: The main application server
- **PostgreSQL**: Database with pgvector extension for vector operations
- **Redis/Valkey**: Message queue and caching
- **MinIO**: S3-compatible object storage for files and avatars
- **File Storage (default)**: Persistent volume for user files and avatars
- **Optional Object Storage**: MinIO (S3-compatible), external S3, Google Cloud Storage, or Azure Blob Storage
## Prerequisites
@@ -62,11 +63,40 @@ helm install my-colanode ./hosting/kubernetes/chart \
### Dependencies
| Parameter | Description | Default |
| -------------------- | ---------------------------- | ------- |
| `postgresql.enabled` | Enable PostgreSQL deployment | `true` |
| `redis.enabled` | Enable Redis deployment | `true` |
| `minio.enabled` | Enable MinIO deployment | `true` |
| Parameter | Description | Default |
| -------------------- | ----------------------------------------------------------------- | ------- |
| `postgresql.enabled` | Enable PostgreSQL deployment | `true` |
| `redis.enabled` | Enable Redis deployment | `true` |
| `minio.enabled` | Enable bundled MinIO (only required for the in-cluster S3 option) | `false` |
### Storage Configuration
Set `colanode.storage.type` to choose where user files and avatars are stored:
- **File storage (default)** mounts a persistent volume at `/var/lib/colanode/storage`. Adjust `colanode.storage.file.persistence` to control the PVC size, storage class, or reference an existing claim.
- **S3-compatible storage** (Amazon S3, MinIO, Cloudflare R2, etc.) requires `colanode.storage.type=s3`. Enable the bundled MinIO instance with `--set minio.enabled=true` or supply your provider endpoint, bucket, region, and credentials via `colanode.storage.s3.*`.
- **Google Cloud Storage** needs a service-account JSON key. Create a secret:
```bash
kubectl create secret generic gcs-credentials \
--from-file=service-account.json=/path/to/key.json
```
Then configure:
```yaml
colanode:
storage:
type: gcs
gcs:
bucket: your-bucket
projectId: your-project
credentialsSecret:
name: gcs-credentials
key: service-account.json
```
- **Azure Blob Storage** is available with `colanode.storage.type=azure`. Provide the storage `account`, `containerName`, and the account key via `colanode.storage.azure.accountKey` (inline value or an existing secret).
## Important Notes
@@ -78,9 +108,10 @@ The chart includes `global.security.allowInsecureImages: true` because we use a
By default, the chart configures persistent storage for:
- Colanode file storage (PVC): 20Gi
- PostgreSQL: 8Gi
- Redis: 8Gi
- MinIO: 10Gi
- MinIO: 10Gi (only when `minio.enabled=true`)
Adjust these values based on your requirements.

View File

@@ -2,7 +2,8 @@ apiVersion: v2
name: colanode
description: A Helm chart for Colanode - open-source & local-first collaboration workspace
type: application
version: 0.1.0
version: 0.2.0
# appVersion is auto-updated by the release workflow
appVersion: '1.0.0'
dependencies:

View File

@@ -82,6 +82,13 @@ Return the MinIO hostname
{{- printf "%s-minio" .Release.Name -}}
{{- end }}
{{/*
Return the default PVC name used for file storage
*/}}
{{- define "colanode.storagePvcName" -}}
{{- printf "%s-storage" (include "colanode.fullname" .) -}}
{{- end }}
{{/*
Helper to get value from secret key reference or direct value
Usage: {{ include "colanode.getValueOrSecret" (dict "key" "theKey" "value" .Values.path.to.value) }}
@@ -210,34 +217,81 @@ Colanode Server Environment Variables
value: {{ .Values.colanode.config.REDIS_EVENTS_CHANNEL | quote }}
# ───────────────────────────────────────────────────────────────
# S3 Configuration for Storage
# Storage Configuration
# ───────────────────────────────────────────────────────────────
- name: STORAGE_TYPE
value: {{ default "file" .Values.colanode.storage.type | quote }}
{{- $storageType := default "file" .Values.colanode.storage.type }}
{{- if eq $storageType "file" }}
- name: STORAGE_FILE_DIRECTORY
value: {{ required "colanode.storage.file.directory must be set when STORAGE_TYPE is file" .Values.colanode.storage.file.directory | quote }}
{{- end }}
{{- if eq $storageType "s3" }}
{{- $s3 := .Values.colanode.storage.s3 }}
{{- $endpoint := $s3.endpoint }}
{{- if and (not $endpoint) (not .Values.minio.enabled) }}
{{- fail "colanode.storage.s3.endpoint must be provided when MinIO is disabled" }}
{{- end }}
- name: STORAGE_S3_ENDPOINT
value: "http://{{ include "colanode.minio.hostname" . }}:9000"
- name: STORAGE_S3_ACCESS_KEY
{{- if .Values.minio.auth.existingSecret }}
{{- include "colanode.getRequiredValueOrSecret" (dict "key" "minio.auth.rootUser" "value" (dict "value" .Values.minio.auth.rootUser "existingSecret" .Values.minio.auth.existingSecret "secretKey" .Values.minio.auth.rootUserKey )) | nindent 2 }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: {{ .Values.minio.auth.rootUserKey }}
{{- end }}
- name: STORAGE_S3_SECRET_KEY
{{- if .Values.minio.auth.existingSecret }}
{{- include "colanode.getRequiredValueOrSecret" (dict "key" "minio.auth.rootPassword" "value" (dict "value" .Values.minio.auth.rootPassword "existingSecret" .Values.minio.auth.existingSecret "secretKey" .Values.minio.auth.rootPasswordKey )) | nindent 2 }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: {{ .Values.minio.auth.rootPasswordKey }}
{{- end }}
value: {{ if $endpoint }}{{ $endpoint | quote }}{{ else }}{{ printf "http://%s:9000" (include "colanode.minio.hostname" .) | quote }}{{ end }}
- name: STORAGE_S3_BUCKET
value: "colanode"
value: {{ required "colanode.storage.s3.bucket must be set when STORAGE_TYPE is s3" $s3.bucket | quote }}
- name: STORAGE_S3_REGION
value: "us-east-1"
value: {{ required "colanode.storage.s3.region must be set when STORAGE_TYPE is s3" $s3.region | quote }}
- name: STORAGE_S3_FORCE_PATH_STYLE
value: "true"
value: {{ ternary "true" "false" (default true $s3.forcePathStyle) | quote }}
- name: STORAGE_S3_ACCESS_KEY
{{- if or $s3.accessKey.value $s3.accessKey.existingSecret }}
{{- include "colanode.getRequiredValueOrSecret" (dict "key" "colanode.storage.s3.accessKey" "value" $s3.accessKey) | nindent 2 }}
{{- else if .Values.minio.enabled }}
valueFrom:
secretKeyRef:
name: {{ if .Values.minio.auth.existingSecret }}{{ .Values.minio.auth.existingSecret }}{{ else }}{{ printf "%s-minio" .Release.Name }}{{ end }}
key: {{ .Values.minio.auth.rootUserKey }}
{{- else }}
{{- fail "An S3 access key must be provided via colanode.storage.s3.accessKey when STORAGE_TYPE is s3 and MinIO is disabled" }}
{{- end }}
- name: STORAGE_S3_SECRET_KEY
{{- if or $s3.secretKey.value $s3.secretKey.existingSecret }}
{{- include "colanode.getRequiredValueOrSecret" (dict "key" "colanode.storage.s3.secretKey" "value" $s3.secretKey) | nindent 2 }}
{{- else if .Values.minio.enabled }}
valueFrom:
secretKeyRef:
name: {{ if .Values.minio.auth.existingSecret }}{{ .Values.minio.auth.existingSecret }}{{ else }}{{ printf "%s-minio" .Release.Name }}{{ end }}
key: {{ .Values.minio.auth.rootPasswordKey }}
{{- else }}
{{- fail "An S3 secret key must be provided via colanode.storage.s3.secretKey when STORAGE_TYPE is s3 and MinIO is disabled" }}
{{- end }}
{{- end }}
{{- if eq $storageType "gcs" }}
{{- $gcs := .Values.colanode.storage.gcs }}
- name: STORAGE_GCS_BUCKET
value: {{ required "colanode.storage.gcs.bucket must be set when STORAGE_TYPE is gcs" $gcs.bucket | quote }}
- name: STORAGE_GCS_PROJECT_ID
value: {{ required "colanode.storage.gcs.projectId must be set when STORAGE_TYPE is gcs" $gcs.projectId | quote }}
{{- if $gcs.credentialsSecret.name }}
- name: STORAGE_GCS_CREDENTIALS
value: {{ printf "%s/%s" (trimSuffix "/" $gcs.credentialsSecret.mountPath) $gcs.credentialsSecret.fileName | quote }}
{{- else if $gcs.credentialsPath }}
- name: STORAGE_GCS_CREDENTIALS
value: {{ $gcs.credentialsPath | quote }}
{{- else }}
{{- fail "Provide colanode.storage.gcs.credentialsSecret or credentialsPath when STORAGE_TYPE is gcs" }}
{{- end }}
{{- end }}
{{- if eq $storageType "azure" }}
{{- $azure := .Values.colanode.storage.azure }}
- name: STORAGE_AZURE_ACCOUNT
value: {{ required "colanode.storage.azure.account must be set when STORAGE_TYPE is azure" $azure.account | quote }}
- name: STORAGE_AZURE_CONTAINER_NAME
value: {{ required "colanode.storage.azure.containerName must be set when STORAGE_TYPE is azure" $azure.containerName | quote }}
- name: STORAGE_AZURE_ACCOUNT_KEY
{{- if or $azure.accountKey.value $azure.accountKey.existingSecret }}
{{- include "colanode.getRequiredValueOrSecret" (dict "key" "colanode.storage.azure.accountKey" "value" $azure.accountKey) | nindent 2 }}
{{- else }}
{{- fail "An Azure storage account key must be provided via colanode.storage.azure.accountKey when STORAGE_TYPE is azure" }}
{{- end }}
{{- end }}
# ───────────────────────────────────────────────────────────────
# SMTP configuration
@@ -258,4 +312,4 @@ Colanode Server Environment Variables
- name: SMTP_EMAIL_FROM_NAME
value: {{ .Values.colanode.config.SMTP_EMAIL_FROM_NAME | quote }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -38,5 +38,47 @@ spec:
protocol: TCP
env:
{{- include "colanode.serverEnvVars" . | nindent 12 }}
{{- $storageType := default "file" .Values.colanode.storage.type }}
{{- $mountFileStorage := eq $storageType "file" }}
{{- $gcsSecret := .Values.colanode.storage.gcs.credentialsSecret }}
{{- $mountGcsCredentials := and (eq $storageType "gcs") $gcsSecret.name }}
{{- if or $mountFileStorage $mountGcsCredentials }}
volumeMounts:
{{- if $mountFileStorage }}
- name: storage-data
mountPath: {{ required "colanode.storage.file.directory must be set when STORAGE_TYPE is file" .Values.colanode.storage.file.directory }}
{{- end }}
{{- if $mountGcsCredentials }}
- name: gcs-credentials
mountPath: {{ required "colanode.storage.gcs.credentialsSecret.mountPath must be set when mounting GCS credentials" $gcsSecret.mountPath }}
readOnly: true
{{- end }}
{{- end }}
resources:
{{- toYaml .Values.colanode.resources | nindent 12 }}
{{- if or $mountFileStorage $mountGcsCredentials }}
volumes:
{{- if $mountFileStorage }}
- name: storage-data
{{- $persistence := .Values.colanode.storage.file.persistence }}
{{- if $persistence.enabled }}
{{- if $persistence.existingClaim }}
persistentVolumeClaim:
claimName: {{ $persistence.existingClaim }}
{{- else }}
persistentVolumeClaim:
claimName: {{ include "colanode.storagePvcName" . }}
{{- end }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
{{- if $mountGcsCredentials }}
- name: gcs-credentials
secret:
secretName: {{ $gcsSecret.name }}
items:
- key: {{ required "colanode.storage.gcs.credentialsSecret.key must be set when providing a secret" $gcsSecret.key }}
path: {{ required "colanode.storage.gcs.credentialsSecret.fileName must be set when providing a secret" $gcsSecret.fileName }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,21 @@
{{- $storageType := default "file" .Values.colanode.storage.type -}}
{{- $persistence := .Values.colanode.storage.file.persistence -}}
{{- if and (eq $storageType "file") $persistence.enabled (not $persistence.existingClaim) -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "colanode.storagePvcName" . }}
labels:
{{- include "colanode.labels" . | nindent 4 }}
spec:
accessModes:
{{- range $persistence.accessModes }}
- {{ . }}
{{- end }}
resources:
requests:
storage: {{ required "colanode.storage.file.persistence.size must be set when creating a storage PVC" $persistence.size }}
{{- if $persistence.storageClass }}
storageClassName: {{ $persistence.storageClass }}
{{- end }}
{{- end }}

View File

@@ -114,14 +114,6 @@ colanode:
REDIS_TUS_KV_PREFIX: 'colanode:tus:kv'
REDIS_EVENTS_CHANNEL: 'events'
# S3 storage for files
STORAGE_S3_ENDPOINT: 'http://{{ .Release.Name }}-minio:9000'
STORAGE_S3_ACCESS_KEY: 'minioadmin'
STORAGE_S3_SECRET_KEY: '$(MINIO_ROOT_PASSWORD)'
STORAGE_S3_BUCKET: 'colanode'
STORAGE_S3_REGION: 'us-east-1'
STORAGE_S3_FORCE_PATH_STYLE: 'true'
# Email configuration
SMTP_ENABLED: 'false'
# SMTP_HOST: ""
@@ -131,6 +123,80 @@ colanode:
# SMTP_EMAIL_FROM: ""
# SMTP_EMAIL_FROM_NAME: "Colanode"
storage:
# -- Storage backend type. Supported values: file, s3, gcs, azure
type: file
file:
# -- Directory inside the pod where files are stored when using file storage
directory: /var/lib/colanode/storage
persistence:
# -- Enable a PersistentVolumeClaim for file storage
enabled: true
# -- Use an existing PVC instead of creating one
existingClaim: ''
# -- Requested storage size for the PVC (only when creating a new claim)
size: 20Gi
# -- Storage class for the PVC. Leave empty to use the cluster default
storageClass: ''
# -- Access modes for the PVC
accessModes:
- ReadWriteOnce
s3:
# -- Custom endpoint for S3-compatible storage. Leave empty to use the bundled MinIO service hostname
endpoint: ''
# -- Bucket name to use when STORAGE_TYPE is s3
bucket: colanode
# -- Region for the S3 bucket
region: us-east-1
# -- Force path-style URLs (required for MinIO and most S3-compatible providers)
forcePathStyle: true
accessKey:
# -- Optional plain-text access key (prefer using secrets in production)
value: ''
# -- Name of an existing secret that stores the access key (takes precedence over value)
existingSecret: ''
# -- Key within the existing secret that stores the access key
secretKey: ''
secretKey:
# -- Optional plain-text secret key (prefer using secrets in production)
value: ''
# -- Name of an existing secret that stores the secret key (takes precedence over value)
existingSecret: ''
# -- Key within the existing secret that stores the secret key
secretKey: ''
gcs:
# -- Bucket name when STORAGE_TYPE is gcs
bucket: ''
# -- GCP project ID associated with the bucket
projectId: ''
# -- Optional direct path to the credentials file (used when credentialsSecret.name is empty)
credentialsPath: ''
credentialsSecret:
# -- Name of the secret that contains the GCP service account JSON key
name: ''
# -- Key inside the secret that stores the JSON key
key: ''
# -- Directory where the secret will be mounted
mountPath: /var/secrets/gcp
# -- File name to use when mounting the JSON key
fileName: service-account.json
azure:
# -- Storage account name when STORAGE_TYPE is azure
account: ''
# -- Blob container name when STORAGE_TYPE is azure
containerName: ''
accountKey:
# -- Optional plain-text storage account key
value: ''
# -- Name of an existing secret that stores the account key (takes precedence over value)
existingSecret: ''
# -- Key within the existing secret that stores the account key
secretKey: ''
global:
security:
# Required for custom PostgreSQL image with pgvector extension
@@ -178,6 +244,7 @@ redis:
# MinIO object storage
minio:
enabled: false
auth:
rootUser: 'minioadmin'
# password: "" # Leave empty to auto-generate

690
package-lock.json generated
View File

@@ -76,12 +76,16 @@
"@colanode/crdt": "*",
"@fastify/cors": "^11.1.0",
"@fastify/websocket": "^11.2.0",
"@google-cloud/storage": "^7.15.0",
"@langchain/core": "^0.3.78",
"@langchain/google-genai": "^0.2.18",
"@langchain/langgraph": "^0.4.9",
"@langchain/openai": "^0.6.14",
"@node-rs/argon2": "^2.0.2",
"@redis/client": "^5.8.3",
"@tus/azure-store": "^2.0.0",
"@tus/file-store": "^2.0.0",
"@tus/gcs-store": "^2.0.0",
"@tus/s3-store": "^2.0.1",
"@tus/server": "^2.3.0",
"bullmq": "^5.61.0",
@@ -1719,6 +1723,206 @@
"node": ">=18.0.0"
}
},
"node_modules/@azure/abort-controller": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-auth": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz",
"integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-util": "^1.13.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/core-client": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.10.0",
"@azure/core-rest-pipeline": "^1.22.0",
"@azure/core-tracing": "^1.3.0",
"@azure/core-util": "^1.13.0",
"@azure/logger": "^1.3.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/core-http-compat": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz",
"integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-client": "^1.10.0",
"@azure/core-rest-pipeline": "^1.22.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/core-lro": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.2.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-paging": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.1.tgz",
"integrity": "sha512-UVZlVLfLyz6g3Hy7GNDpooMQonUygH7ghdiSASOOHy97fKj/mPLqgDX7aidOijn+sCMU+WU8NjlPlNTgnvbcGA==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.10.0",
"@azure/core-tracing": "^1.3.0",
"@azure/core-util": "^1.13.0",
"@azure/logger": "^1.3.0",
"@typespec/ts-http-runtime": "^0.3.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/core-tracing": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
"integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/core-util": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz",
"integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@typespec/ts-http-runtime": "^0.3.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/core-xml": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz",
"integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==",
"license": "MIT",
"dependencies": {
"fast-xml-parser": "^5.0.7",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/logger": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
"integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
"license": "MIT",
"dependencies": {
"@typespec/ts-http-runtime": "^0.3.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/storage-blob": {
"version": "12.28.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.28.0.tgz",
"integrity": "sha512-VhQHITXXO03SURhDiGuHhvc/k/sD2WvJUS7hqhiVNbErVCuQoLtWql7r97fleBlIRKHJaa9R7DpBjfE0pfLYcA==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.9.0",
"@azure/core-client": "^1.9.3",
"@azure/core-http-compat": "^2.2.0",
"@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.6.2",
"@azure/core-rest-pipeline": "^1.19.1",
"@azure/core-tracing": "^1.2.0",
"@azure/core-util": "^1.11.0",
"@azure/core-xml": "^1.4.5",
"@azure/logger": "^1.1.4",
"@azure/storage-common": "^12.0.0-beta.2",
"events": "^3.0.0",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@azure/storage-common": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.0.0.tgz",
"integrity": "sha512-QyEWXgi4kdRo0wc1rHum9/KnaWZKCdQGZK1BjU4fFL6Jtedp7KLbQihgTTVxldFy1z1ZPtuDPx8mQ5l3huPPbA==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.9.0",
"@azure/core-http-compat": "^2.2.0",
"@azure/core-rest-pipeline": "^1.19.1",
"@azure/core-tracing": "^1.2.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.1.4",
"events": "^3.3.0",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -5527,6 +5731,102 @@
"dev": true,
"license": "MIT"
},
"node_modules/@google-cloud/paginator": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
"integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
"license": "Apache-2.0",
"dependencies": {
"arrify": "^2.0.0",
"extend": "^3.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@google-cloud/projectify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
"integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@google-cloud/promisify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
"integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@google-cloud/storage": {
"version": "7.17.1",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.1.tgz",
"integrity": "sha512-2FMQbpU7qK+OtBPaegC6n+XevgZksobUGo6mGKnXNmeZpvLiAo1gTAE3oTKsrMGDV4VtL8Zzpono0YsK/Q7Iqg==",
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/paginator": "^5.0.0",
"@google-cloud/projectify": "^4.0.0",
"@google-cloud/promisify": "<4.1.0",
"abort-controller": "^3.0.0",
"async-retry": "^1.3.3",
"duplexify": "^4.1.3",
"fast-xml-parser": "^4.4.1",
"gaxios": "^6.0.2",
"google-auth-library": "^9.6.3",
"html-entities": "^2.5.2",
"mime": "^3.0.0",
"p-limit": "^3.0.1",
"retry-request": "^7.0.0",
"teeny-request": "^9.0.0",
"uuid": "^8.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google-cloud/storage/node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/@google-cloud/storage/node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/@google-cloud/storage/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@google/generative-ai": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
@@ -10770,7 +11070,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
@@ -10785,6 +11084,67 @@
"node": ">=10.13.0"
}
},
"node_modules/@tus/azure-store": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tus/azure-store/-/azure-store-2.0.0.tgz",
"integrity": "sha512-V9oPt5YqtTmQzQ3MAY6t4SoZVc665E/6SaK1QffFo7MRCF16TE7fRie5LsTkNX9Zf9vEwUbdQswZSNYPKuGi1Q==",
"license": "MIT",
"dependencies": {
"@azure/storage-blob": "^12.24.0",
"@tus/utils": "^0.6.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@tus/file-store": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tus/file-store/-/file-store-2.0.0.tgz",
"integrity": "sha512-LTh9L/RoWoo2TbBGPZOuhuyEIIqweoTekT77ZkIVkpYkLK8zTt++PRdY+VyJsLDbFMO9RzvKSBRmj1H8SPdDew==",
"license": "MIT",
"dependencies": {
"@tus/utils": "^0.6.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">=20.19.0"
},
"optionalDependencies": {
"@redis/client": "^1.6.0"
}
},
"node_modules/@tus/file-store/node_modules/@redis/client": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"optional": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@tus/gcs-store": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tus/gcs-store/-/gcs-store-2.0.0.tgz",
"integrity": "sha512-YCSSI8PesTSFEmfJaYBHRN+kvmPBElW+w2w4ODr3JsemH2JTeEbc47BgOC9PgEIEMdo+3FgmmyUcq5jsyO2vmQ==",
"license": "MIT",
"dependencies": {
"@tus/utils": "^0.6.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@google-cloud/storage": "^7.15.2"
}
},
"node_modules/@tus/s3-store": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tus/s3-store/-/s3-store-2.0.1.tgz",
@@ -11519,6 +11879,20 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typespec/ts-http-runtime": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz",
"integrity": "sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==",
"license": "MIT",
"dependencies": {
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -11688,6 +12062,18 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@@ -11730,7 +12116,6 @@
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -12101,6 +12486,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/arrify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -12135,6 +12529,15 @@
"integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==",
"license": "MIT"
},
"node_modules/async-retry": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
"integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
"license": "MIT",
"dependencies": {
"retry": "0.13.1"
}
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -12300,6 +12703,15 @@
"node": "20.x || 22.x || 23.x || 24.x"
}
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -12469,6 +12881,12 @@
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -13946,6 +14364,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -14583,7 +15010,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -15364,12 +15790,30 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
@@ -15495,6 +15939,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -16194,6 +16644,48 @@
"license": "MIT",
"optional": true
},
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -16531,6 +17023,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -16583,6 +17101,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@@ -16723,6 +17254,22 @@
"dev": true,
"license": "ISC"
},
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -16734,7 +17281,6 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
@@ -16762,7 +17308,6 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
@@ -16786,7 +17331,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -17757,6 +18301,15 @@
"node": ">=6"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -17891,6 +18444,27 @@
"node": ">=8"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -18994,6 +19568,18 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -19427,7 +20013,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
@@ -19448,21 +20033,18 @@
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
@@ -20114,7 +20696,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -21922,6 +22503,20 @@
"node": ">= 4"
}
},
"node_modules/retry-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
"integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
"license": "MIT",
"dependencies": {
"@types/request": "^2.48.8",
"extend": "^3.0.2",
"teeny-request": "^9.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -22151,7 +22746,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/sax": {
@@ -22826,6 +23421,15 @@
"node": ">= 0.10.0"
}
},
"node_modules/stream-events": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
"license": "MIT",
"dependencies": {
"stubs": "^3.0.0"
}
},
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
@@ -23164,6 +23768,12 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
"license": "MIT"
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -23465,6 +24075,61 @@
"node": ">=8"
}
},
"node_modules/teeny-request": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
"integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
"license": "Apache-2.0",
"dependencies": {
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.9",
"stream-events": "^1.0.5",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/teeny-request/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/teeny-request/node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"license": "MIT",
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/teeny-request/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/temp": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
@@ -25843,7 +26508,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"