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.chunkSize;Returns 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, the human CLI can derive an
asset encryption key from the vault's KDF configuration when you supply
--vault-passphrase or --vault-passphrase-file during store and restore:
// Initialize vault with encryption
await cas.initVault({ passphrase: 'secret' });
// Store with vault-configured passphrase derivation (human CLI convenience)
// git-cas store file.txt --slug demo/hello --tree --vault-passphrase secret
// Restore with vault-configured passphrase derivation
// 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.
This does not make refs/cas/vault itself confidential. The vault remains a
readable slug-to-tree index for repository readers. See
docs/THREAT_MODEL.md for the explicit boundary.
This is not an implicit library-level store() or restore() behavior.
Library callers still pass explicit encryptionKey or passphrase values, or
derive keys themselves through getVaultMetadata() plus deriveKey() before
calling the content APIs.
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.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';
}
}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>
}