npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kirak

v0.2.0

Published

Isomorphic TypeScript client for a deployed Kirak app - session-aware auth plus a generic api() call reaching every endpoint.

Readme

kirak

License: MIT TypeScript Node

Part of the kirak-sdk monorepo. Using React or React Native? See kirak-react for hooks built on top of this package.

Isomorphic TypeScript client for a deployed Kirak app - session-aware auth plus a generic api() call that reaches every endpoint. Framework-agnostic: any JS framework (Vue, Svelte, Angular, Next, plain JS), React Native, and Node backends all use the same package, one import, no framework-specific build.

import { createClient } from "kirak";

const kirak = createClient("https://my-app.kirak.io");

await kirak.api("auth.login", { email: "[email protected]", password: "s3cr3t!" });

const { data, error } = await kirak.api("posts.fetch", { status: "published" });

Current surface (v0.1): session-aware authentication (.session, auth operations via api("auth.*")) and a generic .api(operation, params) call that reaches every endpoint a Kirak app exposes. There is no typed query builder yet - see What's not built yet.

Contents

What you can do with it

  • Sign users up and in, and keep them signed in across reloads/restarts, with no manual token handling.
  • Read and write any model in your app's schema - fetch, search, count, exists, create, update, delete/destroy, restore - through one call.
  • Run GraphQL queries against the same backend.
  • Reach anything else the backend exposes, including something added after this SDK version shipped, with no update required.
  • Get back typed { data, error } results instead of a raw response to parse yourself.
  • Point at any Kirak backend - self-hosted or Kirak Studio-deployed - by changing the URL.

Supported platforms

Plain TypeScript over the standard fetch API - runs anywhere fetch does:

| Platform | Works | Notes | |---|---|---| | Any browser frontend | Yes | React, Vue, Angular, Svelte, plain JS - framework-agnostic | | Meta-frameworks (SSR) | Yes | Next.js, Nuxt, SvelteKit, Remix, Astro - server and client code paths | | React Native / Expo | Yes | Pass a custom auth.storage (e.g. AsyncStorage) for session persistence | | Node.js 18+ | Yes | Scripts, backend services, serverless/edge functions | | Deno, Bun | Expected to work | Standard fetch + ES modules, not yet in the test matrix |

18+ is the Node floor because that's when fetch shipped in Node itself - older runtimes need a fetch polyfill passed via the fetch option below.

Using this package directly works everywhere in the table above, React and React Native included. If you're in React or React Native specifically, kirak-react adds hooks (useSession, useUser, useApi, useMutation) on top of it - see kirak-react.

Install

npm install kirak

Authentication

Requests are authenticated with a JWT access token, not an API key. Once kirak.api("auth.login", ...) succeeds, every kirak.api(...) call automatically sends Authorization: Bearer <accessToken> - nothing to attach yourself, and the token refreshes itself as it nears expiry (see Session lifecycle below). This is the credential a normal signed-in end user, in a browser or mobile app, uses for every call.

serverKey (X-API-Key, a kk_... key - see the option below) is a separate, optional credential for trusted server-side code that isn't acting as any particular signed-in user - a cron job, a backend service calling its own Kirak app. Most apps using this SDK never set it. Never ship it in a browser bundle.

| Credential | Header | Who | Set by | |---|---|---|---| | JWT access token | Authorization: Bearer <token> | any signed-in user - the default | automatically, after login | | API key | X-API-Key: kk_... | trusted server-side code only | createClient(url, { serverKey }) |

Sign up

const { data, error } = await kirak.api<{ user: { email: string } }>("auth.register", {
  email: "[email protected]",
  password: "s3cr3t!",
  first_name: "Alice", // optional
});

Does not log the user in - kirak-core issues no tokens on register. Follow with auth.login.

Sign in

