@kindgi/blob-binding
npm install @kindgi/blob-binding · source
Storage contract for Kindgi artifacts (blobs). BlobStorageBinding is the interface a storage adapter implements and @kindgi/api consumes through its blobStorage input; it backs both the /v1/artifacts/* surface and the S3-compatible /s3/* surface. This package contains types only and has no runtime code.
Purpose
Section titled “Purpose”Keep storage out of the API package while giving both wire surfaces one store. Every blob has two addresses kept in a single metadata record: a blobId (used by /v1/artifacts/*) and a (bucket, key) pair (used by /s3/*). Uploads through put get a synthetic (bucket, key) so they are also reachable over S3, and head(blobId) and headByKey(bucket, key) return the same BlobMeta for the same blob. Every method takes tenantId explicitly so adapters can partition storage per tenant. Cursors and continuation tokens are opaque and adapter-defined.
Contract rules adapters follow:
BlobMeta.hashis the lowercase hex SHA-256 of the exact bytes persisted. WhenBlobPutInput.expectedHashis set, a mismatch fails the write withblob-hash-mismatch. The one exception is a multipart-assembled object, whosehashis the S3 multipart ETag.deleteanddeleteByKeyare idempotent:{ deleted: true }on the first call,{ deleted: false }afterwards. Adapters may tombstone or remove data.listByPrefixsorts lexicographically bykey, as S3 does;putByKeyoverwrites an existing key.
Exports
Section titled “Exports”BlobStorageBinding— the adapter interface, in three groups:- By
blobId—put(tenantId, input),get(tenantId, blobId)(body as aReadableStream),head(tenantId, blobId)(nullwhen unknown),list(tenantId, filter, cursor?, limit?),delete(tenantId, blobId). - By
(bucket, key)—putByKey,getByKey,headByKey,deleteByKey, andlistByPrefix(tenantId, bucket, prefix, continuationToken?, maxKeys?)(maxKeys1 to 1000, default 1000). - Multipart upload —
initiateMultipartUpload(returnsuploadId),uploadPart(returns the partetag),completeMultipartUpload(parts in ascendingpartNumber),abortMultipartUpload,listParts.
- By
- Inputs
BlobPutInput—name,contentType,bytes(ReadableStream<Uint8Array>orUint8Array),size?,tags?,ownerRunId?,expectedHash?.MultipartInitiateInput—contentType,tags?,ownerRunId?.BlobFilter—ownerRunId,contentType,tags(all must match),scope(aScopefrom@kindgi/platform), andinherit(no effect here: blobs always live at project level).
- Results —
BlobMeta(blobId,tenantId,name,contentType,size,hash,tags,ownerRunId?,createdAt,bucket?,key?),BlobRead(meta+stream),BlobListPage(data,nextCursor?),BlobDeleteOutcome(deleted),S3ListPage(contents,isTruncated,nextContinuationToken?),MultipartListPartsPage(partswithpartNumber,etag,size,lastModified). BlobError— union discriminated bycode:blob-not-found(blobId),blob-hash-mismatch(expected,actual),blob-size-mismatch(declared,actual),blob-storage-error(cause?).
Example
Section titled “Example”import { createHash } from 'node:crypto';
import type { BlobMeta, BlobStorageBinding } from '@kindgi/blob-binding';import type { RunId, TenantId } from '@kindgi/types';
async function storeReport( blobs: BlobStorageBinding, tenantId: TenantId, runId: RunId, report: string,): Promise<BlobMeta> { const bytes = new TextEncoder().encode(report); const stored = await blobs.put(tenantId, { name: 'q3-report.txt', contentType: 'text/plain', bytes, size: bytes.byteLength, tags: { kind: 'report' }, ownerRunId: runId, // Optional integrity check: the binding rejects the write if its own sha256 differs. expectedHash: createHash('sha256').update(bytes).digest('hex'), }); if (stored.kind === 'err') throw new Error(`${stored.error.code}: ${stored.error.message}`); const meta = stored.value;
// The same object is addressable by (bucket, key) on the S3-compatible surface. if (meta.bucket !== undefined && meta.key !== undefined) { const viaKey = await blobs.headByKey(tenantId, meta.bucket, meta.key); console.log(viaKey?.blobId === meta.blobId); // true }
// Everything this run has produced with the same tag, first page. const page = await blobs.list(tenantId, { ownerRunId: runId, tags: { kind: 'report' } }, undefined, 20); if (page.kind === 'ok') console.log(page.value.data.map((b) => `${b.name} ${b.size}B`));
return meta;}Non-goals
Section titled “Non-goals”- No storage implementation. Filesystem, object-store, and other adapters implement
BlobStorageBindingin their own packages. - No
Content-MD5verification. The S3-compatible route verifies it before calling the binding. - No object versioning. Writing an existing
(bucket, key)replaces the object (last write wins). - No scope inheritance for blobs. Blobs are project-level content;
BlobFilter.inheritexists only to keep one filter shape across scope-aware bindings.
Related
Section titled “Related”@kindgi/api— mounts/v1/artifacts/*over aBlobStorageBinding, and/s3/*when an S3 credential binding is also supplied.@kindgi/platform—Scope.@kindgi/types—ArtifactId,Cursor,RunId,TenantId,Timestamp,Result.