agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
98 lines (97 loc) • 5.25 kB
TypeScript
/**
* hosting/browserSession — the client half of a session, for a page.
*
* A standing agent remembers a conversation per `sessionId`. Something has to
* decide what that id IS, and in a browser that is one line:
*
* const sessionId = browserSessionId();
* await fetch('/invoke', {
* method: 'POST',
* headers: { 'content-type': 'application/json', 'x-session-id': sessionId },
* body: JSON.stringify({ input: 'hello' }),
* });
*
* The server half is `nodeHost({ sessionHeader })` (default `'x-session-id'`,
* so the line above needs no server option at all) or
* `nodeHost({ sessionCookie })`, which does the same job without any client
* code — see those options for the trade.
*
* ── Why this file imports NOTHING ────────────────────────────────────────────
* It is the one piece of `hosting/` a browser bundle is meant to contain, and
* the rest of that folder is Node: `httpHost` dynamically imports `node:http`,
* `sqliteSessions` reaches `node:sqlite` through `createRequire`. So this file
* has no imports at all and is exported from the MAIN barrel
* (`import { browserSessionId } from 'agentfootprint'`) rather than from
* `agentfootprint/hosting`, which no browser bundle should have to pull in. It
* lives here, beside the server half it pairs with, so the two cannot drift
* about which header carries what.
*
* Pattern: a plain function. There is no state to own beyond the storage key.
*/
/** Options for {@link browserSessionId}. */
export interface BrowserSessionIdOptions {
/**
* Where the id is kept. Default `'agentfootprint.sessionId'`.
*
* Change it when one origin serves two different agents that must not share a
* conversation — two keys, two sessions, one browser.
*/
readonly storageKey?: string;
}
/** The default `localStorage` key. Named, so a page can clear it deliberately. */
export declare const DEFAULT_SESSION_STORAGE_KEY = "agentfootprint.sessionId";
/**
* The conversation this browser is having with your agent: minted once, kept,
* and handed back on every later call.
*
* Pass it as `x-session-id` (or in the JSON body as `sessionId`) and a
* `standingAgent` will hydrate the same conversation on every turn — and, since
* 9.10.0, scope the agent's memory to it too, with no configuration.
*
* ── What it is, and the thing it is NOT ──────────────────────────────────────
* **A session id is not authentication.** It is a handle the browser carries,
* and anyone who can reach your host can send any string in its place. Nothing
* here signs it, and nothing on the server side checks it: authenticate the
* caller by your own means, then check that the authenticated principal is
* allowed the session they claimed. Storing anything sensitive under a session
* id alone is storing it under a value the client controls.
*
* ── Where it is kept, and what that costs ────────────────────────────────────
* `localStorage`, under {@link BrowserSessionIdOptions.storageKey}. That means
* it survives a reload and a closed tab, is scoped to the ORIGIN, and is
* readable by any script running on that origin — including one you did not
* write. It is per browser profile, so the same person on a phone and a laptop
* is two conversations, and a shared machine is one.
*
* When `localStorage` cannot be reached — private mode in some browsers throws
* on access, and there is no `window` at all during SSR — the id is kept in a
* module-level `Map` instead. Same id for the life of the page, gone on reload.
* This is a fallback and it is stated rather than hidden: a page that must
* survive a reload in private mode needs its own storage.
*
* ── How it is minted ─────────────────────────────────────────────────────────
* `crypto.randomUUID()` where it exists (every current browser, in a secure
* context), then `crypto.getRandomValues`. Both are unguessable. On a runtime
* that has NEITHER — an insecure-context page in an older browser — it falls
* back to `Math.random`, which is **not** unguessable, and that is survivable
* only because of the paragraph above: this id is a handle, never a credential.
*
* @example A chat page that remembers across reloads
* ```ts
* import { browserSessionId } from 'agentfootprint';
*
* const sessionId = browserSessionId();
* const reply = await fetch('/invoke', {
* method: 'POST',
* headers: { 'content-type': 'application/json', 'x-session-id': sessionId },
* body: JSON.stringify({ input: message }),
* }).then((r) => r.json());
* ```
*
* @example Two agents on one origin, two conversations
* ```ts
* const support = browserSessionId({ storageKey: 'support.sessionId' });
* const billing = browserSessionId({ storageKey: 'billing.sessionId' });
* ```
*/
export declare function browserSessionId(options?: BrowserSessionIdOptions): string;