@opengeni/sdk 3.3.2 → 3.4.0

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.
Files changed (299) hide show
  1. package/README.md +1007 -72
  2. package/dist/adapters/express.d.ts +31 -0
  3. package/dist/adapters/express.js +87 -0
  4. package/dist/adapters/express.js.map +1 -0
  5. package/dist/adapters/hono.d.ts +16 -0
  6. package/dist/adapters/hono.js +8 -0
  7. package/dist/adapters/hono.js.map +1 -0
  8. package/dist/adapters/next.d.ts +29 -0
  9. package/dist/adapters/next.js +22 -0
  10. package/dist/adapters/next.js.map +1 -0
  11. package/dist/allowance-refusal.d.ts +2 -0
  12. package/dist/allowance-refusal.js +50 -0
  13. package/dist/allowance-refusal.js.map +1 -0
  14. package/dist/app-links.d.ts +42 -0
  15. package/dist/app-links.js +66 -0
  16. package/dist/app-links.js.map +1 -0
  17. package/dist/artifact-catalog.d.ts +40 -0
  18. package/dist/artifact-client.d.ts +20 -8
  19. package/dist/artifacts.d.ts +1 -1
  20. package/dist/artifacts.js +8 -6
  21. package/dist/automations.js +8 -2
  22. package/dist/automations.js.map +1 -1
  23. package/dist/azure-live-transport.d.ts +32 -0
  24. package/dist/browser.d.ts +7 -2
  25. package/dist/browser.js +27 -5
  26. package/dist/chat/fold.d.ts +75 -0
  27. package/dist/chat/handler.d.ts +41 -0
  28. package/dist/chat/http.d.ts +67 -0
  29. package/dist/chat/ids.d.ts +34 -0
  30. package/dist/chat/index.d.ts +9 -0
  31. package/dist/chat/index.js +1369 -0
  32. package/dist/chat/index.js.map +1 -0
  33. package/dist/chat/openai.d.ts +30 -0
  34. package/dist/chat/opengeni.d.ts +129 -0
  35. package/dist/chat/types.d.ts +207 -0
  36. package/dist/chat/vercel.d.ts +49 -0
  37. package/dist/chats.d.ts +8 -0
  38. package/dist/chunk-4AHR4KGA.js +301 -0
  39. package/dist/chunk-4AHR4KGA.js.map +1 -0
  40. package/dist/chunk-6GYGMLX4.js +332 -0
  41. package/dist/chunk-6GYGMLX4.js.map +1 -0
  42. package/dist/chunk-7EWT3PV6.js +368 -0
  43. package/dist/chunk-7EWT3PV6.js.map +1 -0
  44. package/dist/chunk-FTWVVPR6.js +1665 -0
  45. package/dist/chunk-FTWVVPR6.js.map +1 -0
  46. package/dist/{chunk-VYCTL62C.js → chunk-GZQSWAKD.js} +1 -1
  47. package/dist/chunk-GZQSWAKD.js.map +1 -0
  48. package/dist/chunk-KGEABAID.js +71 -0
  49. package/dist/chunk-KGEABAID.js.map +1 -0
  50. package/dist/{chunk-YKZME56C.js → chunk-KWADQ227.js} +3 -3
  51. package/dist/{chunk-YKZME56C.js.map → chunk-KWADQ227.js.map} +1 -1
  52. package/dist/{chunk-JUS2UYRP.js → chunk-NCU5OAN7.js} +27 -2
  53. package/dist/chunk-NCU5OAN7.js.map +1 -0
  54. package/dist/{chunk-YXQX7RQT.js → chunk-OI5YLASH.js} +73 -2
  55. package/dist/chunk-OI5YLASH.js.map +1 -0
  56. package/dist/chunk-OV3POE62.js +359 -0
  57. package/dist/chunk-OV3POE62.js.map +1 -0
  58. package/dist/chunk-P7UWKORG.js +561 -0
  59. package/dist/chunk-P7UWKORG.js.map +1 -0
  60. package/dist/chunk-QGEIL4TR.js +195 -0
  61. package/dist/chunk-QGEIL4TR.js.map +1 -0
  62. package/dist/{chunk-TRNI7KEO.js → chunk-S2NLS3ES.js} +3669 -1281
  63. package/dist/chunk-S2NLS3ES.js.map +1 -0
  64. package/dist/chunk-UM4ZXXJR.js +9 -0
  65. package/dist/chunk-UM4ZXXJR.js.map +1 -0
  66. package/dist/{chunk-QT3KFVDP.js → chunk-UME3X7CP.js} +28 -32
  67. package/dist/chunk-UME3X7CP.js.map +1 -0
  68. package/dist/chunk-VT4YM4XQ.js +210 -0
  69. package/dist/chunk-VT4YM4XQ.js.map +1 -0
  70. package/dist/chunk-WSYYXONG.js +1602 -0
  71. package/dist/chunk-WSYYXONG.js.map +1 -0
  72. package/dist/{chunk-C2H7HBNP.js → chunk-WVCJRSGJ.js} +119 -14
  73. package/dist/chunk-WVCJRSGJ.js.map +1 -0
  74. package/dist/chunk-X5ITQVXE.js +15 -0
  75. package/dist/chunk-X5ITQVXE.js.map +1 -0
  76. package/dist/chunk-XA7HVIHI.js +126 -0
  77. package/dist/chunk-XA7HVIHI.js.map +1 -0
  78. package/dist/{chunk-KKRO2VCP.js → chunk-YJCHB7R3.js} +60 -12
  79. package/dist/chunk-YJCHB7R3.js.map +1 -0
  80. package/dist/chunk-YKIHNLWD.js +153 -0
  81. package/dist/chunk-YKIHNLWD.js.map +1 -0
  82. package/dist/chunk-ZDUBPV33.js +277 -0
  83. package/dist/chunk-ZDUBPV33.js.map +1 -0
  84. package/dist/client.d.ts +1068 -118
  85. package/dist/codex-realtime-controller.d.ts +13 -0
  86. package/dist/codex-realtime-controller.js +4 -2
  87. package/dist/codex-realtime-v3-wire.d.ts +3 -2
  88. package/dist/codex-realtime.d.ts +5 -1
  89. package/dist/company-brain.d.ts +1 -11
  90. package/dist/core.d.ts +7 -2
  91. package/dist/core.js +25 -6
  92. package/dist/deprecation.d.ts +47 -0
  93. package/dist/document-authority-client.d.ts +24 -2
  94. package/dist/document-authority.js +7 -5
  95. package/dist/editable-artifacts/index.d.ts +1 -1
  96. package/dist/editable-artifacts/types.d.ts +12 -2
  97. package/dist/editable-artifacts.js +19 -3
  98. package/dist/editable-artifacts.js.map +1 -1
  99. package/dist/embedding-client.d.ts +129 -0
  100. package/dist/errors.d.ts +76 -1
  101. package/dist/external-vault-receiver.d.ts +203 -0
  102. package/dist/external-vault-receiver.js +564 -0
  103. package/dist/external-vault-receiver.js.map +1 -0
  104. package/dist/feedback.d.ts +23 -0
  105. package/dist/gateway-realtime-transport.js +1 -1
  106. package/dist/github-repositories.d.ts +10 -0
  107. package/dist/github-repositories.js +36 -0
  108. package/dist/github-repositories.js.map +1 -0
  109. package/dist/index.d.ts +62 -11
  110. package/dist/index.js +557 -338
  111. package/dist/index.js.map +1 -1
  112. package/dist/insights-usage.d.ts +33 -0
  113. package/dist/insights-usage.js +7 -0
  114. package/dist/insights-usage.js.map +1 -0
  115. package/dist/interaction.d.ts +165 -71
  116. package/dist/interaction.js +2 -2
  117. package/dist/knowledge.d.ts +7 -0
  118. package/dist/knowledge.js +20 -0
  119. package/dist/knowledge.js.map +1 -0
  120. package/dist/known-tools.d.ts +4 -0
  121. package/dist/message-links.d.ts +68 -0
  122. package/dist/model-context.d.ts +68 -0
  123. package/dist/model-display.d.ts +2 -0
  124. package/dist/model-display.js +17 -0
  125. package/dist/model-display.js.map +1 -0
  126. package/dist/model-picker-order.d.ts +1 -0
  127. package/dist/model-picker-order.js +10 -0
  128. package/dist/model-picker-order.js.map +1 -0
  129. package/dist/organization-integration-policy.d.ts +26 -0
  130. package/dist/organization-integration-policy.js +26 -0
  131. package/dist/organization-integration-policy.js.map +1 -0
  132. package/dist/pr-review-client.d.ts +1 -1
  133. package/dist/pr-review.d.ts +1 -1
  134. package/dist/pr-review.js.map +1 -1
  135. package/dist/preference-registry.d.ts +1 -1
  136. package/dist/prepared-mcp-setup.d.ts +5 -0
  137. package/dist/proxy.d.ts +13 -11
  138. package/dist/realtime.d.ts +1 -1
  139. package/dist/realtime.js +253 -7
  140. package/dist/realtime.js.map +1 -1
  141. package/dist/session-history-import.d.ts +22 -0
  142. package/dist/session-history-import.js +36 -0
  143. package/dist/session-history-import.js.map +1 -0
  144. package/dist/session-list-entries.d.ts +1 -0
  145. package/dist/session-list-entries.js +7 -0
  146. package/dist/session-list-entries.js.map +1 -0
  147. package/dist/session-message-search.d.ts +53 -0
  148. package/dist/session-proxy-client.d.ts +28 -0
  149. package/dist/session-proxy.d.ts +334 -0
  150. package/dist/session-proxy.js +24 -0
  151. package/dist/session-proxy.js.map +1 -0
  152. package/dist/session-titles.d.ts +2 -28
  153. package/dist/session-usage-limits.d.ts +10 -0
  154. package/dist/session-usage-limits.js +43 -0
  155. package/dist/session-usage-limits.js.map +1 -0
  156. package/dist/shared-catalog-loader.d.ts +34 -0
  157. package/dist/site-ai.d.ts +35 -0
  158. package/dist/site-bridge-host.d.ts +112 -0
  159. package/dist/site-bridge-host.js +286 -0
  160. package/dist/site-bridge-host.js.map +1 -0
  161. package/dist/site-browser-entry.d.ts +1 -0
  162. package/dist/site-browser-runtime.gen.d.ts +1 -0
  163. package/dist/site-data.d.ts +178 -0
  164. package/dist/site-document.d.ts +5 -0
  165. package/dist/site-document.js +20 -0
  166. package/dist/site-document.js.map +1 -0
  167. package/dist/site-document.test.d.ts +1 -0
  168. package/dist/site-http.d.ts +17 -0
  169. package/dist/site-sql.d.ts +31 -0
  170. package/dist/site-tool-bridge.d.ts +57 -0
  171. package/dist/site.d.ts +186 -0
  172. package/dist/site.js +63 -0
  173. package/dist/site.js.map +1 -0
  174. package/dist/skills.d.ts +75 -0
  175. package/dist/sse.d.ts +1 -1
  176. package/dist/stream.d.ts +2 -0
  177. package/dist/tenant-workspaces.d.ts +47 -0
  178. package/dist/tenant-workspaces.js +8 -0
  179. package/dist/tenant-workspaces.js.map +1 -0
  180. package/dist/tool-auth.d.ts +87 -0
  181. package/dist/tool-auth.js +23 -0
  182. package/dist/tool-auth.js.map +1 -0
  183. package/dist/tool-display-metadata.d.ts +3 -0
  184. package/dist/tools-37DCTCNZ.js +12 -0
  185. package/dist/tools-37DCTCNZ.js.map +1 -0
  186. package/dist/tools.d.ts +81 -0
  187. package/dist/transcription.d.ts +1 -1
  188. package/dist/types.d.ts +2807 -583
  189. package/dist/usage-allowances.d.ts +36 -0
  190. package/dist/usage-allowances.js +80 -0
  191. package/dist/usage-allowances.js.map +1 -0
  192. package/dist/vault-types.d.ts +324 -0
  193. package/dist/vaults-api.d.ts +65 -0
  194. package/dist/workspace-artifacts.d.ts +47 -5
  195. package/dist/workspace-instruction-policies.d.ts +2 -2
  196. package/dist/workspace-integrations.d.ts +74 -0
  197. package/dist/workspace-integrations.js +67 -0
  198. package/dist/workspace-integrations.js.map +1 -0
  199. package/dist/workspace-state.d.ts +8 -71
  200. package/package.json +103 -5
  201. package/src/adapters/express.ts +136 -0
  202. package/src/adapters/hono.ts +18 -0
  203. package/src/adapters/next.ts +42 -0
  204. package/src/allowance-refusal.ts +5 -0
  205. package/src/app-links.ts +117 -0
  206. package/src/artifact-catalog.ts +51 -0
  207. package/src/artifact-client.ts +40 -49
  208. package/src/artifacts.ts +1 -0
  209. package/src/automations-client.ts +8 -2
  210. package/src/azure-live-transport.ts +286 -0
  211. package/src/browser.ts +37 -0
  212. package/src/chat/fold.ts +431 -0
  213. package/src/chat/handler.ts +240 -0
  214. package/src/chat/http.ts +257 -0
  215. package/src/chat/ids.ts +85 -0
  216. package/src/chat/index.ts +88 -0
  217. package/src/chat/openai.ts +439 -0
  218. package/src/chat/opengeni.ts +737 -0
  219. package/src/chat/types.ts +219 -0
  220. package/src/chat/vercel.ts +204 -0
  221. package/src/chats.ts +16 -0
  222. package/src/client.ts +5697 -1833
  223. package/src/codex-realtime-controller.ts +163 -8
  224. package/src/codex-realtime-v3-wire.ts +4 -3
  225. package/src/codex-realtime-v3.ts +12 -3
  226. package/src/codex-realtime.ts +11 -3
  227. package/src/company-brain.ts +0 -40
  228. package/src/core.ts +20 -0
  229. package/src/deprecation.ts +149 -0
  230. package/src/document-authority-client.ts +104 -1
  231. package/src/editable-artifacts/controller.ts +36 -2
  232. package/src/editable-artifacts/index.ts +1 -0
  233. package/src/editable-artifacts/types.ts +29 -16
  234. package/src/embedding-client.ts +519 -0
  235. package/src/errors.ts +303 -4
  236. package/src/external-vault-receiver.ts +914 -0
  237. package/src/feedback.ts +20 -0
  238. package/src/gateway-realtime-transport.ts +3 -3
  239. package/src/github-repositories.ts +60 -0
  240. package/src/index.ts +603 -65
  241. package/src/insights-usage.ts +64 -0
  242. package/src/interaction.ts +315 -93
  243. package/src/knowledge.ts +72 -0
  244. package/src/known-tools.ts +279 -0
  245. package/src/message-links.ts +178 -0
  246. package/src/model-context.ts +113 -0
  247. package/src/model-display.ts +11 -0
  248. package/src/model-picker-order.ts +6 -0
  249. package/src/organization-integration-policy.ts +54 -0
  250. package/src/pr-review-client.ts +1 -1
  251. package/src/pr-review.ts +1 -1
  252. package/src/preference-registry.ts +2 -0
  253. package/src/prepared-mcp-setup.ts +123 -0
  254. package/src/proxy.ts +23 -12
  255. package/src/realtime.ts +15 -6
  256. package/src/session-history-import.ts +84 -0
  257. package/src/session-list-entries.ts +1 -0
  258. package/src/session-message-search.ts +47 -0
  259. package/src/session-proxy-client.ts +68 -0
  260. package/src/session-proxy.ts +2098 -0
  261. package/src/session-titles.ts +8 -82
  262. package/src/session-usage-limits.ts +71 -0
  263. package/src/shared-catalog-loader.ts +169 -0
  264. package/src/site-ai.ts +284 -0
  265. package/src/site-bridge-host.ts +422 -0
  266. package/src/site-browser-entry.ts +6 -0
  267. package/src/site-browser-runtime.gen.ts +2 -0
  268. package/src/site-data.ts +372 -0
  269. package/src/site-document.test.ts +64 -0
  270. package/src/site-document.ts +18 -0
  271. package/src/site-http.ts +171 -0
  272. package/src/site-sql.ts +153 -0
  273. package/src/site-tool-bridge.ts +401 -0
  274. package/src/site.ts +878 -0
  275. package/src/skills.ts +69 -0
  276. package/src/sse.ts +9 -3
  277. package/src/stream.ts +33 -10
  278. package/src/tenant-workspaces.ts +148 -0
  279. package/src/tool-auth.ts +301 -0
  280. package/src/tool-display-metadata.ts +27 -0
  281. package/src/tools.ts +452 -0
  282. package/src/transcription.ts +3 -0
  283. package/src/types.ts +3170 -582
  284. package/src/usage-allowances.ts +189 -0
  285. package/src/vault-types.ts +379 -0
  286. package/src/vaults-api.ts +339 -0
  287. package/src/workspace-artifacts.ts +55 -6
  288. package/src/workspace-instruction-policies.ts +2 -1
  289. package/src/workspace-integrations.ts +402 -0
  290. package/src/workspace-state.ts +8 -82
  291. package/dist/chunk-C2H7HBNP.js.map +0 -1
  292. package/dist/chunk-C4DJPZRU.js +0 -119
  293. package/dist/chunk-C4DJPZRU.js.map +0 -1
  294. package/dist/chunk-JUS2UYRP.js.map +0 -1
  295. package/dist/chunk-KKRO2VCP.js.map +0 -1
  296. package/dist/chunk-QT3KFVDP.js.map +0 -1
  297. package/dist/chunk-TRNI7KEO.js.map +0 -1
  298. package/dist/chunk-VYCTL62C.js.map +0 -1
  299. package/dist/chunk-YXQX7RQT.js.map +0 -1
