@m6d/cortex-angular 1.3.0 → 1.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.
package/README.md CHANGED
@@ -27,16 +27,17 @@ No root-level providers needed — each widget instance is fully self-contained.
27
27
 
28
28
  ### Configuration
29
29
 
30
- | Property | Type | Description |
31
- | ---------------------- | ------------------------------- | ------------------------------------------------------- |
32
- | `locale` | `Signal<string>` | Active locale (`"en"`, `"ar"`) |
33
- | `transport.baseUrl` | `string \| Signal<string>` | Agent API base URL |
34
- | `transport.getHeaders` | `() => Record<string, string>` | Request headers provider |
35
- | `wsUrl` | `string` | WebSocket URL for real-time events (optional) |
36
- | `theme` | `CortexClientTheme` | `'light' \| 'dark' \| 'system'` (default: `'light'`) |
37
- | `showDebugButton` | `boolean` | Show/hide the debug toggle button (default: `false`) |
38
- | `toolComponents` | `Record<string, Type<unknown>>` | Custom Angular components for tool rendering (optional) |
39
- | `onToolCall` | `(toolCall) => unknown` | Client-side tool call handler (optional) |
30
+ | Property | Type | Description |
31
+ | ----------------------- | ------------------------------- | ------------------------------------------------------------------------------- |
32
+ | `locale` | `Signal<string>` | Active locale (`"en"`, `"ar"`) |
33
+ | `transport.baseUrl` | `string \| Signal<string>` | Agent API base URL |
34
+ | `transport.getHeaders` | `() => Record<string, string>` | Request headers provider |
35
+ | `wsUrl` | `string` | WebSocket URL for real-time events (optional) |
36
+ | `theme` | `CortexClientTheme` | `'light' \| 'dark' \| 'system'` (default: `'light'`) |
37
+ | `showDebugButton` | `boolean` | Show/hide the debug toggle button (default: `false`) |
38
+ | `toolComponents` | `Record<string, Type<unknown>>` | Custom Angular components for tool rendering (optional) |
39
+ | `onToolCall` | `(toolCall) => unknown` | Client-side tool call handler (optional) |
40
+ | `trustSameOriginEmbeds` | `boolean` | Allow embedded tool pages served from this page's own origin (default: `false`) |
40
41
 
41
42
  `transport.getHeaders` can return any request headers needed by the host app. WebSocket auth continues to derive the `token` query param from a bearer `Authorization` header when present.
42
43
 
@@ -48,7 +49,13 @@ initiate, shows the returned page in a sandboxed iframe (inline card or
48
49
  modal, per tool config), and submits the page's `cortex:client-tool`
49
50
  postMessage outcome back to the run as-is — anything that must actually be
50
51
  true lives in the integrating backend's own records. No widget configuration
