Skip to content
Pilotoo
v0.1

Sign in with your existing session

You will connect the chat to a SaaS app that already has its own login. The browser sends the app’s session token, and the server asks your /auth/me endpoint who it belongs to, caching the answer for a minute.

  • A login that issues a bearer token, and a GET /auth/me endpoint that returns the current user.
  • The server from The server and a React client from Add chat to a React app.

getToken runs before every request and every socket (re)connection, so it must read the live token, not a copy.

src/assistant/Assistant.tsx
import { ChatProvider } from '@pilotoo/ui/chat';
import { getAccessToken } from '../auth/session';
export function Assistant({ children }: { children: React.ReactNode }) {
return (
<ChatProvider baseUrl={import.meta.env.VITE_ASSISTANT_URL} getToken={() => getAccessToken() ?? undefined}>
{children}
</ChatProvider>
);
}

createHttpIdentityResolver forwards the caller’s Authorization header to your endpoint and reads the answer. map turns your response into the kit’s identity: { subject, tenant, roles }.

src/identity.ts
import { createHttpIdentityResolver, type IdentityResolver } from '@pilotoo/agent-runtime';
type Me = {
user?: { id: string; permissions?: string[] };
tenant?: { id: string };
};
export function createAppIdentityResolver(apiUrl: string): IdentityResolver {
return createHttpIdentityResolver({
url: `${apiUrl}/auth/me`,
timeoutMs: 3000,
map: (body) => {
const me = body as Me;
return {
subject: me.user?.id,
tenant: me.tenant?.id,
// The kit matches role names, so each permission becomes a role.
roles: (me.user?.permissions ?? []).map((code) => `permission:${code}`),
};
},
});
}

A 401 or 403 from your endpoint means “not signed in”. A body that does not map to an identity is an error, and the request fails.

The identity is asked for on every request and every connection. Wrap the resolver to avoid calling your API each time.

src/cached-identity.ts
import { createHash } from 'node:crypto';
import type { Credentials, Identity, IdentityResolver } from '@pilotoo/agent-runtime';
export function cachedIdentity(inner: IdentityResolver, ttlMs = 60_000): IdentityResolver {
const entries = new Map<string, { identity: Identity | undefined; expires: number }>();
return {
async resolve(credentials: Credentials) {
const key = createHash('sha256')
.update(JSON.stringify([credentials.authorization, credentials.headers]))
.digest('hex');
const hit = entries.get(key);
if (hit && hit.expires > Date.now()) return hit.identity;
// A thrown error is not cached: the next request asks again.
const identity = await inner.resolve(credentials);
entries.set(key, { identity, expires: Date.now() + ttlMs });
return identity;
},
};
}
src/app.module.ts
import { Module } from '@nestjs/common';
import { PilotooModule } from '@pilotoo/server';
import { cachedIdentity } from './cached-identity';
import { createAppIdentityResolver } from './identity';
@Module({
imports: [
PilotooModule.forRoot({
env: process.env,
identityResolver: cachedIdentity(createAppIdentityResolver(process.env.API_URL!)),
identityHeaders: ['x-tenant-id'],
}),
],
})
export class AppModule {}

identityHeaders lists request headers that are copied to the resolver (credentials.headers, names lower-case) and forwarded to your endpoint, for an identity service that picks the tenant by header. It is optional.

  • Every REST call and socket handshake goes through the resolver. The result decides which conversations and attachments the caller can see.
  • With AUTH_IDENTITY_URL set and no identityResolver, the kit builds a plain HTTP resolver for you. Use the code above when you need map, a timeout or a cache.
  • With neither, every caller is a local owner. Never run that on a public host.