@agent-native/core 0.11.1 → 0.11.2

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 (34) hide show
  1. package/dist/agent/thread-data-builder.d.ts +1 -0
  2. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  3. package/dist/agent/thread-data-builder.js +1 -0
  4. package/dist/agent/thread-data-builder.js.map +1 -1
  5. package/dist/client/AgentPanel.js +2 -2
  6. package/dist/client/AgentPanel.js.map +1 -1
  7. package/dist/client/AssistantChat.d.ts.map +1 -1
  8. package/dist/client/AssistantChat.js +71 -6
  9. package/dist/client/AssistantChat.js.map +1 -1
  10. package/dist/client/components/ui/dropdown-menu.js +2 -2
  11. package/dist/client/components/ui/dropdown-menu.js.map +1 -1
  12. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  13. package/dist/client/resources/ResourcesPanel.js +6 -6
  14. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  15. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  16. package/dist/client/settings/useBuilderStatus.js +6 -2
  17. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  18. package/dist/client/sharing/ShareButton.d.ts +2 -0
  19. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  20. package/dist/client/sharing/ShareButton.js +26 -3
  21. package/dist/client/sharing/ShareButton.js.map +1 -1
  22. package/dist/server/agent-chat-plugin.d.ts +14 -0
  23. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  24. package/dist/server/agent-chat-plugin.js +77 -12
  25. package/dist/server/agent-chat-plugin.js.map +1 -1
  26. package/dist/server/builder-browser.d.ts +8 -6
  27. package/dist/server/builder-browser.d.ts.map +1 -1
  28. package/dist/server/builder-browser.js +54 -32
  29. package/dist/server/builder-browser.js.map +1 -1
  30. package/dist/server/core-routes-plugin.d.ts +7 -0
  31. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  32. package/dist/server/core-routes-plugin.js +100 -74
  33. package/dist/server/core-routes-plugin.js.map +1 -1
  34. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { createPollHandler } from "./poll.js";
6
6
  import { createSSEHandler } from "./sse.js";
7
7
  import { upsertEnvFile } from "./create-server.js";
8
8
  import { readBody } from "./h3-helpers.js";
9
- import { BUILDER_ENV_KEYS, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, getBuilderBranchProjectId, getBuilderBrowserStatusForEvent, isBuilderBranchingEnabled, resolveSafePreviewUrl, runBuilderAgent, } from "./builder-browser.js";
9
+ import { BUILDER_CONNECT_PARAM, BUILDER_ENV_KEYS, appendBuilderConnectToken, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, getBuilderBranchProjectId, getBuilderBrowserStatusForEvent, isBuilderBranchingEnabled, resolveSafePreviewUrl, runBuilderAgent, verifyBuilderConnectToken, } from "./builder-browser.js";
10
10
  import { getState, putState, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "../application-state/handlers.js";
11
11
  import { getSetting, putSetting, deleteSetting } from "../settings/store.js";
12
12
  import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
@@ -288,16 +288,35 @@ export function createCoreRoutesPlugin(options = {}) {
288
288
  message: process.env.PING_MESSAGE ?? "pong",
289
289
  })));
290
290
  }