package/src/skills.ts ADDED
@@ -0,0 +1,69 @@
1
+ /** Public shared Skill folder/history shapes; no runtime contracts dependency. */
2
+ export type SkillFile = { path: string; content: string };
3
+ export type SkillScope = "workspace" | "organization" | "user";
4
+ export type SkillRecord = {
5
+ removalOperationId?: string | null;
6
+ activationMode: "workspace_managed" | "session_selected";
7
+ pendingRevisionIds: string[];
8
+ id: string;
9
+ stableKey: string;
10
+ scope: SkillScope;
11
+ scopeVersion: number;
12
+ status: string;
13
+ activeRevisionId: string | null;
14
+ revisionId: string | null;
15
+ title: string | null;
16
+ description: string | null;
17
+ contentHash: string | null;
18
+ files: SkillFile[];
19
+ source: { pluginId: string; facetKey: string; skillFacetId: string } | null;
20
+ };
21
+ export type SkillSummary = Omit<SkillRecord, "files">;
22
+ export type SkillWriteReceipt = {
23
+ removed?: boolean | undefined;
24
+ operationId: string;
25
+ skillId: string;
26
+ revisionId: string;
27
+ outcome: "applied" | "pending" | "preserved";
28
+ pendingReason?: "approval" | "source_finalization" | undefined;
29
+ replayed: boolean;
30
+ decision?: "rejected" | undefined;
31
+ skillReview?: import("./types").SkillReviewReference | undefined;
32
+ };
33
+ export type SkillPublicationReceipt = SkillWriteReceipt & {
34
+ sourceOperationId: string;
35
+ activationEventId: string | null;
36
+ };
37
+ export type SkillSourceReleaseReceipt = {
38
+ skillId: string;
39
+ revisionId: string | null;
40
+ disposition: "deactivated" | "preserved" | "inactive";
41
+ eventId: string | null;
42
+ warning: string | null;
43
+ };
44
+ export type SaveWorkspaceSkillRequest = {
45
+ operationId: string;
46
+ skillId: string;
47
+ expectedRevisionId: string | null;
48
+ expectedScopeVersion: number;
49
+ scope?: SkillScope;
50
+ stableKey: string;
51
+ files: SkillFile[];
52
+ deletions?: string[];
53
+ reason: string;
54
+ };
55
+ export type ApplyWorkspaceSkillRevisionRequest = {
56
+ removalOperationId?: string;
57
+ operationId: string;
58
+ revisionId: string;
59
+ expectedRevisionId: string | null;
60
+ expectedScopeVersion: number;
61
+ reason: string;
62
+ };
63
+
64
+ export type RemoveWorkspaceSkillRequest = {
65
+ operationId: string;
66
+ expectedRevisionId: string | null;
67
+ expectedScopeVersion: number;
68
+ reason: string;
69
+ };
package/src/sse.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Minimal incremental Server-Sent Events parser over a byte stream.
3
3
  *
