# SkillPatch skill: azure-storage-blob-ts

This skill provides comprehensive guidance for using the Azure Blob Storage TypeScript/JavaScript SDK (@azure/storage-blob). It covers authentication methods (Entra token credentials, connection strings, shared key, SAS tokens), container and blob operations, and includes working code snippets for uploading, downloading, listing, and managing blobs. Designed to help agents perform Azure Blob Storage tasks end-to-end in a Node.js/TypeScript environment.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/azure-storage-blob-ts
curl -sSL https://skillpatch.dev/install_skill/azure-storage-blob-ts | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `references/sas-tokens.md`
- `references/streaming.md`


### `SKILL.md`

````markdown
---
name: azure-storage-blob-ts
description: |
  Azure Blob Storage JavaScript/TypeScript SDK (@azure/storage-blob) for blob operations. Use for uploading, downloading, listing, and managing blobs and containers. Supports block blobs, append blobs, page blobs, SAS tokens, and streaming. Triggers: "blob storage", "@azure/storage-blob", "BlobServiceClient", "ContainerClient", "upload blob", "download blob", "SAS token", "block blob".
license: MIT
metadata:
  author: Microsoft
  version: "1.0.0"
  package: '@azure/storage-blob'
---

# @azure/storage-blob (TypeScript/JavaScript)

SDK for Azure Blob Storage operations — upload, download, list, and manage blobs and containers.

## Installation

```bash
npm install @azure/storage-blob @azure/identity
```

**Current Version**: 12.x  
**Node.js**: >= 18.0.0

## Environment Variables

```bash
AZURE_STORAGE_ACCOUNT_NAME=<account-name>
AZURE_STORAGE_ACCOUNT_KEY=<account-key>
# OR connection string
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
AZURE_TOKEN_CREDENTIALS=prod # Required only if DefaultAzureCredential is used in production
```

## Authentication

### Microsoft Entra Token Credential (Recommended)

```typescript
import { BlobServiceClient } from "@azure/storage-blob";
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";

// Local dev: DefaultAzureCredential. Production: set AZURE_TOKEN_CREDENTIALS=prod or AZURE_TOKEN_CREDENTIALS=<specific_credential>
const credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]});
// Or use a specific credential directly in production:
// See https://learn.microsoft.com/javascript/api/overview/azure/identity-readme?view=azure-node-latest#credential-classes
// const credential = new ManagedIdentityCredential();

const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME!;
const client = new BlobServiceClient(
  `https://${accountName}.blob.core.windows.net`,
  credential
);
```

### Connection String

```typescript
import { BlobServiceClient } from "@azure/storage-blob";

const client = BlobServiceClient.fromConnectionString(
  process.env.AZURE_STORAGE_CONNECTION_STRING!
);
```

### StorageSharedKeyCredential (Node.js only)

```typescript
import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob";

const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME!;
const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY!;

const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);
const client = new BlobServiceClient(
  `https://${accountName}.blob.core.windows.net`,
  sharedKeyCredential
);
```

### SAS Token

```typescript
import { BlobServiceClient } from "@azure/storage-blob";

const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME!;
const sasToken = process.env.AZURE_STORAGE_SAS_TOKEN!; // starts with "?"

const client = new BlobServiceClient(
  `https://${accountName}.blob.core.windows.net${sasToken}`
);
```

## Client Hierarchy

```
BlobServiceClient (account level)
└── ContainerClient (container level)
    └── BlobClient (blob level)
        ├── BlockBlobClient (block blobs - most common)
        ├── AppendBlobClient (append-only blobs)
        └── PageBlobClient (page blobs - VHDs)
```

## Container Operations

### Create Container

```typescript
const containerClient = client.getContainerClient("my-container");
await containerClient.create();

// Or create if not exists
await containerClient.createIfNotExists();
```

### List Containers

```typescript
for await (const container of client.listContainers()) {
  console.log(container.name);
}

// With prefix filter
for await (const container of client.listContainers({ prefix: "logs-" })) {
  console.log(container.name);
}
```

### Delete Container

```typescript
await containerClient.delete();
// Or delete if exists
await containerClient.deleteIfExists();
```

## Blob Operations

### Upload Blob (Simple)

```typescript
const containerClient = client.getContainerClient("my-container");
const blockBlobClient = containerClient.getBlockBlobClient("my-file.txt");

// Upload string
await blockBlobClient.upload("Hello, World!", 13);

