diff --git a/README.md b/README.md index 9971de72..c547acfe 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,20 @@ learning_ai_common_plat/ ## Shared Libraries -| Package | Description | Peer Dependencies | -| ------------------------- | --------------------------------------------------------- | ------------------ | -| `@bytelyst/errors` | Typed HTTP service errors (400–429) | — | -| `@bytelyst/cosmos` | Azure Cosmos DB client singleton + container registry | `@azure/cosmos` | -| `@bytelyst/config` | Zod-based env config loader + product identity | `zod` | -| `@bytelyst/auth` | JWT utilities, auth middleware, password hashing | `jose`, `bcryptjs` | -| `@bytelyst/api-client` | Configurable fetch wrapper with auth token injection | — | -| `@bytelyst/react-auth` | React auth context factory (typed provider + hook) | `react` | -| `@bytelyst/design-tokens` | Cross-platform design tokens (JSON → CSS/TS/Kotlin/Swift) | — | +| Package | Description | Peer Dependencies | +| ------------------------- | --------------------------------------------------------- | ---------------------- | +| `@bytelyst/errors` | Typed HTTP service errors (400–429) | — | +| `@bytelyst/logger` | Structured logger factory for services/dashboards | — | +| `@bytelyst/cosmos` | Azure Cosmos DB client singleton + container registry | `@azure/cosmos` | +| `@bytelyst/blob` | Azure Blob Storage helpers + SAS URL generation | `@azure/storage-blob` | +| `@bytelyst/config` | Zod-based env config loader + product identity | `zod` | +| `@bytelyst/auth` | JWT utilities, auth middleware, password hashing | `jose`, `bcryptjs` | +| `@bytelyst/fastify-core` | Fastify service bootstrap (request-id, /health, errors) | `fastify` | +| `@bytelyst/api-client` | Configurable fetch wrapper with auth token injection | — | +| `@bytelyst/react-auth` | React auth context factory (typed provider + hook) | `react` | +| `@bytelyst/design-tokens` | Cross-platform design tokens (JSON → CSS/TS/Kotlin/Swift) | — | +| `@bytelyst/extraction` | Extraction service client + shared types | `@bytelyst/api-client` | +| `@bytelyst/testing` | Shared test helpers (Fastify inject, schema asserts) | `vitest` | ## Shared Services diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 104b4e0c..11a40e37 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -532,7 +532,7 @@ The following gaps were identified by scanning every import in the actual codeba - [ ] **7.1** Publish `@bytelyst/*` packages to GitHub Packages (private npm registry) - [ ] **7.2** Add Changesets for automated version management and changelogs - [ ] **7.3** Create reusable GitHub Actions workflow templates for service CI -- [ ] **7.4** Add `@bytelyst/blob` package (extract blob storage client + SAS generation) +- [x] **7.4** Add `@bytelyst/blob` package (extract blob storage client + SAS generation) - [ ] **7.5** Add `@bytelyst/monitoring` package (health check aggregator) - [x] **7.6** Add `@bytelyst/testing` package (shared test utilities, mock factories) — created with 10 tests - [ ] **7.7** Evaluate Python shared package for `cosmos_client.py` + `blob_client.py` if MindLyst adds Python backend @@ -557,7 +557,7 @@ The following gaps were identified by scanning every import in the actual codeba | **4** | `@bytelyst/design-tokens` (4 platforms) | 24 | 23 | ✅ CSS synced to MindLyst; CONTRIBUTING updated; visual verify pending | | **5** | CI/CD + Docker (pre-copy strategy) | 23 | 21 | ⚠️ All Dockerfiles rewritten, CI workflows created; Docker build blocked by proxy | | **6** | Verification + docs + cleanup | 28 | 21 | ✅ Docs updated, depcheck done, git clean; E2E needs services | -| **7** | Future enhancements (+testing pkg) | 10 | 1 | 🔲 @bytelyst/testing created (10 tests) | +| **7** | Future enhancements (+testing pkg) | 10 | 2 | 🔲 @bytelyst/testing + @bytelyst/blob created | | **Total** | **10 packages (+1 bonus: logger)** | **278** | **251** | **~90% complete** | ### Bonus Package (not in original roadmap) diff --git a/docs/workstreams/ALL_OTHER_WORKSTREAMS_REMAINING.md b/docs/workstreams/ALL_OTHER_WORKSTREAMS_REMAINING.md index 634328fa..fa2a2a58 100644 --- a/docs/workstreams/ALL_OTHER_WORKSTREAMS_REMAINING.md +++ b/docs/workstreams/ALL_OTHER_WORKSTREAMS_REMAINING.md @@ -12,6 +12,7 @@ - 2026-02-14: Checklist created (`docs/workstreams/*`) in commit `5113b56` - 2026-02-14: Added Dependabot config (`.github/dependabot.yml`) - 2026-02-14: Added pre-commit token auto-generation (`lint-staged` for `packages/design-tokens`) +- 2026-02-14: Added `@bytelyst/blob` shared package (Blob client helpers + SAS URL generation) ## Prereqs (Local) @@ -71,7 +72,7 @@ Publishing + repo hygiene - [ ] **7.1** Publish `@bytelyst/*` packages to GitHub Packages (private npm registry) - [ ] **7.2** Add Changesets for automated version management and changelogs - [ ] **7.3** Create reusable GitHub Actions workflow templates for service CI -- [ ] **7.4** Add `@bytelyst/blob` package (extract blob storage client + SAS generation) +- [x] **7.4** Add `@bytelyst/blob` package (extract blob storage client + SAS generation) - [ ] **7.5** Add `@bytelyst/monitoring` package (health check aggregator) - [ ] **7.7** Evaluate Python shared package for `cosmos_client.py` + `blob_client.py` if MindLyst adds Python backend - [ ] **7.8** Integrate `@bytelyst/design-tokens` into LysnrAI dashboards (unified design language) diff --git a/packages/blob/package.json b/packages/blob/package.json new file mode 100644 index 00000000..d8e479c1 --- /dev/null +++ b/packages/blob/package.json @@ -0,0 +1,23 @@ +{ + "name": "@bytelyst/blob", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "vitest run" + }, + "peerDependencies": { + "@azure/storage-blob": ">=12.0.0" + } +} diff --git a/packages/blob/src/__tests__/blob.test.ts b/packages/blob/src/__tests__/blob.test.ts new file mode 100644 index 00000000..67187bf3 --- /dev/null +++ b/packages/blob/src/__tests__/blob.test.ts @@ -0,0 +1,146 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { + MockBlobServiceClient, + mockFromConnectionString, + mockGetContainerClient, + MockStorageSharedKeyCredential, + mockGenerateBlobSASQueryParameters, +} = vi.hoisted(() => { + const mockGetContainerClient = vi.fn(() => ({ + createIfNotExists: vi.fn(), + })); + + const mockFromConnectionString = vi.fn(() => ({ + getContainerClient: mockGetContainerClient, + })); + + const MockBlobServiceClient = vi.fn(() => ({ + getContainerClient: mockGetContainerClient, + })) as unknown as { + new (...args: any[]): any; + fromConnectionString: typeof mockFromConnectionString; + }; + + (MockBlobServiceClient as any).fromConnectionString = mockFromConnectionString; + + const MockStorageSharedKeyCredential = vi.fn(() => ({ kind: 'shared-key-cred' })); + + const mockGenerateBlobSASQueryParameters = vi.fn(() => ({ + toString: () => 'sig=fake', + })); + + return { + MockBlobServiceClient, + mockFromConnectionString, + mockGetContainerClient, + MockStorageSharedKeyCredential, + mockGenerateBlobSASQueryParameters, + }; +}); + +vi.mock('@azure/storage-blob', () => ({ + BlobServiceClient: MockBlobServiceClient, + ContainerClient: class {}, + StorageSharedKeyCredential: MockStorageSharedKeyCredential, + BlobSASPermissions: class { + read = false; + write = false; + create = false; + delete = false; + }, + generateBlobSASQueryParameters: mockGenerateBlobSASQueryParameters, + SASProtocol: { Https: 'Https' }, +})); + +import { + _resetBlobClient, + generateSasUrl, + getBlobServiceClient, + getContainerClient, + isBlobStorageConfigured, +} from '../index.js'; + +describe('blob', () => { + beforeEach(() => { + _resetBlobClient(); + MockBlobServiceClient.mockClear(); + mockFromConnectionString.mockClear(); + mockGetContainerClient.mockClear(); + MockStorageSharedKeyCredential.mockClear(); + mockGenerateBlobSASQueryParameters.mockClear(); + + delete process.env.AZURE_BLOB_CONNECTION_STRING; + delete process.env.AZURE_BLOB_ACCOUNT_NAME; + delete process.env.AZURE_BLOB_ACCOUNT_KEY; + }); + + afterEach(() => { + delete process.env.AZURE_BLOB_CONNECTION_STRING; + delete process.env.AZURE_BLOB_ACCOUNT_NAME; + delete process.env.AZURE_BLOB_ACCOUNT_KEY; + }); + + it('isBlobStorageConfigured is false when unset', () => { + expect(isBlobStorageConfigured()).toBe(false); + }); + + it('isBlobStorageConfigured is true when connection string is set', () => { + process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=x;AccountKey=y;'; + expect(isBlobStorageConfigured()).toBe(true); + }); + + it('getBlobServiceClient uses connection string when present', () => { + process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=acc;AccountKey=key==;'; + const c1 = getBlobServiceClient(); + const c2 = getBlobServiceClient(); + expect(c1).toBe(c2); + expect(mockFromConnectionString).toHaveBeenCalledTimes(1); + expect(MockBlobServiceClient).not.toHaveBeenCalled(); // should not call constructor in this path + }); + + it('getBlobServiceClient uses account name + key when provided', () => { + process.env.AZURE_BLOB_ACCOUNT_NAME = 'acc'; + process.env.AZURE_BLOB_ACCOUNT_KEY = 'key=='; + + getBlobServiceClient(); + + expect(MockStorageSharedKeyCredential).toHaveBeenCalledWith('acc', 'key=='); + expect(MockBlobServiceClient).toHaveBeenCalledWith( + 'https://acc.blob.core.windows.net', + expect.anything() + ); + }); + + it('getBlobServiceClient throws when credentials missing', () => { + expect(() => getBlobServiceClient()).toThrow('Azure Blob Storage not configured'); + }); + + it('getContainerClient creates container and caches client', async () => { + process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=acc;AccountKey=key==;'; + + const c1 = await getContainerClient('audio'); + const c2 = await getContainerClient('audio'); + expect(c1).toBe(c2); + + expect(mockGetContainerClient).toHaveBeenCalledTimes(1); + expect(c1.createIfNotExists).toHaveBeenCalledTimes(1); + }); + + it('generateSasUrl uses parsed connection string when account vars not set', () => { + process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=acc;AccountKey=key==;'; + + const url = generateSasUrl('audio', 'path/file.wav', 'rw', 10); + expect(url).toContain('https://acc.blob.core.windows.net/audio/path/file.wav?'); + expect(mockGenerateBlobSASQueryParameters).toHaveBeenCalledTimes(1); + }); + + it('generateSasUrl prefers explicit account vars when provided', () => { + process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=fromconn;AccountKey=key==;'; + process.env.AZURE_BLOB_ACCOUNT_NAME = 'acc'; + process.env.AZURE_BLOB_ACCOUNT_KEY = 'key=='; + + const url = generateSasUrl('audio', 'x', 'r', 1); + expect(url).toContain('https://acc.blob.core.windows.net/audio/x?'); + }); +}); diff --git a/packages/blob/src/blob.ts b/packages/blob/src/blob.ts new file mode 100644 index 00000000..001c9a7d --- /dev/null +++ b/packages/blob/src/blob.ts @@ -0,0 +1,161 @@ +/** + * Shared Azure Blob Storage utilities. + * + * Provides singleton BlobServiceClient, container helpers, and SAS token generation. + * Containers are lazily created on first access (createIfNotExists). + * + * Expected env vars: + * AZURE_BLOB_CONNECTION_STRING — full connection string (preferred) + * — OR — + * AZURE_BLOB_ACCOUNT_NAME + AZURE_BLOB_ACCOUNT_KEY + */ + +import { + BlobServiceClient, + ContainerClient, + StorageSharedKeyCredential, + BlobSASPermissions, + generateBlobSASQueryParameters, + SASProtocol, +} from '@azure/storage-blob'; + +let serviceClient: BlobServiceClient | null = null; +const containerClients = new Map(); + +/** + * Known blob containers and their purposes. + * + * Note: This is a convenience list (not enforced). Products can add their own + * containers as needed. + */ +export const BLOB_CONTAINERS = { + audio: 'audio', // Dictation audio recordings + transcripts: 'transcripts', // Exported transcript files (PDF, DOCX, TXT) + attachments: 'attachments', // Tracker item attachments (screenshots, docs) + avatars: 'avatars', // User profile images + releases: 'releases', // Desktop app update binaries + backups: 'backups', // Cosmos DB JSON backups +} as const; + +export type BlobContainerName = (typeof BLOB_CONTAINERS)[keyof typeof BLOB_CONTAINERS]; + +/** + * Get or create the BlobServiceClient singleton. + */ +export function getBlobServiceClient(): BlobServiceClient { + if (serviceClient) return serviceClient; + + const connectionString = process.env.AZURE_BLOB_CONNECTION_STRING; + if (connectionString) { + serviceClient = BlobServiceClient.fromConnectionString(connectionString); + return serviceClient; + } + + const accountName = process.env.AZURE_BLOB_ACCOUNT_NAME; + const accountKey = process.env.AZURE_BLOB_ACCOUNT_KEY; + if (accountName && accountKey) { + const credential = new StorageSharedKeyCredential(accountName, accountKey); + serviceClient = new BlobServiceClient( + `https://${accountName}.blob.core.windows.net`, + credential + ); + return serviceClient; + } + + throw new Error( + 'Azure Blob Storage not configured. Set AZURE_BLOB_CONNECTION_STRING or AZURE_BLOB_ACCOUNT_NAME + AZURE_BLOB_ACCOUNT_KEY' + ); +} + +/** + * Get a container client, creating the container if it doesn't exist. + */ +export async function getContainerClient(containerName: string): Promise { + const cached = containerClients.get(containerName); + if (cached) return cached; + + const client = getBlobServiceClient().getContainerClient(containerName); + await client.createIfNotExists({ access: undefined }); // private by default + containerClients.set(containerName, client); + return client; +} + +/** + * Generate a SAS URL for direct browser upload (or download). + * + * @param containerName - Target container + * @param blobName - Full blob path (e.g., "product/user123/audio/recording.wav") + * @param permissions - SAS permissions (default: read) + * @param expiresInMinutes - Token lifetime (default: 60) + * @returns Full SAS URL for the blob + */ +export function generateSasUrl( + containerName: string, + blobName: string, + permissions: 'r' | 'w' | 'rw' | 'rwc' | 'rwd' = 'r', + expiresInMinutes = 60 +): string { + const connectionString = process.env.AZURE_BLOB_CONNECTION_STRING; + const accountName = process.env.AZURE_BLOB_ACCOUNT_NAME; + const accountKey = process.env.AZURE_BLOB_ACCOUNT_KEY; + + let credAccountName: string; + let credential: StorageSharedKeyCredential; + + if (accountName && accountKey) { + credAccountName = accountName; + credential = new StorageSharedKeyCredential(accountName, accountKey); + } else if (connectionString) { + // Parse account name and key from connection string + const nameMatch = connectionString.match(/AccountName=([^;]+)/); + const keyMatch = connectionString.match(/AccountKey=([^;]+)/); + if (!nameMatch || !keyMatch) { + throw new Error('Cannot parse AccountName/AccountKey from connection string'); + } + credAccountName = nameMatch[1]; + credential = new StorageSharedKeyCredential(nameMatch[1], keyMatch[1]); + } else { + throw new Error('Blob storage credentials not configured'); + } + + const sasPermissions = new BlobSASPermissions(); + if (permissions.includes('r')) sasPermissions.read = true; + if (permissions.includes('w')) sasPermissions.write = true; + if (permissions.includes('c')) sasPermissions.create = true; + if (permissions.includes('d')) sasPermissions.delete = true; + + const now = new Date(); + const expiresOn = new Date(now.getTime() + expiresInMinutes * 60 * 1000); + + const sasToken = generateBlobSASQueryParameters( + { + containerName, + blobName, + permissions: sasPermissions, + startsOn: now, + expiresOn, + protocol: SASProtocol.Https, + }, + credential + ).toString(); + + return `https://${credAccountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`; +} + +/** + * Check if blob storage is configured. + */ +export function isBlobStorageConfigured(): boolean { + return !!( + process.env.AZURE_BLOB_CONNECTION_STRING || + (process.env.AZURE_BLOB_ACCOUNT_NAME && process.env.AZURE_BLOB_ACCOUNT_KEY) + ); +} + +/** + * Test helper: reset module singletons/caches. + */ +export function _resetBlobClient(): void { + serviceClient = null; + containerClients.clear(); +} diff --git a/packages/blob/src/index.ts b/packages/blob/src/index.ts new file mode 100644 index 00000000..10b6d7c5 --- /dev/null +++ b/packages/blob/src/index.ts @@ -0,0 +1 @@ +export * from './blob.js'; diff --git a/packages/blob/tsconfig.json b/packages/blob/tsconfig.json new file mode 100644 index 00000000..5edad813 --- /dev/null +++ b/packages/blob/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8bc13b6..af4bc4b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,12 @@ importers: specifier: '>=5.0.0' version: 6.1.3 + packages/blob: + dependencies: + '@azure/storage-blob': + specifier: '>=12.0.0' + version: 12.31.0 + packages/config: dependencies: zod: @@ -335,6 +341,9 @@ importers: '@azure/storage-blob': specifier: ^12.31.0 version: 12.31.0 + '@bytelyst/blob': + specifier: workspace:* + version: link:../../packages/blob '@bytelyst/config': specifier: workspace:* version: link:../../packages/config diff --git a/services/platform-service/package.json b/services/platform-service/package.json index 4111699f..1b3b29c2 100644 --- a/services/platform-service/package.json +++ b/services/platform-service/package.json @@ -13,6 +13,7 @@ "lint": "eslint src/" }, "dependencies": { + "@bytelyst/blob": "workspace:*", "@bytelyst/config": "workspace:*", "@bytelyst/cosmos": "workspace:*", "@bytelyst/errors": "workspace:*", diff --git a/services/platform-service/src/lib/blob.ts b/services/platform-service/src/lib/blob.ts index cddf605f..98e07a0e 100644 --- a/services/platform-service/src/lib/blob.ts +++ b/services/platform-service/src/lib/blob.ts @@ -1,150 +1,8 @@ -/** - * Shared Azure Blob Storage client for the Platform Service. - * - * Provides singleton BlobServiceClient, container helpers, and SAS token generation. - * Containers are lazily created on first access (createIfNotExists). - * - * Expected env vars: - * AZURE_BLOB_CONNECTION_STRING — full connection string (preferred) - * — OR — - * AZURE_BLOB_ACCOUNT_NAME + AZURE_BLOB_ACCOUNT_KEY - */ - -import { - BlobServiceClient, - ContainerClient, - StorageSharedKeyCredential, - BlobSASPermissions, - generateBlobSASQueryParameters, - SASProtocol, -} from '@azure/storage-blob'; - -let serviceClient: BlobServiceClient | null = null; -const containerClients = new Map(); - -/** - * Known blob containers and their purposes. - */ -export const BLOB_CONTAINERS = { - audio: 'audio', // Dictation audio recordings - transcripts: 'transcripts', // Exported transcript files (PDF, DOCX, TXT) - attachments: 'attachments', // Tracker item attachments (screenshots, docs) - avatars: 'avatars', // User profile images - releases: 'releases', // Desktop app update binaries - backups: 'backups', // Cosmos DB JSON backups -} as const; - -export type BlobContainerName = (typeof BLOB_CONTAINERS)[keyof typeof BLOB_CONTAINERS]; - -/** - * Get or create the BlobServiceClient singleton. - */ -export function getBlobServiceClient(): BlobServiceClient { - if (serviceClient) return serviceClient; - - const connectionString = process.env.AZURE_BLOB_CONNECTION_STRING; - if (connectionString) { - serviceClient = BlobServiceClient.fromConnectionString(connectionString); - return serviceClient; - } - - const accountName = process.env.AZURE_BLOB_ACCOUNT_NAME; - const accountKey = process.env.AZURE_BLOB_ACCOUNT_KEY; - if (accountName && accountKey) { - const credential = new StorageSharedKeyCredential(accountName, accountKey); - serviceClient = new BlobServiceClient( - `https://${accountName}.blob.core.windows.net`, - credential - ); - return serviceClient; - } - - throw new Error( - 'Azure Blob Storage not configured. Set AZURE_BLOB_CONNECTION_STRING or AZURE_BLOB_ACCOUNT_NAME + AZURE_BLOB_ACCOUNT_KEY' - ); -} - -/** - * Get a container client, creating the container if it doesn't exist. - */ -export async function getContainerClient(containerName: string): Promise { - const cached = containerClients.get(containerName); - if (cached) return cached; - - const client = getBlobServiceClient().getContainerClient(containerName); - await client.createIfNotExists({ access: undefined }); // private by default - containerClients.set(containerName, client); - return client; -} - -/** - * Generate a SAS URL for direct browser upload (or download). - * - * @param containerName - Target container - * @param blobName - Full blob path (e.g., "lysnrai/user123/audio/recording.wav") - * @param permissions - SAS permissions (default: read) - * @param expiresInMinutes - Token lifetime (default: 60) - * @returns Full SAS URL for the blob - */ -export function generateSasUrl( - containerName: string, - blobName: string, - permissions: 'r' | 'w' | 'rw' | 'rwc' | 'rwd' = 'r', - expiresInMinutes = 60 -): string { - const connectionString = process.env.AZURE_BLOB_CONNECTION_STRING; - const accountName = process.env.AZURE_BLOB_ACCOUNT_NAME; - const accountKey = process.env.AZURE_BLOB_ACCOUNT_KEY; - - let credAccountName: string; - let credential: StorageSharedKeyCredential; - - if (accountName && accountKey) { - credAccountName = accountName; - credential = new StorageSharedKeyCredential(accountName, accountKey); - } else if (connectionString) { - // Parse account name and key from connection string - const nameMatch = connectionString.match(/AccountName=([^;]+)/); - const keyMatch = connectionString.match(/AccountKey=([^;]+)/); - if (!nameMatch || !keyMatch) { - throw new Error('Cannot parse AccountName/AccountKey from connection string'); - } - credAccountName = nameMatch[1]; - credential = new StorageSharedKeyCredential(nameMatch[1], keyMatch[1]); - } else { - throw new Error('Blob storage credentials not configured'); - } - - const sasPermissions = new BlobSASPermissions(); - if (permissions.includes('r')) sasPermissions.read = true; - if (permissions.includes('w')) sasPermissions.write = true; - if (permissions.includes('c')) sasPermissions.create = true; - if (permissions.includes('d')) sasPermissions.delete = true; - - const now = new Date(); - const expiresOn = new Date(now.getTime() + expiresInMinutes * 60 * 1000); - - const sasToken = generateBlobSASQueryParameters( - { - containerName, - blobName, - permissions: sasPermissions, - startsOn: now, - expiresOn, - protocol: SASProtocol.Https, - }, - credential - ).toString(); - - return `https://${credAccountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`; -} - -/** - * Check if blob storage is configured. - */ -export function isBlobStorageConfigured(): boolean { - return !!( - process.env.AZURE_BLOB_CONNECTION_STRING || - (process.env.AZURE_BLOB_ACCOUNT_NAME && process.env.AZURE_BLOB_ACCOUNT_KEY) - ); -} +export { + BLOB_CONTAINERS, + type BlobContainerName, + getBlobServiceClient, + getContainerClient, + generateSasUrl, + isBlobStorageConfigured, +} from '@bytelyst/blob';