Skip to content

Commit 209e434

Browse files
committed
feat(sdk): add KVStorage, narrow public discovery API
- Add KVStorage adapter for Redis/Cloudflare KV/DynamoDB backends - Only re-export discoverProvider from the public API (directory search helpers remain internal to AgentAuthClient) - Bump to 0.4.6 Made-with: Cursor
1 parent 48ba434 commit 209e434

File tree

4 files changed

+157
-6
lines changed

4 files changed

+157
-6
lines changed

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@auth/agent",
3-
"version": "0.4.5",
3+
"version": "0.4.6",
44
"description": "Client SDK for the Agent Auth Protocol — agent identity, registration, and capability-based authorization",
55
"license": "MIT",
66
"repository": {

packages/sdk/src/discovery.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export async function discoverProvider(
4545

4646
/**
4747
* Search a directory for providers matching an intent — §7.10.
48+
* Internal: used by AgentAuthClient but not part of the public API.
4849
*/
4950
export async function searchProviders(
5051
directoryUrl: string,
@@ -64,8 +65,7 @@ export async function searchProviders(
6465

6566
/**
6667
* Search the directory and return full ProviderConfig objects.
67-
* The response includes complete config data (endpoints, modes, etc.)
68-
* which allows callers to cache and use providers immediately.
68+
* Internal: used by AgentAuthClient but not part of the public API.
6969
*/
7070
export async function searchDirectoryFull(
7171
directoryUrl: string,
@@ -137,8 +137,7 @@ function extractHostname(input: string): string | null {
137137

138138
/**
139139
* Look up a provider from the directory by URL or domain.
140-
* Extracts the hostname, searches the directory using it as the intent,
141-
* then matches results by issuer hostname.
140+
* Internal: used by AgentAuthClient but not part of the public API.
142141
*/
143142
export async function lookupByUrl(
144143
directoryUrl: string,

packages/sdk/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export { AgentAuthClient } from "./client";
22
export { MemoryStorage } from "./storage";
3+
export type { KVStorageOptions, KVStore } from "./kv-storage";
4+
export { KVStorage } from "./kv-storage";
35
export { generateKeypair, signHostJWT, signAgentJWT } from "./crypto";
4-
export { discoverProvider, searchProviders, searchDirectoryFull, lookupByUrl } from "./discovery";
6+
export { discoverProvider } from "./discovery";
57
export { detectHostName, detectTool } from "./host-name";
68
export { matchQuery, matchQueryScored } from "./search";
79
export type { ScoredMatch } from "./search";

packages/sdk/src/kv-storage.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import type {
2+
AgentConnection,
3+
HostIdentity,
4+
ProviderConfig,
5+
Storage,
6+
} from "./types";
7+
8+
/**
9+
* Minimal key-value store interface.
10+
*
11+
* Implement this with Redis, Vercel KV, Cloudflare KV,
12+
* DynamoDB, a `Map`, or anything that can store strings by key.
13+
*
14+
* ```ts
15+
* import Redis from "ioredis";
16+
* const redis = new Redis();
17+
* const kv: KVStore = {
18+
* get: (key) => redis.get(key),
19+
* set: (key, value) => redis.set(key, value, "EX", 86400).then(() => {}),
20+
* del: (key) => redis.del(key).then(() => {}),
21+
* };
22+
* ```
23+
*/
24+
export interface KVStore {
25+
get(key: string): Promise<string | null>;
26+
set(key: string, value: string): Promise<void>;
27+
del(key: string): Promise<void>;
28+
}
29+
30+
export interface KVStorageOptions {
31+
/** Key prefix to namespace all storage keys. @default "agent-auth" */
32+
prefix?: string;
33+
}
34+
35+
/**
36+
* Storage adapter backed by any key-value store.
37+
*
38+
* Uses index keys to support listing without prefix scanning,
39+
* making it compatible with every KV backend.
40+
*
41+
* ```ts
42+
* import { AgentAuthClient, KVStorage } from "@auth/agent";
43+
* import Redis from "ioredis";
44+
*
45+
* const redis = new Redis();
46+
* const storage = new KVStorage({
47+
* get: (key) => redis.get(key),
48+
* set: (key, value) => redis.set(key, value, "EX", 86400).then(() => {}),
49+
* del: (key) => redis.del(key).then(() => {}),
50+
* });
51+
*
52+
* const client = new AgentAuthClient({ storage });
53+
* ```
54+
*/
55+
export class KVStorage implements Storage {
56+
private kv: KVStore;
57+
private prefix: string;
58+
59+
constructor(kv: KVStore, opts?: KVStorageOptions) {
60+
this.kv = kv;
61+
this.prefix = opts?.prefix ?? "agent-auth";
62+
}
63+
64+
private k(...parts: string[]): string {
65+
return [this.prefix, ...parts].join(":");
66+
}
67+
68+
private async getJSON<T>(key: string): Promise<T | null> {
69+
const raw = await this.kv.get(key);
70+
if (!raw) return null;
71+
return JSON.parse(raw) as T;
72+
}
73+
74+
private async setJSON(key: string, value: unknown): Promise<void> {
75+
await this.kv.set(key, JSON.stringify(value));
76+
}
77+
78+
private async getIndex(key: string): Promise<string[]> {
79+
return (await this.getJSON<string[]>(key)) ?? [];
80+
}
81+
82+
private async addToIndex(key: string, id: string): Promise<void> {
83+
const ids = await this.getIndex(key);
84+
if (!ids.includes(id)) {
85+
ids.push(id);
86+
await this.setJSON(key, ids);
87+
}
88+
}
89+
90+
private async removeFromIndex(key: string, id: string): Promise<void> {
91+
const ids = await this.getIndex(key);
92+
const filtered = ids.filter((i) => i !== id);
93+
if (filtered.length !== ids.length) {
94+
await this.setJSON(key, filtered);
95+
}
96+
}
97+
98+
async getHostIdentity(): Promise<HostIdentity | null> {
99+
return this.getJSON<HostIdentity>(this.k("host"));
100+
}
101+
102+
async setHostIdentity(host: HostIdentity): Promise<void> {
103+
await this.setJSON(this.k("host"), host);
104+
}
105+
106+
async deleteHostIdentity(): Promise<void> {
107+
await this.kv.del(this.k("host"));
108+
}
109+
110+
async getAgentConnection(agentId: string): Promise<AgentConnection | null> {
111+
return this.getJSON<AgentConnection>(this.k("agent", agentId));
112+
}
113+
114+
async setAgentConnection(agentId: string, conn: AgentConnection): Promise<void> {
115+
await this.setJSON(this.k("agent", agentId), conn);
116+
await this.addToIndex(this.k("agents"), agentId);
117+
}
118+
119+
async deleteAgentConnection(agentId: string): Promise<void> {
120+
await this.kv.del(this.k("agent", agentId));
121+
await this.removeFromIndex(this.k("agents"), agentId);
122+
}
123+
124+
async listAgentConnections(): Promise<AgentConnection[]> {
125+
const ids = await this.getIndex(this.k("agents"));
126+
const results = await Promise.all(
127+
ids.map((id) => this.getAgentConnection(id)),
128+
);
129+
return results.filter((c): c is AgentConnection => c !== null);
130+
}
131+
132+
async getProviderConfig(issuer: string): Promise<ProviderConfig | null> {
133+
return this.getJSON<ProviderConfig>(
134+
this.k("provider", encodeURIComponent(issuer)),
135+
);
136+
}
137+
138+
async setProviderConfig(issuer: string, config: ProviderConfig): Promise<void> {
139+
await this.setJSON(this.k("provider", encodeURIComponent(issuer)), config);
140+
await this.addToIndex(this.k("providers"), issuer);
141+
}
142+
143+
async listProviderConfigs(): Promise<ProviderConfig[]> {
144+
const issuers = await this.getIndex(this.k("providers"));
145+
const results = await Promise.all(
146+
issuers.map((issuer) => this.getProviderConfig(issuer)),
147+
);
148+
return results.filter((c): c is ProviderConfig => c !== null);
149+
}
150+
}

0 commit comments

Comments
 (0)