Skip to content

Latest commit

 

History

History
1567 lines (1077 loc) · 38.2 KB

File metadata and controls

1567 lines (1077 loc) · 38.2 KB

API Reference

This document provides the complete API reference for git-cas.

Table of Contents

  1. ContentAddressableStore
  2. Vault
  3. CasService
  4. Events
  5. Value Objects
  6. Ports
  7. Codecs
  8. Error Codes

ContentAddressableStore

The main facade class providing high-level API for content-addressable storage.

Constructor

new ContentAddressableStore(options)

Parameters:

  • options.plumbing (required): Plumbing instance from @git-stunts/plumbing
  • options.chunkSize (optional): Chunk size in bytes (default: 262144 / 256 KiB)
  • options.codec (optional): CodecPort implementation (default: JsonCodec)
  • options.crypto (optional): CryptoPort implementation (default: auto-detected)
  • options.policy (optional): Resilience policy from @git-stunts/alfred for Git I/O
  • options.merkleThreshold (optional): Chunk count threshold for Merkle manifests (default: 1000)

Example:

import ContentAddressableStore from 'git-cas';
import Plumbing from '@git-stunts/plumbing';

const plumbing = await Plumbing.create({ repoPath: '/path/to/repo' });
const cas = new ContentAddressableStore({ plumbing });

Factory Methods

createJson

ContentAddressableStore.createJson({ plumbing, chunkSize, policy })

Creates a CAS instance with JSON codec.

Parameters:

  • plumbing (required): Plumbing instance
  • chunkSize (optional): Chunk size in bytes
  • policy (optional): Resilience policy

Returns: ContentAddressableStore

Example:

const cas = ContentAddressableStore.createJson({ plumbing });

createCbor

ContentAddressableStore.createCbor({ plumbing, chunkSize, policy })

Creates a CAS instance with CBOR codec.

Parameters:

  • plumbing (required): Plumbing instance
  • chunkSize (optional): Chunk size in bytes
  • policy (optional): Resilience policy

Returns: ContentAddressableStore

Example:

const cas = ContentAddressableStore.createCbor({ plumbing });

Methods

getService

await cas.getService()

Lazily initializes and returns the underlying CasService instance.

Returns: Promise<CasService>

Example:

const service = await cas.getService();

store

await cas.store({ source, slug, filename, encryptionKey, passphrase, kdfOptions, compression })

Stores content from an async iterable source.

Parameters:

  • source (required): AsyncIterable<Buffer> - Content stream
  • slug (required): string - Unique identifier for the asset
  • filename (required): string - Original filename
  • encryptionKey (optional): Buffer - 32-byte encryption key
  • passphrase (optional): string - Derive encryption key from passphrase (alternative to encryptionKey)
  • kdfOptions (optional): Object - KDF options when using passphrase ({ algorithm, iterations, cost, ... })
  • compression (optional): { algorithm: 'gzip' } - Enable compression before encryption/chunking

Returns: Promise<Manifest>

Throws:

  • CasError with code INVALID_KEY_TYPE if encryptionKey is not a Buffer
  • CasError with code INVALID_KEY_LENGTH if encryptionKey is not 32 bytes
  • CasError with code STREAM_ERROR if the source stream fails
  • CasError with code INVALID_OPTIONS if both passphrase and encryptionKey are provided
  • CasError with code INVALID_OPTIONS if an unsupported compression algorithm is specified

Example:

import { createReadStream } from 'node:fs';

const stream = createReadStream('/path/to/file.txt');
const manifest = await cas.store({
  source: stream,
  slug: 'my-asset',
  filename: 'file.txt'
});

storeFile

await cas.storeFile({ filePath, slug, filename, encryptionKey, passphrase, kdfOptions, compression })

Convenience method that opens a file and stores it.

Parameters:

  • filePath (required): string - Path to file
  • slug (required): string - Unique identifier for the asset
  • filename (optional): string - Filename (defaults to basename of filePath)
  • encryptionKey (optional): Buffer - 32-byte encryption key
  • passphrase (optional): string - Derive encryption key from passphrase
  • kdfOptions (optional): Object - KDF options when using passphrase
  • compression (optional): { algorithm: 'gzip' } - Enable compression

Returns: Promise<Manifest>

Throws: Same as store()

Example:

const manifest = await cas.storeFile({
  filePath: '/path/to/file.txt',
  slug: 'my-asset'
});

restore

await cas.restore({ manifest, encryptionKey, passphrase })

Restores content from a manifest and returns the buffer.

