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).
Blob.http:// or https:// URL as each object's identity and
remote location.MAX_OBJECT_SIZE_EXCEEDED; malformed
decrypted padding is rejected with INVALID_PADDING.Cache-Control, Date, and Expires with a 90-day freshness hint.finalize() function succeeds. Enqueuing another operation for the same
URL replaces the older pending operation.StorageError; the
browser's original error is available through its cause property.Apache-2.0