TypeScript SDK — @osgarden/sdk
The browser/JS client over the gateway API. It runs the OAuth/PKCE flow, sends
credentialed requests, and exposes a chainable query builder. Session tokens live in
httpOnly cookies, so the SDK can only observe "signed in or not" — never the token.
Setup#
import { createClient } from "@osgarden/sdk";
export const osgarden = createClient({
gatewayUrl: "http://localhost:3000",
clientId: "your-oauth-client-id",
redirectUri: window.location.origin, // optional
});The app never configures the issuer directly — signIn() redirects to the gateway's
/auth/authorize, which 302s to Ory. See
authentication.md.
Every method returns a promise and throws OsgardenError ({ message, status, data })
on failure — see error codes.
auth#
await osgarden.auth.signIn(); // redirect to Ory
await osgarden.auth.handleRedirectCallback(); // on ?code return; resolves a Session | null
const { authenticated, userId } = await osgarden.auth.getSession();
await osgarden.auth.refreshSession(); // → boolean
await osgarden.auth.signOut();Data#
Private tables#
osgarden.table("notes"); // → TableQuery (private scope)Resource handle#
const r = osgarden.resource(roomId);
r.table("messages"); // → TableQuery (resource scope)
r.members; // → membership methods (below)
await r.get(); // → ResourceInfo
await r.delete();Resource management#
await osgarden.resource.create("room", { name: "General" }); // → ResourceInfo
await osgarden.resource.list({ type: "room" }); // → ResourceInfo[] (active memberships)
await osgarden.resource.browse({ type: "room", query, limit, cursor }); // → Page<ResourceInfo>
await osgarden.resource.static("feed", "global"); // → ResourceInfoFederated read#
osgarden.resources({ type: "room" }).table("messages"); // → TableQuery (federated, read-only)Atomic batch#
await osgarden.batch(
osgarden.table("notes").insert({ title: "a" }),
osgarden.table("notes").insert({ title: "b" })
); // pass unawaited buildersTableQuery#
Chainable and thenable — awaiting it runs the query. Building selectors:
await osgarden.table('notes')
.select('id, title, createdAt') // projection; default '*'
.eq('archived', false)
.gt('rank', 3).gte(…).lt(…).lte(…)
.in('status', ['a', 'b'])
.like('title', 'draft%')
.isNull('deletedAt')
.orderBy('createdAt', 'desc')
.limit(20)
.after(cursor) // opaque cursor paginationMutations & terminals
await osgarden.table("notes").insert({ title: "Hi" });
await osgarden.table("notes").update({ title: "Edited" }).eq("id", id); // filter required
await osgarden
.table("notes")
.upsert({ id, title: "x" }, { onConflict: ["id"] });
await osgarden.table("notes").delete().eq("id", id); // filter required
await osgarden.table("notes").count(); // → number
await osgarden.table("notes").eq("id", id).single(); // → Row (404 if none, 409 if >1)
await osgarden.table("notes").eq("id", id).maybeSingle(); // → Row | null
await osgarden.table("notes").select().limit(20).page(); // → { rows, nextCursor }Shared-table verbs (resource scope)
await osgarden.resource(id).table("photos").link(rowId); // publish an existing owned row here
await osgarden.resource(id).table("photos").unlink(rowId); // withdraw from this context (moderation)
await osgarden.table("photos").contexts(rowId); // → string[] of resource ids (private scope)Thenable reads resolve to Row[]; the next-page cursor is also attached as .nextCursor
on the returned array.
Resources & membership#
const m = osgarden.resource(roomId).members;
await m.invite("user_123", { role: "writer" }); // → { ok, state: 'invited' }
await m.request({ role: "writer" }); // → { ok, state: 'requested' }
await m.accept(); // accept my invite (or accept(userId) as admin)
await m.reject(); // reject(userId) as admin
await m.join(); // public self-join → { ok, state: 'active' }
await m.leave();
await m.setRole("user_123", "admin");
await m.remove("user_123");
await m.list(); // → MemberInfo[]AI#
const res = await osgarden.ai.chat({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Hi" }],
});
const { models } = await osgarden.ai.models(); // catalog: id, rates, output limitsWallet & apps (dashboard)#
await osgarden.wallet.get(); // → Wallet
await osgarden.wallet.topup(25); // → { url }
await osgarden.apps.list(); // → { apps }
await osgarden.apps.setCap("app_client_id", 20); // USD/month; 0 blocksEscape hatch#
await osgarden.gateway.request<T>("/v1/…", { method, headers, body });Sends a credentialed request with the session cookie + CSRF header and a single transparent refresh-on-401 retry.
Type safety (typegen)#
Row types are loose (Record<string, unknown>) until an app augments the Register
interface via a generated declare module '@osgarden/sdk'. The SDK is fully usable
without the build step — just not statically typed to your schema.