const { data, error } = await kirak.api<{ accessToken: string; refreshToken: string; expiresIn: number; user: { email: string } }>(
  "auth.login",
  { email: "[email protected]", password: "s3cr3t!" },
);
// The session is adopted automatically. Read it back with kirak.session.get():
// { accessToken, refreshToken, expiresIn, expiresAt, user } - user is the wire (snake_case) object, or null

Handle MFA

If the account has MFA enabled, a first call without mfa_code returns an MFA_REQUIRED error - retry with the code:

let { data, error } = await kirak.api("auth.login", { email, password });
if (error?.code === "MFA_REQUIRED") {
  ({ data, error } = await kirak.api("auth.login", { email, password, mfa_code: "123456" }));
}

Sign out

await kirak.api("auth.logout"); // clears the local session even if the network call fails

Note: if the network call fails, the refresh token stays valid server-side until it expires.

Get the current session

const session = await kirak.session.get();
// async - resolves once a persisted session (if any) has finished loading; null if signed out

Get the current user

const { data: user, error } = await kirak.api<{ email: string }>("auth.me", undefined, { method: "GET" }); // re-fetches from the server

api("auth.me") returns the server object verbatim, including a token field that echoes the caller's access token. The SDK strips token from the session user and from useUser(), so avoid logging or persisting the raw auth.me result.

Listen for auth changes

const unsubscribe = kirak.session.onChange((event, session) => {
  // event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED"
  if (event === "SIGNED_OUT") redirectToLogin();
});

Signing in with OTP, social and other providers