// Upload Buffer
const buffer = Buffer.from("Hello, World!");
await blockBlobClient.upload(buffer, buffer.length);
```

### Upload from File (Node.js only)

```typescript
const blockBlobClient = containerClient.getBlockBlobClient("uploaded-file.txt");
await blockBlobClient.uploadFile("/path/to/local/file.txt");
```

### Upload from Stream (Node.js only)

```typescript
import * as fs from "fs";

const blockBlobClient = containerClient.getBlockBlobClient("streamed-file.txt");
const readStream = fs.createReadStream("/path/to/local/file.txt");

await blockBlobClient.uploadStream(readStream, 4 * 1024 * 1024, 5, {
  // bufferSize: 4MB, maxConcurrency: 5
  onProgress: (progress) => console.log(`Uploaded ${progress.loadedBytes} bytes`),
});
```

### Upload from Browser

```typescript
const blockBlobClient = containerClient.getBlockBlobClient("browser-upload.txt");

// From File input
const fileInput = document.getElementById("fileInput") as HTMLInputElement;
const file = fileInput.files![0];
await blockBlobClient.uploadData(file);

// From Blob/ArrayBuffer
const arrayBuffer = new ArrayBuffer(1024);
await blockBlobClient.uploadData(arrayBuffer);
```

### Download Blob

```typescript
const blobClient = containerClient.getBlobClient("my-file.txt");
const downloadResponse = await blobClient.download();

// Read as string (browser & Node.js)
const downloaded = await streamToText(downloadResponse.readableStreamBody!);

async function streamToText(readable: NodeJS.ReadableStream): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of readable) {
    chunks.push(Buffer.from(chunk));
  }
  return Buffer.concat(chunks).toString("utf-8");
}
```

### Download to File (Node.js only)

```typescript
const blockBlobClient = containerClient.getBlockBlobClient("my-file.txt");
await blockBlobClient.downloadToFile("/path/to/local/destination.txt");
```

### Download to Buffer (Node.js only)

```typescript
const blockBlobClient = containerClient.getBlockBlobClient("my-file.txt");
const buffer = await blockBlobClient.downloadToBuffer();
console.log(buffer.toString());
```

### List Blobs

```typescript
// List all blobs
for await (const blob of containerClient.listBlobsFlat()) {
  console.log(blob.name, blob.properties.contentLength);
}

// List with prefix
for await (const blob of containerClient.listBlobsFlat({ prefix: "logs/" })) {
  console.log(blob.name);
}

// List by hierarchy (virtual directories)
for await (const item of containerClient.listBlobsByHierarchy("/")) {
  if (item.kind === "prefix") {
    console.log(`Directory: ${item.name}`);
  } else {
    console.log(`Blob: ${item.name}`);
  }
}
```

### Delete Blob

```typescript
const blobClient = containerClient.getBlobClient("my-file.txt");
await blobClient.delete();

// Delete if exists
await blobClient.deleteIfExists();

// Delete with snapshots
await blobClient.delete({ deleteSnapshots: "include" });
```

### Copy Blob

```typescript
const sourceBlobClient = containerClient.getBlobClient("source.txt");
const destBlobClient = containerClient.getBlobClient("destination.txt");

// Start copy operation
const copyPoller = await destBlobClient.beginCopyFromURL(sourceBlobClient.url);
await copyPoller.pollUntilDone();
```

## Blob Properties & Metadata

### Get Properties

```typescript
const blobClient = containerClient.getBlobClient("my-file.txt");
const properties = await blobClient.getProperties();

console.log("Content-Type:", properties.contentType);
console.log("Content-Length:", properties.contentLength);
console.log("Last Modified:", properties.lastModified);
console.log("ETag:", properties.etag);
```

### Set Metadata

```typescript
await blobClient.setMetadata({
  author: "John Doe",
  category: "documents",
});
```

### Set HTTP Headers

```typescript
await blobClient.setHTTPHeaders({
  blobContentType: "text/plain",
  blobCacheControl: "max-age=3600",
  blobContentDisposition: "attachment; filename=download.txt",
});
```

## SAS Token Generation (Node.js only)

### Generate Blob SAS

```typescript
import {
  BlobSASPermissions,
  generateBlobSASQueryParameters,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";

const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);

const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    blobName: "my-file.txt",
    permissions: BlobSASPermissions.parse("r"), // read only
    startsOn: new Date(),
    expiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour
  },
  sharedKeyCredential
).toString();

const sasUrl = `https://${accountName}.blob.core.windows.net/my-container/my-file.txt?${sasToken}`;
```

### Generate Container SAS

```typescript
import { ContainerSASPermissions, generateBlobSASQueryParameters } from "@azure/storage-blob";

