@dudko.dev/agent-web 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/chunk-H7W6MBK2.js +125 -0
- package/dist/chunk-H7W6MBK2.js.map +1 -0
- package/dist/index.d.cts +3 -35
- package/dist/index.d.ts +3 -35
- package/dist/index.js +10 -114
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +520 -54
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +197 -6
- package/dist/mcp.d.ts +197 -6
- package/dist/mcp.js +407 -54
- package/dist/mcp.js.map +1 -1
- package/dist/vault-BHlMmKXM.d.cts +35 -0
- package/dist/vault-BHlMmKXM.d.ts +35 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -191,6 +191,55 @@ Browsers can only speak the **HTTP (StreamableHTTP)** transport — stdio MCP is
|
|
|
191
191
|
Node-only. The connector lives in the `./mcp` subpath so the MCP SDK never
|
|
192
192
|
enters your core bundle.
|
|
193
193
|
|
|
194
|
+
`connectMcpHttp` returns `{ tools, catalog, results, refreshServer, close }`.
|
|
195
|
+
`tools` and `catalog` are mutated **in place** by `refreshServer(name)`, so an
|
|
196
|
+
agent built from them picks up a server's new tool list (react to it via the
|
|
197
|
+
`onToolsChanged` option) without being rebuilt. Filtering is plain object work:
|
|
198
|
+
delete the keys you don't want before merging.
|
|
199
|
+
|
|
200
|
+
### OAuth 2.1 + Dynamic Client Registration
|
|
201
|
+
|
|
202
|
+
A server behind OAuth needs more than a header: access tokens expire mid-run.
|
|
203
|
+
Pass an `authProvider` and the MCP SDK discovers the authorization server
|
|
204
|
+
(RFC 9728), registers this app dynamically (RFC 7591 — no client_id to
|
|
205
|
+
pre-provision), runs PKCE, and **refreshes the access token on any 401 and
|
|
206
|
+
retries the request**.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { BrowserOAuthProvider, connectMcpHttp, finishMcpOAuth, readOAuthCallback }
|
|
210
|
+
from '@dudko.dev/agent-web/mcp'
|
|
211
|
+
|
|
212
|
+
const provider = new BrowserOAuthProvider({
|
|
213
|
+
serverUrl: 'https://my-mcp-server.example/mcp',
|
|
214
|
+
redirectUrl: `${location.origin}/callback`, // must match on the way back
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// 1. On the callback page: finish the flow before anything else.
|
|
218
|
+
const callback = readOAuthCallback()
|
|
219
|
+
if (callback) await finishMcpOAuth(provider, callback)
|
|
220
|
+
|
|
221
|
+
// 2. Connect. `needsAuthorization` means "send the user to authorize".
|
|
222
|
+
const mcp = await connectMcpHttp({ docs: { url: provider.serverUrl, authProvider: provider } })
|
|
223
|
+
if (mcp.results[0]?.needsAuthorization) {
|
|
224
|
+
location.href = String(provider.authorizationUrl) // a button, a popup — your call
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Tokens, the dynamic registration and the PKCE verifier are stored in the same
|
|
229
|
+
**encrypted IndexedDB vault** as provider API keys (`VaultOAuthStorage`; swap in
|
|
230
|
+
`MemoryOAuthStorage` or your own via `storage`). `redirectToAuthorization` never
|
|
231
|
+
navigates on its own — the SDK can demand authorization from inside a running
|
|
232
|
+
tool call, and evicting the user then would discard the run. The URL is recorded
|
|
233
|
+
on `provider.authorizationUrl` and the app decides when to use it.
|
|
234
|
+
|
|
235
|
+
Two things to check on the **server** side, both invisible until they bite:
|
|
236
|
+
|
|
237
|
+
- CORS must allow `Authorization` and expose `WWW-Authenticate` and
|
|
238
|
+
`mcp-session-id` (`Access-Control-Expose-Headers`). Without the first, the
|
|
239
|
+
browser cannot read the 401 challenge and discovery never starts.
|
|
240
|
+
- The authorization server's `/register`, metadata and token endpoints must be
|
|
241
|
+
CORS-enabled too — a browser client calls them directly.
|
|
242
|
+
|
|
194
243
|
## Configuration (highlights)
|
|
195
244
|
|
|
196
245
|
| Option | Default | Purpose |
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/storage/db.ts
|
|
2
|
+
import { openDB } from "idb";
|
|
3
|
+
var KEYS_STORE = "keys";
|
|
4
|
+
var SECRETS_STORE = "secrets";
|
|
5
|
+
var SESSIONS_STORE = "sessions";
|
|
6
|
+
var cache = /* @__PURE__ */ new Map();
|
|
7
|
+
var openAgentWebDB = (opts = {}) => {
|
|
8
|
+
const name = opts.dbName ?? "agent-web";
|
|
9
|
+
let p = cache.get(name);
|
|
10
|
+
if (!p) {
|
|
11
|
+
p = openDB(name, 1, {
|
|
12
|
+
upgrade(db) {
|
|
13
|
+
if (!db.objectStoreNames.contains(KEYS_STORE)) db.createObjectStore(KEYS_STORE);
|
|
14
|
+
if (!db.objectStoreNames.contains(SECRETS_STORE)) db.createObjectStore(SECRETS_STORE);
|
|
15
|
+
if (!db.objectStoreNames.contains(SESSIONS_STORE)) db.createObjectStore(SESSIONS_STORE);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
cache.set(name, p);
|
|
19
|
+
p.catch(() => {
|
|
20
|
+
if (cache.get(name) === p) cache.delete(name);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return p;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/secrets/crypto.ts
|
|
27
|
+
var KEY_ID = "vault-key-v1";
|
|
28
|
+
var webcrypto = () => {
|
|
29
|
+
const c = globalThis.crypto;
|
|
30
|
+
if (!c?.subtle || !c.getRandomValues) {
|
|
31
|
+
throw new Error("WebCrypto (crypto.subtle) is unavailable in this environment");
|
|
32
|
+
}
|
|
33
|
+
return c;
|
|
34
|
+
};
|
|
35
|
+
var loadOrCreateKey = async (dbName) => {
|
|
36
|
+
const db = await openAgentWebDB({ dbName });
|
|
37
|
+
const existing = await db.get(KEYS_STORE, KEY_ID);
|
|
38
|
+
if (existing) return existing;
|
|
39
|
+
const key = await webcrypto().subtle.generateKey(
|
|
40
|
+
{ name: "AES-GCM", length: 256 },
|
|
41
|
+
/* extractable */
|
|
42
|
+
false,
|
|
43
|
+
["encrypt", "decrypt"]
|
|
44
|
+
);
|
|
45
|
+
const tx = db.transaction(KEYS_STORE, "readwrite");
|
|
46
|
+
const winner = await tx.store.get(KEY_ID);
|
|
47
|
+
if (winner) {
|
|
48
|
+
await tx.done;
|
|
49
|
+
return winner;
|
|
50
|
+
}
|
|
51
|
+
await tx.store.put(key, KEY_ID);
|
|
52
|
+
await tx.done;
|
|
53
|
+
return key;
|
|
54
|
+
};
|
|
55
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
56
|
+
var getOrCreateVaultKey = (dbName) => {
|
|
57
|
+
const cacheKey = dbName ?? "agent-web";
|
|
58
|
+
let p = keyCache.get(cacheKey);
|
|
59
|
+
if (!p) {
|
|
60
|
+
p = loadOrCreateKey(dbName);
|
|
61
|
+
keyCache.set(cacheKey, p);
|
|
62
|
+
p.catch(() => {
|
|
63
|
+
if (keyCache.get(cacheKey) === p) keyCache.delete(cacheKey);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return p;
|
|
67
|
+
};
|
|
68
|
+
var textEncoder = new TextEncoder();
|
|
69
|
+
var textDecoder = new TextDecoder();
|
|
70
|
+
var encryptJSON = async (key, value) => {
|
|
71
|
+
const c = webcrypto();
|
|
72
|
+
const iv = c.getRandomValues(new Uint8Array(12));
|
|
73
|
+
const data = textEncoder.encode(JSON.stringify(value));
|
|
74
|
+
const ciphertext = await c.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
|
|
75
|
+
return { iv, ciphertext };
|
|
76
|
+
};
|
|
77
|
+
var decryptJSON = async (key, blob) => {
|
|
78
|
+
const iv = new Uint8Array(blob.iv);
|
|
79
|
+
const plain = await webcrypto().subtle.decrypt({ name: "AES-GCM", iv }, key, blob.ciphertext);
|
|
80
|
+
return JSON.parse(textDecoder.decode(plain));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/secrets/vault.ts
|
|
84
|
+
var IndexedDBVault = class {
|
|
85
|
+
dbName;
|
|
86
|
+
constructor(opts = {}) {
|
|
87
|
+
this.dbName = opts.dbName;
|
|
88
|
+
}
|
|
89
|
+
key() {
|
|
90
|
+
return getOrCreateVaultKey(this.dbName);
|
|
91
|
+
}
|
|
92
|
+
/** Encrypt and store a secret string under `id`. */
|
|
93
|
+
async setSecret(id, value) {
|
|
94
|
+
const blob = await encryptJSON(await this.key(), value);
|
|
95
|
+
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
96
|
+
await db.put(SECRETS_STORE, blob, id);
|
|
97
|
+
}
|
|
98
|
+
/** Fetch and decrypt a secret, or undefined if absent. */
|
|
99
|
+
async getSecret(id) {
|
|
100
|
+
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
101
|
+
const blob = await db.get(SECRETS_STORE, id);
|
|
102
|
+
if (!blob) return void 0;
|
|
103
|
+
return decryptJSON(await this.key(), blob);
|
|
104
|
+
}
|
|
105
|
+
async deleteSecret(id) {
|
|
106
|
+
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
107
|
+
await db.delete(SECRETS_STORE, id);
|
|
108
|
+
}
|
|
109
|
+
async listSecretIds() {
|
|
110
|
+
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
111
|
+
return (await db.getAllKeys(SECRETS_STORE)).map(String);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
KEYS_STORE,
|
|
117
|
+
SECRETS_STORE,
|
|
118
|
+
SESSIONS_STORE,
|
|
119
|
+
openAgentWebDB,
|
|
120
|
+
getOrCreateVaultKey,
|
|
121
|
+
encryptJSON,
|
|
122
|
+
decryptJSON,
|
|
123
|
+
IndexedDBVault
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=chunk-H7W6MBK2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/db.ts","../src/secrets/crypto.ts","../src/secrets/vault.ts"],"sourcesContent":["import { openDB, type IDBPDatabase } from 'idb'\n\nexport interface AgentWebDBOptions {\n /** Database name (default 'agent-web'). Use a distinct name to isolate apps. */\n dbName?: string\n}\n\nexport const KEYS_STORE = 'keys'\nexport const SECRETS_STORE = 'secrets'\nexport const SESSIONS_STORE = 'sessions'\n\nlet cache = new Map<string, Promise<IDBPDatabase>>()\n\n/**\n * The single owner of the IndexedDB database. Every persistent store — the\n * non-extractable crypto key, encrypted secrets, and session transcripts —\n * lives under ONE database name + version. Centralising `openDB` here prevents\n * the classic bug where two modules open the same DB name with different store\n * sets and the second call throws a VersionError.\n */\nexport const openAgentWebDB = (opts: AgentWebDBOptions = {}): Promise<IDBPDatabase> => {\n const name = opts.dbName ?? 'agent-web'\n let p = cache.get(name)\n if (!p) {\n p = openDB(name, 1, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(KEYS_STORE)) db.createObjectStore(KEYS_STORE)\n if (!db.objectStoreNames.contains(SECRETS_STORE)) db.createObjectStore(SECRETS_STORE)\n if (!db.objectStoreNames.contains(SESSIONS_STORE)) db.createObjectStore(SESSIONS_STORE)\n },\n })\n cache.set(name, p)\n // A failed open (private mode, storage denied) must not stay cached, or\n // every later call would replay the same rejection forever.\n p.catch(() => {\n if (cache.get(name) === p) cache.delete(name)\n })\n }\n return p\n}\n\n/** Test hook: drop cached connections so a fresh (e.g. fake-indexeddb) DB is opened. */\nexport const _resetDBCache = (): void => {\n cache = new Map()\n}\n","import { KEYS_STORE, openAgentWebDB } from '../storage/db.js'\n\nconst KEY_ID = 'vault-key-v1'\n\nconst webcrypto = (): Crypto => {\n const c = (globalThis as { crypto?: Crypto }).crypto\n if (!c?.subtle || !c.getRandomValues) {\n throw new Error('WebCrypto (crypto.subtle) is unavailable in this environment')\n }\n return c\n}\n\nconst loadOrCreateKey = async (dbName?: string): Promise<CryptoKey> => {\n const db = await openAgentWebDB({ dbName })\n const existing = (await db.get(KEYS_STORE, KEY_ID)) as CryptoKey | undefined\n if (existing) return existing\n const key = await webcrypto().subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n /* extractable */ false,\n ['encrypt', 'decrypt'],\n )\n // Double-check inside one readwrite transaction: another tab may have created\n // a key while ours was generating. Losing that race and overwriting would\n // leave the other tab's secrets permanently undecryptable.\n const tx = db.transaction(KEYS_STORE, 'readwrite')\n const winner = (await tx.store.get(KEY_ID)) as CryptoKey | undefined\n if (winner) {\n await tx.done\n return winner\n }\n await tx.store.put(key, KEY_ID)\n await tx.done\n return key\n}\n\nconst keyCache = new Map<string, Promise<CryptoKey>>()\n\n/**\n * Fetch (or lazily create) the vault's AES-GCM key. The key is generated\n * NON-EXTRACTABLE and stored as a live `CryptoKey` in IndexedDB — a CryptoKey\n * is structured-cloneable, so it persists across reloads WITHOUT its raw bytes\n * ever being exposed to JavaScript. Not even in-page script can read it out; it\n * can only be used to encrypt / decrypt. That is what makes at-rest storage of\n * the secrets meaningful (see IndexedDBVault's threat model).\n *\n * Concurrent callers share one in-flight promise per DB, and creation is\n * double-checked in a single IDB transaction, so two racing writers can never\n * end up encrypting under different keys.\n */\nexport const getOrCreateVaultKey = (dbName?: string): Promise<CryptoKey> => {\n const cacheKey = dbName ?? 'agent-web'\n let p = keyCache.get(cacheKey)\n if (!p) {\n p = loadOrCreateKey(dbName)\n keyCache.set(cacheKey, p)\n p.catch(() => {\n if (keyCache.get(cacheKey) === p) keyCache.delete(cacheKey)\n })\n }\n return p\n}\n\nexport interface EncryptedBlob {\n /** 96-bit random IV, fresh per write. */\n iv: Uint8Array\n ciphertext: ArrayBuffer\n}\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\n/** Encrypt a JSON-serialisable value with a fresh 96-bit IV (AES-GCM). */\nexport const encryptJSON = async (key: CryptoKey, value: unknown): Promise<EncryptedBlob> => {\n const c = webcrypto()\n const iv = c.getRandomValues(new Uint8Array(12))\n const data = textEncoder.encode(JSON.stringify(value))\n const ciphertext = await c.subtle.encrypt({ name: 'AES-GCM', iv }, key, data)\n return { iv, ciphertext }\n}\n\n/** Decrypt a blob produced by encryptJSON. Throws if the key or blob don't match. */\nexport const decryptJSON = async <T = unknown>(key: CryptoKey, blob: EncryptedBlob): Promise<T> => {\n // Copy the IV into a fresh ArrayBuffer-backed view: after a round-trip through\n // IndexedDB its element buffer is typed as ArrayBufferLike, which no longer\n // satisfies BufferSource under TS's stricter typed-array generics.\n const iv = new Uint8Array(blob.iv)\n const plain = await webcrypto().subtle.decrypt({ name: 'AES-GCM', iv }, key, blob.ciphertext)\n return JSON.parse(textDecoder.decode(plain)) as T\n}\n","import { openAgentWebDB, SECRETS_STORE } from '../storage/db.js'\nimport { decryptJSON, encryptJSON, getOrCreateVaultKey, type EncryptedBlob } from './crypto.js'\n\nexport interface VaultOptions {\n /** Database name, forwarded to the shared IDB owner (default 'agent-web'). */\n dbName?: string\n}\n\n/**\n * Encrypted key/value secret store backed by IndexedDB. Values are encrypted at\n * rest with the non-extractable AES-GCM vault key (see crypto.ts).\n *\n * ── THREAT MODEL (read before shipping any app-owned key to a browser) ──\n * This vault PROTECTS secrets **at rest** — an IndexedDB / disk dump yields\n * only ciphertext — and against **casual DevTools inspection** — no plaintext\n * key is stored, and the AES key cannot be exported even by in-page script.\n *\n * It does **NOT** defend against **active XSS on your origin**: any script that\n * already runs on the page can call `getSecret()`, or use the live `CryptoKey`\n * to decrypt. Browser storage is not a secrets manager.\n *\n * Therefore this vault is for the **end user's OWN key on their OWN device**\n * (bring-your-own-key). A shared, app-owned key must NEVER reach the client —\n * put it behind a server proxy (`ProviderModelSpec.baseURL`) or the Vercel AI\n * Gateway instead.\n */\nexport class IndexedDBVault {\n private readonly dbName?: string\n\n constructor(opts: VaultOptions = {}) {\n this.dbName = opts.dbName\n }\n\n private key(): Promise<CryptoKey> {\n return getOrCreateVaultKey(this.dbName)\n }\n\n /** Encrypt and store a secret string under `id`. */\n async setSecret(id: string, value: string): Promise<void> {\n const blob = await encryptJSON(await this.key(), value)\n const db = await openAgentWebDB({ dbName: this.dbName })\n await db.put(SECRETS_STORE, blob, id)\n }\n\n /** Fetch and decrypt a secret, or undefined if absent. */\n async getSecret(id: string): Promise<string | undefined> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n const blob = (await db.get(SECRETS_STORE, id)) as EncryptedBlob | undefined\n if (!blob) return undefined\n return decryptJSON<string>(await this.key(), blob)\n }\n\n async deleteSecret(id: string): Promise<void> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n await db.delete(SECRETS_STORE, id)\n }\n\n async listSecretIds(): Promise<string[]> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n return (await db.getAllKeys(SECRETS_STORE)).map(String)\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as ai from 'ai';
|
|
2
2
|
import { LanguageModel, ToolSet, Tool, ModelMessage, generateText, streamText } from 'ai';
|
|
3
|
+
import { V as VaultOptions } from './vault-BHlMmKXM.cjs';
|
|
4
|
+
export { I as IndexedDBVault } from './vault-BHlMmKXM.cjs';
|
|
3
5
|
import { z, ZodType } from 'zod';
|
|
4
6
|
import { IDBPDatabase } from 'idb';
|
|
5
7
|
|
|
@@ -199,40 +201,6 @@ declare const defaultPrompts: Prompts;
|
|
|
199
201
|
/** Prepend a host-supplied systemPrompt to a phase system prompt, if present. */
|
|
200
202
|
declare const withSystem: (base: string, extra?: string) => string;
|
|
201
203
|
|
|
202
|
-
interface VaultOptions {
|
|
203
|
-
/** Database name, forwarded to the shared IDB owner (default 'agent-web'). */
|
|
204
|
-
dbName?: string;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Encrypted key/value secret store backed by IndexedDB. Values are encrypted at
|
|
208
|
-
* rest with the non-extractable AES-GCM vault key (see crypto.ts).
|
|
209
|
-
*
|
|
210
|
-
* ── THREAT MODEL (read before shipping any app-owned key to a browser) ──
|
|
211
|
-
* This vault PROTECTS secrets **at rest** — an IndexedDB / disk dump yields
|
|
212
|
-
* only ciphertext — and against **casual DevTools inspection** — no plaintext
|
|
213
|
-
* key is stored, and the AES key cannot be exported even by in-page script.
|
|
214
|
-
*
|
|
215
|
-
* It does **NOT** defend against **active XSS on your origin**: any script that
|
|
216
|
-
* already runs on the page can call `getSecret()`, or use the live `CryptoKey`
|
|
217
|
-
* to decrypt. Browser storage is not a secrets manager.
|
|
218
|
-
*
|
|
219
|
-
* Therefore this vault is for the **end user's OWN key on their OWN device**
|
|
220
|
-
* (bring-your-own-key). A shared, app-owned key must NEVER reach the client —
|
|
221
|
-
* put it behind a server proxy (`ProviderModelSpec.baseURL`) or the Vercel AI
|
|
222
|
-
* Gateway instead.
|
|
223
|
-
*/
|
|
224
|
-
declare class IndexedDBVault {
|
|
225
|
-
private readonly dbName?;
|
|
226
|
-
constructor(opts?: VaultOptions);
|
|
227
|
-
private key;
|
|
228
|
-
/** Encrypt and store a secret string under `id`. */
|
|
229
|
-
setSecret(id: string, value: string): Promise<void>;
|
|
230
|
-
/** Fetch and decrypt a secret, or undefined if absent. */
|
|
231
|
-
getSecret(id: string): Promise<string | undefined>;
|
|
232
|
-
deleteSecret(id: string): Promise<void>;
|
|
233
|
-
listSecretIds(): Promise<string[]>;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
204
|
/**
|
|
237
205
|
* The seam the provider registry uses to fetch API keys at model-build time.
|
|
238
206
|
* Swap in any backend: the encrypted vault (default), an in-memory map (tests /
|
|
@@ -959,4 +927,4 @@ interface CompressOptions {
|
|
|
959
927
|
*/
|
|
960
928
|
declare const compressHistory: (messages: StoredMessage[], model: LanguageModel, opts?: CompressOptions) => Promise<StoredMessage[]>;
|
|
961
929
|
|
|
962
|
-
export { type Agent, type AgentContext, type AgentEvent, type AgentEventHandler, type AgentLogger, type AgentLoggerSink, type AgentTool, type AgentToolSet, type AgentWebDBOptions, type BrowserAgentConfig, type CompressOptions, type ContextStore, type CredentialStore, type EncryptedBlob, type ExecutorPromptContext, type ExecutorResult, type GenerateOptions, type IPlan, type IPlanStep, type IStepResult, type IToolCall, type IUsage, IndexedDBStore, type IndexedDBStoreOptions,
|
|
930
|
+
export { type Agent, type AgentContext, type AgentEvent, type AgentEventHandler, type AgentLogger, type AgentLoggerSink, type AgentTool, type AgentToolSet, type AgentWebDBOptions, type BrowserAgentConfig, type CompressOptions, type ContextStore, type CredentialStore, type EncryptedBlob, type ExecutorPromptContext, type ExecutorResult, type GenerateOptions, type IPlan, type IPlanStep, type IStepResult, type IToolCall, type IUsage, IndexedDBStore, type IndexedDBStoreOptions, KEYS_STORE, type LogLevel, MemoryCredentialStore, MemoryStore, type ModelInput, type Phase, type PhaseBudgets, PlanSchema, type PlanShape, PlanStepSchema, type PlannerPromptContext, type PlannerResult, type PromptParts, type Prompts, type ProviderModelSpec, type ProviderType, type RawAction, type ReplanDecision, type ReplanMode, ReplanSchema, type ReplanShape, type ReplanTrigger, type ReplannerPromptContext, type ReplannerResult, type ResolvedConfig, type ResolvedStage, type RunOptions, type RunResult, SECRETS_STORE, SESSIONS_STORE, type StageInput, type StageOverride, type StoredMessage, type SynthesizerPromptContext, type ToolCallMode, type ToolLoopCallbacks, type ToolLoopOptions, type ToolLoopResult, type ToolMode, type ToolSelectionStrategy, VaultCredentialStore, VaultOptions, type WebLLMModelOptions, buildModelFromStage, compressHistory, createAgent, createLogger, createPlan, createWebLLMModel, decideReplan, decryptJSON, defaultPrompts, defineTool, directBrowserOk, dispatch, encryptJSON, executeStep, generate, generateStructured, getOrCreateVaultKey, isDirectModel, isProviderSpec, isWebGPUAvailable, looksLikeJson, normalizeSteps, normalizeUsage, openAgentWebDB, parseExecutorResponse, parsePlainText, parsePlannerResponse, parseReplannerResponse, preloadWebLLMModel, promptHintOf, renderCatalog, replanWanted, resolveConfig, resolveModel, resolveStage, runToolLoop, selectToolMode, shouldReplan, splitBlocker, stream, supportsNativeTools, supportsStructuredOutput, synthesizeAnswer, unloadWebLLMModel, withSystem };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as ai from 'ai';
|
|
2
2
|
import { LanguageModel, ToolSet, Tool, ModelMessage, generateText, streamText } from 'ai';
|
|
3
|
+
import { V as VaultOptions } from './vault-BHlMmKXM.js';
|
|
4
|
+
export { I as IndexedDBVault } from './vault-BHlMmKXM.js';
|
|
3
5
|
import { z, ZodType } from 'zod';
|
|
4
6
|
import { IDBPDatabase } from 'idb';
|
|
5
7
|
|
|
@@ -199,40 +201,6 @@ declare const defaultPrompts: Prompts;
|
|
|
199
201
|
/** Prepend a host-supplied systemPrompt to a phase system prompt, if present. */
|
|
200
202
|
declare const withSystem: (base: string, extra?: string) => string;
|
|
201
203
|
|
|
202
|
-
interface VaultOptions {
|
|
203
|
-
/** Database name, forwarded to the shared IDB owner (default 'agent-web'). */
|
|
204
|
-
dbName?: string;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Encrypted key/value secret store backed by IndexedDB. Values are encrypted at
|
|
208
|
-
* rest with the non-extractable AES-GCM vault key (see crypto.ts).
|
|
209
|
-
*
|
|
210
|
-
* ── THREAT MODEL (read before shipping any app-owned key to a browser) ──
|
|
211
|
-
* This vault PROTECTS secrets **at rest** — an IndexedDB / disk dump yields
|
|
212
|
-
* only ciphertext — and against **casual DevTools inspection** — no plaintext
|
|
213
|
-
* key is stored, and the AES key cannot be exported even by in-page script.
|
|
214
|
-
*
|
|
215
|
-
* It does **NOT** defend against **active XSS on your origin**: any script that
|
|
216
|
-
* already runs on the page can call `getSecret()`, or use the live `CryptoKey`
|
|
217
|
-
* to decrypt. Browser storage is not a secrets manager.
|
|
218
|
-
*
|
|
219
|
-
* Therefore this vault is for the **end user's OWN key on their OWN device**
|
|
220
|
-
* (bring-your-own-key). A shared, app-owned key must NEVER reach the client —
|
|
221
|
-
* put it behind a server proxy (`ProviderModelSpec.baseURL`) or the Vercel AI
|
|
222
|
-
* Gateway instead.
|
|
223
|
-
*/
|
|
224
|
-
declare class IndexedDBVault {
|
|
225
|
-
private readonly dbName?;
|
|
226
|
-
constructor(opts?: VaultOptions);
|
|
227
|
-
private key;
|
|
228
|
-
/** Encrypt and store a secret string under `id`. */
|
|
229
|
-
setSecret(id: string, value: string): Promise<void>;
|
|
230
|
-
/** Fetch and decrypt a secret, or undefined if absent. */
|
|
231
|
-
getSecret(id: string): Promise<string | undefined>;
|
|
232
|
-
deleteSecret(id: string): Promise<void>;
|
|
233
|
-
listSecretIds(): Promise<string[]>;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
204
|
/**
|
|
237
205
|
* The seam the provider registry uses to fetch API keys at model-build time.
|
|
238
206
|
* Swap in any backend: the encrypted vault (default), an in-memory map (tests /
|
|
@@ -959,4 +927,4 @@ interface CompressOptions {
|
|
|
959
927
|
*/
|
|
960
928
|
declare const compressHistory: (messages: StoredMessage[], model: LanguageModel, opts?: CompressOptions) => Promise<StoredMessage[]>;
|
|
961
929
|
|
|
962
|
-
export { type Agent, type AgentContext, type AgentEvent, type AgentEventHandler, type AgentLogger, type AgentLoggerSink, type AgentTool, type AgentToolSet, type AgentWebDBOptions, type BrowserAgentConfig, type CompressOptions, type ContextStore, type CredentialStore, type EncryptedBlob, type ExecutorPromptContext, type ExecutorResult, type GenerateOptions, type IPlan, type IPlanStep, type IStepResult, type IToolCall, type IUsage, IndexedDBStore, type IndexedDBStoreOptions,
|
|
930
|
+
export { type Agent, type AgentContext, type AgentEvent, type AgentEventHandler, type AgentLogger, type AgentLoggerSink, type AgentTool, type AgentToolSet, type AgentWebDBOptions, type BrowserAgentConfig, type CompressOptions, type ContextStore, type CredentialStore, type EncryptedBlob, type ExecutorPromptContext, type ExecutorResult, type GenerateOptions, type IPlan, type IPlanStep, type IStepResult, type IToolCall, type IUsage, IndexedDBStore, type IndexedDBStoreOptions, KEYS_STORE, type LogLevel, MemoryCredentialStore, MemoryStore, type ModelInput, type Phase, type PhaseBudgets, PlanSchema, type PlanShape, PlanStepSchema, type PlannerPromptContext, type PlannerResult, type PromptParts, type Prompts, type ProviderModelSpec, type ProviderType, type RawAction, type ReplanDecision, type ReplanMode, ReplanSchema, type ReplanShape, type ReplanTrigger, type ReplannerPromptContext, type ReplannerResult, type ResolvedConfig, type ResolvedStage, type RunOptions, type RunResult, SECRETS_STORE, SESSIONS_STORE, type StageInput, type StageOverride, type StoredMessage, type SynthesizerPromptContext, type ToolCallMode, type ToolLoopCallbacks, type ToolLoopOptions, type ToolLoopResult, type ToolMode, type ToolSelectionStrategy, VaultCredentialStore, VaultOptions, type WebLLMModelOptions, buildModelFromStage, compressHistory, createAgent, createLogger, createPlan, createWebLLMModel, decideReplan, decryptJSON, defaultPrompts, defineTool, directBrowserOk, dispatch, encryptJSON, executeStep, generate, generateStructured, getOrCreateVaultKey, isDirectModel, isProviderSpec, isWebGPUAvailable, looksLikeJson, normalizeSteps, normalizeUsage, openAgentWebDB, parseExecutorResponse, parsePlainText, parsePlannerResponse, parseReplannerResponse, preloadWebLLMModel, promptHintOf, renderCatalog, replanWanted, resolveConfig, resolveModel, resolveStage, runToolLoop, selectToolMode, shouldReplan, splitBlocker, stream, supportsNativeTools, supportsStructuredOutput, synthesizeAnswer, unloadWebLLMModel, withSystem };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IndexedDBVault,
|
|
3
|
+
KEYS_STORE,
|
|
4
|
+
SECRETS_STORE,
|
|
5
|
+
SESSIONS_STORE,
|
|
6
|
+
decryptJSON,
|
|
7
|
+
encryptJSON,
|
|
8
|
+
getOrCreateVaultKey,
|
|
9
|
+
openAgentWebDB
|
|
10
|
+
} from "./chunk-H7W6MBK2.js";
|
|
1
11
|
import {
|
|
2
12
|
createWebLLMModel,
|
|
3
13
|
isWebGPUAvailable,
|
|
@@ -1281,120 +1291,6 @@ var supportsNativeTools = (p) => CLOUD.has(p);
|
|
|
1281
1291
|
var supportsStructuredOutput = (p) => CLOUD.has(p);
|
|
1282
1292
|
var directBrowserOk = (p) => p === "google" || p === "gateway" || p === "openai-compatible" || p === "anthropic";
|
|
1283
1293
|
|
|
1284
|
-
// src/storage/db.ts
|
|
1285
|
-
import { openDB } from "idb";
|
|
1286
|
-
var KEYS_STORE = "keys";
|
|
1287
|
-
var SECRETS_STORE = "secrets";
|
|
1288
|
-
var SESSIONS_STORE = "sessions";
|
|
1289
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1290
|
-
var openAgentWebDB = (opts = {}) => {
|
|
1291
|
-
const name = opts.dbName ?? "agent-web";
|
|
1292
|
-
let p = cache.get(name);
|
|
1293
|
-
if (!p) {
|
|
1294
|
-
p = openDB(name, 1, {
|
|
1295
|
-
upgrade(db) {
|
|
1296
|
-
if (!db.objectStoreNames.contains(KEYS_STORE)) db.createObjectStore(KEYS_STORE);
|
|
1297
|
-
if (!db.objectStoreNames.contains(SECRETS_STORE)) db.createObjectStore(SECRETS_STORE);
|
|
1298
|
-
if (!db.objectStoreNames.contains(SESSIONS_STORE)) db.createObjectStore(SESSIONS_STORE);
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1301
|
-
cache.set(name, p);
|
|
1302
|
-
p.catch(() => {
|
|
1303
|
-
if (cache.get(name) === p) cache.delete(name);
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
return p;
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
|
-
// src/secrets/crypto.ts
|
|
1310
|
-
var KEY_ID = "vault-key-v1";
|
|
1311
|
-
var webcrypto = () => {
|
|
1312
|
-
const c = globalThis.crypto;
|
|
1313
|
-
if (!c?.subtle || !c.getRandomValues) {
|
|
1314
|
-
throw new Error("WebCrypto (crypto.subtle) is unavailable in this environment");
|
|
1315
|
-
}
|
|
1316
|
-
return c;
|
|
1317
|
-
};
|
|
1318
|
-
var loadOrCreateKey = async (dbName) => {
|
|
1319
|
-
const db = await openAgentWebDB({ dbName });
|
|
1320
|
-
const existing = await db.get(KEYS_STORE, KEY_ID);
|
|
1321
|
-
if (existing) return existing;
|
|
1322
|
-
const key = await webcrypto().subtle.generateKey(
|
|
1323
|
-
{ name: "AES-GCM", length: 256 },
|
|
1324
|
-
/* extractable */
|
|
1325
|
-
false,
|
|
1326
|
-
["encrypt", "decrypt"]
|
|
1327
|
-
);
|
|
1328
|
-
const tx = db.transaction(KEYS_STORE, "readwrite");
|
|
1329
|
-
const winner = await tx.store.get(KEY_ID);
|
|
1330
|
-
if (winner) {
|
|
1331
|
-
await tx.done;
|
|
1332
|
-
return winner;
|
|
1333
|
-
}
|
|
1334
|
-
await tx.store.put(key, KEY_ID);
|
|
1335
|
-
await tx.done;
|
|
1336
|
-
return key;
|
|
1337
|
-
};
|
|
1338
|
-
var keyCache = /* @__PURE__ */ new Map();
|
|
1339
|
-
var getOrCreateVaultKey = (dbName) => {
|
|
1340
|
-
const cacheKey = dbName ?? "agent-web";
|
|
1341
|
-
let p = keyCache.get(cacheKey);
|
|
1342
|
-
if (!p) {
|
|
1343
|
-
p = loadOrCreateKey(dbName);
|
|
1344
|
-
keyCache.set(cacheKey, p);
|
|
1345
|
-
p.catch(() => {
|
|
1346
|
-
if (keyCache.get(cacheKey) === p) keyCache.delete(cacheKey);
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
return p;
|
|
1350
|
-
};
|
|
1351
|
-
var textEncoder = new TextEncoder();
|
|
1352
|
-
var textDecoder = new TextDecoder();
|
|
1353
|
-
var encryptJSON = async (key, value) => {
|
|
1354
|
-
const c = webcrypto();
|
|
1355
|
-
const iv = c.getRandomValues(new Uint8Array(12));
|
|
1356
|
-
const data = textEncoder.encode(JSON.stringify(value));
|
|
1357
|
-
const ciphertext = await c.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
|
|
1358
|
-
return { iv, ciphertext };
|
|
1359
|
-
};
|
|
1360
|
-
var decryptJSON = async (key, blob) => {
|
|
1361
|
-
const iv = new Uint8Array(blob.iv);
|
|
1362
|
-
const plain = await webcrypto().subtle.decrypt({ name: "AES-GCM", iv }, key, blob.ciphertext);
|
|
1363
|
-
return JSON.parse(textDecoder.decode(plain));
|
|
1364
|
-
};
|
|
1365
|
-
|
|
1366
|
-
// src/secrets/vault.ts
|
|
1367
|
-
var IndexedDBVault = class {
|
|
1368
|
-
dbName;
|
|
1369
|
-
constructor(opts = {}) {
|
|
1370
|
-
this.dbName = opts.dbName;
|
|
1371
|
-
}
|
|
1372
|
-
key() {
|
|
1373
|
-
return getOrCreateVaultKey(this.dbName);
|
|
1374
|
-
}
|
|
1375
|
-
/** Encrypt and store a secret string under `id`. */
|
|
1376
|
-
async setSecret(id, value) {
|
|
1377
|
-
const blob = await encryptJSON(await this.key(), value);
|
|
1378
|
-
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
1379
|
-
await db.put(SECRETS_STORE, blob, id);
|
|
1380
|
-
}
|
|
1381
|
-
/** Fetch and decrypt a secret, or undefined if absent. */
|
|
1382
|
-
async getSecret(id) {
|
|
1383
|
-
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
1384
|
-
const blob = await db.get(SECRETS_STORE, id);
|
|
1385
|
-
if (!blob) return void 0;
|
|
1386
|
-
return decryptJSON(await this.key(), blob);
|
|
1387
|
-
}
|
|
1388
|
-
async deleteSecret(id) {
|
|
1389
|
-
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
1390
|
-
await db.delete(SECRETS_STORE, id);
|
|
1391
|
-
}
|
|
1392
|
-
async listSecretIds() {
|
|
1393
|
-
const db = await openAgentWebDB({ dbName: this.dbName });
|
|
1394
|
-
return (await db.getAllKeys(SECRETS_STORE)).map(String);
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
|
|
1398
1294
|
// src/secrets/store.ts
|
|
1399
1295
|
var VaultCredentialStore = class {
|
|
1400
1296
|
vault;
|