/** * Cross-site Tangle SSO for agent apps: signed-state CSRF cookies plus the * full start/callback orchestration against the platform's /cross-site * bridge. The platform wire client and account persistence are structural * seams (`TangleSsoAuthClient` / `TangleSsoAccountStore`), so this module * never imports agent-runtime, an auth framework, or a database driver. * WebCrypto only — runs in workerd without node compatibility flags. */ /** Define configuration options for managing SSO state including secret, lifetime, and clock injection */ export interface SsoStateConfig { /** HMAC-SHA256 secret (e.g. the app's auth secret). */ secret: string; /** State lifetime in ms. Default 600 000. */ ttlMs?: number; /** Injectable clock (ms since epoch). Default Date.now. */ now?: () => number; } /** Mint a `..` state value. The timestamp * is inside the signed payload, so expiry survives cookie-attribute tampering. */ export declare function createSignedSsoState(config: SsoStateConfig): Promise; /** Verify the MAC (constant-time) and the signed TTL. */ export declare function verifySignedSsoState(state: string, config: SsoStateConfig): Promise; /** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */ export interface TangleSsoExchangeResult { apiKey: string; user: { id: string; email: string; name?: string | null; }; plan?: { tier: string; } | null; } /** Structural mirror of the platform auth wire client — any object with these * two methods satisfies it without this module importing the concrete class. */ export interface TangleSsoAuthClient { authorizeUrl(options: { state: string; redirectUri?: string; }): string; exchange(code: string): Promise; } /** Thrown by `upsertUserByEmail` when the app-local user row cannot be * created; the callback handler maps it to `?error=tangle_user_create_failed`. * Any other store error propagates. */ export declare class TangleSsoUserCreateError extends Error { constructor(message?: string); } /** * Account persistence seam. Covers both storage styles in use: link-table * apps (a per-user platform-link row) and session-column apps (the key on the * session row) — `saveTangleLink` receives both `userId` and `sessionToken`, * and each app persists with the key it needs. `createSession` runs first so * the token is always available to `saveTangleLink`. */ export interface TangleSsoAccountStore { /** Find-or-create the app-local user. `tangleUserId` is the platform's * stable user id — match on it first when the app stores it (emails are * mutable on the platform; the id is not), falling back to email for * first-time logins. */ upsertUserByEmail(input: { email: string; name: string | null; tangleUserId: string; }): Promise<{ userId: string; }>; /** Create an app session row; returns the session-cookie token value. */ createSession(input: { userId: string; expiresAt: Date; ipAddress: string | null; userAgent: string | null; }): Promise<{ token: string; }>; /** Persist the platform link (API key + platform identity). */ saveTangleLink(input: { userId: string; sessionToken: string; tangleUserId: string; email: string; name: string | null; apiKey: string; planTier: string | null; }): Promise; } /** Successful-login context handed to the `setSessionCookie` seam. */ export interface TangleSsoSessionCookieArgs { /** Session token returned by `store.createSession`. */ token: string; /** Session expiry (now + `sessionTtlSeconds`). */ expiresAt: Date; /** Mirrors `sessionTtlSeconds` after defaulting. */ ttlSeconds: number; /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */ secure: boolean; } /** * Sign a session token to better-call's signed-cookie contract — the value * better-auth's `getSignedCookie` verifies: `.` where the * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call * rejects any other length or suffix, so url-safe/unpadded variants read back * as a null session). The joined value is percent-encoded once at cookie * serialization, matching better-call's `serializeSignedCookie` byte-exactly. */ export declare function signSessionCookieValue(token: string, secret: string): Promise; /** Structural slice of a `betterAuth()` instance — only what cookie minting * reads. No better-auth import: the signing contract is implemented by * `signSessionCookieValue`, byte-compatible with better-auth's own * `makeSignature`. */ export interface BetterAuthSessionCookieSource { $context: PromiseLike<{ secret: string; authCookies: { sessionToken: { /** Final cookie name — better-auth decides the `__Secure-` prefix * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */ name: string; attributes: { secure?: boolean; sameSite?: string; path?: string; httpOnly?: boolean; domain?: string; }; }; }; }>; } /** Define options to customize warning behavior for shadowed cookie names in authentication sessions */ export interface BetterAuthSessionCookieMinterOptions { /** Receives the shadowed-cookie-name warning (see below). Default * console.warn. */ warn?: (message: string) => void; } /** * Canonical `setSessionCookie` wiring for better-auth apps: mint the session * Set-Cookie exactly as better-auth's own login flows do — name + attributes * from `auth.$context.authCookies.sessionToken` (better-auth stays * authoritative over prefix/name/attributes) and the value signed to * better-call's `getSignedCookie` contract. A raw unprefixed * `better-auth.session_token` left by an earlier login is explicitly expired * so it cannot shadow the real cookie. * * Warns when the app's session cookie still has better-auth's DEFAULT name: * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie * under that exact name, and equal-path cookies are sent oldest-first — the * platform's cookie is always older (the user signs in there before the app's * callback runs), so the app reads the platform's token, fails its own * signature check, and every fresh login lands logged-out. Per-app * `advanced.cookiePrefix` is the fix. * * Throws on a domain-scoped session cookie for the same reason: a * `Domain=`-wide session cookie is exactly the shadowing footgun. */ export declare function createBetterAuthSessionCookieMinter(auth: BetterAuthSessionCookieSource, options?: BetterAuthSessionCookieMinterOptions): (args: TangleSsoSessionCookieArgs) => Promise; /** Define configuration options for handling Tangle SSO authentication and session management */ export interface TangleSsoHandlerOptions { auth: TangleSsoAuthClient; store: TangleSsoAccountStore; /** HMAC secret for the state cookie. */ stateSecret: string; /** Absolute callback URL registered with the platform. */ callbackUrl: string; stateCookieName: string; /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is * provided. The default path prepends `__Secure-` iff `secureCookies`. */ sessionCookieName?: string; /** Mint the host auth framework's own session cookie(s); return complete * Set-Cookie header values (the handler appends them verbatim and sets no * session cookie itself). Supply this when the framework should stay * authoritative over name/prefix/signing/attributes — e.g. better-auth: * `auth.$context.authCookies.sessionToken` + `makeSignature`. */ setSessionCookie?: (args: TangleSsoSessionCookieArgs) => readonly string[] | Promise; /** HMAC-SHA256 secret the host auth framework verifies session cookies with * (better-auth: its `secret`). Required when `setSessionCookie` is absent — * the default cookie is minted to better-call's signed contract via * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a * null session, so there is deliberately no fallback to `stateSecret` * (which is not guaranteed to be the auth secret). */ sessionCookieSecret?: string; /** Adds `Secure` to every cookie this module sets, and (default session * cookie only) the `__Secure-` name prefix. Must match the auth * framework's own secure-cookie decision (better-auth: https `baseURL` / * `advanced.useSecureCookies`), or it will look up a different cookie name * than the one set here. */ secureCookies: boolean; /** Default 604 800 (7 days). */ sessionTtlSeconds?: number; /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */ stateTtlSeconds?: number; /** Default '/app'. */ defaultRedirectPath?: string; /** Default '/login'. */ loginPath?: string; /** Failure log hook (e.g. console.error). Default no-op. */ log?: (message: string, error?: unknown) => void; now?: () => number; } /** Define handlers for SSO start and callback routes managing authentication flow and session cookies */ export interface TangleSsoHandlers { /** GET start route: mint + sign state, set the state cookie, 302 to the * platform authorize URL. `?redirect=` carries the post-login path. */ start(request: Request): Promise; /** GET callback route: verify state, exchange the code, upsert the user, * create the session, save the platform link, set the session cookie * (via the `setSessionCookie` seam, else signed to better-call's contract * with `sessionCookieSecret`), 302 to the saved redirect. Every failure * 302s to `loginPath?error=…` with the state cookie cleared. */ callback(request: Request): Promise; } /** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */ export declare function createTangleSsoHandlers(opts: TangleSsoHandlerOptions): TangleSsoHandlers;