const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    permissions: ContainerSASPermissions.parse("racwdl"), // read, add, create, write, delete, list
    expiresOn: new Date(Date.now() + 24 * 3600 * 1000), // 24 hours
  },
  sharedKeyCredential
).toString();
```

### Generate Account SAS

```typescript
import {
  AccountSASPermissions,
  AccountSASResourceTypes,
  AccountSASServices,
  generateAccountSASQueryParameters,
} from "@azure/storage-blob";

const sasToken = generateAccountSASQueryParameters(
  {
    services: AccountSASServices.parse("b").toString(), // blob
    resourceTypes: AccountSASResourceTypes.parse("sco").toString(), // service, container, object
    permissions: AccountSASPermissions.parse("rwdlacupi"), // all permissions
    expiresOn: new Date(Date.now() + 24 * 3600 * 1000),
  },
  sharedKeyCredential
).toString();
```

## Blob Types

### Block Blob (Default)

Most common type for text and binary files.

```typescript
const blockBlobClient = containerClient.getBlockBlobClient("document.pdf");
await blockBlobClient.uploadFile("/path/to/document.pdf");
```

### Append Blob

Optimized for append operations (logs, audit trails).

```typescript
const appendBlobClient = containerClient.getAppendBlobClient("app.log");

// Create the append blob
await appendBlobClient.create();

// Append data
await appendBlobClient.appendBlock("Log entry 1\n", 12);
await appendBlobClient.appendBlock("Log entry 2\n", 12);
```

### Page Blob

Fixed-size blobs for random read/write (VHDs).

```typescript
const pageBlobClient = containerClient.getPageBlobClient("disk.vhd");

// Create 512-byte aligned page blob
await pageBlobClient.create(1024 * 1024); // 1MB

// Write pages (must be 512-byte aligned)
const buffer = Buffer.alloc(512);
await pageBlobClient.uploadPages(buffer, 0, 512);
```

## Error Handling

```typescript
import { RestError } from "@azure/storage-blob";

try {
  await containerClient.create();
} catch (error) {
  if (error instanceof RestError) {
    switch (error.statusCode) {
      case 404:
        console.log("Container not found");
        break;
      case 409:
        console.log("Container already exists");
        break;
      case 403:
        console.log("Access denied");
        break;
      default:
        console.error(`Storage error ${error.statusCode}: ${error.message}`);
    }
  }
  throw error;
}
```

## TypeScript Types Reference

```typescript
import {
  // Clients
  BlobServiceClient,
  ContainerClient,
  BlobClient,
  BlockBlobClient,
  AppendBlobClient,
  PageBlobClient,

  // Authentication
  StorageSharedKeyCredential,
  AnonymousCredential,

  // SAS
  BlobSASPermissions,
  ContainerSASPermissions,
  AccountSASPermissions,
  AccountSASServices,
  AccountSASResourceTypes,
  generateBlobSASQueryParameters,
  generateAccountSASQueryParameters,

  // Options & Responses
  BlobDownloadResponseParsed,
  BlobUploadCommonResponse,
  ContainerCreateResponse,
  BlobItem,
  ContainerItem,

  // Errors
  RestError,
} from "@azure/storage-blob";
```

## Best Practices

1. **Use `DefaultAzureCredential` for local development; use `ManagedIdentityCredential` or `WorkloadIdentityCredential` for production**
2. **Use streaming for large files** — `uploadStream`/`downloadToFile` for files > 256MB
3. **Set appropriate content types** — Use `setHTTPHeaders` for correct MIME types
4. **Use SAS tokens for client access** — Generate short-lived tokens for browser uploads
5. **Handle errors gracefully** — Check `RestError.statusCode` for specific handling
6. **Use `*IfNotExists` methods** — For idempotent container/blob creation
7. **Close clients** — Not required but good practice in long-running apps

## Platform Differences

| Feature | Node.js | Browser |
|---------|---------|---------|
| `StorageSharedKeyCredential` | ✅ | ❌ |
| `uploadFile()` | ✅ | ❌ |
| `uploadStream()` | ✅ | ❌ |
| `downloadToFile()` | ✅ | ❌ |
| `downloadToBuffer()` | ✅ | ❌ |
| `uploadData()` | ✅ | ✅ |
| SAS generation | ✅ | ❌ |
| DefaultAzureCredential | ✅ | ❌ |
| Anonymous/SAS access | ✅ | ✅ |

````


### `references/sas-tokens.md`

````markdown
# @azure/storage-blob - SAS Token Patterns

Reference documentation for generating Shared Access Signatures (SAS) in the Azure Blob Storage TypeScript SDK.

**Source**: [Azure SDK for JS - storage-blob](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-blob)

---

## Installation

```bash
npm install @azure/storage-blob @azure/identity
```

---

## SAS Types Overview

| SAS Type | Scope | Use Case |
|----------|-------|----------|
| **Service SAS** | Single blob or container | Grant access to specific resources |
| **Account SAS** | Entire storage account | Grant broad access to multiple services |
| **User Delegation SAS** | Single blob or container | Most secure, uses Entra ID credentials |

---

## User Delegation SAS (Recommended)

Most secure option—uses Entra ID credentials instead of account keys.

### Generate User Delegation Key

```typescript
import {
  BlobServiceClient,
  generateBlobSASQueryParameters,
  BlobSASPermissions,
  SASProtocol,
} from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";

