learning_ai_common_plat/packages/blob/src/blob.ts

162 lines
5.2 KiB
TypeScript

/**
* 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<string, ContainerClient>();
/**
* 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<ContainerClient> {
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();
}