Any successful /auth/* response that carries accessToken + refreshToken is adopted into the session automatically - not just password login.

// One-time code (SMS to the phone number): request it, then verify it. The session is adopted on verify.
await kirak.api("auth.request-otp", { phone_number: "+14155552671" });
await kirak.api("auth.verify-otp", { phone_number: "+14155552671", otp: "123456" });

// Native social sign-in (mobile): pass the provider's user ID and email from the SDK
await kirak.api("auth.google-oauth2.mobile", { provider_user_id, email });

// Apple on the web: exchange the token from the redirect
await kirak.api("auth.verify-web-auth", { token });

// Anything else (your own server, a deep-link callback): adopt tokens yourself
await kirak.session.adoptSession({ accessToken, refreshToken, expiresIn, user });

expiresIn and user are optional in adoptSession; when omitted the SDK reads the expiry from the token and looks the user up. Deep links that return tokens to your app should use verified app links (Android App Links, iOS Universal Links), not custom URL schemes any app can claim.

Servers

Clients created with serverKey do not adopt sessions by default (auth.autoAdopt defaults to false there). On a server, use one client per request so one user's tokens never leak into another's; set auth.autoAdopt: true only if you want the shared-session behavior.

With autoAdopt: false (or any serverKey client), api("auth.logout") does not fill the refresh token or clear the local session, so pass { token: refreshToken } explicitly and call session.clear() yourself.

Session lifecycle

Once signed in, every subsequent kirak.api(...) call automatically carries the session's access token - you never attach it yourself. The SDK also handles the token going stale:

  • Refresh-on-401: any request that comes back 401 (other than a call to /auth/* itself) triggers one refresh and one retry, transparently.
  • Single-flight: if several requests 401 around the same time, they share exactly one refresh call instead of each racing their own.
  • Rotation: kirak-core issues a new refresh token on every refresh and invalidates the old one - the SDK always stores the newest one.
  • Proactive refresh (auth.autoRefresh: true): refreshes ~60 seconds before the access token's known expiry, so a long-lived tab never has to rely on hitting a 401 first.

If a refresh ever fails (the refresh token itself is expired or revoked), the session is cleared and a "SIGNED_OUT" event fires - handle that in session.onChange to redirect to a sign-in screen.

Session persistence

By default, the session is written to localStorage if it's available (browsers), or kept in memory otherwise (Node, SSR, a worker - it simply won't survive a process restart there). Pass your own store for anything else (React Native's AsyncStorage, a server-side session table):

createClient(baseUrl, {
  auth: {
    storage: {
      getItem: (key) => AsyncStorage.getItem(key),
      setItem: (key, value) => AsyncStorage.setItem(key, value),
      removeItem: (key) => AsyncStorage.removeItem(key),
    },
  },
});

getItem/setItem/removeItem may return a value directly or a Promise - both work. Set auth.persistSession: false to keep the session in memory only (it disappears on reload).

createClient(baseUrl, options?)

const kirak = createClient(baseUrl, {
  prefix: "/api",                 // if your app's CRUD/graphql routes are mounted under a path prefix
  headers: { "X-Client": "web" }, // extra headers sent with every request
  fetch: customFetch,             // override the fetch implementation (defaults to global fetch)
  serverKey: process.env.KIRAK_SERVER_KEY, // kk_... - server-to-server only, NEVER ship in a browser bundle
  auth: {
    storage: myAuthStore,   // where the session is persisted - see Session persistence above
    persistSession: true,   // default true
    autoRefresh: false,     // default false - see Session lifecycle
    autoAdopt: true,        // default true; false when serverKey is set - see Servers above
  },
});

| Option | Default | Notes | |---|---|---| | prefix | "" | Only affects model CRUD (/{model}/{action}) and graphql - auth/storage/other modules are never affected, matching how kirak-core mounts them | | headers | {} | Merged into every request | | fetch | global fetch | Inject a polyfill or a wrapped fetch (logging, retries, a proxy) | | serverKey | - | Sends X-API-Key. Server-side use only | | publishableKey | - | Reserved for a future anonymous/browser-safe credential mode - accepted, not yet used | | auth.storage | localStorage if present, else in-memory | See Session persistence | | auth.persistSession | true | Set false to keep the session in memory only | | auth.autoRefresh | false | Proactively refresh ~60s before the access token expires | | auth.flow | "header" | "cookie" is reserved and not implemented yet; the SDK always uses the Authorization header |

baseUrl and prefix are read back off the returned client (kirak.baseUrl, kirak.prefix), both with any trailing slash trimmed.

Making requests: api()

api<T = unknown>(operation: string, params?: Record<string, unknown>, opts?: { method?: HttpMethod }): Promise<{ data: T | null; error: KirakError | null; pagination?: KirakPagination }>

operation is the URL path with / written as .:

| Operation | Request | |---|---| | "posts.fetch" | GET /posts/fetch | | "posts.create" | POST /posts/create | | "auth.login" | POST /auth/login | | "graphql" | POST /graphql | | "storage.upload.image" | POST /storage/upload/image |

For the standard model CRUD verbs, the HTTP method is inferred automatically from kirak-core's own fixed table - you never pass method for these:

| Action | Method | |---|---| | fetch, search, count, exists | GET | | create, upsert, restore | POST | | update | PUT | | delete, destroy | DELETE |

Anything else (auth internals, storage, other modules) defaults to POST unless you pass opts.method - most of those endpoints are POST anyway, but check http-api-reference.md for the exceptions (e.g. GET /auth/me, GET /storage/url).

params is sent as a query string for GET, a JSON body otherwise. api() does not reshape or validate params - what you pass is the wire body/query verbatim, so it must already match the shape kirak-core expects.

Reading data

// GET /posts/fetch?status=published&limit=20
await kirak.api("posts.fetch", { status: "published", limit: 20 });

// filter operators are query keys, same as the raw HTTP API
await kirak.api("posts.fetch", { created_at__gte: "2026-01-01", "author_id__in": "1,2,3" });

// GET /posts/search?q=hello
await kirak.api("posts.search", { q: "hello" });

// GET /posts/count / GET /posts/exists
const { data: total } = await kirak.api<{ count: number }>("posts.count", { status: "draft" });
const { data: exists } = await kirak.api<{ exists: boolean }>("posts.exists", { id: 42 });

Writing data

// POST /posts/create - body shape is create's own contract: { data: {...} }
await kirak.api("posts.create", { data: { title: "Hello", body: "..." } });

// PUT /posts/update - { data: {...}, where: {...} }
await kirak.api("posts.update", { data: { status: "published" }, where: { id: 42 } });

// POST /posts/upsert
await kirak.api("posts.upsert", { data: { email: "[email protected]", name: "Ada" } });

// DELETE /posts/delete (soft) and /posts/destroy (hard) - { query: {...} } or { ids: [...] }
await kirak.api("posts.delete", { query: { id: 42 } });
await kirak.api("posts.destroy", { ids: [42, 43] });

// POST /posts/restore
await kirak.api("posts.restore", { data: {}, where: { id: 42 } });

GraphQL

const { data, error } = await kirak.api("graphql", {
  query: "query { posts { id title } }",
});

Anything else

Any endpoint the SDK doesn't have a typed method for yet - social login, MFA setup, API-key management, storage - is still reachable the same way:

// GET /storage/url is a GET, not the POST default, so pass opts.method
await kirak.api("storage.url", { path: "avatars/1.png" }, { method: "GET" });

await kirak.api("auth.setup-mfa", {});
await kirak.api("auth.social-login.google", { code: "..." });

Handling results

Every call resolves to { data, error, pagination? } - never throws for an HTTP or network failure, so you destructure rather than try/catch:

const { data, error } = await kirak.api("posts.fetch");
if (error) {
  // error is a KirakError: { code, message, httpStatus, operation, details?, retryAfter? }
  if (error.code === "PERMISSION_DENIED") { /* ... */ }
  return;
}
// data is populated