const credential = new DefaultAzureCredential();
const blobServiceClient = new BlobServiceClient(
  `https://${accountName}.blob.core.windows.net`,
  credential
);

// Get user delegation key (valid for up to 7 days)
const startsOn = new Date();
const expiresOn = new Date(startsOn.valueOf() + 3600 * 1000); // 1 hour

const userDelegationKey = await blobServiceClient.getUserDelegationKey(
  startsOn,
  expiresOn
);
```

### Generate User Delegation SAS for Blob

```typescript
const containerName = "my-container";
const blobName = "my-blob.txt";

const sasToken = generateBlobSASQueryParameters(
  {
    containerName,
    blobName,
    permissions: BlobSASPermissions.parse("r"), // read only
    startsOn,
    expiresOn,
    protocol: SASProtocol.Https,
  },
  userDelegationKey,
  accountName
).toString();

const sasUrl = `https://${accountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`;
console.log("SAS URL:", sasUrl);
```

---

## Service SAS (Account Key)

Uses storage account key. Less secure but simpler for some scenarios.

### Generate Blob SAS

```typescript
import {
  BlobServiceClient,
  generateBlobSASQueryParameters,
  BlobSASPermissions,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";

const accountName = process.env["STORAGE_ACCOUNT_NAME"]!;
const accountKey = process.env["STORAGE_ACCOUNT_KEY"]!;

const sharedKeyCredential = new StorageSharedKeyCredential(
  accountName,
  accountKey
);

const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    blobName: "my-blob.txt",
    permissions: BlobSASPermissions.parse("racwd"), // read, add, create, write, delete
    startsOn: new Date(),
    expiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour
  },
  sharedKeyCredential
).toString();

const sasUrl = `https://${accountName}.blob.core.windows.net/my-container/my-blob.txt?${sasToken}`;
```

### Generate Container SAS

```typescript
import {
  ContainerSASPermissions,
  generateBlobSASQueryParameters,
} from "@azure/storage-blob";

const containerSasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    permissions: ContainerSASPermissions.parse("rl"), // read, list
    startsOn: new Date(),
    expiresOn: new Date(Date.now() + 86400 * 1000), // 24 hours
  },
  sharedKeyCredential
).toString();

