JavaScript SDK reference
Repository lifecycle handles and typed clients exported by @thegarage/sdk.
@thegarage/sdk provides authenticated and anonymous repository handles plus
typed low-level clients. Install it with:
npm install @thegarage/sdk
The package targets Node.js 24 and later. Its authenticated porcelain APIs can
also run in browser and Worker-style runtimes when token, baseUrl, and
optionally fetch are passed explicitly.
Lazy authenticated handle
garage(name, options?): Promise<AuthenticatedRepo>
Creates a lazy repository handle. Construction performs zero network I/O.
| Parameter | Type | Description |
|---|---|---|
name |
string |
Bare repository name in the API key’s active organization (name resolution) |
options.token |
string |
grg_ API key; falls back to GARAGE_API_KEY in Node.js |
options.defaultBranch |
string |
Branch requested if a later write provisions the repository; defaults to main |
options.baseUrl |
string |
API origin; falls back to GARAGE_SERVER_URL, then production |
options.fetch |
typeof fetch |
Optional fetch implementation |
const repo = await garage('my-repo', {
token,
defaultBranch: 'trunk',
})
console.log(repo.defaultBranch) // null until server metadata is observed
Reads never create a repository or call repos.ensure, so a read-only flow
continues to work with only repos:read. The first commit() provisions a
missing repository. ensure() provisions without committing and shares its
memo with the first commit.
Strict create and import
garage.create(name, options?): Promise<AuthenticatedRepo>
Always performs I/O. Creation is strict: an existing name throws
RepoExistsError rather than opening the existing repository.
const created = await garage.create('new-repo', {
token,
defaultBranch: 'trunk',
})
const imported = await garage.create('imported-repo', {
token,
from: 'https://github.com/acme/project.git',
})
from routes to the GitHub import procedure. Only https://github.com
remotes are accepted. Rejections throw ImportRemoteRejectedError with that
allowlist in the SDK message.
Created and imported handles retain their complete RepoView, including the
clone remote, effective permissions, size, source, and timestamps.
List repositories
garage.list(options?): Promise<RepoPage<AuthenticatedRepo>>
Returns { items, nextCursor }. Each item is an authenticated handle seeded
with its complete cached RepoView.
| Option | Type | Description |
|---|---|---|
cursor |
string |
Cursor returned by a previous bounded call |
limit |
number |
Total-result cap; defaults to 100 |
token |
string |
Explicit API key, or GARAGE_API_KEY in Node.js |
baseUrl |
string |
Explicit API origin, GARAGE_SERVER_URL, or the production default |
fetch |
typeof fetch |
Optional fetch implementation |
The server page maximum is 100. A total limit over 100 auto-paginates with
requests of min(100, remaining) and stops exactly at the requested cap. The
default does not enumerate an entire organization implicitly.
const first = await garage.list({ token })
if (first.nextCursor) {
const second = await garage.list({ token, cursor: first.nextCursor })
}
Repository handles
The shared handle is Repo<TView extends RepoView>. Public handles are split
into AuthenticatedRepo and AnonymousRepo so anonymous-only view fields and
authenticated-only mutations remain correctly typed.
Metadata and lifecycle
| Property or method | Result | Behavior |
|---|---|---|
name |
string |
Repository name |
defaultBranch |
string | null |
Server-observed branch; null before a lazy handle obtains a view |
remote |
string | null |
Clone URL after create, import, list, ensure, commit, or info |
expiresAt |
string | null |
Anonymous expiration; null for authenticated repositories |
info({ refresh? }) |
Promise<TView> |
Returns a copy of the cached view, or fetches with repos.get |
exists() |
Promise<boolean> |
Always checks the correct authenticated or anonymous lifecycle lane |
delete() |
Promise<void> |
Deletes through the correct lane and makes the handle terminal on success |
ensure() |
Promise<RepoInfo> |
Provisions a lazy authenticated repo; no-op RPC-wise for known handles |
setDefaultBranch(branch) |
Promise<void> |
Authenticated only; updates through repos.update |
info() does not expose mutable cache state. Use info({ refresh: true })
to force a server read. exists() never provisions a repository.
An exists() that finds the repository gone clears the cached view and the
memoized ensure() result together. The next ensure() re-runs create-or-open
against the server instead of replaying the earlier outcome; anonymous handles
cannot provision, so theirs throws RepoNotFoundError.
After successful deletion, exists() returns false; cached metadata is
cleared and all other operations throw DeletedRepoHandleError. Construct a
new handle to recreate or reattach. Anonymous deletion does not clear the
persisted anonymous identity.
Git operations
| Method | Result | Behavior |
|---|---|---|
add(path, content) |
Promise<void> |
Stages a string or Uint8Array in memory |
remove(path) |
Promise<void> |
Stages a deletion |
commit(message, options?) |
Promise<Commit> |
Atomically writes all staged changes |
read(path, ref?) |
Promise<Blob> |
Returns UTF-8 text, bytes, and the blob SHA |
ls(ref?, options?) |
Promise<TreeEntry[]> |
Lists tree entries; recursive by default |
log(options?) |
Promise<Commit[]> |
Lists commits; defaults to 50 |
For a missing lazy authenticated repository, ls() and log() return [].
read() throws PathNotFoundError, matching an empty create-or-open handle. An
existing repository with a missing path throws the same typed error. Anonymous
404s are never normalized.
Anonymous repository
garage.ephemeral(options?): Promise<AnonymousRepo>
With no name, creates a new randomly named repository. With name, first
reattaches if the persisted identity owns that repository and creates it when
missing. Concurrent creators converge by retrying the read when another creator
wins the race. Supplying a TTL makes the named creation strict: if the repo
already exists, TtlReattachUnsupportedError is thrown instead of silently
ignoring the requested lifetime.
| Option | Type | Description |
|---|---|---|
name |
string |
Named repository to reattach or create |
ttl |
string |
Lifetime such as 30m, 6h, or 2d |
ttlSeconds |
number |
Lifetime in seconds |
baseUrl |
string |
API origin |
configDir |
string |
Node.js directory used to persist the anonymous identity securely |
fetch |
typeof fetch |
Optional fetch implementation |
Pass either ttl or ttlSeconds; supplying both throws
TtlConflictError. The accepted range is 60 seconds through 72 hours.
configDir treats a missing anon.json as a new identity, but a damaged file
throws InvalidAnonIdFileError.
Authentication resolution
Authenticated entry points use this precedence:
options.token, then Node.jsGARAGE_API_KEYoptions.baseUrl, then Node.jsGARAGE_SERVER_URL, thenhttps://api.thegarage.sh
An explicitly empty token throws MissingTokenError instead of falling back.
Environment lookup uses guarded globalThis.process?.env access, so runtimes
without process can use explicit options without a ReferenceError.
Errors
Every validation or lifecycle failure defined by the SDK extends
GarageError and has a stable code. Porcelain RPC failures extend
GarageRpcError, preserving status, structured data, the server message,
and the raw oRPC error as cause. Known subclasses include:
RepoNotFoundError,RepoExistsError,ForbiddenErrorQuotaExceededError,PayloadTooLargeErrorImportRemoteRejectedError,ImportTooLargeError,ImportFailedErrorMissingTokenError,InvalidRepoNameError,InvalidApiKeyErrorInvalidTtlError,TtlConflictError,TtlReattachUnsupportedError,DeletedRepoHandleErrorInvalidAnonIdFileError,ConfigDirPersistenceUnsupportedError
Native network/runtime failures remain native errors.
Low-level clients
createClient(options): GarageSdkClient
createAnonClient(options): GarageAnonSdkClient
These expose the typed raw oRPC contracts. They intentionally retain raw oRPC
errors rather than the porcelain GarageRpcError mapping.