@sovereignbase/storage
    Preparing search index...

    @sovereignbase/storage

    npm version JSR CI codecov license

    storage

    Encrypted, cache-first object storage with a persistent write queue for browser applications. Objects are available locally immediately; the application owns the worker that synchronizes queued changes to their HTTP(S) URLs.

    npm install @sovereignbase/storage
    # or
    pnpm add @sovereignbase/storage
    # or
    yarn add @sovereignbase/storage
    # or
    bun add @sovereignbase/storage
    # or
    deno add jsr:@sovereignbase/storage
    # or
    vlt install jsr:@sovereignbase/storage

    storeObject and deleteObject commit locally and append an operation to WriteQueue. Register a callback that drains the queue and performs your remote writes. Call finalize() only after the server accepts an operation, so a failed write remains available for retry.

    import { WriteQueue } from '@sovereignbase/storage'

    let syncing = false

    async function syncWrites() {
    if (syncing) return
    syncing = true

    try {
    let queued = await WriteQueue.dequeue()

    while (queued) {
    const { operation, finalize } = queued

    if (operation.kind === 'store') {
    const cached = await caches.match(operation.url)
    if (!cached) throw new Error(`Missing cached object: ${operation.url}`)

    const response = await fetch(operation.url, {
    method: 'PUT',
    headers: { 'content-type': 'application/octet-stream' },
    body: await cached.arrayBuffer(),
    })
    if (!response.ok) throw new Error(`Store failed: ${response.status}`)
    } else {
    const response = await fetch(operation.url, { method: 'DELETE' })
    if (!response.ok) throw new Error(`Delete failed: ${response.status}`)
    }

    await finalize()
    queued = await WriteQueue.dequeue()
    }
    } finally {
    syncing = false
    }
    }

    WriteQueue.onQueued = () => void syncWrites()
    void syncWrites() // resume operations left over from an earlier session

    Queue operations contain URLs, not secret keys or plaintext values. A store consumer obtains the already encrypted bytes from the Cache API.

    import { storeObject } from '@sovereignbase/storage'
    import { Cryptographic } from '@sovereignbase/cryptosuite'

    const url = 'https://objects.example/profile'
    const cipherKey = await Cryptographic.cipherMessage.generateKey()

    await storeObject(url, { name: 'Ada' }, cipherKey)

    The returned promise settles after the encrypted cache entry and its persistent queue operation have both been written.

    import { loadObject } from '@sovereignbase/storage'

    const objectPromise = loadObject(url, cipherKey)

    // Build the UI while loading is already in progress.
    const output = document.createElement('output')
    output.textContent = 'Loading…'
    document.body.append(output)

    const object = await objectPromise
    if (object) output.textContent = object.name

    loadObject reads from the cache first and fetches url on a miss. Successful reads refresh the cache in the background. Start the load as early as useful and await its promise only when the value is needed. A non-successful HTTP response resolves to undefined.

    Pass true as the third argument to avoid network access. A cache miss then resolves to undefined:

    const cachedObject = await loadObject(url, cipherKey, true)
    
    import { deleteObject } from '@sovereignbase/storage'

    await deleteObject(url)

    Deletion removes the local cache entry and persists a delete operation for the sync worker.

    storeObject and loadObject accept a cryptosuite CipherKey JWK. Keep the key secret and persist it separately from the encrypted object. Loading requires the same key that was used for storage. Generate a random key with Cryptographic.cipherMessage.generateKey() or derive one with Cryptographic.cipherMessage.deriveKey(sourceKeyMaterial, salt).

    • Browser-only ESM.
    • Requires the Cache API, IndexedDB, Fetch, Web Crypto, and Blob.
    • Accepts an absolute http:// or https:// URL as each object's identity and remote location.
    • Uses MessagePack, gzip compression, 1 KiB length-hiding padding, and cryptosuite encryption. Objects whose compressed representation exceeds 24 MiB are rejected with error code MAX_OBJECT_SIZE_EXCEEDED; malformed decrypted padding is rejected with INVALID_PADDING.
    • Cache hits take precedence over the network. Successful reads refresh Cache-Control, Date, and Expires with a 90-day freshness hint.
    • Cache-only loads never make a network request.
    • Remote responses need to allow the browser origin when used cross-origin.
    • Write operations are stored in IndexedDB in FIFO order and remain queued until their finalize() function succeeds. Enqueuing another operation for the same URL replaces the older pending operation.
    • IndexedDB failures reject with an operation-specific StorageError; the browser's original error is available through its cause property.
    • Dependencies are not bundled.
    • Unit and integration tests run in Vitest with TypeScript.
    • Browser E2E tests run in Playwright on Chromium, Firefox, WebKit, Pixel 7 mobile Chromium, mobile Firefox emulation, and iPhone 15 mobile WebKit.
    • Statement, branch, function, and line coverage are all held at 100%.

    Apache-2.0