code is kirak-core's own machine error code (PERMISSION_DENIED, NOT_FOUND, VALIDATION_ERROR, TOKEN_EXPIRED, ...) verbatim, plus two SDK-only network codes: NETWORK_ERROR (the response body wasn't valid JSON) and CORS_OR_UNREACHABLE (the request never reached the server at all - offline, CORS, DNS). There's no request-timeout handling yet, so no TIMEOUT code - a hung request currently waits on the runtime's own default.

The one thing that does reject the promise rather than returning { error } is a usage bug in the call itself - a malformed operation string ("", a leading/trailing ., ..). That's a programmer error, not a runtime condition, so it surfaces immediately:

try {
  await kirak.api(".posts.fetch"); // throws/rejects with a VALIDATION_ERROR KirakError
} catch (err) {
  // only reachable for a malformed call, never for a normal API failure
}

TypeScript

api() is generic over its response type - data comes back typed, error stays KirakError | null:

interface Post { id: number; title: string; status: string }

const { data, error } = await kirak.api<Post[]>("posts.fetch");
// data: Post[] | null

There's no generated Database type yet (that lands with the query builder in v0.2, powered by kirak gen types), so today you supply the shape per call as shown above.

What's not built yet (and how to reach it anyway)

Nothing is ever unreachable - it just isn't typed yet. Everything below already works through api(), using the operation-string convention described earlier:

  • A typed, chainable query builder (kirak.from('posts').eq(...).fetch()) - use kirak.api("posts.fetch", {...}) until it ships.
  • .storage (file/image upload, list, delete, public URLs) - upload specifically needs multipart handling that isn't wired into api() yet either.
  • Password reset, OTP, MFA setup/verify/disable, social login, API-key management - reachable via kirak.api("auth.<action>", {...}), see http-api-reference.md for each shape.
  • auth.flow: "cookie" (HttpOnly-cookie mode) - the option is accepted but not implemented; only header-based (Bearer token) auth works today.
  • Realtime / subscriptions - not supported by kirak-core at all yet.

Compatibility

Targets kirak-core with the unified response contract - one canonical envelope ({ statusCode, status, error, message, data }) on every route including /auth/* and /storage/*. See kirak-core/docs/http-api-reference.md for the full wire contract this package is built against.

License

MIT