@aginix/vulcan-js
v0.2.1
Published
Universal client SDK for the @aginix/vulcan family — Supabase-style createClient() with a storage module that speaks to @aginix/adonis-vulcan-storage.
Maintainers
Readme
@aginix/vulcan-js
Universal client SDK for the
@aginix/vulcanfamily. Supabase-stylecreateClient()returns a single client with named sub-modules. Today it ships astoragemodule that speaks to@aginix/adonis-vulcan-storage.
Install
npm install @aginix/vulcan-jsESM only, Node 24+, all modern browsers. No runtime dependencies — uses native fetch.
Quick start
import { createClient } from '@aginix/vulcan-js'
const vulcan = createClient('/api')
// or
const vulcan = createClient('http://api.localhost')
// or with auth + extra headers
const vulcan = createClient('http://api.localhost', {
getToken: async () => session?.accessToken ?? null,
headers: { 'X-App': 'web' },
})createClient(baseUrl, options?) returns a VulcanClient. Sub-modules hang off named properties — today vulcan.storage; auth/query/realtime will plug in the same way as they ship.
Result envelope
Every method returns a Supabase-style Promise<{ data, error }>. Inspect error first. Methods never throw on HTTP failures.
const { data, error } = await vulcan.storage.get(id)
if (error) {
// error: { message, status?, code?, cause? }
return showError(error)
}
data // typed payloadvulcan.storage
upload(path, file, options?) — full flow
Reserves a path with the server, PUTs the file raw bytes to whatever uploadUrl the server returned, then re-fetches the saved metadata. If the PUT fails the half-created row is best-effort cleaned up server-side so the disk and DB stay in sync.
The SDK PUTs the same way regardless of disk capability — no proxied/signed branching at the call site. The server returns either:
- an external signed URL (S3 / GCS) — fast direct-to-storage,
- or a same-origin URL with a server-minted token in the query string (FS driver in dev).
Either way the PUT semantics are identical from the SDK's perspective: raw body, Content-Type from the file, no Authorization header. The URL is self-authenticating.
const { data, error } = await vulcan.storage.upload(
`users/${userId}/avatar.png`,
file,
{
// all optional
disk: 'avatars',
name: 'avatar.png', // defaults to file.name
contentType: 'image/png', // defaults to file.type
metadata: { uploadedBy: userId },
signal: abortController.signal,
}
)
// data: StorageObject { id, name, disk, path, mimetype, size, createdAt, updatedAt, metadata }disk is a logical handle that matches a service key in the server's config/drive.ts (e.g. 's3', 'gcs', 'avatars') — not the underlying cloud bucket name. path is the key inside that disk, relative, no prefix.
get(id) — fetch metadata
const { data, error } = await vulcan.storage.get(id)getDownloadUrl(id, opts?) — download URL
const { data, error } = await vulcan.storage.getDownloadUrl(id, { expiresInSeconds: 300 })
// data: { url, expiresIn }
imgEl.src = data.urlReturns either an external signed URL (S3 / GCS) or a same-origin proxy URL whose query token is the authorization (FS in dev). Either way, the URL is safe to drop into <img src> / window.location — no extra headers needed.
The server clamps expiresInSeconds to whatever the matching security rule + global ceiling allow.
Preview a PDF (or image) in a new tab — pass disposition: 'inline' so the server sends Content-Disposition: inline instead of forcing a download, then open the URL:
const { data, error } = await vulcan.storage.getDownloadUrl(id, {
disposition: 'inline',
})
if (data) window.open(data.url, '_blank')The browser still picks behavior from Content-Type: PDFs and images render inline; types the browser can't preview fall back to download. Default disposition is 'attachment' (always downloads), which is what you want for a regular Save / Download button.
remove(id) — delete
const { error } = await vulcan.storage.remove(id)
// data: trueLower-level building blocks
For custom upload flows (e.g. progress tracking, resumable uploads later) the two underlying steps are exposed:
const { data: reservation, error } = await vulcan.storage.reserveUpload(path, {
name,
contentType,
size,
disk,
metadata,
})
// then PUT to reservation.uploadUrl yourself, e.g. using XHR with onprogressOptions
| Option | Type | Notes |
|---|---|---|
| getToken | () => string \| null \| Promise<string \| null> | Called before each request to set Authorization: Bearer <token>. Return null for anonymous. |
| headers | Record<string, string> or () => Record<string, string> | Extra headers applied to every request (e.g. tenant id). |
| fetch | typeof fetch | Override the transport. Useful for retries, telemetry, or tests. |
Per-request headers (passed via options) take precedence over the static ones.
Error shape
type VulcanError = {
message: string
status?: number // HTTP status, when known
code?: string // server-side error code, e.g. 'E_STORAGE_AUTHORIZATION'
cause?: unknown // original Error for network failures
}Common codes from the storage backend:
E_INVALID_STORAGE_PATH(400) — path failed validationE_STORAGE_AUTHORIZATION(403) — security rule deniedE_STORAGE_OBJECT_NOT_FOUND(404) — object id doesn't exist
License
Proprietary — see LICENSE.md.
