@qoniai/qoni 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -490,6 +490,87 @@ var ActionKinds = {
490
490
  };
491
491
  var ACTION_KIND_VALUES = Object.values(ActionKinds);
492
492
 
493
+ // src/scopes.ts
494
+ var QoniScopes = {
495
+ GUMEM_SESSION_CREATE: "gumem.session:create",
496
+ GUMEM_MESSAGE_WRITE: "gumem.message:write",
497
+ GUMEM_MEMORY_READ: "gumem.memory:read",
498
+ GUMEM_MEMORY_WRITE: "gumem.memory:write",
499
+ GUMEM_MEMORY_DELETE: "gumem.memory:delete",
500
+ GUMEM_SEARCH_RUN: "gumem.search:run",
501
+ GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
502
+ GUMEM_RESOURCE_WRITE: "gumem.resource:write",
503
+ GUMEM_ACTION_WRITE: "gumem.action:write",
504
+ GUMEM_ACTION_READ: "gumem.action:read",
505
+ GUMEM_PROFILE_READ: "gumem.profile:read",
506
+ // WebAgent products: exactly two verbs per product — `read` (observe) and
507
+ // `manage` (anything that changes execution state). See
508
+ // wa/docs/qoni-sdk-public-surface.md §3.
509
+ DO_ANYTHING_READ: "webagent.do_anything:read",
510
+ DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
511
+ WEB_SEARCH_READ: "webagent.web_search:read",
512
+ WEB_SEARCH_MANAGE: "webagent.web_search:manage",
513
+ DEEP_RESEARCH_READ: "webagent.deep_research:read",
514
+ DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
515
+ TRACK_READ: "webagent.track:read",
516
+ TRACK_MANAGE: "webagent.track:manage",
517
+ // Interactive site login (`InteractionHandle.openLogin()` on a `site_login`
518
+ // interaction). Not part of any product bundle: the AccessKey policy must
519
+ // allow it, and the delegation must request it explicitly.
520
+ SITE_LOGIN_REQUEST: "webagent.site_login:request",
521
+ // Persist that login into the browser profile (`OpenLoginResult.confirm()`).
522
+ SITE_LOGIN_CONFIRM: "webagent.site_login:confirm"
523
+ };
524
+ var QoniProductScopes = {
525
+ doAnything: [
526
+ QoniScopes.DO_ANYTHING_READ,
527
+ QoniScopes.DO_ANYTHING_MANAGE
528
+ ],
529
+ webSearch: [
530
+ QoniScopes.WEB_SEARCH_READ,
531
+ QoniScopes.WEB_SEARCH_MANAGE
532
+ ],
533
+ deepResearch: [
534
+ QoniScopes.DEEP_RESEARCH_READ,
535
+ QoniScopes.DEEP_RESEARCH_MANAGE
536
+ ],
537
+ track: [
538
+ QoniScopes.TRACK_READ,
539
+ QoniScopes.TRACK_MANAGE
540
+ ]
541
+ };
542
+ var KNOWN_SCOPES = Object.values(QoniScopes);
543
+ var QoniScopeBundles = {
544
+ GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
545
+ GUMEM_SESSION_RECALL: [
546
+ QoniScopes.GUMEM_MEMORY_READ,
547
+ QoniScopes.GUMEM_MEMORY_WRITE,
548
+ QoniScopes.GUMEM_MESSAGE_WRITE
549
+ ],
550
+ GUMEM_WRITE: [
551
+ QoniScopes.GUMEM_SESSION_CREATE,
552
+ QoniScopes.GUMEM_MESSAGE_WRITE,
553
+ QoniScopes.GUMEM_ACTION_WRITE
554
+ ],
555
+ // One bundle per WebAgent product = [read, manage].
556
+ DO_ANYTHING: [
557
+ QoniScopes.DO_ANYTHING_READ,
558
+ QoniScopes.DO_ANYTHING_MANAGE
559
+ ],
560
+ WEB_SEARCH: [
561
+ QoniScopes.WEB_SEARCH_READ,
562
+ QoniScopes.WEB_SEARCH_MANAGE
563
+ ],
564
+ DEEP_RESEARCH: [
565
+ QoniScopes.DEEP_RESEARCH_READ,
566
+ QoniScopes.DEEP_RESEARCH_MANAGE
567
+ ],
568
+ TRACK: [
569
+ QoniScopes.TRACK_READ,
570
+ QoniScopes.TRACK_MANAGE
571
+ ]
572
+ };
573
+
493
574
  // src/interactions.ts