51
- is needed; other pending tool calls keep the existing `toolComponents` /
52
+ is needed. One caveat: an embed served from the host page's **own origin** is
53
+ refused by default, because a same-origin iframe cannot be sandboxed — it
54
+ could reach into the host page, and embed URLs come from Control Center tool
55
+ config, whose administrator is not necessarily the site owner. When both are
56
+ the same party and the embeds are the site's own pages, set
57
+ `trustSameOriginEmbeds: true` to render them anyway (with full access, as any
58
+ of the site's own pages has). Other pending tool calls keep the existing `toolComponents` /
52
59
  `hooks.onToolCall` behavior. That hook is also how static client tools work:
53
60
  declare an executor-less tool in the server config and answer it here by
54
61
  returning its output from `hooks.onToolCall` for that tool name. Consumer `hooks.onToolCall` implementations must
@@ -483,14 +483,22 @@ function parseClientToolHandshake(data, origin, embedOrigin) {
483
483
  }
484
484
 
485
485
  // src/internal/client/src/client-tools.ts
486
- function embedSandbox(embedUrl, pageOrigin) {
486
+ function embedSandbox(embedUrl, pageOrigin, trustSameOrigin) {
487
487
  const base = "allow-scripts allow-forms allow-popups";
488
488
  try {
489
- return new URL(embedUrl).origin === pageOrigin ? base : `${base} allow-same-origin`;
489
+ const sameOrigin = new URL(embedUrl).origin === pageOrigin;
490
+ return sameOrigin && !trustSameOrigin ? base : `${base} allow-same-origin`;
490
491
  } catch {
491
492
  return base;
492
493
  }
493
494
  }
495
+ function embedTrustedOnHostOrigin(embedUrl, pageOrigin, trustSameOrigin) {
496
+ try {
497
+ return trustSameOrigin === true && new URL(embedUrl).origin === pageOrigin;
498
+ } catch {
499
+ return false;
500
+ }
501
+ }
494
502
  function frameBreachedHostOrigin(frame, pageOrigin) {
495
503
  try {
496
504
  return frame.contentWindow?.location.origin === pageOrigin;
@@ -3464,8 +3472,10 @@ class ClientToolEmbedComponent {
3464
3472
  }, ...ngDevMode ? [{ debugName: "isModal" }] : []);
3465
3473
  sandbox = computed(() => {
3466
3474
  const state = this.state();
3467
- return state.phase === "embedded" ? embedSandbox(state.embedUrl, window.location.origin) : "";
3475
+ return state.phase === "embedded" ? embedSandbox(state.embedUrl, window.location.origin, this.trustSameOriginEmbeds) : "";
3468
3476
  }, ...ngDevMode ? [{ debugName: "sandbox" }] : []);
3477
+ config = inject(CORTEX_CLIENT_CONFIG);
3478
+ trustSameOriginEmbeds;
3469
3479
  api = inject(CortexApiClient);
3470
3480
  chatService = inject(CortexChatService);
3471
3481
  sanitizer = inject(DomSanitizer);
@@ -3486,6 +3496,7 @@ class ClientToolEmbedComponent {
3486
3496
  });
3487
3497
  }
3488
3498
  ngOnInit() {
3499
+ this.trustSameOriginEmbeds = this.config().trustSameOriginEmbeds;
3489
3500
  const threadId = this.chatService.selectedThread()?.id;
3490
3501
  const part = this.toolCallPart();
3491
3502
  if (!threadId) {
@@ -3516,7 +3527,11 @@ class ClientToolEmbedComponent {
3516
3527
  const frame = event.target;
3517
3528
  if (!(frame instanceof HTMLIFrameElement))
3518
3529
  return;
3519
- if (frameBreachedHostOrigin(frame, window.location.origin)) {
3530
+ const state = this.state();
3531
+ if (state.phase !== "embedded")
3532
+ return;
3533
+ const trusted = embedTrustedOnHostOrigin(state.embedUrl, window.location.origin, this.trustSameOriginEmbeds);
3534
+ if (!trusted && frameBreachedHostOrigin(frame, window.location.origin)) {
3520
3535
  this.embed?.terminate();
3521
3536
  }
3522
3537
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-angular",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Reusable AI agent chat UI library for Angular",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -106,6 +106,16 @@ export type CortexConfig = {
106
106
  /** Color scheme; `'system'` follows the OS preference. Defaults to `'light'`. */
107
107
  theme?: CortexTheme;
108
108
  showDebugButton?: boolean;
109
+ /**
110
+ * Render embedded tool pages served from this page's own origin with their
111
+ * full identity — which, same-origin, means no effective sandbox at all: such
112
+ * an embed can reach into the host page. By default they are refused
113
+ * (rendered inert), because embed URLs come from Control Center tool config,
114
+ * and whoever administers that is not necessarily whoever owns this site.
115
+ * Enable only when both are the same party and the embeds are this site's
116
+ * own pages — then a second origin just for embeds is no longer needed.
117
+ */
118
+ trustSameOriginEmbeds?: boolean;
109
119
  hooks?: CortexHooks;
110
120
  };
111
121
  export type QueuedAttachment = {