Parameters:

  • manifest (required): Manifest - Manifest object
  • encryptionKey (optional): Buffer - 32-byte encryption key (required if content is encrypted)
  • passphrase (optional): string - Passphrase for KDF-based decryption (alternative to encryptionKey)

Returns: Promise<{ buffer: Buffer, bytesWritten: number }>

Throws:

  • CasError with code MISSING_KEY if content is encrypted but no key provided
  • CasError with code INVALID_KEY_TYPE if encryptionKey is not a Buffer
  • CasError with code INVALID_KEY_LENGTH if encryptionKey is not 32 bytes
  • CasError with code INTEGRITY_ERROR if chunk digest verification fails
  • CasError with code INTEGRITY_ERROR if decryption fails
  • CasError with code INTEGRITY_ERROR if decompression fails
  • CasError with code INVALID_OPTIONS if both passphrase and encryptionKey are provided

Example:

const { buffer, bytesWritten } = await cas.restore({ manifest });

restoreFile

await cas.restoreFile({ manifest, encryptionKey, passphrase, outputPath })

Restores content from a manifest and writes it to a file.

Parameters:

  • manifest (required): Manifest - Manifest object
  • encryptionKey (optional): Buffer - 32-byte encryption key
  • passphrase (optional): string - Passphrase for KDF-based decryption
  • outputPath (required): string - Path to write the restored file

Returns: Promise<{ bytesWritten: number }>

Throws: Same as restore()

Example:

await cas.restoreFile({
  manifest,
  outputPath: '/path/to/output.txt'
});

createTree

await cas.createTree({ manifest })

Creates a Git tree object from a manifest.

Parameters:

  • manifest (required): Manifest - Manifest object

Returns: Promise<string> - Git tree OID

Example:

const treeOid = await cas.createTree({ manifest });

verifyIntegrity

await cas.verifyIntegrity(manifest)

Verifies the integrity of stored content by re-hashing all chunks.

Parameters:

  • manifest (required): Manifest - Manifest object

Returns: Promise<boolean> - True if all chunks pass verification

Example:

const isValid = await cas.verifyIntegrity(manifest);
if (!isValid) {
  console.log('Integrity check failed');
}

readManifest

await cas.readManifest({ treeOid })

Reads a Git tree, locates the manifest entry, decodes it, and returns a validated Manifest value object.

Parameters:

  • treeOid (required): string - Git tree OID

Returns: Promise<Manifest> - Frozen, Zod-validated Manifest

Throws:

  • CasError with code MANIFEST_NOT_FOUND if no manifest entry exists in the tree
  • CasError with code GIT_ERROR if the underlying Git command fails
  • Zod validation error if the manifest blob is corrupt

Example:

const treeOid = 'a1b2c3d4e5f6...';
const manifest = await cas.readManifest({ treeOid });
console.log(manifest.slug);      // "photos/vacation"
console.log(manifest.chunks);    // array of Chunk objects

deleteAsset

await cas.deleteAsset({ treeOid })

Returns logical deletion metadata for an asset. Does not perform any destructive Git operations — the caller must remove refs, and physical deletion requires git gc --prune.

Parameters:

  • treeOid (required): string - Git tree OID

Returns: Promise<{ slug: string, chunksOrphaned: number }>

Throws:

  • CasError with code MANIFEST_NOT_FOUND (delegates to readManifest)
  • CasError with code GIT_ERROR if the underlying Git command fails

Example:

const { slug, chunksOrphaned } = await cas.deleteAsset({ treeOid });
console.log(`Asset "${slug}" has ${chunksOrphaned} chunks to clean up`);
// Caller must remove refs pointing to treeOid; run `git gc --prune` to reclaim space

deriveKey

await cas.deriveKey(options)

Derives an encryption key from a passphrase using PBKDF2 or scrypt.

Parameters:

  • options.passphrase (required): string - The passphrase
  • options.salt (optional): Buffer - Salt (random if omitted)
  • options.algorithm (optional): 'pbkdf2' | 'scrypt' - KDF algorithm (default: 'pbkdf2')
  • options.iterations (optional): number - PBKDF2 iterations (default: 100000)
  • options.cost (optional): number - scrypt cost parameter N (default: 16384)
  • options.blockSize (optional): number - scrypt block size r (default: 8)
  • options.parallelization (optional): number - scrypt parallelization p (default: 1)
  • options.keyLength (optional): number - Derived key length (default: 32)

Returns: Promise<{ key: Buffer, salt: Buffer, params: Object }>

  • key — the derived 32-byte encryption key
  • salt — the salt used (save this for re-derivation)
  • params — full KDF parameters object (stored in manifest when using passphrase option)

Example:

const { key, salt, params } = await cas.deriveKey({
  passphrase: 'my secret passphrase',
  algorithm: 'pbkdf2',
  iterations: 200000,
});

// Use the derived key for encryption
const manifest = await cas.storeFile({
  filePath: '/path/to/file.txt',
  slug: 'my-asset',
  encryptionKey: key,
});

findOrphanedChunks

await cas.findOrphanedChunks({ treeOids })

Aggregates all chunk blob OIDs referenced across multiple assets and returns a report. Analysis only — does not delete or modify anything.

Parameters:

  • treeOids (required): Array<string> - Array of Git tree OIDs

Returns: Promise<{ referenced: Set<string>, total: number }>

  • referenced — deduplicated Set of all chunk blob OIDs across the given trees
  • total — total number of chunk references (before deduplication)

Throws:

  • CasError with code MANIFEST_NOT_FOUND if any treeOid lacks a manifest (fail closed)
  • CasError with code GIT_ERROR if the underlying Git command fails

Example:

const { referenced, total } = await cas.findOrphanedChunks({
  treeOids: [treeOid1, treeOid2, treeOid3]
});
console.log(`${referenced.size} unique blobs across ${total} total chunk references`);

encrypt

await cas.encrypt({ buffer, key })

Encrypts a buffer using AES-256-GCM.

Parameters:

  • buffer (required): Buffer - Data to encrypt
  • key (required): Buffer - 32-byte encryption key

Returns: Promise<{ buf: Buffer, meta: Object }>

Throws:

  • CasError with code INVALID_KEY_TYPE if key is not a Buffer
  • CasError with code INVALID_KEY_LENGTH if key is not 32 bytes

Example:

const { buf, meta } = await cas.encrypt({
  buffer: Buffer.from('secret data'),
  key: crypto.randomBytes(32)
});

decrypt

await cas.decrypt({ buffer, key, meta })

Decrypts a buffer using AES-256-GCM.

Parameters:

  • buffer (required): Buffer - Encrypted data
  • key (required): Buffer - 32-byte encryption key
  • meta (required): Object - Encryption metadata (from encrypt result)

Returns: Promise<Buffer> - Decrypted data

Throws:

  • CasError with code INTEGRITY_ERROR if decryption fails

Example:

const decrypted = await cas.decrypt({ buffer: buf, key, meta });

rotateKey

await cas.rotateKey({ manifest, oldKey, newKey, label })

Rotates a recipient's encryption key without re-encrypting data blobs. Unwraps the DEK with oldKey, re-wraps with newKey, and increments keyVersion counters.

Parameters:

  • manifest (required): Manifest - Envelope-encrypted manifest
  • oldKey (required): Buffer - Current 32-byte KEK
  • newKey (required): Buffer - New 32-byte KEK
  • label (optional): string - If provided, only rotate the named recipient

Returns: Promise<Manifest> - Updated manifest with re-wrapped DEK and incremented keyVersion

Throws:

  • CasError with code ROTATION_NOT_SUPPORTED if manifest has no recipients (legacy/unencrypted)
  • CasError with code RECIPIENT_NOT_FOUND if label doesn't exist
  • CasError with code DEK_UNWRAP_FAILED if oldKey doesn't match the recipient
  • CasError with code NO_MATCHING_RECIPIENT if no label is provided and oldKey matches no entry

Example:

const rotated = await cas.rotateKey({
  manifest, oldKey: aliceOldKey, newKey: aliceNewKey, label: 'alice',
});
const treeOid = await cas.createTree({ manifest: rotated });
await cas.addToVault({ slug: 'my-asset', treeOid, force: true });

rotateVaultPassphrase

await cas.rotateVaultPassphrase({ oldPassphrase, newPassphrase, kdfOptions })

Rotates the vault-level encryption passphrase. Re-wraps every envelope-encrypted entry's DEK with a new KEK derived from newPassphrase. Non-envelope entries are skipped.

Parameters:

  • oldPassphrase (required): string - Current vault passphrase
  • newPassphrase (required): string - New vault passphrase
  • kdfOptions (optional): Object - KDF options for new passphrase (e.g., { algorithm: 'scrypt' })

Returns: Promise<{ commitOid: string, rotatedSlugs: string[], skippedSlugs: string[] }>

Throws:

  • CasError with code VAULT_METADATA_INVALID if vault is not encrypted
  • CasError with code DEK_UNWRAP_FAILED or NO_MATCHING_RECIPIENT if old passphrase is wrong
  • CasError with code VAULT_CONFLICT if concurrent vault updates exhaust retries

Example:

const { commitOid, rotatedSlugs, skippedSlugs } = await cas.rotateVaultPassphrase({
  oldPassphrase: 'old-secret', newPassphrase: 'new-secret',
});
console.log(`Rotated: ${rotatedSlugs.join(', ')}`);
console.log(`Skipped: ${skippedSlugs.join(', ')}`);

Properties

chunkSize

cas.chunkSize

Returns the configured chunk size in bytes.

Type: number

Example:

console.log(cas.chunkSize); // 262144

Vault

The vault provides GC-safe storage by maintaining a single Git ref (refs/cas/vault) pointing to a commit chain. The commit's tree indexes all stored assets by slug. This prevents git gc from garbage-collecting stored data.

Vault Tree Structure

refs/cas/vault → commit → tree
                            ├── 100644 blob <oid>  .vault.json
                            ├── 040000 tree <oid>  demo/hello
                            ├── 040000 tree <oid>  photos/beach

Types

VaultEntry

interface VaultEntry {
  slug: string;
  treeOid: string;
}

VaultMetadata

interface VaultMetadata {
  version: number;
  encryption?: {
    cipher: string;
    kdf: {
      algorithm: string;
      salt: string;
      iterations?: number;
      cost?: number;
      blockSize?: number;
      parallelization?: number;
      keyLength: number;
    };
  };
}

Methods

initVault

await cas.initVault({ passphrase?, kdfOptions? })

Initializes the vault. Optionally configures vault-level encryption with a passphrase.

Parameters:

  • passphrase (optional): string - Passphrase for vault-level key derivation
  • kdfOptions (optional): Object - KDF options ({ algorithm, iterations, cost, ... })

Returns: Promise<{ commitOid: string }>

Throws:

  • CasError with code VAULT_ENCRYPTION_ALREADY_CONFIGURED if vault already has encryption

Example:

// Without encryption
await cas.initVault();

// With encryption
await cas.initVault({
  passphrase: 'my secret passphrase',
  kdfOptions: { algorithm: 'pbkdf2' },
});

addToVault

await cas.addToVault({ slug, treeOid, force? })

Adds an entry to the vault. Auto-initializes the vault if it doesn't exist.

Parameters:

  • slug (required): string - Entry slug (e.g., "demo/hello", "photos/beach-2024")
  • treeOid (required): string - Git tree OID
  • force (optional): boolean - Overwrite existing entry (default: false)

Returns: Promise<{ commitOid: string }>

Throws:

  • CasError with code INVALID_SLUG if slug fails validation
  • CasError with code VAULT_ENTRY_EXISTS if slug exists and force is false
  • CasError with code VAULT_CONFLICT if concurrent update detected after retries

Example:

const treeOid = await cas.createTree({ manifest });
await cas.addToVault({ slug: 'demo/hello', treeOid });

listVault

await cas.listVault()

Lists all vault entries sorted by slug.

Returns: Promise<VaultEntry[]>

Example:

const entries = await cas.listVault();
for (const { slug, treeOid } of entries) {
  console.log(`${slug}\t${treeOid}`);
}

removeFromVault

await cas.removeFromVault({ slug })

Removes an entry from the vault.

Parameters:

  • slug (required): string - Entry slug to remove

Returns: Promise<{ commitOid: string, removedTreeOid: string }>

Throws:

  • CasError with code VAULT_ENTRY_NOT_FOUND if slug does not exist

Example:

const { removedTreeOid } = await cas.removeFromVault({ slug: 'demo/hello' });

resolveVaultEntry

await cas.resolveVaultEntry({ slug })

Resolves a vault entry slug to its tree OID.

Parameters:

  • slug (required): string - Entry slug

Returns: Promise<string> - The tree OID

Throws:

  • CasError with code VAULT_ENTRY_NOT_FOUND if slug does not exist

Example:

const treeOid = await cas.resolveVaultEntry({ slug: 'demo/hello' });
const manifest = await cas.readManifest({ treeOid });

getVaultMetadata

await cas.getVaultMetadata()

Returns the vault metadata, or null if no vault exists.

Returns: Promise<VaultMetadata | null>

Example:

const metadata = await cas.getVaultMetadata();
if (metadata?.encryption) {
  console.log('Vault is encrypted with', metadata.encryption.kdf.algorithm);
}

Slug Validation