const containerSasUrl = `https://${accountName}.blob.core.windows.net/my-container?${containerSasToken}`;
```

---

## Account SAS

Grants access to multiple services (blob, queue, table, file).

```typescript
import {
  generateAccountSASQueryParameters,
  AccountSASPermissions,
  AccountSASServices,
  AccountSASResourceTypes,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";

const accountSasToken = generateAccountSASQueryParameters(
  {
    services: AccountSASServices.parse("btqf").toString(), // blob, table, queue, file
    resourceTypes: AccountSASResourceTypes.parse("sco").toString(), // service, container, object
    permissions: AccountSASPermissions.parse("rwdlacupi"), // all permissions
    startsOn: new Date(),
    expiresOn: new Date(Date.now() + 3600 * 1000),
    protocol: SASProtocol.Https,
  },
  sharedKeyCredential
).toString();

const accountSasUrl = `https://${accountName}.blob.core.windows.net?${accountSasToken}`;
```

---

## SAS Permissions

### Blob Permissions (`BlobSASPermissions`)

| Permission | Code | Description |
|------------|------|-------------|
| Read | `r` | Read blob content and metadata |
| Add | `a` | Add blocks to append blob |
| Create | `c` | Create new blob |
| Write | `w` | Write to blob |
| Delete | `d` | Delete blob |
| Delete Version | `x` | Delete blob version |
| Tag | `t` | Read/write blob tags |
| Move | `m` | Move blob |
| Execute | `e` | Execute (for DataLake) |
| Set Immutability | `i` | Set immutability policy |
| Permanent Delete | `y` | Permanently delete (soft-deleted) |

```typescript
// Parse from string
const permissions = BlobSASPermissions.parse("rwd");

// Build programmatically
const permissions = new BlobSASPermissions();
permissions.read = true;
permissions.write = true;
permissions.delete = true;
```

### Container Permissions (`ContainerSASPermissions`)

Same as blob permissions, plus:

| Permission | Code | Description |
|------------|------|-------------|
| List | `l` | List blobs in container |

```typescript
const permissions = ContainerSASPermissions.parse("rl"); // read and list
```

### Account Permissions (`AccountSASPermissions`)

| Permission | Code | Description |
|------------|------|-------------|
| Read | `r` | Read |
| Write | `w` | Write |
| Delete | `d` | Delete |
| Delete Version | `x` | Delete version |
| List | `l` | List |
| Add | `a` | Add |
| Create | `c` | Create |
| Update | `u` | Update |
| Process | `p` | Process messages |
| Tag | `t` | Tags |
| Filter | `f` | Filter by tags |
| Set Immutability | `i` | Immutability policy |

---

## SAS Options

### Content Headers

Override response headers:

```typescript
const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    blobName: "document.pdf",
    permissions: BlobSASPermissions.parse("r"),
    expiresOn: new Date(Date.now() + 3600 * 1000),
    
    // Override response headers
    contentDisposition: "attachment; filename=download.pdf",
    contentType: "application/pdf",
    cacheControl: "max-age=3600",
    contentEncoding: "gzip",
    contentLanguage: "en-US",
  },
  sharedKeyCredential
).toString();
```

### IP Restrictions

Restrict SAS to specific IP addresses:

```typescript
const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    blobName: "my-blob.txt",
    permissions: BlobSASPermissions.parse("r"),
    expiresOn: new Date(Date.now() + 3600 * 1000),
    
    // Single IP
    ipRange: { start: "168.1.5.60" },
    
    // IP range
    // ipRange: { start: "168.1.5.60", end: "168.1.5.70" },
  },
  sharedKeyCredential
).toString();
```

### Protocol Restriction

Require HTTPS:

```typescript
import { SASProtocol } from "@azure/storage-blob";

const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    blobName: "my-blob.txt",
    permissions: BlobSASPermissions.parse("r"),
    expiresOn: new Date(Date.now() + 3600 * 1000),
    protocol: SASProtocol.Https, // HTTPS only
    // protocol: SASProtocol.HttpsAndHttp, // Allow both
  },
  sharedKeyCredential
).toString();
```

### Blob Versioning

Access specific blob version or snapshot:

```typescript
const sasToken = generateBlobSASQueryParameters(
  {
    containerName: "my-container",
    blobName: "my-blob.txt",
    permissions: BlobSASPermissions.parse("r"),
    expiresOn: new Date(Date.now() + 3600 * 1000),
    
    // For snapshots
    snapshotTime: "2023-01-15T10:30:00.0000000Z",
    
    // For versions
    // versionId: "2023-01-15T10:30:00.0000000Z",
  },
  sharedKeyCredential
).toString();
```

---

## Using SAS URLs

### Download with SAS

```typescript
// Client-side (browser or Node.js)
const response = await fetch(sasUrl);
const blob = await response.blob();
```

### Upload with SAS (Write Permission)

```typescript
const sasUrlWithWrite = `https://${accountName}.blob.core.windows.net/container/blob.txt?${writeSasToken}`;

await fetch(sasUrlWithWrite, {
  method: "PUT",
  headers: {
    "x-ms-blob-type": "BlockBlob",
    "Content-Type": "text/plain",
  },
  body: "Hello, World!",
});
```

### Create Anonymous Client with SAS

```typescript
import { BlobClient } from "@azure/storage-blob";

