This document provides the complete API reference for git-cas.
The main facade class providing high-level API for content-addressable storage.
new ContentAddressableStore(options)Parameters:
options.plumbing(required): Plumbing instance from@git-stunts/plumbingoptions.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/alfredfor Git I/Ooptions.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 });ContentAddressableStore.createJson({ plumbing, chunkSize, policy })Creates a CAS instance with JSON codec.
Parameters:
plumbing(required): Plumbing instancechunkSize(optional): Chunk size in bytespolicy(optional): Resilience policy
Returns: ContentAddressableStore
Example:
const cas = ContentAddressableStore.createJson({ plumbing });ContentAddressableStore.createCbor({ plumbing, chunkSize, policy })Creates a CAS instance with CBOR codec.
Parameters:
plumbing(required): Plumbing instancechunkSize(optional): Chunk size in bytespolicy(optional): Resilience policy
Returns: ContentAddressableStore
Example:
const cas = ContentAddressableStore.createCbor({ plumbing });await cas.getService()Lazily initializes and returns the underlying CasService instance.
Returns: Promise<CasService>
Example:
const service = await cas.getService();await cas.store({ source, slug, filename, encryptionKey, passphrase, kdfOptions, compression })Stores content from an async iterable source.
Parameters:
source(required):AsyncIterable<Buffer>- Content streamslug(required):string- Unique identifier for the assetfilename(required):string- Original filenameencryptionKey(optional):Buffer- 32-byte encryption keypassphrase(optional):string- Derive encryption key from passphrase (alternative toencryptionKey)kdfOptions(optional):Object- KDF options when usingpassphrase({ algorithm, iterations, cost, ... })compression(optional):{ algorithm: 'gzip' }- Enable compression before encryption/chunking
Returns: Promise<Manifest>
Throws:
CasErrorwith codeINVALID_KEY_TYPEif encryptionKey is not a BufferCasErrorwith codeINVALID_KEY_LENGTHif encryptionKey is not 32 bytesCasErrorwith codeSTREAM_ERRORif the source stream failsCasErrorwith codeINVALID_OPTIONSif bothpassphraseandencryptionKeyare providedCasErrorwith codeINVALID_OPTIONSif 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'
});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 fileslug(required):string- Unique identifier for the assetfilename(optional):string- Filename (defaults to basename of filePath)encryptionKey(optional):Buffer- 32-byte encryption keypassphrase(optional):string- Derive encryption key from passphrasekdfOptions(optional):Object- KDF options when usingpassphrasecompression(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'
});await cas.restore({ manifest, encryptionKey, passphrase })Restores content from a manifest and returns the buffer.
Parameters:
manifest(required):Manifest- Manifest objectencryptionKey(optional):Buffer- 32-byte encryption key (required if content is encrypted)passphrase(optional):string- Passphrase for KDF-based decryption (alternative toencryptionKey)
Returns: Promise<{ buffer: Buffer, bytesWritten: number }>
Throws:
CasErrorwith codeMISSING_KEYif content is encrypted but no key providedCasErrorwith codeINVALID_KEY_TYPEif encryptionKey is not a BufferCasErrorwith codeINVALID_KEY_LENGTHif encryptionKey is not 32 bytesCasErrorwith codeINTEGRITY_ERRORif chunk digest verification failsCasErrorwith codeINTEGRITY_ERRORif decryption failsCasErrorwith codeINTEGRITY_ERRORif decompression failsCasErrorwith codeINVALID_OPTIONSif bothpassphraseandencryptionKeyare provided
Example:
const { buffer, bytesWritten } = await cas.restore({ manifest });await cas.restoreFile({ manifest, encryptionKey, passphrase, outputPath })Restores content from a manifest and writes it to a file.
Parameters:
manifest(required):Manifest- Manifest objectencryptionKey(optional):Buffer- 32-byte encryption keypassphrase(optional):string- Passphrase for KDF-based decryptionoutputPath(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'
});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 });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');
}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:
CasErrorwith codeMANIFEST_NOT_FOUNDif no manifest entry exists in the treeCasErrorwith codeGIT_ERRORif 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 objectsawait 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:
CasErrorwith codeMANIFEST_NOT_FOUND(delegates toreadManifest)CasErrorwith codeGIT_ERRORif 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 spaceawait cas.deriveKey(options)Derives an encryption key from a passphrase using PBKDF2 or scrypt.
Parameters:
options.passphrase(required):string- The passphraseoptions.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 keysalt— the salt used (save this for re-derivation)params— full KDF parameters object (stored in manifest when usingpassphraseoption)
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,
});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 treestotal— total number of chunk references (before deduplication)
Throws:
CasErrorwith codeMANIFEST_NOT_FOUNDif anytreeOidlacks a manifest (fail closed)CasErrorwith codeGIT_ERRORif 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`);await cas.encrypt({ buffer, key })Encrypts a buffer using AES-256-GCM.
Parameters:
buffer(required):Buffer- Data to encryptkey(required):Buffer- 32-byte encryption key
Returns: Promise<{ buf: Buffer, meta: Object }>
Throws:
CasErrorwith codeINVALID_KEY_TYPEif key is not a BufferCasErrorwith codeINVALID_KEY_LENGTHif key is not 32 bytes
Example:
const { buf, meta } = await cas.encrypt({
buffer: Buffer.from('secret data'),
key: crypto.randomBytes(32)
});await cas.decrypt({ buffer, key, meta })Decrypts a buffer using AES-256-GCM.
Parameters:
buffer(required):Buffer- Encrypted datakey(required):Buffer- 32-byte encryption keymeta(required):Object- Encryption metadata (from encrypt result)
Returns: Promise<Buffer> - Decrypted data
Throws:
CasErrorwith codeINTEGRITY_ERRORif decryption fails
Example:
const decrypted = await cas.decrypt({ buffer: buf, key, meta });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 manifestoldKey(required):Buffer- Current 32-byte KEKnewKey(required):Buffer- New 32-byte KEKlabel(optional):string- If provided, only rotate the named recipient
Returns: Promise<Manifest> - Updated manifest with re-wrapped DEK and incremented keyVersion
Throws:
CasErrorwith codeROTATION_NOT_SUPPORTEDif manifest has no recipients (legacy/unencrypted)CasErrorwith codeRECIPIENT_NOT_FOUNDiflabeldoesn't existCasErrorwith codeDEK_UNWRAP_FAILEDifoldKeydoesn't match the recipientCasErrorwith codeNO_MATCHING_RECIPIENTif no label is provided andoldKeymatches 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 });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 passphrasenewPassphrase(required):string- New vault passphrasekdfOptions(optional):Object- KDF options for new passphrase (e.g.,{ algorithm: 'scrypt' })
Returns: Promise<{ commitOid: string, rotatedSlugs: string[], skippedSlugs: string[] }>
Throws:
CasErrorwith codeVAULT_METADATA_INVALIDif vault is not encryptedCasErrorwith codeDEK_UNWRAP_FAILEDorNO_MATCHING_RECIPIENTif old passphrase is wrongCasErrorwith codeVAULT_CONFLICTif 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(', ')}`);cas.chunkSizeReturns the configured chunk size in bytes.
Type: number
Example:
console.log(cas.chunkSize); // 262144The 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.
refs/cas/vault → commit → tree
├── 100644 blob <oid> .vault.json
├── 040000 tree <oid> demo/hello
├── 040000 tree <oid> photos/beach
interface VaultEntry {
slug: string;
treeOid: string;
}interface VaultMetadata {
version: number;
encryption?: {
cipher: string;
kdf: {
algorithm: string;
salt: string;
iterations?: number;
cost?: number;
blockSize?: number;
parallelization?: number;
keyLength: number;
};
};
}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 derivationkdfOptions(optional):Object- KDF options ({ algorithm, iterations, cost, ... })
Returns: Promise<{ commitOid: string }>
Throws:
CasErrorwith codeVAULT_ENCRYPTION_ALREADY_CONFIGUREDif vault already has encryption
Example:
// Without encryption
await cas.initVault();
// With encryption
await cas.initVault({
passphrase: 'my secret passphrase',
kdfOptions: { algorithm: 'pbkdf2' },
});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 OIDforce(optional):boolean- Overwrite existing entry (default:false)
Returns: Promise<{ commitOid: string }>
Throws:
CasErrorwith codeINVALID_SLUGif slug fails validationCasErrorwith codeVAULT_ENTRY_EXISTSif slug exists andforceis falseCasErrorwith codeVAULT_CONFLICTif concurrent update detected after retries
Example:
const treeOid = await cas.createTree({ manifest });
await cas.addToVault({ slug: 'demo/hello', treeOid });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}`);
}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:
CasErrorwith codeVAULT_ENTRY_NOT_FOUNDif slug does not exist
Example:
const { removedTreeOid } = await cas.removeFromVault({ slug: 'demo/hello' });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:
CasErrorwith codeVAULT_ENTRY_NOT_FOUNDif slug does not exist
Example:
const treeOid = await cas.resolveVaultEntry({ slug: 'demo/hello' });
const manifest = await cas.readManifest({ treeOid });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);
}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
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 secretThe vault stores the KDF parameters (algorithm, salt, iterations) in .vault.json — the passphrase is never stored.
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# 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| 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: .) |
| 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: .) |
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.
Domain service for vault operations. Requires three ports:
persistence(GitPersistencePort) — blob/tree read/writeref(GitRefPort) — ref resolution, commits, atomic updatescrypto(CryptoPort) — KDF for vault-level encryption
import { VaultService } from '@git-stunts/cas'; // or via facade
const vault = await cas.getVaultService();Core domain service implementing CAS operations. Usually accessed via ContentAddressableStore, but can be used directly for advanced scenarios.
new CasService({ persistence, codec, crypto, chunkSize, merkleThreshold })Parameters:
persistence(required):GitPersistencePortimplementationcodec(required):CodecPortimplementationcrypto(required):CryptoPortimplementationchunkSize(optional):number- Chunk size in bytes (default: 262144, minimum: 1024)merkleThreshold(optional):number- Chunk count threshold for Merkle manifests (default: 1000)
Throws:
Errorif chunkSize is less than 1024 bytesErrorif 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
});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)
CasService extends Node.js EventEmitter. See Events section for all emitted 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);
});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
}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)
}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
}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
}Emitted when integrity verification passes for all chunks.
Payload:
{
slug: string // Asset slug
}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
}Emitted when an error occurs during streaming operations (if listeners are registered).
Payload:
{
code: string, // CasError code
message: string // Error message
}Immutable value object representing a file manifest.
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 arraydata.encryption(optional):Object- Encryption metadata (may includekdffield 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'
}
]
});slug:string- Asset identifierfilename:string- Original filenamesize:number- Total file sizechunks:Array<Chunk>- Array of Chunk objectsencryption:Object | undefined- Encryption metadata (may includekdfsub-object)version:number- Manifest version (1 or 2, default: 1)compression:Object | undefined- Compression metadata{ algorithm }subManifests:Array | undefined- Sub-manifest references (v2 only)
manifest.toJSON()Returns a plain object representation suitable for serialization.
Returns: Object
Example:
const json = manifest.toJSON();
console.log(JSON.stringify(json, null, 2));Immutable value object representing a content chunk.
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'
});index:number- Chunk index (0-based)size:number- Chunk size in bytesdigest:string- SHA-256 hex digestblob:string- Git blob OID
Ports define the interfaces for pluggable adapters. Implementations are provided but you can create custom adapters.
Interface for Git persistence operations.
await port.writeBlob(content)Writes content as a Git blob.
Parameters:
content:Buffer | string- Content to store
Returns: Promise<string> - Git blob OID
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
await port.readBlob(oid)Reads a Git blob.
Parameters:
oid:string- Git blob OID
Returns: Promise<Buffer> - Blob content
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
}
}Interface for encoding/decoding manifest data.
port.encode(data)Encodes data to Buffer or string.
Parameters:
data:Object- Data to encode
Returns: Buffer | string - Encoded data
port.decode(buffer)Decodes data from Buffer or string.
Parameters:
buffer:Buffer | string- Encoded data
Returns: Object - Decoded data
port.extensionFile 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';
}
}Interface for cryptographic operations.
port.sha256(buf)Computes SHA-256 hash.
Parameters:
buf:Buffer- Data to hash
Returns: string - 64-character hex digest
port.randomBytes(n)Generates cryptographically random bytes.
Parameters:
n:number- Number of bytes
Returns: Buffer - Random bytes
port.encryptBuffer(buffer, key)Encrypts a buffer using AES-256-GCM.
Parameters:
buffer:Buffer- Data to encryptkey:Buffer- 32-byte encryption key
Returns: { buf: Buffer, meta: { algorithm: string, nonce: string, tag: string, encrypted: boolean } }
port.decryptBuffer(buffer, key, meta)Decrypts a buffer using AES-256-GCM.
Parameters:
buffer:Buffer- Encrypted datakey:Buffer- 32-byte encryption keymeta:Object- Encryption metadata withalgorithm,nonce,tag,encrypted
Returns: Buffer - Decrypted data
Throws: On authentication failure
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 functionfinalize:() => { algorithm: string, nonce: string, tag: string, encrypted: boolean }- Get metadata
await port.deriveKey(options)Derives an encryption key from a passphrase using PBKDF2 or scrypt.
Parameters:
options.passphrase:string- The passphraseoptions.salt(optional):Buffer- Salt (random if omitted)options.algorithm(optional):'pbkdf2' | 'scrypt'- KDF algorithm (default:'pbkdf2')options.iterations(optional):number- PBKDF2 iterationsoptions.cost(optional):number- scrypt cost Noptions.blockSize(optional):number- scrypt block size roptions.parallelization(optional):number- scrypt parallelization poptions.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
}
}Built-in codec implementations.
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'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'All errors thrown by git-cas are instances of CasError.
import CasError from 'git-cas/src/domain/errors/CasError.js';new CasError(message, code, meta)Parameters:
message:string- Error messagecode:string- Error code (see below)meta:Object- Additional error context (default:{})
name:string- Always "CasError"message:string- Error messagecode:string- Error codemeta:Object- Additional contextstack:string- Stack trace
| 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() |
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;
}
}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>
}