@ima-jin/auth-client
v0.8.18
Published
"Sign in with Imajin" SDK for federated apps — JWT session management and ready-made Next.js route handlers
Readme
@ima-jin/auth-client
"Sign in with Imajin" SDK for federated apps. Lightweight JWT session management + ready-made Next.js route handlers.
Install
npm install @ima-jin/auth-clientQuick Start
1. Configure
// src/lib/auth-config.ts
import type { ImajinAuthConfig } from '@ima-jin/auth-client';
export const authConfig: ImajinAuthConfig = {
secret: process.env.SESSION_SECRET!,
authUrl: process.env.IMAJIN_AUTH_URL!,
appDid: process.env.IMAJIN_APP_DID,
publicUrl: process.env.NEXT_PUBLIC_APP_URL,
loginRedirect: '/dashboard',
};2. Create route handlers
// app/api/auth/callback/route.ts
import { createCallbackHandler } from '@ima-jin/auth-client';
import { authConfig } from '@/lib/auth-config';
export const GET = createCallbackHandler(authConfig);
// app/api/auth/session/route.ts
import { createSessionHandler } from '@ima-jin/auth-client';
import { authConfig } from '@/lib/auth-config';
export const GET = createSessionHandler(authConfig);
// app/api/auth/logout/route.ts
import { createLogoutHandler } from '@ima-jin/auth-client';
import { authConfig } from '@/lib/auth-config';
export const POST = createLogoutHandler(authConfig);3. Check session in server components
import { getSession } from '@ima-jin/auth-client';
import { authConfig } from '@/lib/auth-config';
const user = await getSession(authConfig);
if (!user) redirect('/');4. Fetch this app's own signing key at boot (#2411)
An app provisioned via the kernel's apps.provision (see
ima-jin/imajin-ai's docs/REGISTRATION.md) never holds a raw
IMAJIN_APP_PRIVATE_KEY in any env file. Instead, .env.local carries
IMAJIN_KERNEL_URL, IMAJIN_APP_DID, and — only until first boot — a
one-time IMAJIN_APP_CLAIM_CODE, shown once on the kernel operator's
/jin approval card:
// src/lib/signing-identity.ts
import { loadAppSigningKey } from '@ima-jin/auth-client';
let signingKey: Awaited<ReturnType<typeof loadAppSigningKey>> | null = null;
export async function bootstrapSigningIdentity(): Promise<void> {
signingKey = await loadAppSigningKey();
}
export function getSigningIdentity() {
if (!signingKey) throw new Error('signing identity not bootstrapped yet');
return signingKey; // { appDid, privateKey, publicKey }
}First boot vs. every later boot (Ryan's restart-authentication ruling,
#2411): loadAppSigningKey() mints its own Ed25519 "bootstrap" keypair on
first boot, exchanges the claim code + that keypair's public half for the
real signing key, and persists ONLY the bootstrap keypair — never the
signing key — in a local keystore file (IMAJIN_APP_KEYSTORE, default
./.imajin/keystore.json, mode 0600). Every later boot signs a fresh
challenge with that persisted bootstrap key instead of spending another
claim code, so no operator action is needed on an ordinary restart.
loadAppSigningKey() throws on any failure (missing config, an
already-redeemed or expired claim code, an invalid bootstrap signature, a
revoked grant) rather than degrading silently — a signing key is the app's
own identity, so a misconfigured deploy should fail loudly at boot. If the
local keystore is ever lost (disk wipe, redeploy to a fresh host), ask the
kernel operator to re-approve apps.provision with reissueClaim: true
for a fresh claim code — redeeming it also revokes the lost keystore's
bootstrap key so it can never authenticate a fetch again.
| Env var | When needed |
|---|---|
| IMAJIN_KERNEL_URL | Always |
| IMAJIN_APP_DID | Every later boot (once a keystore exists) |
| IMAJIN_APP_CLAIM_CODE | First boot only (or after a reissueClaim: true rebind) |
| IMAJIN_APP_KEYSTORE | Optional — defaults to ./.imajin/keystore.json |
5. Mint an app token from client components (/browser)
The . entry also imports fs, crypto and path (via loadAppSigningKey()
and the keystore), so importing anything from it into a client component
fails the Next.js build (Module not found: Can't resolve 'fs'). For code
that runs in the browser, import from the browser-safe subpath instead:
'use client';
import { requestAppToken } from '@ima-jin/auth-client/browser';
import type { RequestAppTokenOptions, RequestAppTokenResult } from '@ima-jin/auth-client/browser';
const minted = await requestAppToken({
authUrl: process.env.NEXT_PUBLIC_IMAJIN_KERNEL_URL!,
aud: window.location.host,
scopes: ['profile:read'],
});
// minted: { token, expiresIn, scopes } | nullrequestAppToken calls POST {authUrl}/auth/api/tokens/app with
credentials: 'include' (so the user's kernel session cookie is sent) and
returns null on any failure.
When to use which entry
| Entry | Use it for |
|---|---|
| @ima-jin/auth-client/browser | Client components / any browser bundle — only requestAppToken and its types |
| @ima-jin/auth-client | Server code — sessions, route handlers, loadAppSigningKey() (it also still exports requestAppToken, so existing imports keep working) |
| @ima-jin/auth-client/handlers | Next.js route handler factories |
The browser entry's import graph is guaranteed free of node built-ins,
next/headers, and keystore/signing-key code; tests/browser-entry.test.ts
bundles it for a browser target to enforce that.
App-declared secrets (#2799)
After loadAppSigningKey() establishes the bootstrap keystore, server code
can call loadFromVault({ keys: [{ key: 'SESSION_SECRET', onMissing: 'fail' }] }).
It returns { values, degraded }. appDid, kernelUrl, and keystorePath
use the same environment defaults as loadAppSigningKey(). Names must match
the app manifest's secrets declarations; no grant IDs or generation recipes
are supplied by the caller.
This auth-client helper is not the generic grant loader exported by
@imajin/auth; that API and its deferred acknowledgement handles are unchanged.
Here, the bound bootstrap keystore (not the registered app signing key)
authenticates every fetch. Domain, app DID, requested names, recipient key,
nonce and timestamp are signed. The kernel returns only a vault-core sealed
envelope; the SDK checks the decrypted DID, nonce and requested names before
returning values. Rotation is picked up on the next fetch.
Values stay in process memory: never persist, log, or copy them into environment
variables. onMissing: 'fail' throws a value-free error; 'degrade' omits
the unavailable name from values and lists it in degraded. Each key uses
an independent fresh request so optional keys cannot hide required ones.
The SDK requires HTTPS except for literal loopback hosts; redirects are refused.
Production ingress must enforce public TLS. The handler permits internal HTTP
URLs under a TLS-terminating reverse proxy without trusting forwarding headers.
The endpoint refuses inactive apps, revoked/undeclared names, wrong keys,
altered requests and nonce replay. Nonces are currently tracked in the kernel
process, matching the existing single-instance bootstrap fetch mechanism.
This helper does not issue acknowledgements or claim that a fetched value was used.
Part of Imajin
Imajin — sovereign technology infrastructure. Open source.