// No credential needed - SAS in URL
const blobClient = new BlobClient(sasUrl);
const downloadResponse = await blobClient.download(0);
```

---

## Complete Example

```typescript
import {
  BlobServiceClient,
  generateBlobSASQueryParameters,
  BlobSASPermissions,
  ContainerSASPermissions,
  SASProtocol,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";

async function generateSasTokens() {
  const accountName = process.env["STORAGE_ACCOUNT_NAME"]!;
  const accountKey = process.env["STORAGE_ACCOUNT_KEY"]!;
  
  // Method 1: User Delegation SAS (recommended)
  const credential = new DefaultAzureCredential();
  const blobServiceClient = new BlobServiceClient(
    `https://${accountName}.blob.core.windows.net`,
    credential
  );
  
  const startsOn = new Date();
  const expiresOn = new Date(startsOn.valueOf() + 3600 * 1000);
  
  const userDelegationKey = await blobServiceClient.getUserDelegationKey(
    startsOn,
    expiresOn
  );
  
  const userDelegationSas = generateBlobSASQueryParameters(
    {
      containerName: "uploads",
      blobName: "user-file.txt",
      permissions: BlobSASPermissions.parse("r"),
      startsOn,
      expiresOn,
      protocol: SASProtocol.Https,
    },
    userDelegationKey,
    accountName
  ).toString();
  
  console.log("User Delegation SAS URL:");
  console.log(`https://${accountName}.blob.core.windows.net/uploads/user-file.txt?${userDelegationSas}`);
  
  // Method 2: Service SAS with account key
  const sharedKeyCredential = new StorageSharedKeyCredential(
    accountName,
    accountKey
  );
  
  // Read-only SAS for download
  const readSas = generateBlobSASQueryParameters(
    {
      containerName: "public-files",
      blobName: "document.pdf",
      permissions: BlobSASPermissions.parse("r"),
      expiresOn: new Date(Date.now() + 86400 * 1000), // 24 hours
      contentDisposition: "attachment; filename=download.pdf",
      protocol: SASProtocol.Https,
    },
    sharedKeyCredential
  ).toString();
  
  console.log("\nRead SAS URL:");
  console.log(`https://${accountName}.blob.core.windows.net/public-files/document.pdf?${readSas}`);
  
  // Write SAS for upload
  const writeSas = generateBlobSASQueryParameters(
    {
      containerName: "uploads",
      blobName: "new-upload.txt",
      permissions: BlobSASPermissions.parse("cw"), // create, write
      expiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour
      protocol: SASProtocol.Https,
    },
    sharedKeyCredential
  ).toString();
  
  console.log("\nWrite SAS URL:");
  console.log(`https://${accountName}.blob.core.windows.net/uploads/new-upload.txt?${writeSas}`);
  
  // Container list SAS
  const listSas = generateBlobSASQueryParameters(
    {
      containerName: "public-files",
      permissions: ContainerSASPermissions.parse("rl"), // read, list
      expiresOn: new Date(Date.now() + 3600 * 1000),
      protocol: SASProtocol.Https,
    },
    sharedKeyCredential
  ).toString();
  
  console.log("\nContainer List SAS URL:");
  console.log(`https://${accountName}.blob.core.windows.net/public-files?${listSas}`);
}

generateSasTokens().catch(console.error);
```

---

## Best Practices

1. **Prefer User Delegation SAS** - More secure, tied to Entra ID, auditable
2. **Use shortest possible expiry** - Minimize exposure window
3. **Use HTTPS only** - Set `protocol: SASProtocol.Https`
4. **Apply least privilege** - Grant only necessary permissions
5. **Use IP restrictions** - When client IPs are known
6. **Rotate keys regularly** - If using account key SAS
7. **Store SAS securely** - Never expose in client-side code permanently
8. **Set start time** - Prevent "not yet valid" errors due to clock skew
9. **Use stored access policies** - For container-level SAS management

---

## Security Considerations

| Risk | Mitigation |
|------|------------|
| SAS token leaked | Short expiry, IP restrictions, HTTPS only |
| Over-privileged access | Least privilege permissions |
| Account key compromise | Use User Delegation SAS instead |
| Clock skew issues | Set `startsOn` slightly in the past |
| Token reuse | Use unique blob names or short expiry |

---

## See Also

- [Streaming Patterns](./streaming.md) - Upload/download with SAS
- [Official Documentation](https://learn.microsoft.com/azure/storage/common/storage-sas-overview)

````


### `references/streaming.md`

````markdown
# @azure/storage-blob - Streaming Patterns

Reference documentation for upload/download streaming in the Azure Blob Storage TypeScript SDK.

**Source**: [Azure SDK for JS - storage-blob](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-blob)

---

## Installation

```bash
npm install @azure/storage-blob @azure/identity
```

---

## Client Setup

```typescript
import {
  BlobServiceClient,
  ContainerClient,
  BlockBlobClient,
} from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";

const credential = new DefaultAzureCredential();
const accountUrl = `https://${process.env["STORAGE_ACCOUNT_NAME"]}.blob.core.windows.net`;

const blobServiceClient = new BlobServiceClient(accountUrl, credential);
const containerClient = blobServiceClient.getContainerClient("my-container");
const blobClient = containerClient.getBlockBlobClient("my-blob.txt");
```

---

## Download Streaming

### Download to Buffer

```typescript
const downloadResponse = await blobClient.download(0);
const downloaded = await streamToBuffer(downloadResponse.readableStreamBody!);

async function streamToBuffer(readableStream: NodeJS.ReadableStream): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    readableStream.on("data", (data) => chunks.push(Buffer.from(data)));
    readableStream.on("end", () => resolve(Buffer.concat(chunks)));
    readableStream.on("error", reject);
  });
}
```

### Download to File (Node.js)

```typescript
import fs from "node:fs";
import { pipeline } from "node:stream/promises";