4
- * Implements the parts of the SSE wire format OpenGeni uses: `id`, `event`,
4
+ * Implements the parts of the SSE wire format Opengeni uses: `id`, `event`,
5
5
  * and `data` fields, multi-line data, comment lines, and both LF and CRLF
6
6
  * line endings. Messages without any `data` (comments, id-only blocks) are
7
7
  * not emitted.
@@ -87,7 +87,13 @@ export async function* parseSseStream(
87
87
  // still pending at end-of-stream came from a truncated connection and is
88
88
  // discarded — the streaming layer replays from its cursor on reconnect.
89
89
  } finally {
90
- await reader.cancel().catch(() => {});
91
- reader.releaseLock();
90
+ // Never await cancel(): some fetch bodies settle it only once their request
91
+ // aborts, which would deadlock the consumer's unwinding.
92
+ void reader.cancel().catch(() => {});
93
+ try {
94
+ reader.releaseLock();
95
+ } catch {
96
+ // A pending read (older stream implementations) keeps the lock; cancel releases it.
97
+ }
92
98
  }
93
99
  }
package/src/stream.ts CHANGED
@@ -2,6 +2,10 @@ import { isAbortError, isRetryableStreamError, OpenGeniStreamError } from "./err
2
2
  import { parseSseStream } from "./sse";
