Skip to content

Authority Headers & OBO

The MemoryLayer TypeScript SDK does not (today) ship a first-class Aether transport the way the Python SDK does — it talks HTTP. What it does support is the client-side half of Aether’s on-behalf-of (OBO) delegation: passing an AuthorityContext with each call so the SDK emits the four signed X-Aether-* headers that an Aether terminator (or a directly-authenticated MemoryLayer server) will verify.

This page describes that surface. For the broader picture of what Aether brings to MemoryLayer, see Server -> Aether Transport and aetherlayer.ai.


When to Use This

Use authority context any time the calling principal is not the same as the principal whose memories you want to read or write — typically:

  • A SaaS backend handling a request from end user Alice and storing memories as Alice
  • An agent service running across many tenants that needs per-tenant scoping
  • A trusted internal service acting on behalf of a customer for support / debugging

If your application is single-tenant and the API key already represents the right principal, you don’t need any of this.


The AuthorityContext Type

interface AuthorityContext {
grantId: string;
subject: PrincipalRef;
}
interface PrincipalRef {
type: string; // e.g. "User", "Application"
id: string; // tenant-stable principal ID
}

The grantId is an opaque token issued by your identity layer (typically Aether’s token gRPC service); the MemoryLayer server / Aether terminator verifies it and refuses requests with bogus grants.


Three Ways to Attach an Authority

Per-call

Every memory operation that supports OBO accepts an optional authority field:

import { MemoryLayerClient } from "@scitrera/memorylayer-sdk";
const client = new MemoryLayerClient({
baseUrl: "https://memorylayer.example.com",
apiKey: process.env.MEMORYLAYER_API_KEY,
});
await client.remember("Alice prefers dark mode", {
type: "semantic",
importance: 0.7,
authority: {
grantId: "grant_abc123",
subject: { type: "User", id: "alice@example.com" },
},
});

The SDK emits these headers for that single request:

HeaderValue
X-Aether-Grant-IDgrant_abc123
X-Aether-Authority-Modeon_behalf_of
X-Aether-Subject-TypeUser
X-Aether-Subject-IDalice@example.com

Default authority on the client

If every call from a given client should run as the same principal, pass defaultAuthority to the constructor (when supported in your SDK version). Any per-call authority argument overrides it.

const aliceClient = new MemoryLayerClient({
baseUrl: "https://memorylayer.example.com",
apiKey: process.env.MEMORYLAYER_API_KEY,
defaultAuthority: {
grantId: "grant_abc123",
subject: { type: "User", id: "alice@example.com" },
},
});

Short-lived per-request clients

For multi-tenant services it is often clearest to build a short-lived client per request:

function clientFor(req: Request) {
return new MemoryLayerClient({
baseUrl: process.env.MEMORYLAYER_URL!,
apiKey: process.env.MEMORYLAYER_API_KEY!,
workspaceId: tenantWorkspace(req),
defaultAuthority: req.user.toAuthority(), // your auth layer
});
}

The SDK is intentionally cheap to construct; per-request instantiation is fine.


What the Server Does With These Headers

When MemoryLayer is fronted by an Aether terminator (the default in mesh deployments), the terminator validates the grant signature, then rewrites the inbound headers into trusted X-Auth-* headers that FastAPI consumes. In strict mode, any inbound X-Auth-* or unsigned X-Aether-* headers are stripped before minting. Header-based propagation only works because the mesh-signed X-Aether-Grant-ID is what makes the request trustworthy.

When MemoryLayer is configured with MEMORYLAYER_AUTHENTICATION_SERVICE=aether, the same path is used internally even when calls arrive on plain HTTP (still requiring a verifiable grant).

In OSS open-auth mode (MEMORYLAYER_AUTHENTICATION_SERVICE=default) these headers are accepted but not cryptographically verified — they are useful for local development and demos but should not be relied on for security boundaries.

See Server -> Aether Transport for the server-side details.


Migration to a First-Class Aether Transport

If a future SDK release ships a TypeScript Aether transport analogous to the Python one, the API shape is expected to mirror Python’s:

const client = new MemoryLayerClient({
transport: "aether",
aetherClient,
aetherTarget: "sv::memorylayer::default",
defaultAuthority: ...,
});

The OBO surface (authority per call / defaultAuthority) is already transport-agnostic, so code using authority headers today will continue to work without changes when that lands.