494
575
  function asString2(value) {
495
576
  return typeof value === "string" && value ? value : void 0;
@@ -588,6 +669,9 @@ function normalizeInteraction(payload) {
588
669
  if (evidence) interaction.evidence = evidence;
589
670
  return interaction;
590
671
  }
672
+ function interactionActionScopes(action, productManageScope) {
673
+ return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
674
+ }
591
675
  var InteractionHandle = class {
592
676
  constructor(interaction, post) {
593
677
  this.post = post;
@@ -612,48 +696,122 @@ var InteractionHandle = class {
612
696
  return this.interaction.actions.some((a) => a.kind === kind);
613
697
  }
614
698
  /** Answer a clarification ask. */
615
- answer(text) {
616
- return this.invoke(ActionKinds.answer, { response: text });
699
+ async answer(text) {
700
+ await this.invoke(ActionKinds.answer, { response: text });
617
701
  }
618
702
  /** Skip / dismiss the interaction (proceed without acting on it). */
619
- skip() {
620
- return this.invoke(ActionKinds.skip);
703
+ async skip() {
704
+ await this.invoke(ActionKinds.skip);
621
705
  }
622
706
  /** Confirm a pending confirmation (approve the plan / destructive op). */
623
- confirm() {
624
- return this.invoke(ActionKinds.confirm);
707
+ async confirm() {
708
+ await this.invoke(ActionKinds.confirm);
625
709
  }
626
710
  /** Reject a pending confirmation. */
627
- reject() {
628
- return this.invoke(ActionKinds.reject);
711
+ async reject() {
712
+ await this.invoke(ActionKinds.reject);
629
713
  }
630
- /** Open the login surface for a site_login interaction. */
631
- openLogin() {
632
- return this.invoke(ActionKinds.openLogin);
714
+ /**
715
+ * Start an interactive login session for a `site_login` interaction. The
716
+ * backend needs the site to sign in to: pass `siteId`, or omit it when the
717
+ * interaction lists exactly one site. Show `liveUrl` to the user; when they
718
+ * finish, call `confirm()` on the result to save the login, and act on its
719
+ * outcome (see {@link OpenLoginResult.confirm}) before {@link confirmSignedIn}.
720
+ * The delegation must include `QoniScopes.SITE_LOGIN_REQUEST` and
721
+ * `QoniScopes.SITE_LOGIN_CONFIRM`; the AccessKey policy must allow both.
722
+ */
723
+ async openLogin(options = {}) {
724
+ this.#declaredAction(ActionKinds.openLogin);
725
+ const body = { site_id: this.#loginSiteId(options.siteId) };
726
+ if (options.profileId !== void 0) {
727
+ if (typeof options.profileId !== "string" || !options.profileId) {
728
+ throw new QoniValidationError("openLogin profileId must be a non-empty string");
729
+ }
730
+ body.profile_id = options.profileId;
731
+ }
732
+ const response = await this.invoke(ActionKinds.openLogin, body);
733
+ const raw = isJsonObject(response) ? response : {};
734
+ const fields = camelizeRecord(raw);
735
+ const profileId = asString2(fields.profileId) ?? "";
736
+ const siteId = asString2(fields.siteId) ?? String(body.site_id);
737
+ return {
738
+ profileId,
739
+ siteId,
740
+ sessionId: asString2(fields.sessionId) ?? "",
741
+ liveUrl: asString2(fields.liveUrl) ?? "",
742
+ raw,
743
+ confirm: async (confirmOptions = {}) => {
744
+ if (!profileId) {
745
+ throw new QoniValidationError("openLogin response did not include a profileId to confirm");
746
+ }
747
+ const confirmBody = { site_id: siteId };
748
+ if (confirmOptions.displayLabel !== void 0) confirmBody.display_label = confirmOptions.displayLabel;
749
+ const response2 = await this.post(
750
+ {
751
+ kind: ActionKinds.openLogin,
752
+ label: "Confirm login",
753
+ method: "POST",
754
+ endpoint: `/profiles/${encodeURIComponent(profileId)}/confirm`
755
+ },
756
+ confirmBody,
757
+ { requiredScopes: [QoniScopes.SITE_LOGIN_CONFIRM] }
758
+ );
759
+ const raw2 = isJsonObject(response2) ? response2 : {};
760
+ const fields2 = camelizeRecord(raw2);
761
+ const state = asString2(fields2.state) ?? "";
762
+ return {
763
+ profileId: asString2(fields2.profileId) ?? profileId,
764
+ active: state === "active",
765
+ state,
766
+ probeResult: asString2(fields2.probeResult) ?? "",
767
+ raw: raw2
768
+ };
769
+ }
770
+ };
633
771
  }
634
772
  /** Tell the agent the user has finished signing in. */
635
- confirmSignedIn() {
636
- return this.invoke(ActionKinds.confirmSignedIn);
773
+ async confirmSignedIn() {
774
+ await this.invoke(ActionKinds.confirmSignedIn);
637
775
  }
638
776
  /** Connect to the live browser for a take_control interaction. */
639
- connectControl() {
640
- return this.invoke(ActionKinds.connectControl);
777
+ async connectControl() {
778
+ await this.invoke(ActionKinds.connectControl);
641
779
  }
642
780
  /** Refresh the live control surface / live url. */
643
- refreshControl() {
644
- return this.invoke(ActionKinds.refreshControl);
781
+ async refreshControl() {
782
+ await this.invoke(ActionKinds.refreshControl);
645
783
  }
646
784
  /** Hand control back to the agent. */
647
- releaseControl() {
648
- return this.invoke(ActionKinds.releaseControl);
785
+ async releaseControl() {
786
+ await this.invoke(ActionKinds.releaseControl);
649
787
  }
650
788
  /** Retry a wait interaction now. */
651
- retry() {
652
- return this.invoke(ActionKinds.retry);
789
+ async retry() {
790
+ await this.invoke(ActionKinds.retry);
653
791
  }
654
792
  /** Switch the profile a wait/login interaction is bound to. */
655
- switchProfile() {
656
- return this.invoke(ActionKinds.switchProfile);
793
+ async switchProfile() {
794
+ await this.invoke(ActionKinds.switchProfile);
795
+ }
796
+ /** Resolve the site for `openLogin`: explicit, or the only listed site. */
797
+ #loginSiteId(requested) {
798
+ const payload = this.interaction.payload;
799
+ const listed = Array.isArray(payload?.sites) ? payload.sites.map((site) => site.siteId).filter((id) => typeof id === "string" && id.length > 0) : [];
800
+ if (requested !== void 0) {
801
+ if (typeof requested !== "string" || !requested) {
802
+ throw new QoniValidationError("openLogin siteId must be a non-empty string");
803
+ }
804
+ if (listed.length > 0 && !listed.includes(requested)) {
805
+ throw new QoniValidationError(
806
+ `openLogin siteId "${requested}" is not one of the sites this interaction asks for: ${listed.join(", ")}`
807
+ );
808
+ }
809
+ return requested;
810
+ }
811
+ if (listed.length === 1) return listed[0];
812
+ throw new QoniValidationError(
813
+ listed.length === 0 ? "openLogin needs a siteId: this interaction does not list the site to sign in to" : `openLogin needs a siteId: choose one of ${listed.join(", ")}`
814
+ );
657
815
  }
658
816
  /**
659
817
  * Invoke an action by kind with an optional body. `async` so a missing-action
@@ -661,6 +819,10 @@ var InteractionHandle = class {
661
819
  * method contract uniformly promise-based.
662
820
  */
663
821
  async invoke(kind, body) {
822
+ return this.post(this.#declaredAction(kind), body);
823
+ }
824
+ /** The declared action of `kind`; throws when the backend did not offer it. */
825
+ #declaredAction(kind) {
664
826
  const action = this.interaction.actions.find((a) => a.kind === kind);
665
827
  if (!action) {
666
828
  const offered = this.interaction.actions.map((a) => a.kind).join(", ") || "none";
@@ -668,7 +830,7 @@ var InteractionHandle = class {
668
830
  `interaction "${this.interaction.id}" (${this.interaction.type}) does not offer the "${kind}" action \u2014 the backend declared: ${offered}. Only call methods whose action the interaction currently offers.`
669
831
  );
670
832
  }
671
- await this.post(action, body);
833
+ return action;
672
834
  }
673
835
  };
674
836
 
@@ -738,7 +900,7 @@ var RunHandle = class {
738
900
  break settled;
739
901
  }
740
902
  }
741
- const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
903
+ const detail2 = await settleRead(() => this.ops.getDetail(signal), signal);
742
904
  if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
743
905
  terminalDetail = detail2;
744
906
  break;
@@ -752,10 +914,10 @@ var RunHandle = class {
752
914
  sawCleanClose = true;
753
915
  eventIdAtLastClose = lastEventId;
754
916
  }
755
- const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
917
+ const detail = terminalDetail ?? await settleRead(() => this.ops.getDetail(signal), signal);
756
918
  this.#adoptSessionRef(detail);
757
919
  const result = settleRunResult(this.id, detail, [], terminalDoneReason);
758
- result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
920
+ result.artifacts = this.ops.listArtifacts ? await settleRead(() => this.ops.listArtifacts(signal), signal) : [];
759
921
  return result;
760
922
  } catch (err) {
761
923
  if (timedOut() && !opts.signal?.aborted) {
@@ -778,7 +940,7 @@ var RunHandle = class {
778
940
  interactionHandle(interaction) {
779
941
  return new InteractionHandle(
780
942
  interaction,
781
- (action, body) => this.ops.postAction(action, body)
943
+ (action, body, options) => this.ops.postAction(action, body, options)
782
944
  );
783
945
  }
784
946
  /**
@@ -872,6 +1034,35 @@ function withTimeout(signal, timeoutMs) {
872
1034
  }
873
1035
  return { signal: controller.signal, timedOut: () => fired, dispose };
874
1036
  }
1037
+ var SETTLE_READ_BACKOFF_MS = [250, 1e3];
1038
+ async function settleRead(read, signal) {
1039
+ for (let attempt = 0; ; attempt++) {
1040
+ try {
1041
+ return await waitWithSignal(read, signal);
1042
+ } catch (err) {
1043
+ const delay = SETTLE_READ_BACKOFF_MS[attempt];
1044
+ if (delay === void 0 || signal?.aborted || !(err instanceof QoniError) || !err.retryable) throw err;
1045
+ await sleepWithSignal(delay, signal);
1046
+ }
1047
+ }
1048
+ }
1049
+ function sleepWithSignal(ms, signal) {
1050
+ return new Promise((resolve, reject) => {
1051
+ if (signal?.aborted) {
1052
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1053
+ return;
1054
+ }
1055
+ const onAbort = () => {
1056
+ clearTimeout(timer);
1057
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1058
+ };
1059
+ const timer = setTimeout(() => {
1060
+ signal?.removeEventListener("abort", onAbort);
1061
+ resolve();
1062
+ }, ms);
1063
+ signal?.addEventListener("abort", onAbort, { once: true });
1064
+ });
1065
+ }
875
1066
  function waitWithSignal(operation, signal) {
876
1067
  if (!signal) return Promise.resolve().then(operation);
877
1068
  return new Promise((resolve, reject) => {
@@ -969,17 +1160,15 @@ function createDeepResearchNamespace(transport) {
969
1160
  signal: opts.signal,
970
1161
  onReconnect: opts.onReconnect
971
1162
  }),
972
- postAction: async (action, body) => {
973
- await transport.webAgentJson(
974
- action.method || "POST",
975
- action.endpoint,
976
- token,
977
- {
978
- ...body !== void 0 ? { body } : {},
979
- requiredScopes: ["webagent.deep_research:manage"]
980
- }
981
- );
982
- },
1163
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1164
+ action.method || "POST",
1165
+ action.endpoint,
1166
+ token,
1167
+ {
1168
+ ...body !== void 0 ? { body } : {},
1169
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.deep_research:manage")
1170
+ }
1171
+ )).data,
983
1172
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
984
1173
  listArtifacts: async (signal) => {
985
1174
  const res = await api.listArtifacts({ token, runId, signal });
@@ -1244,17 +1433,15 @@ function createDoAnythingNamespace(transport) {
1244
1433
  // Skip screenshot rows in backfill when the caller did not opt in.
1245
1434
  includeScreenshots: capture?.screenshots ? void 0 : false
1246
1435
  }),
1247
- postAction: async (action, body) => {
1248
- await transport.webAgentJson(
1249
- action.method || "POST",
1250
- action.endpoint,
1251
- token,
1252
- {
1253
- ...body !== void 0 ? { body } : {},
1254
- requiredScopes: ["webagent.do_anything:manage"]
1255
- }
1256
- );
1257
- },
1436
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1437
+ action.method || "POST",
1438
+ action.endpoint,
1439
+ token,
1440
+ {
1441
+ ...body !== void 0 ? { body } : {},
1442
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.do_anything:manage")
1443
+ }
1444
+ )).data,
1258
1445
  cancel: async (reason) => asRecord2(
1259
1446
  (await api.cancel({
1260
1447
  token,
@@ -1375,84 +1562,16 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1375
1562
  };
1376
1563
  }
1377
1564
 
1378
- // src/scopes.ts
1379
- var QoniScopes = {
1380
- GUMEM_SESSION_CREATE: "gumem.session:create",
1381
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
1382
- GUMEM_MEMORY_READ: "gumem.memory:read",
1383
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
1384
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1385
- GUMEM_SEARCH_RUN: "gumem.search:run",
1386
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1387
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1388
- GUMEM_ACTION_WRITE: "gumem.action:write",
1389
- GUMEM_ACTION_READ: "gumem.action:read",
1390
- GUMEM_PROFILE_READ: "gumem.profile:read",
1391
- // WebAgent products: exactly two verbs per product — `read` (observe) and
1392
- // `manage` (anything that changes execution state). See
1393
- // wa/docs/qoni-sdk-public-surface.md §3.
1394
- DO_ANYTHING_READ: "webagent.do_anything:read",
1395
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1396
- WEB_SEARCH_READ: "webagent.web_search:read",
1397
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1398
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
1399
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1400
- TRACK_READ: "webagent.track:read",
1401
- TRACK_MANAGE: "webagent.track:manage"
1402
- };
1403
- var QoniProductScopes = {
1404
- doAnything: [
1405
- QoniScopes.DO_ANYTHING_READ,
1406
- QoniScopes.DO_ANYTHING_MANAGE
1407
- ],
1408
- webSearch: [
1409
- QoniScopes.WEB_SEARCH_READ,
1410
- QoniScopes.WEB_SEARCH_MANAGE
1411
- ],
1412
- deepResearch: [
1413
- QoniScopes.DEEP_RESEARCH_READ,
1414
- QoniScopes.DEEP_RESEARCH_MANAGE
1415
- ],
1416
- track: [
1417
- QoniScopes.TRACK_READ,
1418
- QoniScopes.TRACK_MANAGE
1419
- ]
1420
- };
1421
- var KNOWN_SCOPES = Object.values(QoniScopes);
1422
- var QoniScopeBundles = {
1423
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1424
- GUMEM_SESSION_RECALL: [
1425
- QoniScopes.GUMEM_MEMORY_READ,
1426
- QoniScopes.GUMEM_MEMORY_WRITE,
1427
- QoniScopes.GUMEM_MESSAGE_WRITE
1428
- ],
1429
- GUMEM_WRITE: [
1430
- QoniScopes.GUMEM_SESSION_CREATE,
1431
- QoniScopes.GUMEM_MESSAGE_WRITE,
1432
- QoniScopes.GUMEM_ACTION_WRITE
1433
- ],
1434
- // One bundle per WebAgent product = [read, manage].
1435
- DO_ANYTHING: [
1436
- QoniScopes.DO_ANYTHING_READ,
1437
- QoniScopes.DO_ANYTHING_MANAGE
1438
- ],
1439
- WEB_SEARCH: [
1440
- QoniScopes.WEB_SEARCH_READ,
1441
- QoniScopes.WEB_SEARCH_MANAGE
1442
- ],
1443
- DEEP_RESEARCH: [
1444
- QoniScopes.DEEP_RESEARCH_READ,
1445
- QoniScopes.DEEP_RESEARCH_MANAGE
1446
- ],
1447
- TRACK: [
1448
- QoniScopes.TRACK_READ,
1449
- QoniScopes.TRACK_MANAGE
1450
- ]
1451
- };
1452
-
1453
1565
  // src/genauth.ts
1454
1566
  function createGenAuthNamespace(transport, options = {}) {
1567
+ async function delegateAgent(input) {
1568
+ if (!options.delegateTokenFor) {
1569
+ throw new QoniValidationError("GenAuth delegateAgent requires a Qoni delegation provider");
1570
+ }
1571
+ return (await options.delegateTokenFor(input)).data;
1572
+ }
1455
1573
  return {
1574
+ delegateAgent,
1456
1575
  userInfo: (input) => transport.genauthJson("GET", "/oidc/me", input.accessToken),
1457
1576
  jwks: () => transport.genauthJson("GET", "/oidc/.well-known/jwks.json"),
1458
1577
  discovery: () => transport.genauthJson("GET", "/oidc/.well-known/openid-configuration"),
@@ -1648,7 +1767,7 @@ var MonitorHandle = class {
1648
1767
  id;
1649
1768
  /** Refresh and return the monitor (normalized camelCase fields). */
1650
1769
  async get() {
1651
- return camelizeRecord(await this.ops.get());
1770
+ return normalizeMonitorRecord(await this.ops.get());
1652
1771
  }
1653
1772
  /**
1654
1773
  * Pause the monitor — stops scheduled ticks until {@link resume}. The
@@ -1656,11 +1775,11 @@ var MonitorHandle = class {
1656
1775
  * deleting.
1657
1776
  */
1658
1777
  async pause() {
1659
- return camelizeRecord(await this.ops.update({ action: "pause" }));
1778
+ return normalizeMonitorRecord(await this.ops.update({ action: "pause" }));
1660
1779
  }
1661
1780
  /** Resume a paused monitor. */
1662
1781
  async resume() {
1663
- return camelizeRecord(await this.ops.update({ action: "resume" }));
1782
+ return normalizeMonitorRecord(await this.ops.update({ action: "resume" }));
1664
1783
  }
1665
1784
  /**
1666
1785
  * Change the monitor definition. Pass any subset of fields (at least one);
@@ -1678,11 +1797,11 @@ var MonitorHandle = class {
1678
1797
  "track refine requires at least one field to change (schedule / targetUrls / extractionSchema / triggerDsl / stopConditionDsl / notifyChannel)"
1679
1798
  );
1680
1799
  }
1681
- return camelizeRecord(await this.ops.update({ action: "refine", patch: body }));
1800
+ return normalizeMonitorRecord(await this.ops.update({ action: "refine", patch: body }));
1682
1801
  }
1683
1802
  /** Trigger an immediate tick outside the schedule. */
1684
1803
  async runNow() {
1685
- return camelizeRecord(await this.ops.runNow());
1804
+ return normalizeMonitorRecord(await this.ops.runNow());
1686
1805
  }
1687
1806
  /**
1688
1807
  * Stream semantic monitor events. Monitors are resident, so the stream has
@@ -1703,7 +1822,7 @@ var MonitorHandle = class {
1703
1822
  interactionHandle(interaction) {
1704
1823
  return new InteractionHandle(
1705
1824
  interaction,
1706
- (action, body) => this.ops.postAction(action, body)
1825
+ (action, body, options) => this.ops.postAction(action, body, options)
1707
1826
  );
1708
1827
  }
1709
1828
  /**
@@ -1808,17 +1927,15 @@ function createTrackNamespace(transport) {
1808
1927
  update: async (input) => asRecord3((await api.updateMonitor({ token, monitorId, ...input })).data),
1809
1928
  runNow: async () => asRecord3((await api.runNow({ token, monitorId })).data),
1810
1929
  streamEvents: (opts) => api.events({ token, monitorId, lastEventId: opts.lastEventId, signal: opts.signal }),
1811
- postAction: async (action, body) => {
1812
- await transport.webAgentJson(
1813
- action.method || "POST",
1814
- action.endpoint,
1815
- token,
1816
- {
1817
- ...body !== void 0 ? { body } : {},
1818
- requiredScopes: ["webagent.track:manage"]
1819
- }
1820
- );
1821
- },
1930
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1931
+ action.method || "POST",
1932
+ action.endpoint,
1933
+ token,
1934
+ {
1935
+ ...body !== void 0 ? { body } : {},
1936
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.track:manage")
1937
+ }
1938
+ )).data,
1822
1939
  listRuns: async (opts) => asRecord3((await api.listRuns({ token, monitorId, ...opts })).data),
1823
1940
  getRun: async (runId) => asRecord3((await api.getRun({ token, monitorId, runId })).data),
1824
1941
  delete: async () => {
@@ -1833,10 +1950,12 @@ function createTrackNamespace(transport) {
1833
1950
  if (!prompt || typeof prompt !== "string") {
1834
1951
  throw new QoniValidationError("track.create requires a prompt string");
1835
1952
  }
1953
+ const body = snakeifyKeys(rest);
1954
+ if (body.schedule !== void 0) body.schedule = normalizeSchedule(body.schedule);
1836
1955
  const created = await api.createMonitor({
1837
1956
  token,
1838
1957
  intent: prompt,
1839
- ...snakeifyKeys(rest)
1958
+ ...body
1840
1959
  });
1841
1960
  const monitorId = created.data.id || created.data.monitor_id;
1842
1961
  if (!monitorId) {
@@ -1854,7 +1973,7 @@ function createTrackNamespace(transport) {
1854
1973
  };
1855
1974
  }
1856
1975
  function normalizeMonitorRun(item) {
1857
- const normalized = camelizeRecord(item);
1976
+ const normalized = normalizeMonitorRecord(item);
1858
1977
  if (typeof normalized.id !== "string" || !normalized.id) {
1859
1978
  const id = normalized.runId ?? normalized.snapshotId;
1860
1979
  if (typeof id === "string" && id) normalized.id = id;
@@ -1864,11 +1983,44 @@ function normalizeMonitorRun(item) {
1864
1983
  function normalizeTrackInterveneInput(input) {
1865
1984
  return renameKeys4(input, { requestId: "request_id" });
1866
1985
  }
1986
+ var USER_KEYED_FIELDS = /* @__PURE__ */ new Set(["extraction_schema", "baseline_extracted", "extracted"]);
1987
+ var USER_KEYED_DIFF_FIELDS = /* @__PURE__ */ new Set(["before", "after", "added", "removed", "sample_unchanged"]);
1988
+ function normalizeMonitorRecord(value) {
1989
+ const out = {};
1990
+ for (const [key, item] of Object.entries(value)) {
1991
+ if (USER_KEYED_FIELDS.has(key)) out[camelKey2(key)] = item;
1992
+ else if (key === "diffs" && Array.isArray(item)) out.diffs = item.map(normalizeDiff);
1993
+ else out[camelKey2(key)] = camelizeKeys(item);
1994
+ }
1995
+ return out;
1996
+ }
1997
+ function normalizeDiff(entry) {
1998
+ if (!isJsonObject(entry)) return camelizeKeys(entry);
1999
+ const out = {};
2000
+ for (const [key, item] of Object.entries(entry)) {
2001
+ out[camelKey2(key)] = USER_KEYED_DIFF_FIELDS.has(key) ? item : camelizeKeys(item);
2002
+ }
2003
+ return out;
2004
+ }
2005
+ function camelKey2(key) {
2006
+ return key.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase());
2007
+ }
1867
2008
  function normalizeSchedule(schedule) {
2009
+ if (!isJsonObject(schedule)) return schedule;
1868
2010
  if (schedule.kind === "interval") {
1869
- return { kind: "interval", interval_seconds: schedule.intervalSeconds };
2011
+ const { intervalSeconds, interval_seconds, ...rest } = schedule;
2012
+ const seconds = intervalSeconds ?? interval_seconds;
2013
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
2014
+ throw new QoniValidationError(
2015
+ 'track interval schedule needs a positive number: { kind: "interval", intervalSeconds }'
2016
+ );
2017
+ }
2018
+ return { ...rest, interval_seconds: seconds };
1870
2019
  }
1871
- return { kind: "cron", expr: schedule.expr };
2020
+ if (schedule.kind === "cron" && (typeof schedule.expr !== "string" || !schedule.expr.trim())) {
2021
+ throw new QoniValidationError('track cron schedule needs an expression: { kind: "cron", expr }');
2022
+ }
2023
+ return schedule;
1872
2024
  }
1873
2025
  function snakeifyKeys(value) {
1874
2026
  const out = {};
@@ -2055,7 +2207,8 @@ var Qoni = class {
2055
2207
  webAgentSSE: (...args) => this.webAgentSSE(...args)
2056
2208
  };
2057
2209
  this.genauth = createGenAuthNamespace(transport, {
2058
- adminTokenFor: () => this.genauthAdminTokenFor()
2210
+ adminTokenFor: () => this.genauthAdminTokenFor(),
2211
+ delegateTokenFor: (input) => this.delegateToken(input)
2059
2212
  });
2060
2213
  this.qoni = createQoniNamespace(
2061
2214
  transport,
@@ -2768,19 +2921,43 @@ function toRecord(value) {
2768
2921
  }
2769
2922
  function normalizeDelegateTokenInput(input) {
2770
2923
  const body = toRecord(input);
2924
+ if (input.deniedScopes !== void 0) {
2925
+ throw new QoniValidationError(
2926
+ "delegateToken: deniedScopes is not supported by the delegation service. Do not rely on this field to prevent mailbox or browser actions."
2927
+ );
2928
+ }
2929
+ if (input.agent !== void 0 && input.agentKey !== void 0 && input.agent !== input.agentKey) {
2930
+ throw new QoniValidationError("delegateToken: agent and agentKey must identify the same Agent");
2931
+ }
2771
2932
  if (!body.userId && isJsonObject(input.user) && typeof input.user.id === "string") {
2772
2933
  body.userId = input.user.id;
2773
2934
  }
2774
2935
  if (input.mode !== "interactive" && !body.userId) {
2775
2936
  throw new QoniValidationError(
2776
- 'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the deprecated top-level `userId`). Interactive mode (`mode: "interactive"`) does not need one.'
2937
+ 'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the top-level `userId`). Interactive mode (`mode: "interactive"`) does not need one.'
2777
2938
  );
2778
2939
  }
2779
- body.agent = input.agent ?? "sdk";
2940
+ body.agent = input.agent ?? input.agentKey ?? "sdk";
2941
+ delete body.agentKey;
2942
+ if (input.expiresIn !== void 0) body.expiresIn = delegationDurationSeconds(input.expiresIn);
2780
2943
  body.scopes = resolveDelegationScopes(input.scopes, input.products);
2781
2944
  delete body.products;
2782
2945
  return body;
2783
2946
  }
2947
+ function delegationDurationSeconds(value) {
2948
+ let seconds = value;
2949
+ if (typeof value === "string") {
2950
+ const duration = /^(\d+)(s|m|h|d)?$/.exec(value);
2951
+ const units = { s: 1, m: 60, h: 3600, d: 86400 };
2952
+ seconds = duration ? Number(duration[1]) * units[duration[2] ?? "s"] : NaN;
2953
+ }
2954
+ if (typeof seconds !== "number" || !Number.isInteger(seconds) || seconds < 60 || seconds > 86400) {
2955
+ throw new QoniValidationError(
2956
+ 'delegateToken: expiresIn must be 60\u201386400 integer seconds or a whole-number duration such as "15m"'
2957
+ );
2958
+ }
2959
+ return seconds;
2960
+ }
2784
2961
  function resolveDelegationScopes(scopes, products) {
2785
2962
  const resolved = /* @__PURE__ */ new Set();
2786
2963
  for (const product of products ?? []) {
@@ -2804,6 +2981,11 @@ function resolveDelegationScopes(scopes, products) {
2804
2981
  return [...resolved];
2805
2982
  }
2806
2983
  function validateScopeFormat(scope) {
2984
+ if (scope.startsWith("mailbox.") || scope === "web.search" || scope === "web.extract") {
2985
+ throw new QoniValidationError(
2986
+ `delegateToken: legacy action scope "${scope}" has no enforced mapping in this delegation service. It will not be expanded into broader product permissions.`
2987
+ );
2988
+ }
2807
2989
  if (KNOWN_SCOPES.includes(scope)) return;
2808
2990
  const prefixed = KNOWN_SCOPES.find((known) => known.endsWith(`.${scope}`) || known === `webagent.${scope}`);
2809
2991
  if (prefixed) {