3
3
  import type { SessionEvent } from "./types";
4
4
 
5
+ const SESSION_EVENT_STREAM_COVERAGE = Symbol.for(
6
+ "@opengeni/sdk/session-event-stream-covered-through",
7
+ );
8
+
5
9
  /**
6
10
  * Transport boundary for the streaming core. The client implements it with
7
11
  * `fetch`; unit tests script it directly.
@@ -111,7 +115,8 @@ export async function* streamSessionEvents(
111
115
  if (!event) {
112
116
  continue;
113
117
  }
114
- const coveredSequence = eventResumeSequence(event);
118
+ const coveredSequence = trustedSseSequence(message.id, event.sequence);
119
+ markSessionEventStreamCoverage(event, coveredSequence);
115
120
  if (coveredSequence <= cursor) continue;
116
121
  if (event.sequence > cursor + 1) {
117
122
  for await (const missed of backfillEvents(transport, cursor, event.sequence - 1)) {
@@ -224,8 +229,10 @@ export function withStreamInactivityTimeout(
224
229
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
225
230
  }
226
231
  },
227
- cancel: async (reason) => {
228
- await reader.cancel(reason);
232
+ cancel: (reason) => {
233
+ // Do not wait for the underlying body: it may settle cancel() only after
234
+ // its request aborts (see parseSseStream).
235
+ void reader.cancel(reason).catch(() => {});
229
236
  },
230
237
  });
231
238
  }
@@ -283,13 +290,29 @@ function parseSessionEvent(data: string): SessionEvent | null {
283
290
  return parsed as SessionEvent;
284
291
  }
285
292
 
286
- /** Raw durable cursor covered by a compact SSE event. */
287
- function eventResumeSequence(event: SessionEvent): number {
288
- if (!event.payload || typeof event.payload !== "object" || Array.isArray(event.payload)) {
289
- return event.sequence;
290
- }
291
- const covered = Number((event.payload as Record<string, unknown>).coalescedUntil);
292
- return Math.max(event.sequence, Number.isFinite(covered) ? Math.floor(covered) : event.sequence);
293
+ /** Trusted SSE coverage attached non-enumerably to a streamed event. */
294
+ export function sessionEventStreamCoveredThrough(event: SessionEvent): number | null {
295
+ const covered = (event as SessionEvent & { [SESSION_EVENT_STREAM_COVERAGE]?: unknown })[
296
+ SESSION_EVENT_STREAM_COVERAGE
297
+ ];
298
+ return typeof covered === "number" && Number.isSafeInteger(covered) && covered >= event.sequence
299
+ ? covered
300
+ : null;
301
+ }
302
+
303
+ function trustedSseSequence(id: string | undefined, sequence: number): number {
304
+ if (id === undefined || !/^\d+$/.test(id)) return sequence;
305
+ const covered = Number(id);
306
+ return Number.isSafeInteger(covered) && covered >= sequence ? covered : sequence;
307
+ }
308
+
309
+ function markSessionEventStreamCoverage(event: SessionEvent, covered: number): void {
310
+ Object.defineProperty(event, SESSION_EVENT_STREAM_COVERAGE, {
311
+ value: covered,
312
+ enumerable: false,
313
+ configurable: false,
314
+ writable: false,
315
+ });
293
316
  }
294
317
 
295
318
  async function sleep(delayMs: number, signal: AbortSignal | undefined): Promise<void> {
@@ -0,0 +1,148 @@
1
+ import type { OpenGeniEmbeddingClient } from "./embedding-client";
2
+ import type { Permission } from "./types";
3
+ import { OpenGeniApiError } from "./errors";
4
+ import { uuidV5 } from "./chat/ids";
5
+
6
+ const ISOLATION_NAMESPACE = "fc398712-b4db-5b0b-8842-57cb4f2a65f9";
7
+ /**
8
+ * Conversation-only member permissions. The API grants exactly this set when
9
+ * an organization key first acts as a user on a workspace (keep equal to
10
+ * `EXTERNAL_FIRST_USE_MEMBER_PERMISSIONS` in `packages/core/src/access`).
11
+ */
12
+ const CONVERSATION_PERMISSIONS = [
13
+ "workspace:read",
14
+ "sessions:create",
15
+ "sessions:read",
16
+ "sessions:control",
17
+ "files:upload",
18
+ "files:read",
19
+ "mcp_servers:attach",
20
+ ] as const;
21
+
22
+ export type WorkspaceIdTarget = {
23
+ /**
24
+ * Your tenant id. With user isolation and no tenant, the user gets one
25
+ * workspace of their own, independent of any tenant.
26
+ */
27
+ tenant?: string | undefined;
28
+ user?: string | undefined;
29
+ /** External user identity source; defaults to the resolver's source. */
30
+ source?: string | undefined;
31
+ };
32
+
33
+ export type WorkspaceIdOptions = {
34
+ /**
35
+ * `tenant`: one workspace per tenant. `user`: a separate workspace for the
36
+ * user (per tenant when a tenant is given, otherwise one per user).
37
+ */
38
+ isolation: "tenant" | "user";
39
+ };
40
+
41
+ export type WorkspaceIdResolverOptions = {
42
+ /** The organization id, or a lazy lookup (for example derived from the API key). */
43
+ organizationId: string | (() => Promise<string>);
44
+ source: string;
45
+ /** Workspace display name; receives the tenant id, or the user id for a per-user workspace. */
46
+ workspaceName?: ((tenant: string) => string) | undefined;
47
+ /**
48
+ * Per-user workspaces only (user isolation): permissions for the one member
49
+ * this resolver adds explicitly, its owner. Replaces the defaults: workspace
50
+ * read, session create/read/control (including sending messages), file
51
+ * upload/read, and attaching the host's per-session MCP servers. No admin
52
+ * permissions are granted by default. Tenant workspaces rely on the API,
53
+ * which adds a missing member with those defaults on their first request.
54
+ * Existing grants are never changed here; change them with
55
+ * `updateExternalWorkspaceMember`. The organization API key must also allow
56
+ * the selected permissions.
57
+ */
58
+ memberPermissions?: readonly Permission[] | undefined;
59
+ };
60
+
61
+ /**
62
+ * Server-only workspace resolution using external workspace provisioning.
63
+ * Workspaces are created on first use and cached. A tenant workspace's members
64
+ * are added by the API on their first `asUser` request (the organization key
65
+ * needs `members:manage`). A per-user workspace stays single-user: the API
66
+ * never auto-admits anyone there, and this resolver adds its owner with a
67
+ * stable onboarding key, so retries never restore a revoked grant.
68
+ */
69
+ export function createWorkspaceIdResolver(
70
+ client: Pick<OpenGeniEmbeddingClient, "ensureWorkspace" | "addExternalWorkspaceMember">,
71
+ options: WorkspaceIdResolverOptions,
72
+ ): (target: WorkspaceIdTarget, resolution: WorkspaceIdOptions) => Promise<string> {
73
+ const memberPermissions = [...(options.memberPermissions ?? CONVERSATION_PERMISSIONS)];
74
+ const cache = new Map<string, Promise<string>>();
75
+ const organizationId = async () =>
76
+ typeof options.organizationId === "function"
77
+ ? await options.organizationId()
78
+ : options.organizationId;
79
+ return async (target, resolution) => {
80
+ const isolated = resolution.isolation === "user";
81
+ if (!isolated && !target.tenant) throw new TypeError("workspaceIdFor requires a tenant.");
82
+ if (isolated && !target.user) {
83
+ throw new TypeError("User isolation requires an authenticated product user.");
84
+ }
85
+ const source = target.source ?? options.source;
86
+ // A per-user workspace without a tenant has its own key shape, so it can
87
+ // never alias a tenant-scoped isolated workspace.
88
+ const key = target.tenant
89
+ ? JSON.stringify([
90
+ resolution.isolation,
91
+ options.source,
92
+ source,
93
+ target.tenant,
94
+ isolated ? target.user : null,
95
+ ])
96
+ : JSON.stringify(["user", options.source, source, target.user]);
97
+ let pending = cache.get(key);
98
+ if (!pending) {
99
+ pending = (async () => {
100
+ const productSource = options.source.trim();
101
+ const isolatedSource = `opengeni-sdk:user-isolation:${await uuidV5(productSource, ISOLATION_NAMESPACE)}`;
102
+ const label = target.tenant ?? target.user!;
103
+ const { workspace } = await client.ensureWorkspace({
104
+ accountId: await organizationId(),
105
+ externalSource: isolated
106
+ ? isolatedSource === productSource
107
+ ? `${isolatedSource}:user`
108
+ : isolatedSource
109
+ : options.source,
110
+ externalId: isolated ? await uuidV5(key, ISOLATION_NAMESPACE) : target.tenant!,
111
+ name: options.workspaceName?.(label) ?? label,
112
+ });
113
+ if (isolated) {
114
+ try {
115
+ await client.addExternalWorkspaceMember(workspace.id, {
116
+ identity: { source, externalId: target.user! },
117
+ permissions: [...memberPermissions],
118
+ operationId: await uuidV5(
119
+ JSON.stringify(["member", workspace.id, source, target.user]),
120
+ ISOLATION_NAMESPACE,
121
+ ),
122
+ });
123
+ } catch (error) {
124
+ // Defaults/custom permissions may have changed since this stable
125
+ // onboarding key committed, or the grant may have been cancelled.
126
+ // A definitive conflict must never mint a new key or update access.
127
+ // Return only the address: every asUser operation still checks the
128
+ // live membership, including reduced or withdrawn permissions.
129
+ if (
130
+ !(error instanceof OpenGeniApiError) ||
131
+ error.status !== 409 ||
132
+ error.code !== "conflict" ||
133
+ error.outcomeUnknown
134
+ )
135
+ throw error;
136
+ }
137
+ }
138
+ return workspace.id;
139
+ })();
140
+ pending.catch(() => {
141
+ if (cache.get(key) === pending) cache.delete(key);
142
+ });
143
+ if (cache.size >= 1_000) cache.delete(cache.keys().next().value!);
144
+ cache.set(key, pending);
145
+ }
146
+ return await pending;
147
+ };
148
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Per-user credentials for a product's own MCP tool server.
3
+ *
4
+ * `createSessionProxyHandler({ toolServer })` mints a short-lived token for the
5
+ * user its `resolve` hook authenticated, attaches it to each session it creates,
6
+ * and refreshes it on every message, approval, and answer. The product's MCP
7
+ * endpoint calls {@link verifyToolRequest} on every request and scopes all data
8
+ * access to the returned identity, never to ids the model supplied.
9
+ *
10
+ * Wire format (for non-Node verifiers): an HS256 JWT in `Authorization: Bearer`,
11
+ * signed with the hex key from {@link deriveToolTokenKey} (`HMAC-SHA256(key =
12
+ * secret, message = "opengeni-tool-token:v1")`, where `secret` is
13
+ * `OPENGENI_API_KEY` unless configured). Give a non-Node verifier that derived
14
+ * key, never the organization key. Claims: `iss` = `"opengeni-session-proxy"`,
15
+ * `aud` = the tool server URL, `sub` = the external user id, `workspace_id`,
16
+ * `source`, optional `tenant`, `iat`, `exp`.
17
+ */
18
+
19
+ /** `iss` of every tool token. */
20
+ export const TOOL_TOKEN_ISSUER = "opengeni-session-proxy";
21
+ /** HMAC message used to derive the signing key from the secret. */
22
+ export const TOOL_TOKEN_KEY_LABEL = "opengeni-tool-token:v1";
23
+ /** Default token lifetime: 24 hours, refreshed on every message, approval, and answer. */
24
+ export const TOOL_TOKEN_DEFAULT_TTL_SECONDS = 24 * 60 * 60;
25
+ const MAX_TTL_SECONDS = 7 * 24 * 60 * 60;
26
+ /** Opengeni's per-value limit for session MCP credential headers. */
27
+ const MAX_HEADER_VALUE_LENGTH = 4096;
28
+ /** Default `toolServer.url` and `verifyToolRequest` audience. */
29
+ export const TOOL_SERVER_URL_ENV = "OPENGENI_TOOL_SERVER_URL";
30
+ const CLOCK_SKEW_SECONDS = 60;
31
+
32
+ /** The user a verified tool request acts for. */
33
+ export type ToolRequestIdentity = {
34
+ /** External user id the session proxy's `resolve` hook authenticated. */
35
+ user: string;
36
+ /** Tenant from `resolve`, when it returned one. */
37
+ tenant?: string | undefined;
38
+ workspaceId: string;
39
+ /** External identity source (same as the proxy's `asUser` source). */
40
+ source: string;
41
+ expiresAt: Date;
42
+ };
43
+
44
+ export type VerifyToolRequestOptions = {
45
+ /** Signing secret. Defaults to `OPENGENI_API_KEY`; must match the proxy's. */
46
+ secret?: string | undefined;
47
+ /**
48
+ * Exact tool server URL the token must be issued for (the proxy's
49
+ * `toolServer.url`). Defaults to `OPENGENI_TOOL_SERVER_URL`.
50
+ */
51
+ audience?: string | undefined;
52
+ };
53
+
54
+ /** A Web `Request`, or a Node/Express request (`headers` plus `originalUrl`/`url`). */
55
+ export type ToolRequestLike =
56
+ | Request
57
+ | {
58
+ headers: Record<string, string | string[] | undefined>;
59
+ originalUrl?: string | undefined;
60
+ url?: string | undefined;
61
+ };
62
+
63
+ /** Missing, expired, forged, or misdirected tool token. Respond with `status` (401). */
64
+ export class ToolRequestError extends Error {
65
+ readonly status = 401;
66
+ constructor(readonly code: string) {
67
+ super("Tool request is not authorized.");
68
+ this.name = "ToolRequestError";
69
+ }
70
+ /** A ready 401 response with a `WWW-Authenticate: Bearer` challenge. */
71
+ toResponse(): Response {
72
+ return new Response(JSON.stringify({ error: { code: this.code, message: this.message } }), {
73
+ status: 401,
74
+ headers: {
75
+ "Content-Type": "application/json; charset=utf-8",
76
+ "WWW-Authenticate": 'Bearer error="invalid_token"',
77
+ },
78
+ });
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Verify the tool token on one MCP request and return who it acts for.
84
+ * Throws {@link ToolRequestError} (401) for every invalid token. Call it on
85
+ * every request, before any tool runs.
86
+ *
87
+ * ```ts
88
+ * const user = await verifyToolRequest(request); // { user, tenant, workspaceId }
89
+ * ```
90
+ */
91
+ export async function verifyToolRequest(
92
+ request: ToolRequestLike,
93
+ options: VerifyToolRequestOptions = {},
94
+ ): Promise<ToolRequestIdentity> {
95
+ const key = await signingKey(resolveToolTokenSecret(options.secret));
96
+ const audience = options.audience ?? environment(TOOL_SERVER_URL_ENV);
97
+ if (!audience) {
98
+ throw new TypeError(
99
+ `verifyToolRequest needs the tool server URL: pass { audience } or set ${TOOL_SERVER_URL_ENV}.`,
100
+ );
101
+ }
102
+ const authorization = requestAuthorization(request);
103
+ const match = /^Bearer[ ]+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/i.exec(
104
+ authorization?.trim() ?? "",
105
+ );
106
+ if (!match) throw new ToolRequestError("token_missing");
107
+ const [header, payload, signature] = match[1]!.split(".") as [string, string, string];
108
+ let protectedHeader: unknown;
109
+ let claims: Record<string, unknown>;
110
+ let signatureBytes: Uint8Array<ArrayBuffer>;
111
+ try {
112
+ signatureBytes = base64UrlDecode(signature);
113
+ protectedHeader = JSON.parse(utf8(base64UrlDecode(header)));
114
+ claims = JSON.parse(utf8(base64UrlDecode(payload)));
115
+ } catch {
116
+ throw new ToolRequestError("token_malformed");
117
+ }
118
+ if (
119
+ !protectedHeader ||
120
+ typeof protectedHeader !== "object" ||
121
+ (protectedHeader as { alg?: unknown }).alg !== "HS256" ||
122
+ !claims ||
123
+ typeof claims !== "object" ||
124
+ Array.isArray(claims)
125
+ ) {
126
+ throw new ToolRequestError("token_malformed");
127
+ }
128
+ const valid = await crypto.subtle.verify(
129
+ "HMAC",
130
+ key,
131
+ signatureBytes,
132
+ new TextEncoder().encode(`${header}.${payload}`),
133
+ );
134
+ if (!valid) throw new ToolRequestError("token_invalid");
135
+ const now = Math.floor(Date.now() / 1000);
136
+ const { iss, aud, sub, exp, iat, workspace_id, source, tenant } = claims;
137
+ if (iss !== TOOL_TOKEN_ISSUER) throw new ToolRequestError("token_invalid");
138
+ if (typeof exp !== "number" || exp <= now) throw new ToolRequestError("token_expired");
139
+ if (typeof iat !== "number" || iat > now + CLOCK_SKEW_SECONDS) {
140
+ throw new ToolRequestError("token_invalid");
141
+ }
142
+ if (aud !== audience) {
143
+ throw new ToolRequestError("token_audience");
144
+ }
145
+ if (
146
+ typeof sub !== "string" ||
147
+ !sub ||
148
+ typeof workspace_id !== "string" ||
149
+ !workspace_id ||
150
+ typeof source !== "string" ||
151
+ (tenant !== undefined && typeof tenant !== "string")
152
+ ) {
153
+ throw new ToolRequestError("token_invalid");
154
+ }
155
+ return {
156
+ user: sub,
157
+ ...(tenant !== undefined ? { tenant } : {}),
158
+ workspaceId: workspace_id,
159
+ source,
160
+ expiresAt: new Date(exp * 1000),
161
+ };
162
+ }
163
+
164
+ /** Mint one tool token. The session proxy calls this; products rarely need to. */
165
+ export async function mintToolToken(input: {
166
+ /** Tool server URL; becomes `aud`. */
167
+ audience: string;
168
+ user: string;
169
+ tenant?: string | undefined;
170
+ workspaceId: string;
171
+ source: string;
172
+ secret?: string | undefined;
173
+ ttlSeconds?: number | undefined;
174
+ }): Promise<string> {
175
+ const ttl = input.ttlSeconds ?? TOOL_TOKEN_DEFAULT_TTL_SECONDS;
176
+ if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > MAX_TTL_SECONDS) {
177
+ throw new TypeError(`ttlSeconds must be an integer from 1 to ${MAX_TTL_SECONDS}.`);
178
+ }
179
+ const key = await signingKey(resolveToolTokenSecret(input.secret));
180
+ const now = Math.floor(Date.now() / 1000);
181
+ const header = base64UrlJson({ alg: "HS256", typ: "JWT" });
182
+ const payload = base64UrlJson({
183
+ iss: TOOL_TOKEN_ISSUER,
184
+ aud: input.audience,
185
+ sub: input.user,
186
+ workspace_id: input.workspaceId,
187
+ source: input.source,
188
+ ...(input.tenant !== undefined ? { tenant: input.tenant } : {}),
189
+ iat: now,
190
+ exp: now + ttl,
191
+ });
192
+ const signature = await crypto.subtle.sign(
193
+ "HMAC",
194
+ key,
195
+ new TextEncoder().encode(`${header}.${payload}`),
196
+ );
197
+ const token = `${header}.${payload}.${base64UrlEncode(new Uint8Array(signature))}`;
198
+ if (`Bearer ${token}`.length > MAX_HEADER_VALUE_LENGTH) {
199
+ throw new TypeError(
200
+ `Tool token exceeds Opengeni's ${MAX_HEADER_VALUE_LENGTH}-character header limit; shorten the user, tenant, or URL.`,
201
+ );
202
+ }
203
+ return token;
204
+ }
205
+
206
+ /**
207
+ * The hex HS256 key tool tokens are signed with. Give this (not the
208
+ * organization key) to a tool server written in Python, Ruby, Go, and so on.
209
+ */
210
+ export async function deriveToolTokenKey(secret?: string): Promise<string> {
211
+ return [...(await derivedKeyBytes(resolveToolTokenSecret(secret)))]
212
+ .map((byte) => byte.toString(16).padStart(2, "0"))
213
+ .join("");
214
+ }
215
+
216
+ /** The configured secret, else `OPENGENI_API_KEY`. Throws when neither is set. */
217
+ export function resolveToolTokenSecret(secret: string | undefined): string {
218
+ const resolved = secret ?? environment("OPENGENI_API_KEY");
219
+ if (!resolved) {
220
+ throw new TypeError(
221
+ "Tool tokens need a signing secret: set OPENGENI_API_KEY, or pass the same `secret` to the session proxy's toolServer and verifyToolRequest.",
222
+ );
223
+ }
224
+ return resolved;
225
+ }
226
+
227
+ function environment(name: string): string | undefined {
228
+ return (
229
+ (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[
230
+ name
231
+ ] || undefined
232
+ );
233
+ }
234
+
235
+ async function derivedKeyBytes(secret: string): Promise<Uint8Array<ArrayBuffer>> {
236
+ const root = await crypto.subtle.importKey(
237
+ "raw",
238
+ new TextEncoder().encode(secret),
239
+ { name: "HMAC", hash: "SHA-256" },
240
+ false,
241
+ ["sign"],
242
+ );
243
+ return new Uint8Array(
244
+ await crypto.subtle.sign("HMAC", root, new TextEncoder().encode(TOOL_TOKEN_KEY_LABEL)),
245
+ );
246
+ }
247
+
248
+ const keys = new Map<string, Promise<CryptoKey>>();
249
+
250
+ /** HS256 key derived from the secret, so the raw secret never signs tokens itself. */
251
+ function signingKey(secret: string): Promise<CryptoKey> {
252
+ let key = keys.get(secret);
253
+ if (!key) {
254
+ key = (async () =>
255
+ await crypto.subtle.importKey(
256
+ "raw",
257
+ await derivedKeyBytes(secret),
258
+ { name: "HMAC", hash: "SHA-256" },
259
+ false,
260
+ ["sign", "verify"],
261
+ ))();
262
+ key.catch(() => keys.delete(secret));
263
+ if (keys.size >= 16) keys.delete(keys.keys().next().value!);
264
+ keys.set(secret, key);
265
+ }
266
+ return key;
267
+ }
268
+
269
+ function requestAuthorization(request: ToolRequestLike): string | undefined {
270
+ if (typeof Request !== "undefined" && request instanceof Request) {
271
+ return request.headers.get("authorization") ?? undefined;
272
+ }
273
+ const headers = (request as Exclude<ToolRequestLike, Request>).headers as unknown;
274
+ const raw =
275
+ headers && typeof (headers as Headers).get === "function"
276
+ ? ((headers as Headers).get("authorization") ?? undefined)
277
+ : (headers as Record<string, string | string[] | undefined>)?.authorization;
278
+ return Array.isArray(raw) ? undefined : raw;
279
+ }
280
+
281
+ function base64UrlJson(value: unknown): string {
282
+ return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
283
+ }
284
+
285
+ function base64UrlEncode(bytes: Uint8Array): string {
286
+ let binary = "";
287
+ for (const byte of bytes) binary += String.fromCharCode(byte);
288
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
289
+ }
290
+
291
+ function base64UrlDecode(value: string): Uint8Array<ArrayBuffer> {
292
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
293
+ const binary = atob(base64 + "=".repeat((4 - (base64.length % 4)) % 4));
294
+ const bytes = new Uint8Array(binary.length);
295
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
296
+ return bytes;
297
+ }
298
+
299
+ function utf8(bytes: Uint8Array): string {
300
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
301
+ }
@@ -0,0 +1,27 @@
1
+ import type { ToolDisplayMetadata } from "@opengeni/contracts";
2
+
3
+ /** Browser/native-safe event projection; do not import server schema runtimes. */
4
+ export function parseToolDisplayMetadata(value: unknown): ToolDisplayMetadata | undefined {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
6
+ const item = value as Record<string, unknown>;
7
+ if (
8
+ Object.keys(item).some(
9
+ (key) => !["toolName", "title", "accountLabel", "connector", "providerDomain"].includes(key),
10
+ )
11
+ )
12
+ return undefined;
13
+ const text = (v: unknown, max: number): v is string =>
14
+ typeof v === "string" && v.length > 0 && v.length <= max;
15
+ if (!text(item.toolName, 512) || /[\u0000-\u001f\u007f]/u.test(item.toolName)) return undefined;
16
+ if (item.title !== undefined && !text(item.title, 512)) return undefined;
17
+ if (item.accountLabel !== undefined && !text(item.accountLabel, 1024)) return undefined;
18
+ if (item.connector !== undefined && !text(item.connector, 512)) return undefined;
19
+ if (item.providerDomain !== undefined && !text(item.providerDomain, 253)) return undefined;
20
+ return {
21
+ toolName: item.toolName,
22
+ ...(item.title !== undefined ? { title: item.title as string } : {}),
23
+ ...(item.accountLabel !== undefined ? { accountLabel: item.accountLabel as string } : {}),
24
+ ...(item.connector !== undefined ? { connector: item.connector as string } : {}),
25
+ ...(item.providerDomain !== undefined ? { providerDomain: item.providerDomain as string } : {}),
26
+ };
27
+ }