291
+ const resolveBuilderOwnerContext = async (event) => {
292
+ const session = await getSession(event).catch(() => null);
293
+ if (session?.email) {
294
+ return { email: session.email, session, anonymous: false };
295
+ }
296
+ const anonymousOwner = await options.anonymousOwner?.(event);
297
+ if (anonymousOwner) {
298
+ return { email: anonymousOwner, session: null, anonymous: true };
299
+ }
300
+ return { email: undefined, session: null, anonymous: false };
301
+ };
291
302
  getH3App(nitroApp).use(`${P}/builder/status`, defineEventHandler(async (event) => {
292
303
  const envStatus = getBuilderBrowserStatusForEvent(event);
293
- const session = await getSession(event).catch(() => null);
294
- const userEmail = session?.email;
304
+ const ownerContext = await resolveBuilderOwnerContext(event);
305
+ const userEmail = ownerContext.email;
306
+ const withConnectToken = (status) => {
307
+ if (!userEmail)
308
+ return status;
309
+ return {
310
+ ...status,
311
+ connectUrl: appendBuilderConnectToken(status.connectUrl, userEmail),
312
+ };
313
+ };
295
314
  // Env-managed mode: BUILDER_PRIVATE_KEY is set at the deployment
296
315
  // level, so every user shares the operator's Builder identity.
297
316
  // Skip per-user lookups entirely — the env key is authoritative
298
317
  // and the UI must hide the connect/disconnect controls.
299
318
  if (envStatus.envManaged) {
300
- return envStatus;
319
+ return withConnectToken(envStatus);
301
320
  }
302
321
  // Pass the user's active orgId so the status read can fall back
303
322
  // to org-scoped credentials. Without it, an admin's org-scope
@@ -305,13 +324,15 @@ export function createCoreRoutesPlugin(options = {}) {
305
324
  // poller and the UI would show "not connected" forever even
306
325
  // though the chat actually resolves the org-shared credential.
307
326
  let orgId = null;
308
- try {
309
- const { getOrgContext } = await import("../org/context.js");
310
- const orgCtx = await getOrgContext(event);
311
- orgId = orgCtx.orgId ?? null;
312
- }
313
- catch {
314
- /* org module not present in this template — keep userEmail-only */
327
+ if (!ownerContext.anonymous) {
328
+ try {
329
+ const { getOrgContext } = await import("../org/context.js");
330
+ const orgCtx = await getOrgContext(event);
331
+ orgId = orgCtx.orgId ?? null;
332
+ }
333
+ catch {
334
+ /* org module not present in this template — keep userEmail-only */
335
+ }
315
336
  }
316
337
  return runWithRequestContext({ userEmail, orgId }, async () => {
317
338
  // Per-user OAuth mode: read the user's app_secrets-stored creds.
@@ -319,7 +340,7 @@ export function createCoreRoutesPlugin(options = {}) {
319
340
  const { resolveBuilderCredentials } = await import("./credential-provider.js");
320
341
  const creds = await resolveBuilderCredentials();
321
342
  if (creds.privateKey) {
322
- return {
343
+ return withConnectToken({
323
344
  ...envStatus,
324
345
  configured: true,
325
346
  privateKeyConfigured: true,
@@ -327,7 +348,7 @@ export function createCoreRoutesPlugin(options = {}) {
327
348
  userId: creds.userId || envStatus.userId,
328
349
  orgName: creds.orgName || envStatus.orgName,
329
350
  orgKind: creds.orgKind || envStatus.orgKind,
330
- };
351
+ });
331
352
  }
332
353
  }
333
354
  catch {
@@ -344,7 +365,7 @@ export function createCoreRoutesPlugin(options = {}) {
344
365
  const errRow = await getSetting(errKey);
345
366
  if (errRow && typeof errRow.message === "string") {
346
367
  await deleteSetting(errKey).catch(() => { });
347
- return {
368
+ return withConnectToken({
348
369
  ...envStatus,
349
370
  configured: false,
350
371
  privateKeyConfigured: false,
@@ -358,7 +379,7 @@ export function createCoreRoutesPlugin(options = {}) {
358
379
  ? errRow.at
359
380
  : Date.now(),
360
381
  },
361
- };
382
+ });
362
383
  }
363
384
  }
364
385
  }
@@ -369,7 +390,7 @@ export function createCoreRoutesPlugin(options = {}) {
369
390
  try {
370
391
  const disconnected = await getSetting("builder-disconnected");
371
392
  if (disconnected) {
372
- return {
393
+ return withConnectToken({
373
394
  ...envStatus,
374
395
  configured: false,
375
396
  privateKeyConfigured: false,
@@ -377,7 +398,7 @@ export function createCoreRoutesPlugin(options = {}) {
377
398
  userId: undefined,
378
399
  orgName: undefined,
379
400
  orgKind: undefined,
380
- };
401
+ });
381
402
  }
382
403
  }
383
404
  catch {
@@ -386,7 +407,7 @@ export function createCoreRoutesPlugin(options = {}) {
386
407
  // No env, no per-user creds → not configured. Both authenticated
387
408
  // and unauthenticated callers see "not connected" so they can
388
409
  // run through the OAuth flow.
389
- return {
410
+ return withConnectToken({
390
411
  ...envStatus,
391
412
  configured: false,
392
413
  privateKeyConfigured: false,
@@ -394,7 +415,7 @@ export function createCoreRoutesPlugin(options = {}) {
394
415
  userId: undefined,
395
416
  orgName: undefined,
396
417
  orgKind: undefined,
397
- };
418
+ });
398
419
  });
399
420
  }));
400
421
  // How long a pending-connect row is valid. Must be long enough for
@@ -440,26 +461,29 @@ export function createCoreRoutesPlugin(options = {}) {
440
461
  // inside a click handler, avoiding the popup-blocker downgrade that
441
462
  // happens when an await sits before window.open.
442
463
  //
443
- // CSRF protection here is two-layer because session cookies are
464
+ // CSRF protection here is layered because session cookies are
444
465
  // SameSite=None;Secure (so the editor iframe can ride along) — that
445
466
  // means a session cookie alone does NOT prevent cross-origin
446
467
  // window.open from initiating a connect flow on the victim's behalf:
447
- // 1. Sec-Fetch-Site header modern browsers stamp every request
448
- // with the navigation context. We only allow `same-origin` or
449
- // `none` (typed/bookmark/extension); cross-site / same-site are
450
- // rejected. The previous "URL-embedded signed state" was dropped
451
- // because Builder's /cli-auth strips arbitrary query params; this
452
- // replaces that defense with a browser-native one.
453
- // 2. Pending row keyed by session email + bound nonce — the callback
468
+ // 1. Signed connect token from /builder/status proves the opener
469
+ // could read same-origin JSON, which cross-site attackers cannot.
470
+ // This covers local/embedded browsers that conservatively label a
471
+ // legitimate popup navigation as same-site/cross-site.
472
+ // 2. Sec-Fetch-Site header fallback modern browsers stamp every
473
+ // request with the navigation context. We allow `same-origin` or
474
+ // `none` (typed/bookmark/extension); cross-site / same-site without
475
+ // a valid connect token are rejected.
476
+ // 3. Pending row keyed by session email + bound nonce — the callback
454
477
  // requires both a valid session and a one-time row that this
455
478
  // handler wrote during the same flow. Without the same-origin
456
- // check above, an attacker could prime the row from cross-site
457
- // and then trick the victim into hitting a callback URL with
458
- // attacker-controlled p-key/api-key, hijacking the victim's
459
- // account. With the check, the attacker can't prime the row.
479
+ // gate or connect token above, an attacker could prime the row from
480
+ // cross-site and then trick the victim into hitting a callback URL
481
+ // with attacker-controlled p-key/api-key, hijacking the victim's
482
+ // account.
460
483
  getH3App(nitroApp).use(`${P}/builder/connect`, defineEventHandler(async (event) => {
461
- const session = await getSession(event).catch(() => null);
462
- if (!session?.email) {
484
+ const ownerContext = await resolveBuilderOwnerContext(event);
485
+ const ownerEmail = ownerContext.email;
486
+ if (!ownerEmail) {
463
487
  setResponseStatus(event, 401);
464
488
  return { error: "Authentication required" };
465
489
  }
@@ -476,15 +500,17 @@ export function createCoreRoutesPlugin(options = {}) {
476
500
  envManaged: true,
477
501
  };
478
502
  }
479
- // Same-origin gate. Sec-Fetch-Site is the primary signal; fall
480
- // back to Origin/Referer for older browsers. Reject any context
481
- // that isn't this exact app's origin — including same-site
482
- // subdomains, since a compromised subdomain shouldn't be able
483
- // to mint Builder credential writes for users of the main app.
484
- if (!isSameOriginConnect(event)) {
485
- trackBuilderLifecycle("builder connect failed", session.email, {
503
+ const requestUrl = new URL(`${event.url?.pathname || "/"}${event.url?.search || ""}`, getOrigin(event));
504
+ const connectToken = requestUrl.searchParams.get(BUILDER_CONNECT_PARAM);
505
+ const hasValidConnectToken = verifyBuilderConnectToken(connectToken, ownerEmail);
506
+ // Same-origin gate. Sec-Fetch-Site remains the fast path; the signed
507
+ // connect token is the compatibility path for legitimate embedded or
508
+ // local desktop popups stamped as same-site/cross-site by the browser.
509
+ if (!isSameOriginConnect(event) && !hasValidConnectToken) {
510
+ trackBuilderLifecycle("builder connect failed", ownerEmail, {
486
511
  reason: "cross_origin",
487
512
  stage: "connect",
513
+ has_connect_token: Boolean(connectToken),
488
514
  });
489
515
  setResponseStatus(event, 403);
490
516
  return { error: "Cross-origin connect requests are not allowed" };
@@ -493,7 +519,7 @@ export function createCoreRoutesPlugin(options = {}) {
493
519
  // useBuilderStatus polling sees the stale error and aborts the
494
520
  // new attempt before it can complete.
495
521
  try {
496
- await deleteSetting(`builder-connect-error:${session.email}`);
522
+ await deleteSetting(`builder-connect-error:${ownerEmail}`);
497
523
  }
498
524
  catch {
499
525
  // No prior error row — fine
@@ -503,12 +529,12 @@ export function createCoreRoutesPlugin(options = {}) {
503
529
  // via BroadcastChannel, rather than letting the popup show raw
504
530
  // JSON and the parent poll for 5 minutes.
505
531
  try {
506
- await putSetting(`builder-pending-connect:${session.email}`, {
532
+ await putSetting(`builder-pending-connect:${ownerEmail}`, {
507
533
  expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
508
534
  });
509
535
  }
510
536
  catch (err) {
511
- trackBuilderLifecycle("builder connect failed", session.email, {
537
+ trackBuilderLifecycle("builder connect failed", ownerEmail, {
512
538
  reason: "pending_storage_unavailable",
513
539
  stage: "connect",
514
540
  });
@@ -516,7 +542,7 @@ export function createCoreRoutesPlugin(options = {}) {
516
542
  console.error("[builder] Could not store pending-connect state:", err?.message ?? err);
517
543
  // Best-effort: also write the error row so the parent's
518
544
  // /builder/status poll picks it up if BroadcastChannel doesn't.
519
- await putSetting(`builder-connect-error:${session.email}`, {
545
+ await putSetting(`builder-connect-error:${ownerEmail}`, {
520
546
  message: msg,
521
547
  at: Date.now(),
522
548
  }).catch(() => { });
@@ -524,7 +550,7 @@ export function createCoreRoutesPlugin(options = {}) {
524
550
  setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
525
551
  return createBuilderBrowserCallbackErrorPage(msg);
526
552
  }
527
- trackBuilderLifecycle("builder connect started", session.email, {
553
+ trackBuilderLifecycle("builder connect started", ownerEmail, {
528
554
  stage: "connect",
529
555
  });
530
556
  // Build the cli-auth URL without embedding state in redirect_url:
@@ -617,14 +643,12 @@ export function createCoreRoutesPlugin(options = {}) {
617
643
  setResponseStatus(event, 405);
618
644
  return { error: "Method not allowed" };
619
645
  }
620
- // Session blocks anonymous callers; the pending-row check below
621
- // (combined with the same-origin gate on /builder/connect) blocks
622
- // CSRF. Session cookies are SameSite=None;Secure for the iframe
623
- // editor, so they ride along on attacker-crafted top-level links —
624
- // the SameSite=None concession is exactly why the connect flow
625
- // can't rely on session-cookie identity alone.
626
- const session = await getSession(event).catch(() => null);
627
- if (!session?.email) {
646
+ // A real session or a template-approved anonymous owner is required;
647
+ // the pending-row check below (combined with the same-origin gate on
648
+ // /builder/connect) blocks CSRF and callback replay.
649
+ const ownerContext = await resolveBuilderOwnerContext(event);
650
+ const ownerEmail = ownerContext.email;
651
+ if (!ownerEmail) {
628
652
  setResponseStatus(event, 401);
629
653
  return { error: "Authentication required" };
630
654
  }
@@ -643,12 +667,12 @@ export function createCoreRoutesPlugin(options = {}) {
643
667
  let pendingValid = false;
644
668
  let pendingError = null;
645
669
  try {
646
- const pending = (await getSetting(`builder-pending-connect:${session.email}`));
670
+ const pending = (await getSetting(`builder-pending-connect:${ownerEmail}`));
647
671
  if (pending &&
648
672
  typeof pending.expiresAt === "number" &&
649
673
  Date.now() < pending.expiresAt) {
650
674
  try {
651
- await deleteSetting(`builder-pending-connect:${session.email}`);
675
+ await deleteSetting(`builder-pending-connect:${ownerEmail}`);
652
676
  pendingValid = true;
653
677
  }
654
678
  catch (err) {
@@ -662,13 +686,13 @@ export function createCoreRoutesPlugin(options = {}) {
662
686
  // DB temporarily unavailable — treat as missing.
663
687
  }
664
688
  if (pendingError) {
665
- trackBuilderLifecycle("builder connect failed", session.email, {
689
+ trackBuilderLifecycle("builder connect failed", ownerEmail, {
666
690
  reason: "pending_consume_storage_error",
667
691
  stage: "callback",
668
692
  });
669
693
  // Best-effort signal to the parent's poll loop, then render the
670
694
  // popup-friendly error page so the BroadcastChannel notify fires.
671
- await putSetting(`builder-connect-error:${session.email}`, {
695
+ await putSetting(`builder-connect-error:${ownerEmail}`, {
672
696
  message: pendingError,
673
697
  at: Date.now(),
674
698
  }).catch(() => { });
@@ -677,7 +701,7 @@ export function createCoreRoutesPlugin(options = {}) {
677
701
  return createBuilderBrowserCallbackErrorPage(pendingError);
678
702
  }
679
703
  if (!pendingValid) {
680
- trackBuilderLifecycle("builder connect failed", session.email, {
704
+ trackBuilderLifecycle("builder connect failed", ownerEmail, {
681
705
  reason: "missing_pending_connect",
682
706
  stage: "callback",
683
707
  });
@@ -685,7 +709,7 @@ export function createCoreRoutesPlugin(options = {}) {
685
709
  // Write an error signal so the polling loop in the parent tab
686
710
  // terminates quickly instead of waiting 5 minutes for the timeout.
687
711
  try {
688
- await putSetting(`builder-connect-error:${session.email}`, {
712
+ await putSetting(`builder-connect-error:${ownerEmail}`, {
689
713
  message: msg,
690
714
  at: Date.now(),
691
715
  });
@@ -700,7 +724,7 @@ export function createCoreRoutesPlugin(options = {}) {
700
724
  const privateKey = requestUrl.searchParams.get("p-key");
701
725
  const publicKey = requestUrl.searchParams.get("api-key");
702
726
  if (!privateKey || !publicKey) {
703
- trackBuilderLifecycle("builder connect failed", session.email, {
727
+ trackBuilderLifecycle("builder connect failed", ownerEmail, {
704
728
  reason: "missing_credentials",
705
729
  stage: "callback",
706
730
  });
@@ -709,7 +733,7 @@ export function createCoreRoutesPlugin(options = {}) {
709
733
  // immediately via BroadcastChannel rather than hanging until the
710
734
  // 5-minute timeout.
711
735
  const msg = "Builder didn't return credentials. Restart the connect flow from settings.";
712
- await putSetting(`builder-connect-error:${session.email}`, {
736
+ await putSetting(`builder-connect-error:${ownerEmail}`, {
713
737
  message: msg,
714
738
  at: Date.now(),
715
739
  }).catch(() => { });
@@ -742,23 +766,25 @@ export function createCoreRoutesPlugin(options = {}) {
742
766
  // legacy per-user behaviour for that connection.
743
767
  let orgId = null;
744
768
  let role = null;
745
- try {
746
- const { getOrgContext } = await import("../org/context.js");
747
- const orgCtx = await getOrgContext(event);
748
- orgId = orgCtx.orgId ?? null;
749
- role = orgCtx.role ?? null;
750
- }
751
- catch {
752
- /* org module not present in this template — keep user scope */
769
+ if (!ownerContext.anonymous) {
770
+ try {
771
+ const { getOrgContext } = await import("../org/context.js");
772
+ const orgCtx = await getOrgContext(event);
773
+ orgId = orgCtx.orgId ?? null;
774
+ role = orgCtx.role ?? null;
775
+ }
776
+ catch {
777
+ /* org module not present in this template — keep user scope */
778
+ }
753
779
  }
754
- await writeBuilderCredentials(session.email, { privateKey, publicKey, userId, orgName, orgKind }, { orgId, role });
780
+ await writeBuilderCredentials(ownerEmail, { privateKey, publicKey, userId, orgName, orgKind }, { orgId, role });
755
781
  }
756
782
  catch (err) {
757
783
  writeError = err?.message ?? String(err);
758
784
  console.error("[builder] Failed to persist Builder credentials:", writeError);
759
785
  }
760
786
  if (writeError) {
761
- trackBuilderLifecycle("builder connect failed", session.email, {
787
+ trackBuilderLifecycle("builder connect failed", ownerEmail, {
762
788
  reason: "credential_write_failed",
763
789
  stage: "callback",
764
790
  });
@@ -766,7 +792,7 @@ export function createCoreRoutesPlugin(options = {}) {
766
792
  // (entire DB unreachable) the popup's postMessage still notifies
767
793
  // the parent. If both fail the parent times out at 5min as today.
768
794
  try {
769
- await putSetting(`builder-connect-error:${session.email}`, {
795
+ await putSetting(`builder-connect-error:${ownerEmail}`, {
770
796
  message: writeError,
771
797
  at: Date.now(),
772
798
  });
@@ -787,13 +813,13 @@ export function createCoreRoutesPlugin(options = {}) {
787
813
  // DB not ready — proceed
788
814
  }
789
815
  try {
790
- await deleteSetting(`builder-connect-error:${session.email}`);
816
+ await deleteSetting(`builder-connect-error:${ownerEmail}`);
791
817
  }
792
818
  catch {
793
819
  // No prior error row — fine
794
820
  }
795
821
  const previewUrl = resolveSafePreviewUrl(requestUrl.searchParams.get("preview-url"), event);
796
- trackBuilderLifecycle("builder connect succeeded", session.email, {
822
+ trackBuilderLifecycle("builder connect succeeded", ownerEmail, {
797
823
  stage: "callback",
798
824
  has_preview_url: Boolean(previewUrl),
799
825
  org_kind: orgKind || undefined,