/** * 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; //# sourceMappingURL=browserSession.d.ts.map