@assistant-ui/react-devtools 1.2.13 → 1.2.15
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/dist/data/useDevToolsClient.js +7 -1
- package/dist/data/useDevToolsClient.js.map +1 -1
- package/dist/utils/serialization.d.ts.map +1 -1
- package/dist/utils/serialization.js +12 -2
- package/dist/utils/serialization.js.map +1 -1
- package/package.json +5 -5
- package/src/data/useDevToolsClient.test.ts +97 -0
- package/src/data/useDevToolsClient.ts +7 -2
- package/src/utils/serialization.test.ts +25 -0
- package/src/utils/serialization.ts +14 -2
|
@@ -42,7 +42,12 @@ const useDevToolsClient = (selectedApiId, t0) => {
|
|
|
42
42
|
let t4;
|
|
43
43
|
if ($[7] !== client) {
|
|
44
44
|
t4 = (apiId_0, threadId) => {
|
|
45
|
-
if (client.switchToThread) return
|
|
45
|
+
if (!client.switchToThread) return;
|
|
46
|
+
try {
|
|
47
|
+
return Promise.resolve(client.switchToThread(apiId_0, threadId)).catch(_temp);
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
46
51
|
};
|
|
47
52
|
$[7] = client;
|
|
48
53
|
$[8] = t4;
|
|
@@ -64,6 +69,7 @@ const useDevToolsClient = (selectedApiId, t0) => {
|
|
|
64
69
|
} else t5 = $[13];
|
|
65
70
|
return t5;
|
|
66
71
|
};
|
|
72
|
+
function _temp() {}
|
|
67
73
|
//#endregion
|
|
68
74
|
export { useDevToolsClient };
|
|
69
75
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDevToolsClient.js","names":["useCallback","useMemo","useSyncExternalStore","inProcessClient","EMPTY_SNAPSHOT","ApiInfo","DevToolsClient","DevToolsClientResult","apiIds","selected","clearEvents","apiId","switchToThread","threadId","Promise","useDevToolsClient","selectedApiId","t0","$","_c","client","undefined","t1","subscribe","listener","getSnapshot","getServerSnapshot","store","snapshot","t2","byId","get","t3","t4","apiId_0","t5"],"sources":["../../src/data/useDevToolsClient.ts"],"sourcesContent":["import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { inProcessClient } from \"./createInProcessClient\";\nimport { EMPTY_SNAPSHOT, type ApiInfo, type DevToolsClient } from \"./types\";\n\nexport interface DevToolsClientResult {\n apiIds: number[];\n selected: ApiInfo | undefined;\n clearEvents: (apiId: number) => void;\n switchToThread: (apiId: number, threadId: string) => void | Promise<void>;\n}\n\n/**\n * Reads a DevToolsClient snapshot and resolves the selected instance. The panel\n * depends only on this, so swapping the client swaps the data source without\n * touching the UI. Pass a stable client reference (the default in-process client\n * is a module singleton; memoize custom clients).\n */\nexport const useDevToolsClient = (\n selectedApiId: number | null,\n client: DevToolsClient = inProcessClient,\n): DevToolsClientResult => {\n // Bind the client methods so class-based or relay clients that rely on `this`\n // work, and keep the references stable so useSyncExternalStore does not\n // resubscribe on every render.\n const store = useMemo(\n () => ({\n subscribe: (listener: () => void) => client.subscribe(listener),\n getSnapshot: () => client.getSnapshot(),\n getServerSnapshot: () =>\n client.getServerSnapshot ? client.getServerSnapshot() : EMPTY_SNAPSHOT,\n }),\n [client],\n );\n\n const snapshot = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n );\n\n const selected =\n selectedApiId !== null ? snapshot.byId.get(selectedApiId) : undefined;\n\n const clearEvents = useCallback(\n (apiId: number) => client.clearEvents(apiId),\n [client],\n );\n\n const switchToThread = useCallback(\n (apiId: number, threadId: string) => {\n if (client.switchToThread) {\n return client.switchToThread(apiId, threadId);\n }\n },\n [client],\n );\n\n return { apiIds: snapshot.apiIds, selected, clearEvents, switchToThread };\n};\n"],"mappings":";;;;;;;;;;;AAiBA,MAAae,qBAAoBC,eAAAC,OAAA;CAAA,MAAAC,IAAAC,EAAA,EAAA;CAE/B,MAAAC,SAAAH,OAAAI,KAAAA,IAAAlB,kBAAAc;CAAwC,IAAAK;CAAA,IAAAJ,EAAA,OAAAE,QAAA;EAM/BE,KAAA;GAAAC,YACMC,aAA0BJ,OAAMG,UAAWC,QAAQ;GAACC,mBAC5CL,OAAMK,YAAa;GAACC,yBAErCN,OAAMM,oBAAqBN,OAAMM,kBAAoC,IAArEtB;EACJ;EAACc,EAAA,KAAAE;EAAAF,EAAA,KAAAI;CAAA,OAAAA,KAAAJ,EAAA;CANH,MAAAS,QACSL;CAST,MAAAM,WAAiB1B,qBACfyB,MAAKJ,WACLI,MAAKF,aACLE,MAAKD,iBACP;CAAE,IAAAG;CAAA,IAAAX,EAAA,OAAAF,iBAAAE,EAAA,OAAAU,SAAAE,MAAA;EAGAD,KAAAb,kBAAkB,OAAOY,SAAQE,KAAKC,IAAKf,aAAyB,IAApEK,KAAAA;EAAqEH,EAAA,KAAAF;EAAAE,EAAA,KAAAU,SAAAE;EAAAZ,EAAA,KAAAW;CAAA,OAAAA,KAAAX,EAAA;CADvE,MAAAT,WACEoB;CAAsE,IAAAG;CAAA,IAAAd,EAAA,OAAAE,QAAA;EAGtEY,MAAArB,UAAmBS,OAAMV,YAAaC,KAAK;EAACO,EAAA,KAAAE;EAAAF,EAAA,KAAAc;CAAA,OAAAA,KAAAd,EAAA;CAD9C,MAAAR,cAAoBsB;CAGlB,IAAAC;CAAA,IAAAf,EAAA,OAAAE,QAAA;EAGAa,MAAAC,SAAArB,aAAA;GACE,
|
|
1
|
+
{"version":3,"file":"useDevToolsClient.js","names":["useCallback","useMemo","useSyncExternalStore","inProcessClient","EMPTY_SNAPSHOT","ApiInfo","DevToolsClient","DevToolsClientResult","apiIds","selected","clearEvents","apiId","switchToThread","threadId","Promise","useDevToolsClient","selectedApiId","t0","$","_c","client","undefined","t1","subscribe","listener","getSnapshot","getServerSnapshot","store","snapshot","t2","byId","get","t3","t4","apiId_0","resolve","catch","_temp","t5"],"sources":["../../src/data/useDevToolsClient.ts"],"sourcesContent":["import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { inProcessClient } from \"./createInProcessClient\";\nimport { EMPTY_SNAPSHOT, type ApiInfo, type DevToolsClient } from \"./types\";\n\nexport interface DevToolsClientResult {\n apiIds: number[];\n selected: ApiInfo | undefined;\n clearEvents: (apiId: number) => void;\n switchToThread: (apiId: number, threadId: string) => void | Promise<void>;\n}\n\n/**\n * Reads a DevToolsClient snapshot and resolves the selected instance. The panel\n * depends only on this, so swapping the client swaps the data source without\n * touching the UI. Pass a stable client reference (the default in-process client\n * is a module singleton; memoize custom clients).\n */\nexport const useDevToolsClient = (\n selectedApiId: number | null,\n client: DevToolsClient = inProcessClient,\n): DevToolsClientResult => {\n // Bind the client methods so class-based or relay clients that rely on `this`\n // work, and keep the references stable so useSyncExternalStore does not\n // resubscribe on every render.\n const store = useMemo(\n () => ({\n subscribe: (listener: () => void) => client.subscribe(listener),\n getSnapshot: () => client.getSnapshot(),\n getServerSnapshot: () =>\n client.getServerSnapshot ? client.getServerSnapshot() : EMPTY_SNAPSHOT,\n }),\n [client],\n );\n\n const snapshot = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n );\n\n const selected =\n selectedApiId !== null ? snapshot.byId.get(selectedApiId) : undefined;\n\n const clearEvents = useCallback(\n (apiId: number) => client.clearEvents(apiId),\n [client],\n );\n\n const switchToThread = useCallback(\n (apiId: number, threadId: string) => {\n if (!client.switchToThread) return;\n try {\n return Promise.resolve(client.switchToThread(apiId, threadId)).catch(\n () => {},\n );\n } catch {\n return;\n }\n },\n [client],\n );\n\n return { apiIds: snapshot.apiIds, selected, clearEvents, switchToThread };\n};\n"],"mappings":";;;;;;;;;;;AAiBA,MAAae,qBAAoBC,eAAAC,OAAA;CAAA,MAAAC,IAAAC,EAAA,EAAA;CAE/B,MAAAC,SAAAH,OAAAI,KAAAA,IAAAlB,kBAAAc;CAAwC,IAAAK;CAAA,IAAAJ,EAAA,OAAAE,QAAA;EAM/BE,KAAA;GAAAC,YACMC,aAA0BJ,OAAMG,UAAWC,QAAQ;GAACC,mBAC5CL,OAAMK,YAAa;GAACC,yBAErCN,OAAMM,oBAAqBN,OAAMM,kBAAoC,IAArEtB;EACJ;EAACc,EAAA,KAAAE;EAAAF,EAAA,KAAAI;CAAA,OAAAA,KAAAJ,EAAA;CANH,MAAAS,QACSL;CAST,MAAAM,WAAiB1B,qBACfyB,MAAKJ,WACLI,MAAKF,aACLE,MAAKD,iBACP;CAAE,IAAAG;CAAA,IAAAX,EAAA,OAAAF,iBAAAE,EAAA,OAAAU,SAAAE,MAAA;EAGAD,KAAAb,kBAAkB,OAAOY,SAAQE,KAAKC,IAAKf,aAAyB,IAApEK,KAAAA;EAAqEH,EAAA,KAAAF;EAAAE,EAAA,KAAAU,SAAAE;EAAAZ,EAAA,KAAAW;CAAA,OAAAA,KAAAX,EAAA;CADvE,MAAAT,WACEoB;CAAsE,IAAAG;CAAA,IAAAd,EAAA,OAAAE,QAAA;EAGtEY,MAAArB,UAAmBS,OAAMV,YAAaC,KAAK;EAACO,EAAA,KAAAE;EAAAF,EAAA,KAAAc;CAAA,OAAAA,KAAAd,EAAA;CAD9C,MAAAR,cAAoBsB;CAGlB,IAAAC;CAAA,IAAAf,EAAA,OAAAE,QAAA;EAGAa,MAAAC,SAAArB,aAAA;GACE,IAAI,CAACO,OAAMR,gBAAe;GAC1B,IAAA;IAAA,OACSE,QAAOqB,QAASf,OAAMR,eAAgBD,SAAOE,QAAQ,CAAC,CAAC,CAAAuB,MAC5DC,KACF;GAAC,QAAA;IAAA;GAAA;EAGF;EACFnB,EAAA,KAAAE;EAAAF,EAAA,KAAAe;CAAA,OAAAA,KAAAf,EAAA;CAVH,MAAAN,iBAAuBqB;CAYrB,IAAAK;CAAA,IAAApB,EAAA,OAAAR,eAAAQ,EAAA,QAAAT,YAAAS,EAAA,QAAAU,SAAApB,UAAAU,EAAA,QAAAN,gBAAA;EAEK0B,KAAA;GAAA9B,QAAUoB,SAAQpB;GAAOC;GAAAC;GAAAE;EAAwC;EAACM,EAAA,KAAAR;EAAAQ,EAAA,MAAAT;EAAAS,EAAA,MAAAU,SAAApB;EAAAU,EAAA,MAAAN;EAAAM,EAAA,MAAAoB;CAAA,OAAAA,KAAApB,EAAA;CAAA,OAAlEoB;AAAkE;AA7C1C,SAAAD,QAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.d.ts","names":[],"sources":["../../src/utils/serialization.ts"],"mappings":";;;cAIa,qBACX,gBACA,OAAA;
|
|
1
|
+
{"version":3,"file":"serialization.d.ts","names":[],"sources":["../../src/utils/serialization.ts"],"mappings":";;;cAIa,qBACX,gBACA,OAAA;cA+DW;;;;;;;;cAwCA,kBAAmB,gBAAgB;cAoBnC,oBAAqB;cAGrB,wBACX,SAAS,6BACR"}
|
|
@@ -3,14 +3,24 @@ import { normalizeToolList } from "./toolNormalization.js";
|
|
|
3
3
|
const sanitizeForMessage = (value, seen = /* @__PURE__ */ new WeakSet()) => {
|
|
4
4
|
if (value === null || value === void 0) return value;
|
|
5
5
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
6
|
+
if (typeof value === "bigint" || typeof value === "symbol") return String(value);
|
|
6
7
|
if (typeof value === "function") return "[Function]";
|
|
7
|
-
if (value instanceof Date) return value.toISOString();
|
|
8
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
|
|
8
9
|
if (value instanceof Map) {
|
|
10
|
+
if (seen.has(value)) return "[Circular]";
|
|
11
|
+
seen.add(value);
|
|
9
12
|
const result = {};
|
|
10
13
|
for (const [key, entry] of value.entries()) result[String(key)] = sanitizeForMessage(entry, seen);
|
|
14
|
+
seen.delete(value);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
if (value instanceof Set) {
|
|
18
|
+
if (seen.has(value)) return "[Circular]";
|
|
19
|
+
seen.add(value);
|
|
20
|
+
const result = Array.from(value).map((entry) => sanitizeForMessage(entry, seen));
|
|
21
|
+
seen.delete(value);
|
|
11
22
|
return result;
|
|
12
23
|
}
|
|
13
|
-
if (value instanceof Set) return Array.from(value).map((entry) => sanitizeForMessage(entry, seen));
|
|
14
24
|
if (Array.isArray(value)) {
|
|
15
25
|
if (seen.has(value)) return "[Circular]";
|
|
16
26
|
seen.add(value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.js","names":["ModelContext","SerializedModelContext","normalizeToolList","NormalizedTool","sanitizeForMessage","value","seen","WeakSet","undefined","Date","toISOString","Map","result","Record","key","entry","entries","String","Set","Array","from","map","isArray","has","add","filter","item","delete","Object","REDACTED","SENSITIVE_KEYS","normalizeKey","toLowerCase","replace","MASK_ALL_KEYS","redactSensitive","maskAll","normalized","sanitizeAndRedact","serializeModelContext","context","modelContext","systemValue","system","length","tools","tool","parameters","providerOptions","providerArgs","server","backendDefault","callSettings","config","keys"],"sources":["../../src/utils/serialization.ts"],"sourcesContent":["import type { ModelContext } from \"@assistant-ui/react\";\nimport type { SerializedModelContext } from \"../types\";\nimport { normalizeToolList, type NormalizedTool } from \"./toolNormalization\";\n\nexport const sanitizeForMessage = (\n value: unknown,\n seen = new WeakSet<object>(),\n): unknown => {\n // Early return for primitives\n if (value === null || value === undefined) return value;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"function\") {\n return \"[Function]\";\n }\n if (value instanceof Date) {\n return value.toISOString();\n }\n if (value instanceof Map) {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of value.entries()) {\n result[String(key)] = sanitizeForMessage(entry, seen);\n }\n return result;\n }\n if (value instanceof Set) {\n return Array.from(value).map((entry) => sanitizeForMessage(entry, seen));\n }\n if (Array.isArray(value)) {\n if (seen.has(value as unknown as object)) return \"[Circular]\";\n seen.add(value as unknown as object);\n const result = value\n .map((entry) => sanitizeForMessage(entry, seen))\n .filter((item) => item !== undefined);\n seen.delete(value as unknown as object);\n return result;\n }\n if (typeof value === \"object\") {\n if (seen.has(value as object)) return \"[Circular]\";\n seen.add(value as object);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n result[key] = sanitizeForMessage(entry, seen);\n }\n seen.delete(value as object);\n return result;\n }\n return value;\n};\n\nexport const REDACTED = \"[redacted]\";\n\nconst SENSITIVE_KEYS = new Set([\n \"apikey\",\n \"xapikey\",\n \"accesskey\",\n \"authorization\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"clientsecret\",\n \"token\",\n \"accesstoken\",\n \"refreshtoken\",\n \"cookie\",\n \"setcookie\",\n \"credential\",\n \"credentials\",\n \"privatekey\",\n \"bearer\",\n \"sessionid\",\n]);\n\nconst normalizeKey = (key: string) => key.toLowerCase().replace(/[-_]/g, \"\");\n\n/**\n * Subtrees that are credential maps with arbitrary, user-defined key names\n * (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch\n * `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these\n * is masked wholesale.\n */\nconst MASK_ALL_KEYS = new Set([\"env\", \"headers\"]);\n\n/**\n * Mask values whose key names a known credential, and mask every value inside\n * an `env`/`headers` subtree wholesale. Operates on already sanitized plain\n * data (primitives, arrays, plain objects). Applied only to config-bearing\n * subtrees, never to a tool's parameters schema, so a schema property literally\n * named `token` is not corrupted.\n */\nexport const redactSensitive = (value: unknown, maskAll = false): unknown => {\n if (Array.isArray(value)) {\n return value.map((entry) => redactSensitive(entry, maskAll));\n }\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n const normalized = normalizeKey(key);\n result[key] =\n maskAll || SENSITIVE_KEYS.has(normalized)\n ? REDACTED\n : redactSensitive(entry, MASK_ALL_KEYS.has(normalized));\n }\n return result;\n }\n return maskAll ? REDACTED : value;\n};\n\nexport const sanitizeAndRedact = (value: unknown): unknown =>\n redactSensitive(sanitizeForMessage(value));\n\nexport const serializeModelContext = (\n context: ModelContext | undefined,\n): SerializedModelContext | undefined => {\n if (!context || typeof context !== \"object\") {\n return undefined;\n }\n\n const modelContext = context as Record<string, unknown>;\n const result: SerializedModelContext = {};\n\n const systemValue = modelContext.system;\n if (typeof systemValue === \"string\" && systemValue.length > 0) {\n result.system = systemValue;\n }\n\n const tools = normalizeToolList(modelContext.tools);\n if (tools.length > 0) {\n result.tools = tools.map((tool): NormalizedTool => {\n return {\n ...tool,\n parameters: sanitizeForMessage(tool.parameters),\n ...(tool.providerOptions !== undefined\n ? { providerOptions: sanitizeAndRedact(tool.providerOptions) }\n : {}),\n ...(tool.providerArgs !== undefined\n ? { providerArgs: sanitizeAndRedact(tool.providerArgs) }\n : {}),\n ...(tool.server !== undefined\n ? { server: sanitizeAndRedact(tool.server) }\n : {}),\n ...(tool.backendDefault !== undefined\n ? { backendDefault: sanitizeForMessage(tool.backendDefault) }\n : {}),\n };\n });\n }\n\n if (modelContext.callSettings !== undefined) {\n const callSettings = sanitizeAndRedact(modelContext.callSettings);\n if (\n callSettings &&\n typeof callSettings === \"object\" &&\n !Array.isArray(callSettings)\n ) {\n result.callSettings = callSettings as Record<string, unknown>;\n }\n }\n\n if (modelContext.config !== undefined) {\n const config = sanitizeAndRedact(modelContext.config);\n if (config && typeof config === \"object\" && !Array.isArray(config)) {\n result.config = config as Record<string, unknown>;\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n};\n"],"mappings":";;AAIA,MAAaI,sBACXC,OACAC,uBAAO,IAAIC,QAAgB,MACf;CAEZ,IAAIF,UAAU,QAAQA,UAAUG,KAAAA,GAAW,OAAOH;CAClD,IACE,OAAOA,UAAU,YACjB,OAAOA,UAAU,YACjB,OAAOA,UAAU,WAEjB,OAAOA;CAET,IAAI,OAAOA,UAAU,YACnB,OAAO;CAET,IAAIA,iBAAiBI,MACnB,OAAOJ,MAAMK,YAAY;CAE3B,IAAIL,iBAAiBM,KAAK;EACxB,MAAMC,SAAkC,CAAC;EACzC,KAAK,MAAM,CAACE,KAAKC,UAAUV,MAAMW,QAAQ,GACvCJ,OAAOK,OAAOH,GAAG,KAAKV,mBAAmBW,OAAOT,IAAI;EAEtD,OAAOM;CACT;CACA,IAAIP,iBAAiBa,KACnB,OAAOC,MAAMC,KAAKf,KAAK,CAAC,CAACgB,KAAKN,UAAUX,mBAAmBW,OAAOT,IAAI,CAAC;CAEzE,IAAIa,MAAMG,QAAQjB,KAAK,GAAG;EACxB,IAAIC,KAAKiB,IAAIlB,KAA0B,GAAG,OAAO;EACjDC,KAAKkB,IAAInB,KAA0B;EACnC,MAAMO,SAASP,MACZgB,KAAKN,UAAUX,mBAAmBW,OAAOT,IAAI,CAAC,CAAC,CAC/CmB,QAAQC,SAASA,SAASlB,KAAAA,CAAS;EACtCF,KAAKqB,OAAOtB,KAA0B;EACtC,OAAOO;CACT;CACA,IAAI,OAAOP,UAAU,UAAU;EAC7B,IAAIC,KAAKiB,IAAIlB,KAAe,GAAG,OAAO;EACtCC,KAAKkB,IAAInB,KAAe;EACxB,MAAMO,SAAkC,CAAC;EACzC,KAAK,MAAM,CAACE,KAAKC,UAAUa,OAAOZ,QAChCX,KACF,GACEO,OAAOE,OAAOV,mBAAmBW,OAAOT,IAAI;EAE9CA,KAAKqB,OAAOtB,KAAe;EAC3B,OAAOO;CACT;CACA,OAAOP;AACT;AAEA,MAAawB,WAAW;AAExB,MAAMC,iCAAiB,IAAIZ,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAAW,CACZ;AAED,MAAMa,gBAAgBjB,QAAgBA,IAAIkB,YAAY,CAAC,CAACC,QAAQ,SAAS,EAAE;;;;;;;AAQ3E,MAAMC,gCAAgB,IAAIhB,IAAI,CAAC,OAAO,SAAS,CAAC;;;;;;;;AAShD,MAAaiB,mBAAmB9B,OAAgB+B,UAAU,UAAmB;CAC3E,IAAIjB,MAAMG,QAAQjB,KAAK,GACrB,OAAOA,MAAMgB,KAAKN,UAAUoB,gBAAgBpB,OAAOqB,OAAO,CAAC;CAE7D,IAAI/B,SAAS,OAAOA,UAAU,UAAU;EACtC,MAAMO,SAAkC,CAAC;EACzC,KAAK,MAAM,CAACE,KAAKC,UAAUa,OAAOZ,QAChCX,KACF,GAAG;GACD,MAAMgC,aAAaN,aAAajB,GAAG;GACnCF,OAAOE,OACLsB,WAAWN,eAAeP,IAAIc,UAAU,IACpCR,WACAM,gBAAgBpB,OAAOmB,cAAcX,IAAIc,UAAU,CAAC;EAC5D;EACA,OAAOzB;CACT;CACA,OAAOwB,UAAUP,WAAWxB;AAC9B;AAEA,MAAaiC,qBAAqBjC,UAChC8B,gBAAgB/B,mBAAmBC,KAAK,CAAC;AAE3C,MAAakC,yBACXC,YACuC;CACvC,IAAI,CAACA,WAAW,OAAOA,YAAY,UACjC;CAGF,MAAMC,eAAeD;CACrB,MAAM5B,SAAiC,CAAC;CAExC,MAAM8B,cAAcD,aAAaE;CACjC,IAAI,OAAOD,gBAAgB,YAAYA,YAAYE,SAAS,GAC1DhC,OAAO+B,SAASD;CAGlB,MAAMG,QAAQ3C,kBAAkBuC,aAAaI,KAAK;CAClD,IAAIA,MAAMD,SAAS,GACjBhC,OAAOiC,QAAQA,MAAMxB,KAAKyB,SAAyB;EACjD,OAAO;GACL,GAAGA;GACHC,YAAY3C,mBAAmB0C,KAAKC,UAAU;GAC9C,GAAID,KAAKE,oBAAoBxC,KAAAA,IACzB,EAAEwC,iBAAiBV,kBAAkBQ,KAAKE,eAAe,EAAE,IAC3D,CAAC;GACL,GAAIF,KAAKG,iBAAiBzC,KAAAA,IACtB,EAAEyC,cAAcX,kBAAkBQ,KAAKG,YAAY,EAAE,IACrD,CAAC;GACL,GAAIH,KAAKI,WAAW1C,KAAAA,IAChB,EAAE0C,QAAQZ,kBAAkBQ,KAAKI,MAAM,EAAE,IACzC,CAAC;GACL,GAAIJ,KAAKK,mBAAmB3C,KAAAA,IACxB,EAAE2C,gBAAgB/C,mBAAmB0C,KAAKK,cAAc,EAAE,IAC1D,CAAC;EACP;CACF,CAAC;CAGH,IAAIV,aAAaW,iBAAiB5C,KAAAA,GAAW;EAC3C,MAAM4C,eAAed,kBAAkBG,aAAaW,YAAY;EAChE,IACEA,gBACA,OAAOA,iBAAiB,YACxB,CAACjC,MAAMG,QAAQ8B,YAAY,GAE3BxC,OAAOwC,eAAeA;CAE1B;CAEA,IAAIX,aAAaY,WAAW7C,KAAAA,GAAW;EACrC,MAAM6C,SAASf,kBAAkBG,aAAaY,MAAM;EACpD,IAAIA,UAAU,OAAOA,WAAW,YAAY,CAAClC,MAAMG,QAAQ+B,MAAM,GAC/DzC,OAAOyC,SAASA;CAEpB;CAEA,OAAOzB,OAAO0B,KAAK1C,MAAM,CAAC,CAACgC,SAAS,IAAIhC,SAASJ,KAAAA;AACnD"}
|
|
1
|
+
{"version":3,"file":"serialization.js","names":["ModelContext","SerializedModelContext","normalizeToolList","NormalizedTool","sanitizeForMessage","value","seen","WeakSet","undefined","String","Date","Number","isNaN","getTime","toISOString","Map","has","add","result","Record","key","entry","entries","delete","Set","Array","from","map","isArray","filter","item","Object","REDACTED","SENSITIVE_KEYS","normalizeKey","toLowerCase","replace","MASK_ALL_KEYS","redactSensitive","maskAll","normalized","sanitizeAndRedact","serializeModelContext","context","modelContext","systemValue","system","length","tools","tool","parameters","providerOptions","providerArgs","server","backendDefault","callSettings","config","keys"],"sources":["../../src/utils/serialization.ts"],"sourcesContent":["import type { ModelContext } from \"@assistant-ui/react\";\nimport type { SerializedModelContext } from \"../types\";\nimport { normalizeToolList, type NormalizedTool } from \"./toolNormalization\";\n\nexport const sanitizeForMessage = (\n value: unknown,\n seen = new WeakSet<object>(),\n): unknown => {\n // Early return for primitives\n if (value === null || value === undefined) return value;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"bigint\" || typeof value === \"symbol\") {\n return String(value);\n }\n if (typeof value === \"function\") {\n return \"[Function]\";\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();\n }\n if (value instanceof Map) {\n if (seen.has(value)) return \"[Circular]\";\n seen.add(value);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of value.entries()) {\n result[String(key)] = sanitizeForMessage(entry, seen);\n }\n seen.delete(value);\n return result;\n }\n if (value instanceof Set) {\n if (seen.has(value)) return \"[Circular]\";\n seen.add(value);\n const result = Array.from(value).map((entry) =>\n sanitizeForMessage(entry, seen),\n );\n seen.delete(value);\n return result;\n }\n if (Array.isArray(value)) {\n if (seen.has(value as unknown as object)) return \"[Circular]\";\n seen.add(value as unknown as object);\n const result = value\n .map((entry) => sanitizeForMessage(entry, seen))\n .filter((item) => item !== undefined);\n seen.delete(value as unknown as object);\n return result;\n }\n if (typeof value === \"object\") {\n if (seen.has(value as object)) return \"[Circular]\";\n seen.add(value as object);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n result[key] = sanitizeForMessage(entry, seen);\n }\n seen.delete(value as object);\n return result;\n }\n return value;\n};\n\nexport const REDACTED = \"[redacted]\";\n\nconst SENSITIVE_KEYS = new Set([\n \"apikey\",\n \"xapikey\",\n \"accesskey\",\n \"authorization\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"clientsecret\",\n \"token\",\n \"accesstoken\",\n \"refreshtoken\",\n \"cookie\",\n \"setcookie\",\n \"credential\",\n \"credentials\",\n \"privatekey\",\n \"bearer\",\n \"sessionid\",\n]);\n\nconst normalizeKey = (key: string) => key.toLowerCase().replace(/[-_]/g, \"\");\n\n/**\n * Subtrees that are credential maps with arbitrary, user-defined key names\n * (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch\n * `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these\n * is masked wholesale.\n */\nconst MASK_ALL_KEYS = new Set([\"env\", \"headers\"]);\n\n/**\n * Mask values whose key names a known credential, and mask every value inside\n * an `env`/`headers` subtree wholesale. Operates on already sanitized plain\n * data (primitives, arrays, plain objects). Applied only to config-bearing\n * subtrees, never to a tool's parameters schema, so a schema property literally\n * named `token` is not corrupted.\n */\nexport const redactSensitive = (value: unknown, maskAll = false): unknown => {\n if (Array.isArray(value)) {\n return value.map((entry) => redactSensitive(entry, maskAll));\n }\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n const normalized = normalizeKey(key);\n result[key] =\n maskAll || SENSITIVE_KEYS.has(normalized)\n ? REDACTED\n : redactSensitive(entry, MASK_ALL_KEYS.has(normalized));\n }\n return result;\n }\n return maskAll ? REDACTED : value;\n};\n\nexport const sanitizeAndRedact = (value: unknown): unknown =>\n redactSensitive(sanitizeForMessage(value));\n\nexport const serializeModelContext = (\n context: ModelContext | undefined,\n): SerializedModelContext | undefined => {\n if (!context || typeof context !== \"object\") {\n return undefined;\n }\n\n const modelContext = context as Record<string, unknown>;\n const result: SerializedModelContext = {};\n\n const systemValue = modelContext.system;\n if (typeof systemValue === \"string\" && systemValue.length > 0) {\n result.system = systemValue;\n }\n\n const tools = normalizeToolList(modelContext.tools);\n if (tools.length > 0) {\n result.tools = tools.map((tool): NormalizedTool => {\n return {\n ...tool,\n parameters: sanitizeForMessage(tool.parameters),\n ...(tool.providerOptions !== undefined\n ? { providerOptions: sanitizeAndRedact(tool.providerOptions) }\n : {}),\n ...(tool.providerArgs !== undefined\n ? { providerArgs: sanitizeAndRedact(tool.providerArgs) }\n : {}),\n ...(tool.server !== undefined\n ? { server: sanitizeAndRedact(tool.server) }\n : {}),\n ...(tool.backendDefault !== undefined\n ? { backendDefault: sanitizeForMessage(tool.backendDefault) }\n : {}),\n };\n });\n }\n\n if (modelContext.callSettings !== undefined) {\n const callSettings = sanitizeAndRedact(modelContext.callSettings);\n if (\n callSettings &&\n typeof callSettings === \"object\" &&\n !Array.isArray(callSettings)\n ) {\n result.callSettings = callSettings as Record<string, unknown>;\n }\n }\n\n if (modelContext.config !== undefined) {\n const config = sanitizeAndRedact(modelContext.config);\n if (config && typeof config === \"object\" && !Array.isArray(config)) {\n result.config = config as Record<string, unknown>;\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-devtools",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.15",
|
|
4
4
|
"description": "React development tools for assistant-ui components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant-ui",
|
|
@@ -48,16 +48,16 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@tailwindcss/cli": "^4.3.3",
|
|
51
|
-
"@types/node": "^26.
|
|
51
|
+
"@types/node": "^26.2.0",
|
|
52
52
|
"@types/react": "^19.2.18",
|
|
53
53
|
"@types/react-dom": "^19.2.4",
|
|
54
54
|
"react": "^19.2.8",
|
|
55
55
|
"react-dom": "^19.2.8",
|
|
56
56
|
"tailwindcss": "^4.3.3",
|
|
57
57
|
"vitest": "^4.1.10",
|
|
58
|
-
"@assistant-ui/react": "0.15.
|
|
59
|
-
"@assistant-ui/tap": "0.9.
|
|
60
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
58
|
+
"@assistant-ui/react": "0.15.16",
|
|
59
|
+
"@assistant-ui/tap": "0.9.14",
|
|
60
|
+
"@assistant-ui/x-buildutils": "0.0.23"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"access": "public",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { EMPTY_SNAPSHOT, type DevToolsClient } from "./types";
|
|
5
|
+
import {
|
|
6
|
+
useDevToolsClient,
|
|
7
|
+
type DevToolsClientResult,
|
|
8
|
+
} from "./useDevToolsClient";
|
|
9
|
+
|
|
10
|
+
const fixtureClient = (
|
|
11
|
+
switchToThread?: DevToolsClient["switchToThread"],
|
|
12
|
+
): DevToolsClient => {
|
|
13
|
+
const client: DevToolsClient = {
|
|
14
|
+
subscribe: () => () => {},
|
|
15
|
+
getSnapshot: () => EMPTY_SNAPSHOT,
|
|
16
|
+
clearEvents: () => {},
|
|
17
|
+
};
|
|
18
|
+
if (switchToThread) client.switchToThread = switchToThread;
|
|
19
|
+
return client;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const hookResult = (client: DevToolsClient): DevToolsClientResult => {
|
|
23
|
+
let result: DevToolsClientResult | undefined;
|
|
24
|
+
const Probe = () => {
|
|
25
|
+
result = useDevToolsClient(1, client);
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
renderToStaticMarkup(createElement(Probe));
|
|
29
|
+
if (result === undefined) {
|
|
30
|
+
throw new Error("expected useDevToolsClient to render");
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const captureUnhandledRejections = async (run: () => void) => {
|
|
36
|
+
const rejections: unknown[] = [];
|
|
37
|
+
const onUnhandledRejection = (reason: unknown) => {
|
|
38
|
+
rejections.push(reason);
|
|
39
|
+
};
|
|
40
|
+
const priorListeners = process.listeners("unhandledRejection");
|
|
41
|
+
process.removeAllListeners("unhandledRejection");
|
|
42
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
43
|
+
try {
|
|
44
|
+
run();
|
|
45
|
+
// Node emits unhandledRejection on a later event-loop turn; fake timers cannot drive that.
|
|
46
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
47
|
+
setImmediate(resolve);
|
|
48
|
+
await promise;
|
|
49
|
+
} finally {
|
|
50
|
+
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
51
|
+
for (const listener of priorListeners) {
|
|
52
|
+
process.on("unhandledRejection", listener);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return rejections;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("useDevToolsClient switchToThread", () => {
|
|
59
|
+
it("forwards a successful switch", async () => {
|
|
60
|
+
const switchToThread = vi.fn(async () => {});
|
|
61
|
+
const run = hookResult(fixtureClient(switchToThread)).switchToThread;
|
|
62
|
+
|
|
63
|
+
await expect(run(1, "thread-1")).resolves.toBeUndefined();
|
|
64
|
+
expect(switchToThread).toHaveBeenCalledWith(1, "thread-1");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("contains a synchronous client throw", () => {
|
|
68
|
+
const run = hookResult(
|
|
69
|
+
fixtureClient(() => {
|
|
70
|
+
throw new Error("failed");
|
|
71
|
+
}),
|
|
72
|
+
).switchToThread;
|
|
73
|
+
|
|
74
|
+
expect(() => run(1, "thread-1")).not.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("does not leak a rejected switch as an unhandled rejection", async () => {
|
|
78
|
+
const error = new Error("failed");
|
|
79
|
+
const run = hookResult(
|
|
80
|
+
fixtureClient(() => Promise.reject(error)),
|
|
81
|
+
).switchToThread;
|
|
82
|
+
|
|
83
|
+
const rejections = await captureUnhandledRejections(() => {
|
|
84
|
+
void run(1, "thread-1");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(rejections).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("lets an ignored rejected switch settle", async () => {
|
|
91
|
+
const run = hookResult(
|
|
92
|
+
fixtureClient(() => Promise.reject(new Error("failed"))),
|
|
93
|
+
).switchToThread;
|
|
94
|
+
|
|
95
|
+
await expect(run(1, "thread-1")).resolves.toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -48,8 +48,13 @@ export const useDevToolsClient = (
|
|
|
48
48
|
|
|
49
49
|
const switchToThread = useCallback(
|
|
50
50
|
(apiId: number, threadId: string) => {
|
|
51
|
-
if (client.switchToThread)
|
|
52
|
-
|
|
51
|
+
if (!client.switchToThread) return;
|
|
52
|
+
try {
|
|
53
|
+
return Promise.resolve(client.switchToThread(apiId, threadId)).catch(
|
|
54
|
+
() => {},
|
|
55
|
+
);
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
53
58
|
}
|
|
54
59
|
},
|
|
55
60
|
[client],
|
|
@@ -33,6 +33,31 @@ describe("sanitizeForMessage", () => {
|
|
|
33
33
|
const cyclicArray: unknown[] = [];
|
|
34
34
|
cyclicArray.push(cyclicArray);
|
|
35
35
|
expect(sanitizeForMessage(cyclicArray)).toEqual(["[Circular]"]);
|
|
36
|
+
|
|
37
|
+
const cyclicMap = new Map<string, unknown>();
|
|
38
|
+
cyclicMap.set("self", cyclicMap);
|
|
39
|
+
expect(sanitizeForMessage(cyclicMap)).toEqual({ self: "[Circular]" });
|
|
40
|
+
|
|
41
|
+
const cyclicSet = new Set<unknown>();
|
|
42
|
+
cyclicSet.add(cyclicSet);
|
|
43
|
+
expect(sanitizeForMessage(cyclicSet)).toEqual(["[Circular]"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("converts non-JSON primitives to strings", () => {
|
|
47
|
+
const result = sanitizeForMessage({
|
|
48
|
+
bigint: 42n,
|
|
49
|
+
symbol: Symbol("status"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
bigint: "42",
|
|
54
|
+
symbol: "Symbol(status)",
|
|
55
|
+
});
|
|
56
|
+
expect(() => JSON.stringify(result)).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("sanitizes invalid dates without throwing", () => {
|
|
60
|
+
expect(sanitizeForMessage(new Date(Number.NaN))).toBe("Invalid Date");
|
|
36
61
|
});
|
|
37
62
|
});
|
|
38
63
|
|
|
@@ -15,21 +15,33 @@ export const sanitizeForMessage = (
|
|
|
15
15
|
) {
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
|
+
if (typeof value === "bigint" || typeof value === "symbol") {
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
18
21
|
if (typeof value === "function") {
|
|
19
22
|
return "[Function]";
|
|
20
23
|
}
|
|
21
24
|
if (value instanceof Date) {
|
|
22
|
-
return value.toISOString();
|
|
25
|
+
return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
|
|
23
26
|
}
|
|
24
27
|
if (value instanceof Map) {
|
|
28
|
+
if (seen.has(value)) return "[Circular]";
|
|
29
|
+
seen.add(value);
|
|
25
30
|
const result: Record<string, unknown> = {};
|
|
26
31
|
for (const [key, entry] of value.entries()) {
|
|
27
32
|
result[String(key)] = sanitizeForMessage(entry, seen);
|
|
28
33
|
}
|
|
34
|
+
seen.delete(value);
|
|
29
35
|
return result;
|
|
30
36
|
}
|
|
31
37
|
if (value instanceof Set) {
|
|
32
|
-
|
|
38
|
+
if (seen.has(value)) return "[Circular]";
|
|
39
|
+
seen.add(value);
|
|
40
|
+
const result = Array.from(value).map((entry) =>
|
|
41
|
+
sanitizeForMessage(entry, seen),
|
|
42
|
+
);
|
|
43
|
+
seen.delete(value);
|
|
44
|
+
return result;
|
|
33
45
|
}
|
|
34
46
|
if (Array.isArray(value)) {
|
|
35
47
|
if (seen.has(value as unknown as object)) return "[Circular]";
|