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"); // → ResourceInfo

Federated 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 builders

TableQuery#

Chainable and thenableawaiting 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 pagination

Mutations & 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 limits

Wallet & 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 blocks

Escape 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.