Slugs are validated with the following rules:

  • Must be a non-empty string
  • Must not start or end with /
  • Must not contain empty segments (a//b)
  • Must not contain . or .. segments
  • Must not contain control characters (0x00–0x1f, 0x7f)
  • Each segment must not exceed 255 bytes
  • Total slug must not exceed 1024 bytes

Vault-Level Encryption

When a vault is initialized with a passphrase, all store/restore operations through the vault derive the encryption key from the vault's KDF configuration:

// Initialize vault with encryption
await cas.initVault({ passphrase: 'secret' });

// Store with vault-level encryption (CLI derives key automatically)
// git-cas store file.txt --slug demo/hello --tree --vault-passphrase secret

// Restore with vault-level encryption
// git-cas restore --slug demo/hello --out file.txt --vault-passphrase secret

The vault stores the KDF parameters (algorithm, salt, iterations) in .vault.json — the passphrase is never stored.

CLI Vault Commands

git cas vault init                               # Initialize vault
git cas vault init --vault-passphrase "secret"   # With encryption
git cas vault list                               # List all entries
git cas vault info <slug>                        # Show slug + tree OID
git cas vault remove <slug>                      # Remove an entry
git cas vault history                            # Show commit history
git cas vault history -n 10                      # Last N commits
git cas vault rotate --old-passphrase "old" --new-passphrase "new"
git cas vault rotate --old-passphrase "old" --new-passphrase "new" --algorithm scrypt

CLI Key Rotation Commands

# Rotate a single asset's key (by vault slug)
git cas rotate --slug demo/hello \
  --old-key-file old.key --new-key-file new.key

# Rotate a single asset's key (by tree OID)
git cas rotate --oid <tree-oid> \
  --old-key-file old.key --new-key-file new.key

# Rotate only a named recipient
git cas rotate --slug demo/hello \
  --old-key-file old.key --new-key-file new.key --label alice

git cas rotate flags

Flag Description
--slug <slug> Resolve tree OID from vault slug (updates vault entry)
--oid <tree-oid> Direct tree OID (outputs updated manifest)
--old-key-file <path> Path to current 32-byte key file (required)
--new-key-file <path> Path to new 32-byte key file (required)
--label <label> Only rotate the named recipient entry
--cwd <dir> Git working directory (default: .)

git cas vault rotate flags

Flag Description
--old-passphrase <pass> Current vault passphrase (required)
--new-passphrase <pass> New vault passphrase (required)
--algorithm <alg> KDF algorithm for new passphrase (pbkdf2 or scrypt)
--cwd <dir> Git working directory (default: .)

Vault History

The vault maintains a full commit history via refs/cas/vault. Each mutation (add, remove, init) creates a new commit. Use vault history (or git log refs/cas/vault) to inspect the audit trail.

VaultService

Domain service for vault operations. Requires three ports:

  • persistence (GitPersistencePort) — blob/tree read/write
  • ref (GitRefPort) — ref resolution, commits, atomic updates
  • crypto (CryptoPort) — KDF for vault-level encryption
import { VaultService } from '@git-stunts/cas'; // or via facade
const vault = await cas.getVaultService();

CasService

Core domain service implementing CAS operations. Usually accessed via ContentAddressableStore, but can be used directly for advanced scenarios.

Constructor

new CasService({ persistence, codec, crypto, chunkSize, merkleThreshold })

Parameters:

  • persistence (required): GitPersistencePort implementation
  • codec (required): CodecPort implementation
  • crypto (required): CryptoPort implementation
  • chunkSize (optional): number - Chunk size in bytes (default: 262144, minimum: 1024)
  • merkleThreshold (optional): number - Chunk count threshold for Merkle manifests (default: 1000)

Throws:

  • Error if chunkSize is less than 1024 bytes
  • Error if merkleThreshold is not a positive integer

Example:

import CasService from 'git-cas/src/domain/services/CasService.js';
import GitPersistenceAdapter from 'git-cas/src/infrastructure/adapters/GitPersistenceAdapter.js';
import JsonCodec from 'git-cas/src/infrastructure/codecs/JsonCodec.js';
import NodeCryptoAdapter from 'git-cas/src/infrastructure/adapters/NodeCryptoAdapter.js';

const service = new CasService({
  persistence: new GitPersistenceAdapter({ plumbing }),
  codec: new JsonCodec(),
  crypto: new NodeCryptoAdapter(),
  chunkSize: 512 * 1024
});

Methods

All methods from ContentAddressableStore delegate to CasService. See ContentAddressableStore documentation above for:

  • store({ source, slug, filename, encryptionKey, passphrase, kdfOptions, compression })
  • restore({ manifest, encryptionKey, passphrase })
  • createTree({ manifest })
  • verifyIntegrity(manifest)
  • readManifest({ treeOid })
  • deleteAsset({ treeOid })
  • findOrphanedChunks({ treeOids })
  • encrypt({ buffer, key })
  • decrypt({ buffer, key, meta })
  • deriveKey(options)

EventEmitter

CasService extends Node.js EventEmitter. See Events section for all emitted events.

Events

CasService emits the following events. Listen using standard EventEmitter API:

const service = await cas.getService();
service.on('chunk:stored', (payload) => {
  console.log('Chunk stored:', payload);
});

chunk:stored

Emitted when a chunk is successfully stored.

Payload:

{
  index: number,      // Chunk index (0-based)
  size: number,       // Chunk size in bytes
  digest: string,     // SHA-256 hex digest (64 chars)
  blob: string        // Git blob OID
}

chunk:restored

Emitted when a chunk is successfully restored and verified.

Payload:

{
  index: number,      // Chunk index (0-based)
  size: number,       // Chunk size in bytes
  digest: string      // SHA-256 hex digest (64 chars)
}

file:stored

Emitted when a complete file is successfully stored.

Payload:

{
  slug: string,       // Asset slug
  size: number,       // Total file size in bytes
  chunkCount: number, // Number of chunks
  encrypted: boolean  // Whether content was encrypted
}

file:restored

Emitted when a complete file is successfully restored.

Payload:

{
  slug: string,       // Asset slug
  size: number,       // Total file size in bytes
  chunkCount: number  // Number of chunks
}

integrity:pass

Emitted when integrity verification passes for all chunks.

Payload:

{
  slug: string        // Asset slug
}

integrity:fail

Emitted when integrity verification fails for a chunk.

Payload:

{
  slug: string,       // Asset slug
  chunkIndex: number, // Failed chunk index
  expected: string,   // Expected SHA-256 digest
  actual: string      // Actual SHA-256 digest
}

error

Emitted when an error occurs during streaming operations (if listeners are registered).

Payload:

{
  code: string,       // CasError code
  message: string     // Error message
}

Value Objects

Manifest

Immutable value object representing a file manifest.

Constructor

new Manifest(data)

Parameters:

  • data.slug (required): string - Unique identifier (min length: 1)
  • data.filename (required): string - Original filename (min length: 1)
  • data.size (required): number - Total file size in bytes (>= 0)
  • data.chunks (required): Array<Object> - Chunk metadata array
  • data.encryption (optional): Object - Encryption metadata (may include kdf field for passphrase-derived keys)
  • data.version (optional): number - Manifest version (1 = flat, 2 = Merkle; default: 1)
  • data.compression (optional): Object - Compression metadata { algorithm: 'gzip' }
  • data.subManifests (optional): Array<Object> - Sub-manifest references (v2 Merkle manifests only)

Throws: Error if data does not match ManifestSchema

Example:

const manifest = new Manifest({
  slug: 'my-asset',
  filename: 'file.txt',
  size: 1024,
  chunks: [
    {
      index: 0,
      size: 1024,
      digest: 'a'.repeat(64),
      blob: 'abc123def456'
    }
  ]
});

Fields

  • slug: string - Asset identifier
  • filename: string - Original filename
  • size: number - Total file size
  • chunks: Array<Chunk> - Array of Chunk objects
  • encryption: Object | undefined - Encryption metadata (may include kdf sub-object)
  • version: number - Manifest version (1 or 2, default: 1)
  • compression: Object | undefined - Compression metadata { algorithm }
  • subManifests: Array | undefined - Sub-manifest references (v2 only)

Methods

toJSON
manifest.toJSON()

Returns a plain object representation suitable for serialization.

Returns: Object

Example:

const json = manifest.toJSON();
console.log(JSON.stringify(json, null, 2));

Chunk

Immutable value object representing a content chunk.

Constructor

new Chunk(data)

Parameters:

  • data.index (required): number - Chunk index (>= 0)
  • data.size (required): number - Chunk size in bytes (> 0)
  • data.digest (required): string - SHA-256 hex digest (exactly 64 chars)
  • data.blob (required): string - Git blob OID (min length: 1)

Throws: Error if data does not match ChunkSchema

Example:

const chunk = new Chunk({
  index: 0,
  size: 262144,
  digest: 'a'.repeat(64),
  blob: 'abc123def456'
});

Fields

  • index: number - Chunk index (0-based)
  • size: number - Chunk size in bytes
  • digest: string - SHA-256 hex digest
  • blob: string - Git blob OID

Ports

Ports define the interfaces for pluggable adapters. Implementations are provided but you can create custom adapters.

GitPersistencePort

Interface for Git persistence operations.

Methods

writeBlob
await port.writeBlob(content)

Writes content as a Git blob.

Parameters:

  • content: Buffer | string - Content to store

Returns: Promise<string> - Git blob OID

writeTree
await port.writeTree(entries)

Creates a Git tree object.

Parameters:

  • entries: Array<string> - Git mktree format lines (e.g., "100644 blob <oid>\t<name>")

Returns: Promise<string> - Git tree OID

readBlob
await port.readBlob(oid)

Reads a Git blob.

Parameters:

  • oid: string - Git blob OID

Returns: Promise<Buffer> - Blob content

readTree
await port.readTree(treeOid)

Reads a Git tree object.

Parameters:

  • treeOid: string - Git tree OID

Returns: Promise<Array<{ mode: string, type: string, oid: string, name: string }>>

Example Implementation:

import GitPersistencePort from 'git-cas/src/ports/GitPersistencePort.js';

class CustomGitAdapter extends GitPersistencePort {
  async writeBlob(content) {
    // Implementation
  }

  async writeTree(entries) {
    // Implementation
  }

  async readBlob(oid) {
    // Implementation
  }

  async readTree(treeOid) {
    // Implementation
  }
}

CodecPort

Interface for encoding/decoding manifest data.

Methods

encode
port.encode(data)

Encodes data to Buffer or string.

Parameters:

  • data: Object - Data to encode

Returns: Buffer | string - Encoded data

decode
port.decode(buffer)

Decodes data from Buffer or string.

Parameters:

  • buffer: Buffer | string - Encoded data

Returns: Object - Decoded data

Properties

extension
port.extension

File extension for this codec (e.g., 'json', 'cbor').

Returns: string

Example Implementation:

import CodecPort from 'git-cas/src/ports/CodecPort.js';

class XmlCodec extends CodecPort {
  encode(data) {
    return convertToXml(data);
  }

  decode(buffer) {
    return parseXml(buffer.toString('utf8'));
  }

  get extension() {
    return 'xml';
  }
}

CryptoPort

Interface for cryptographic operations.

Methods

sha256
port.sha256(buf)

Computes SHA-256 hash.

Parameters:

  • buf: Buffer - Data to hash

Returns: string - 64-character hex digest

randomBytes
port.randomBytes(n)

Generates cryptographically random bytes.

Parameters:

  • n: number - Number of bytes

Returns: Buffer - Random bytes

encryptBuffer
port.encryptBuffer(buffer, key)

Encrypts a buffer using AES-256-GCM.

Parameters:

  • buffer: Buffer - Data to encrypt
  • key: Buffer - 32-byte encryption key

Returns: { buf: Buffer, meta: { algorithm: string, nonce: string, tag: string, encrypted: boolean } }

decryptBuffer
port.decryptBuffer(buffer, key, meta)

Decrypts a buffer using AES-256-GCM.

Parameters:

  • buffer: Buffer - Encrypted data
  • key: Buffer - 32-byte encryption key
  • meta: Object - Encryption metadata with algorithm, nonce, tag, encrypted

Returns: Buffer - Decrypted data

Throws: On authentication failure

createEncryptionStream
port.createEncryptionStream(key)

Creates a streaming encryption context.

Parameters:

  • key: Buffer - 32-byte encryption key

Returns: { encrypt: Function, finalize: Function }

  • encrypt: (source: AsyncIterable<Buffer>) => AsyncIterable<Buffer> - Transform function
  • finalize: () => { algorithm: string, nonce: string, tag: string, encrypted: boolean } - Get metadata
deriveKey
await port.deriveKey(options)

Derives an encryption key from a passphrase using PBKDF2 or scrypt.

Parameters:

  • options.passphrase: string - The passphrase
  • options.salt (optional): Buffer - Salt (random if omitted)
  • options.algorithm (optional): 'pbkdf2' | 'scrypt' - KDF algorithm (default: 'pbkdf2')
  • options.iterations (optional): number - PBKDF2 iterations
  • options.cost (optional): number - scrypt cost N
  • options.blockSize (optional): number - scrypt block size r
  • options.parallelization (optional): number - scrypt parallelization p
  • options.keyLength (optional): number - Derived key length (default: 32)

Returns: Promise<{ key: Buffer, salt: Buffer, params: Object }>

Example Implementation:

import CryptoPort from 'git-cas/src/ports/CryptoPort.js';

class CustomCryptoAdapter extends CryptoPort {
  sha256(buf) {
    // Implementation
  }

  randomBytes(n) {
    // Implementation
  }

  encryptBuffer(buffer, key) {
    // Implementation
  }

  decryptBuffer(buffer, key, meta) {
    // Implementation
  }

  createEncryptionStream(key) {
    // Implementation
  }

  async deriveKey(options) {
    // Implementation
  }
}

Codecs

Built-in codec implementations.

JsonCodec

JSON codec for manifest serialization.

import { JsonCodec } from 'git-cas';

const codec = new JsonCodec();
const encoded = codec.encode({ key: 'value' });
const decoded = codec.decode(encoded);
console.log(codec.extension); // 'json'

CborCodec

CBOR codec for compact binary serialization.

import { CborCodec } from 'git-cas';

const codec = new CborCodec();
const encoded = codec.encode({ key: 'value' });
const decoded = codec.decode(encoded);
console.log(codec.extension); // 'cbor'

Error Codes

All errors thrown by git-cas are instances of CasError.

CasError

import CasError from 'git-cas/src/domain/errors/CasError.js';

Constructor

new CasError(message, code, meta)

Parameters:

  • message: string - Error message
  • code: string - Error code (see below)
  • meta: Object - Additional error context (default: {})

Fields

  • name: string - Always "CasError"
  • message: string - Error message
  • code: string - Error code
  • meta: Object - Additional context
  • stack: string - Stack trace

Error Codes

Code Description Thrown By
INVALID_KEY_TYPE Encryption key must be a Buffer or Uint8Array encrypt(), decrypt(), store(), restore()
INVALID_KEY_LENGTH Encryption key must be exactly 32 bytes encrypt(), decrypt(), store(), restore()
MISSING_KEY Encryption key required to restore encrypted content but none was provided restore()
INTEGRITY_ERROR Chunk digest verification failed or decryption authentication failed restore(), verifyIntegrity(), decrypt()
STREAM_ERROR Stream error occurred during store operation store()
MANIFEST_NOT_FOUND No manifest entry found in the Git tree readManifest(), deleteAsset(), findOrphanedChunks()
GIT_ERROR Underlying Git plumbing command failed readManifest(), deleteAsset(), findOrphanedChunks()
INVALID_OPTIONS Mutually exclusive options provided or unsupported option value store(), restore()
INVALID_SLUG Slug fails validation (empty, control chars, .. segments, etc.) addToVault()
VAULT_ENTRY_NOT_FOUND Slug does not exist in vault removeFromVault(), resolveVaultEntry()
VAULT_ENTRY_EXISTS Slug already exists (use force to overwrite) addToVault()
VAULT_CONFLICT Concurrent vault update detected (CAS failure after retries) addToVault(), removeFromVault(), initVault(), rotateVaultPassphrase()
VAULT_METADATA_INVALID .vault.json malformed, unknown version, or missing required fields readState(), rotateVaultPassphrase()
VAULT_ENCRYPTION_ALREADY_CONFIGURED Cannot reconfigure encryption without key rotation initVault()
NO_MATCHING_RECIPIENT No recipient entry matches the provided KEK restore(), rotateKey()
DEK_UNWRAP_FAILED Failed to unwrap DEK with the provided KEK addRecipient(), rotateKey()
RECIPIENT_NOT_FOUND Recipient label not found in manifest removeRecipient(), rotateKey()
RECIPIENT_ALREADY_EXISTS Recipient label already exists addRecipient()
CANNOT_REMOVE_LAST_RECIPIENT Cannot remove the last recipient removeRecipient()
ROTATION_NOT_SUPPORTED Key rotation requires envelope encryption (recipients) rotateKey()

Error Handling

Example:

import CasError from 'git-cas/src/domain/errors/CasError.js';

try {
  await cas.restore({ manifest, encryptionKey });
} catch (err) {
  if (err instanceof CasError) {
    console.error('CAS Error:', err.code);
    console.error('Message:', err.message);
    console.error('Meta:', err.meta);

    switch (err.code) {
      case 'MISSING_KEY':
        console.log('Content is encrypted - please provide a key');
        break;
      case 'INTEGRITY_ERROR':
        console.log('Content verification failed - may be corrupted');
        break;
      case 'INVALID_KEY_LENGTH':
        console.log('Key must be 32 bytes');
        break;
    }
  } else {
    throw err;
  }
}

Error Metadata

Different error codes include different metadata:

INVALID_KEY_LENGTH:

{
  expected: 32,
  actual: <number>
}

INTEGRITY_ERROR (chunk verification):

{
  chunkIndex: <number>,
  expected: <string>,  // Expected SHA-256 digest
  actual: <string>     // Actual SHA-256 digest
}

INTEGRITY_ERROR (decryption):

{
  originalError: <Error>
}

STREAM_ERROR:

{
  chunksWritten: <number>,
  originalError: <Error>
}