const downloadResponse = await blobClient.download(0);
await pipeline(
  downloadResponse.readableStreamBody!,
  fs.createWriteStream("./local-file.txt")
);
```

### Download with Range

```typescript
// Download bytes 100-199 (100 bytes total)
const downloadResponse = await blobClient.download(100, 100);
```

### Download with Progress

```typescript
const downloadResponse = await blobClient.download(0, undefined, {
  onProgress: (progress) => {
    console.log(`Downloaded ${progress.loadedBytes} bytes`);
  },
});
```

---

## Upload Streaming

### Upload from Buffer

```typescript
const content = Buffer.from("Hello, World!");
await blobClient.upload(content, content.length);
```

### Upload from Stream (Node.js)

```typescript
import fs from "node:fs";

const fileStream = fs.createReadStream("./large-file.zip");
const fileSize = fs.statSync("./large-file.zip").size;

await blobClient.uploadStream(fileStream, fileSize, 4, {
  onProgress: (progress) => {
    const percent = ((progress.loadedBytes / fileSize) * 100).toFixed(2);
    console.log(`Upload progress: ${percent}%`);
  },
});
```

### Upload with Options

```typescript
await blobClient.uploadStream(fileStream, fileSize, 4, {
  // Number of concurrent upload operations
  concurrency: 4,
  
  // Size of each block (4MB default, max 4000MB)
  bufferSize: 4 * 1024 * 1024,
  
  // Progress tracking
  onProgress: (progress) => {
    console.log(`Uploaded: ${progress.loadedBytes} bytes`);
  },
  
  // HTTP headers
  blobHTTPHeaders: {
    blobContentType: "application/zip",
    blobContentEncoding: "gzip",
  },
  
  // Custom metadata
  metadata: {
    uploadedBy: "my-app",
    version: "1.0",
  },
  
  // Access tier
  tier: "Cool",
});
```

---

## Block Upload (Large Files)

For large files, use staged block uploads for better control:

```typescript
import { BlockBlobClient, BlockBlobStageBlockOptions } from "@azure/storage-blob";
import { v4 as uuidv4 } from "uuid";

async function uploadLargeFile(
  blobClient: BlockBlobClient,
  filePath: string,
  blockSize = 4 * 1024 * 1024 // 4MB blocks
) {
  const fileHandle = await fs.promises.open(filePath, "r");
  const fileStats = await fileHandle.stat();
  const fileSize = fileStats.size;
  
  const blockIds: string[] = [];
  let offset = 0;
  let blockIndex = 0;
  
  try {
    while (offset < fileSize) {
      const chunkSize = Math.min(blockSize, fileSize - offset);
      const buffer = Buffer.alloc(chunkSize);
      
      await fileHandle.read(buffer, 0, chunkSize, offset);
      
      // Generate block ID (must be base64 encoded, same length)
      const blockId = Buffer.from(
        blockIndex.toString().padStart(6, "0")
      ).toString("base64");
      
      // Stage the block
      await blobClient.stageBlock(blockId, buffer, buffer.length);
      
      blockIds.push(blockId);
      offset += chunkSize;
      blockIndex++;
      
      console.log(`Staged block ${blockIndex}, offset: ${offset}/${fileSize}`);
    }
    
    // Commit all blocks
    await blobClient.commitBlockList(blockIds, {
      blobHTTPHeaders: {
        blobContentType: "application/octet-stream",
      },
    });
    
    console.log("Upload complete!");
  } finally {
    await fileHandle.close();
  }
}
```

---

## Parallel Upload/Download

### Parallel Download to File

```typescript
import { BlobClient } from "@azure/storage-blob";

// Downloads in parallel chunks automatically
await blobClient.downloadToFile("./local-file.zip", 0, undefined, {
  // Chunk size for parallel download
  blockSize: 4 * 1024 * 1024,
  
  // Number of parallel downloads
  concurrency: 4,
  
  onProgress: (progress) => {
    console.log(`Downloaded: ${progress.loadedBytes} bytes`);
  },
});
```

### Parallel Upload from File

```typescript
// Uploads in parallel chunks automatically
await blobClient.uploadFile("./large-file.zip", {
  // Chunk size
  blockSize: 4 * 1024 * 1024,
  
  // Parallel uploads
  concurrency: 4,
  
  onProgress: (progress) => {
    console.log(`Uploaded: ${progress.loadedBytes} bytes`);
  },
  
  blobHTTPHeaders: {
    blobContentType: "application/zip",
  },
});
```

---

## Browser Upload

For browser environments, use `uploadBrowserData`:

```typescript
// From File input
const fileInput = document.getElementById("file") as HTMLInputElement;
const file = fileInput.files![0];

await blobClient.uploadBrowserData(file, {
  onProgress: (progress) => {
    const percent = ((progress.loadedBytes / file.size) * 100).toFixed(2);
    document.getElementById("progress")!.textContent = `${percent}%`;
  },
  blobHTTPHeaders: {
    blobContentType: file.type,
  },
});

// From ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
await blobClient.uploadBrowserData(arrayBuffer);

// From Blob
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
await blobClient.uploadBrowserData(blob);
```

---

## Abort Operations

Cancel long-running uploads/downloads:

```typescript
const abortController = new AbortController();

// Set up abort after 30 seconds
setTimeout(() => abortController.abort(), 30000);

try {
  await blobClient.uploadFile("./large-file.zip", {
    abortSignal: abortController.signal,
    onProgress: (progress) => {
      console.log(`Progress: ${progress.loadedBytes}`);
    },
  });
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Upload was cancelled");
  } else {
    throw error;
  }
}

// Manual abort
document.getElementById("cancel")?.addEventListener("click", () => {
  abortController.abort();
});
```

---

## Copy Operations

### Copy from URL

```typescript
const sourceUrl = "https://source-account.blob.core.windows.net/container/blob";

// Start async copy
const copyPoller = await blobClient.beginCopyFromURL(sourceUrl);

// Wait for completion
const result = await copyPoller.pollUntilDone();
console.log(`Copy completed: ${result.copyStatus}`);

// Or copy synchronously (for small blobs)
await blobClient.syncCopyFromURL(sourceUrl);
```

### Copy with Progress

```typescript
const copyPoller = await blobClient.beginCopyFromURL(sourceUrl, {
  onProgress: (state) => {
    if (state.copyProgress) {
      const [copied, total] = state.copyProgress.split("/").map(Number);
      console.log(`Copied: ${copied}/${total} bytes`);
    }
  },
});
```

---

## Complete Example

```typescript
import {
  BlobServiceClient,
  ContainerClient,
  BlockBlobClient,
} from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";
import fs from "node:fs";
import { pipeline } from "node:stream/promises";

async function uploadAndDownload() {
  const credential = new DefaultAzureCredential();
  const blobServiceClient = new BlobServiceClient(
    `https://${process.env["STORAGE_ACCOUNT_NAME"]}.blob.core.windows.net`,
    credential
  );

  const containerClient = blobServiceClient.getContainerClient("demo");
  await containerClient.createIfNotExists();

  const blobClient = containerClient.getBlockBlobClient("test-file.txt");

  // Upload with progress
  console.log("Uploading...");
  const uploadData = Buffer.from("Hello, Azure Blob Storage!");
  await blobClient.upload(uploadData, uploadData.length, {
    onProgress: (progress) => {
      console.log(`Upload progress: ${progress.loadedBytes} bytes`);
    },
  });
  console.log("Upload complete!");

  // Download with progress
  console.log("Downloading...");
  const downloadResponse = await blobClient.download(0, undefined, {
    onProgress: (progress) => {
      console.log(`Download progress: ${progress.loadedBytes} bytes`);
    },
  });

  // Stream to file
  await pipeline(
    downloadResponse.readableStreamBody!,
    fs.createWriteStream("./downloaded-file.txt")
  );
  console.log("Download complete!");

  // Get blob properties
  const properties = await blobClient.getProperties();
  console.log(`Blob size: ${properties.contentLength} bytes`);
  console.log(`Content type: ${properties.contentType}`);
  console.log(`Last modified: ${properties.lastModified}`);

  // Cleanup
  await blobClient.delete();
  console.log("Blob deleted");
}

uploadAndDownload().catch(console.error);
```

---

## Best Practices

1. **Use parallel operations** - `uploadFile`/`downloadToFile` automatically parallelize
2. **Set appropriate block sizes** - 4-8MB for most scenarios, larger for huge files
3. **Implement progress tracking** - Use `onProgress` for user feedback
4. **Handle abort signals** - Allow users to cancel long operations
5. **Set content types** - Always set `blobContentType` for proper browser handling
6. **Use streams for large files** - Avoid loading entire files into memory
7. **Implement retry logic** - SDK has built-in retries, configure as needed

---

## See Also

- [SAS Token Patterns](./sas-tokens.md) - Generate secure access tokens
- [Official Samples](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-blob/samples)

````
