@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.cjs CHANGED
@@ -538,6 +538,87 @@ var ActionKinds = {
538
538
  };
539
539
  var ACTION_KIND_VALUES = Object.values(ActionKinds);
540
540
 
541
+ // src/scopes.ts
542
+ var QoniScopes = {
543
+ GUMEM_SESSION_CREATE: "gumem.session:create",
544
+ GUMEM_MESSAGE_WRITE: "gumem.message:write",
545
+ GUMEM_MEMORY_READ: "gumem.memory:read",
546
+ GUMEM_MEMORY_WRITE: "gumem.memory:write",
547
+ GUMEM_MEMORY_DELETE: "gumem.memory:delete",
548
+ GUMEM_SEARCH_RUN: "gumem.search:run",
549
+ GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
550
+ GUMEM_RESOURCE_WRITE: "gumem.resource:write",
551
+ GUMEM_ACTION_WRITE: "gumem.action:write",
552
+ GUMEM_ACTION_READ: "gumem.action:read",
553
+ GUMEM_PROFILE_READ: "gumem.profile:read",
554
+ // WebAgent products: exactly two verbs per product — `read` (observe) and
555
+ // `manage` (anything that changes execution state). See
556
+ // wa/docs/qoni-sdk-public-surface.md §3.
557
+ DO_ANYTHING_READ: "webagent.do_anything:read",
558
+ DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
559
+ WEB_SEARCH_READ: "webagent.web_search:read",
560
+ WEB_SEARCH_MANAGE: "webagent.web_search:manage",
561
+ DEEP_RESEARCH_READ: "webagent.deep_research:read",
562
+ DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
563
+ TRACK_READ: "webagent.track:read",
564
+ TRACK_MANAGE: "webagent.track:manage",
565
+ // Interactive site login (`InteractionHandle.openLogin()` on a `site_login`
566
+ // interaction). Not part of any product bundle: the AccessKey policy must
567
+ // allow it, and the delegation must request it explicitly.
568
+ SITE_LOGIN_REQUEST: "webagent.site_login:request",
569
+ // Persist that login into the browser profile (`OpenLoginResult.confirm()`).
570
+ SITE_LOGIN_CONFIRM: "webagent.site_login:confirm"
571
+ };
572
+ var QoniProductScopes = {
573
+ doAnything: [
574
+ QoniScopes.DO_ANYTHING_READ,
575
+ QoniScopes.DO_ANYTHING_MANAGE
576
+ ],
577
+ webSearch: [
578
+ QoniScopes.WEB_SEARCH_READ,
579
+ QoniScopes.WEB_SEARCH_MANAGE
580
+ ],
581
+ deepResearch: [
582
+ QoniScopes.DEEP_RESEARCH_READ,
583
+ QoniScopes.DEEP_RESEARCH_MANAGE
584
+ ],
585
+ track: [
586
+ QoniScopes.TRACK_READ,
587
+ QoniScopes.TRACK_MANAGE
588
+ ]
589
+ };
590
+ var KNOWN_SCOPES = Object.values(QoniScopes);
591
+ var QoniScopeBundles = {
592
+ GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
593
+ GUMEM_SESSION_RECALL: [
594
+ QoniScopes.GUMEM_MEMORY_READ,
595
+ QoniScopes.GUMEM_MEMORY_WRITE,
596
+ QoniScopes.GUMEM_MESSAGE_WRITE
597
+ ],
598
+ GUMEM_WRITE: [
599
+ QoniScopes.GUMEM_SESSION_CREATE,
600
+ QoniScopes.GUMEM_MESSAGE_WRITE,
601
+ QoniScopes.GUMEM_ACTION_WRITE
602
+ ],
603
+ // One bundle per WebAgent product = [read, manage].
604
+ DO_ANYTHING: [
605
+ QoniScopes.DO_ANYTHING_READ,
606
+ QoniScopes.DO_ANYTHING_MANAGE
607
+ ],
608
+ WEB_SEARCH: [
609
+ QoniScopes.WEB_SEARCH_READ,
610
+ QoniScopes.WEB_SEARCH_MANAGE
611
+ ],
612
+ DEEP_RESEARCH: [
613
+ QoniScopes.DEEP_RESEARCH_READ,
614
+ QoniScopes.DEEP_RESEARCH_MANAGE
615
+ ],
616
+ TRACK: [
617
+ QoniScopes.TRACK_READ,
618
+ QoniScopes.TRACK_MANAGE
619
+ ]
620
+ };
621
+
541
622
  // src/interactions.ts
542
623
  function asString2(value) {
543
624
  return typeof value === "string" && value ? value : void 0;
@@ -636,6 +717,9 @@ function normalizeInteraction(payload) {
636
717
  if (evidence) interaction.evidence = evidence;
637
718
  return interaction;
638
719
  }
720
+ function interactionActionScopes(action, productManageScope) {
721
+ return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
722
+ }
639
723
  var InteractionHandle = class {
640
724
  constructor(interaction, post) {
641
725
  this.post = post;
@@ -660,48 +744,122 @@ var InteractionHandle = class {
660
744
  return this.interaction.actions.some((a) => a.kind === kind);
661
745
  }
662
746
  /** Answer a clarification ask. */
663
- answer(text) {
664
- return this.invoke(ActionKinds.answer, { response: text });
747
+ async answer(text) {
748
+ await this.invoke(ActionKinds.answer, { response: text });
665
749
  }
666
750
  /** Skip / dismiss the interaction (proceed without acting on it). */
667
- skip() {
668
- return this.invoke(ActionKinds.skip);
751
+ async skip() {
752
+ await this.invoke(ActionKinds.skip);
669
753
  }
670
754
  /** Confirm a pending confirmation (approve the plan / destructive op). */
671
- confirm() {
672
- return this.invoke(ActionKinds.confirm);
755
+ async confirm() {
756
+ await this.invoke(ActionKinds.confirm);
673
757
  }
674
758
  /** Reject a pending confirmation. */
675
- reject() {
676
- return this.invoke(ActionKinds.reject);
759
+ async reject() {
760
+ await this.invoke(ActionKinds.reject);
677
761
  }
678
- /** Open the login surface for a site_login interaction. */
679
- openLogin() {
680
- return this.invoke(ActionKinds.openLogin);
762
+ /**
763
+ * Start an interactive login session for a `site_login` interaction. The
764
+ * backend needs the site to sign in to: pass `siteId`, or omit it when the
765
+ * interaction lists exactly one site. Show `liveUrl` to the user; when they
766
+ * finish, call `confirm()` on the result to save the login, and act on its
767
+ * outcome (see {@link OpenLoginResult.confirm}) before {@link confirmSignedIn}.
768
+ * The delegation must include `QoniScopes.SITE_LOGIN_REQUEST` and
769
+ * `QoniScopes.SITE_LOGIN_CONFIRM`; the AccessKey policy must allow both.
770
+ */
771
+ async openLogin(options = {}) {
772
+ this.#declaredAction(ActionKinds.openLogin);
773
+ const body = { site_id: this.#loginSiteId(options.siteId) };
774
+ if (options.profileId !== void 0) {
775
+ if (typeof options.profileId !== "string" || !options.profileId) {
776
+ throw new QoniValidationError("openLogin profileId must be a non-empty string");
777
+ }
778
+ body.profile_id = options.profileId;
779
+ }
780
+ const response = await this.invoke(ActionKinds.openLogin, body);
781
+ const raw = isJsonObject(response) ? response : {};
782
+ const fields = camelizeRecord(raw);
783
+ const profileId = asString2(fields.profileId) ?? "";
784
+ const siteId = asString2(fields.siteId) ?? String(body.site_id);
785
+ return {
786
+ profileId,
787
+ siteId,
788
+ sessionId: asString2(fields.sessionId) ?? "",
789
+ liveUrl: asString2(fields.liveUrl) ?? "",
790
+ raw,
791
+ confirm: async (confirmOptions = {}) => {
792
+ if (!profileId) {
793
+ throw new QoniValidationError("openLogin response did not include a profileId to confirm");
794
+ }
795
+ const confirmBody = { site_id: siteId };
796
+ if (confirmOptions.displayLabel !== void 0) confirmBody.display_label = confirmOptions.displayLabel;
797
+ const response2 = await this.post(
798
+ {
799
+ kind: ActionKinds.openLogin,
800
+ label: "Confirm login",
801
+ method: "POST",
802
+ endpoint: `/profiles/${encodeURIComponent(profileId)}/confirm`
803
+ },
804
+ confirmBody,
805
+ { requiredScopes: [QoniScopes.SITE_LOGIN_CONFIRM] }
806
+ );
807
+ const raw2 = isJsonObject(response2) ? response2 : {};
808
+ const fields2 = camelizeRecord(raw2);
809
+ const state = asString2(fields2.state) ?? "";
810
+ return {
811
+ profileId: asString2(fields2.profileId) ?? profileId,
812
+ active: state === "active",
813
+ state,
814
+ probeResult: asString2(fields2.probeResult) ?? "",
815
+ raw: raw2
816
+ };
817
+ }
818
+ };
681
819
  }
682
820
  /** Tell the agent the user has finished signing in. */
683
- confirmSignedIn() {
684
- return this.invoke(ActionKinds.confirmSignedIn);
821
+ async confirmSignedIn() {
822
+ await this.invoke(ActionKinds.confirmSignedIn);
685
823
  }
686
824
  /** Connect to the live browser for a take_control interaction. */
687
- connectControl() {
688
- return this.invoke(ActionKinds.connectControl);
825
+ async connectControl() {
826
+ await this.invoke(ActionKinds.connectControl);
689
827
  }
690
828
  /** Refresh the live control surface / live url. */
691
- refreshControl() {
692
- return this.invoke(ActionKinds.refreshControl);
829
+ async refreshControl() {
830
+ await this.invoke(ActionKinds.refreshControl);
693
831
  }
694
832
  /** Hand control back to the agent. */
695
- releaseControl() {
696
- return this.invoke(ActionKinds.releaseControl);
833
+ async releaseControl() {
834
+ await this.invoke(ActionKinds.releaseControl);
697
835
  }
698
836
  /** Retry a wait interaction now. */
699
- retry() {
700
- return this.invoke(ActionKinds.retry);
837
+ async retry() {
838
+ await this.invoke(ActionKinds.retry);
701
839
  }
702
840
  /** Switch the profile a wait/login interaction is bound to. */
703
- switchProfile() {
704
- return this.invoke(ActionKinds.switchProfile);
841
+ async switchProfile() {
842
+ await this.invoke(ActionKinds.switchProfile);
843
+ }
844
+ /** Resolve the site for `openLogin`: explicit, or the only listed site. */
845
+ #loginSiteId(requested) {
846
+ const payload = this.interaction.payload;
847
+ const listed = Array.isArray(payload?.sites) ? payload.sites.map((site) => site.siteId).filter((id) => typeof id === "string" && id.length > 0) : [];
848
+ if (requested !== void 0) {
849
+ if (typeof requested !== "string" || !requested) {
850
+ throw new QoniValidationError("openLogin siteId must be a non-empty string");
851
+ }
852
+ if (listed.length > 0 && !listed.includes(requested)) {
853
+ throw new QoniValidationError(
854
+ `openLogin siteId "${requested}" is not one of the sites this interaction asks for: ${listed.join(", ")}`
855
+ );
856
+ }
857
+ return requested;
858
+ }
859
+ if (listed.length === 1) return listed[0];
860
+ throw new QoniValidationError(
861
+ 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(", ")}`
862
+ );
705
863
  }
706
864
  /**
707
865
  * Invoke an action by kind with an optional body. `async` so a missing-action
@@ -709,6 +867,10 @@ var InteractionHandle = class {
709
867
  * method contract uniformly promise-based.
710
868
  */
711
869
  async invoke(kind, body) {
870
+ return this.post(this.#declaredAction(kind), body);
871
+ }
872
+ /** The declared action of `kind`; throws when the backend did not offer it. */
873
+ #declaredAction(kind) {
712
874
  const action = this.interaction.actions.find((a) => a.kind === kind);
713
875
  if (!action) {
714
876
  const offered = this.interaction.actions.map((a) => a.kind).join(", ") || "none";
@@ -716,7 +878,7 @@ var InteractionHandle = class {
716
878
  `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.`
717
879
  );
718
880
  }
719
- await this.post(action, body);
881
+ return action;
720
882
  }
721
883
  };
722
884
 
@@ -786,7 +948,7 @@ var RunHandle = class {
786
948
  break settled;
787
949
  }
788
950
  }
789
- const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
951
+ const detail2 = await settleRead(() => this.ops.getDetail(signal), signal);
790
952
  if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
791
953
  terminalDetail = detail2;
792
954
  break;
@@ -800,10 +962,10 @@ var RunHandle = class {
800
962
  sawCleanClose = true;
801
963
  eventIdAtLastClose = lastEventId;
802
964
  }
803
- const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
965
+ const detail = terminalDetail ?? await settleRead(() => this.ops.getDetail(signal), signal);
804
966
  this.#adoptSessionRef(detail);
805
967
  const result = settleRunResult(this.id, detail, [], terminalDoneReason);
806
- result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
968
+ result.artifacts = this.ops.listArtifacts ? await settleRead(() => this.ops.listArtifacts(signal), signal) : [];
807
969
  return result;
808
970
  } catch (err) {
809
971
  if (timedOut() && !opts.signal?.aborted) {
@@ -826,7 +988,7 @@ var RunHandle = class {
826
988
  interactionHandle(interaction) {
827
989
  return new InteractionHandle(
828
990
  interaction,
829
- (action, body) => this.ops.postAction(action, body)
991
+ (action, body, options) => this.ops.postAction(action, body, options)
830
992
  );
831
993
  }
832
994
  /**
@@ -920,6 +1082,35 @@ function withTimeout(signal, timeoutMs) {
920
1082
  }
921
1083
  return { signal: controller.signal, timedOut: () => fired, dispose };
922
1084
  }
1085
+ var SETTLE_READ_BACKOFF_MS = [250, 1e3];
1086
+ async function settleRead(read, signal) {
1087
+ for (let attempt = 0; ; attempt++) {
1088
+ try {
1089
+ return await waitWithSignal(read, signal);
1090
+ } catch (err) {
1091
+ const delay = SETTLE_READ_BACKOFF_MS[attempt];
1092
+ if (delay === void 0 || signal?.aborted || !(err instanceof QoniError) || !err.retryable) throw err;
1093
+ await sleepWithSignal(delay, signal);
1094
+ }
1095
+ }
1096
+ }
1097
+ function sleepWithSignal(ms, signal) {
1098
+ return new Promise((resolve, reject) => {
1099
+ if (signal?.aborted) {
1100
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1101
+ return;
1102
+ }
1103
+ const onAbort = () => {
1104
+ clearTimeout(timer);
1105
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1106
+ };
1107
+ const timer = setTimeout(() => {
1108
+ signal?.removeEventListener("abort", onAbort);
1109
+ resolve();
1110
+ }, ms);
1111
+ signal?.addEventListener("abort", onAbort, { once: true });
1112
+ });
1113
+ }
923
1114
  function waitWithSignal(operation, signal) {
924
1115
  if (!signal) return Promise.resolve().then(operation);
925
1116
  return new Promise((resolve, reject) => {
@@ -1017,17 +1208,15 @@ function createDeepResearchNamespace(transport) {
1017
1208
  signal: opts.signal,
1018
1209
  onReconnect: opts.onReconnect
1019
1210
  }),
1020
- postAction: async (action, body) => {
1021
- await transport.webAgentJson(
1022
- action.method || "POST",
1023
- action.endpoint,
1024
- token,
1025
- {
1026
- ...body !== void 0 ? { body } : {},
1027
- requiredScopes: ["webagent.deep_research:manage"]
1028
- }
1029
- );
1030
- },
1211
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1212
+ action.method || "POST",
1213
+ action.endpoint,
1214
+ token,
1215
+ {
1216
+ ...body !== void 0 ? { body } : {},
1217
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.deep_research:manage")
1218
+ }
1219
+ )).data,
1031
1220
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
1032
1221
  listArtifacts: async (signal) => {
1033
1222
  const res = await api.listArtifacts({ token, runId, signal });
@@ -1292,17 +1481,15 @@ function createDoAnythingNamespace(transport) {
1292
1481
  // Skip screenshot rows in backfill when the caller did not opt in.
1293
1482
  includeScreenshots: capture?.screenshots ? void 0 : false
1294
1483
  }),
1295
- postAction: async (action, body) => {
1296
- await transport.webAgentJson(
1297
- action.method || "POST",
1298
- action.endpoint,
1299
- token,
1300
- {
1301
- ...body !== void 0 ? { body } : {},
1302
- requiredScopes: ["webagent.do_anything:manage"]
1303
- }
1304
- );
1305
- },
1484
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1485
+ action.method || "POST",
1486
+ action.endpoint,
1487
+ token,
1488
+ {
1489
+ ...body !== void 0 ? { body } : {},
1490
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.do_anything:manage")
1491
+ }
1492
+ )).data,
1306
1493
  cancel: async (reason) => asRecord2(
1307
1494
  (await api.cancel({
1308
1495
  token,
@@ -1423,84 +1610,16 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1423
1610
  };
1424
1611
  }
1425
1612
 
1426
- // src/scopes.ts
1427
- var QoniScopes = {
1428
- GUMEM_SESSION_CREATE: "gumem.session:create",
1429
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
1430
- GUMEM_MEMORY_READ: "gumem.memory:read",
1431
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
1432
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1433
- GUMEM_SEARCH_RUN: "gumem.search:run",
1434
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1435
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1436
- GUMEM_ACTION_WRITE: "gumem.action:write",
1437
- GUMEM_ACTION_READ: "gumem.action:read",
1438
- GUMEM_PROFILE_READ: "gumem.profile:read",
1439
- // WebAgent products: exactly two verbs per product — `read` (observe) and
1440
- // `manage` (anything that changes execution state). See
1441
- // wa/docs/qoni-sdk-public-surface.md §3.
1442
- DO_ANYTHING_READ: "webagent.do_anything:read",
1443
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1444
- WEB_SEARCH_READ: "webagent.web_search:read",
1445
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1446
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
1447
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1448
- TRACK_READ: "webagent.track:read",
1449
- TRACK_MANAGE: "webagent.track:manage"
1450
- };
1451
- var QoniProductScopes = {
1452
- doAnything: [
1453
- QoniScopes.DO_ANYTHING_READ,
1454
- QoniScopes.DO_ANYTHING_MANAGE
1455
- ],
1456
- webSearch: [
1457
- QoniScopes.WEB_SEARCH_READ,
1458
- QoniScopes.WEB_SEARCH_MANAGE
1459
- ],
1460
- deepResearch: [
1461
- QoniScopes.DEEP_RESEARCH_READ,
1462
- QoniScopes.DEEP_RESEARCH_MANAGE
1463
- ],
1464
- track: [
1465
- QoniScopes.TRACK_READ,
1466
- QoniScopes.TRACK_MANAGE
1467
- ]
1468
- };
1469
- var KNOWN_SCOPES = Object.values(QoniScopes);
1470
- var QoniScopeBundles = {
1471
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1472
- GUMEM_SESSION_RECALL: [
1473
- QoniScopes.GUMEM_MEMORY_READ,
1474
- QoniScopes.GUMEM_MEMORY_WRITE,
1475
- QoniScopes.GUMEM_MESSAGE_WRITE
1476
- ],
1477
- GUMEM_WRITE: [
1478
- QoniScopes.GUMEM_SESSION_CREATE,
1479
- QoniScopes.GUMEM_MESSAGE_WRITE,
1480
- QoniScopes.GUMEM_ACTION_WRITE
1481
- ],
1482
- // One bundle per WebAgent product = [read, manage].
1483
- DO_ANYTHING: [
1484
- QoniScopes.DO_ANYTHING_READ,
1485
- QoniScopes.DO_ANYTHING_MANAGE
1486
- ],
1487
- WEB_SEARCH: [
1488
- QoniScopes.WEB_SEARCH_READ,
1489
- QoniScopes.WEB_SEARCH_MANAGE
1490
- ],
1491
- DEEP_RESEARCH: [
1492
- QoniScopes.DEEP_RESEARCH_READ,
1493
- QoniScopes.DEEP_RESEARCH_MANAGE
1494
- ],
1495
- TRACK: [
1496
- QoniScopes.TRACK_READ,
1497
- QoniScopes.TRACK_MANAGE
1498
- ]
1499
- };
1500
-
1501
1613
  // src/genauth.ts
1502
1614
  function createGenAuthNamespace(transport, options = {}) {
1615
+ async function delegateAgent(input) {
1616
+ if (!options.delegateTokenFor) {
1617
+ throw new QoniValidationError("GenAuth delegateAgent requires a Qoni delegation provider");
1618
+ }
1619
+ return (await options.delegateTokenFor(input)).data;
1620
+ }
1503
1621
  return {
1622
+ delegateAgent,
1504
1623
  userInfo: (input) => transport.genauthJson("GET", "/oidc/me", input.accessToken),
1505
1624
  jwks: () => transport.genauthJson("GET", "/oidc/.well-known/jwks.json"),
1506
1625
  discovery: () => transport.genauthJson("GET", "/oidc/.well-known/openid-configuration"),
@@ -1696,7 +1815,7 @@ var MonitorHandle = class {
1696
1815
  id;
1697
1816
  /** Refresh and return the monitor (normalized camelCase fields). */
1698
1817
  async get() {
1699
- return camelizeRecord(await this.ops.get());
1818
+ return normalizeMonitorRecord(await this.ops.get());
1700
1819
  }
1701
1820
  /**
1702
1821
  * Pause the monitor — stops scheduled ticks until {@link resume}. The
@@ -1704,11 +1823,11 @@ var MonitorHandle = class {
1704
1823
  * deleting.
1705
1824
  */
1706
1825
  async pause() {
1707
- return camelizeRecord(await this.ops.update({ action: "pause" }));
1826
+ return normalizeMonitorRecord(await this.ops.update({ action: "pause" }));
1708
1827
  }
1709
1828
  /** Resume a paused monitor. */
1710
1829
  async resume() {
1711
- return camelizeRecord(await this.ops.update({ action: "resume" }));
1830
+ return normalizeMonitorRecord(await this.ops.update({ action: "resume" }));
1712
1831
  }
1713
1832
  /**
1714
1833
  * Change the monitor definition. Pass any subset of fields (at least one);
@@ -1726,11 +1845,11 @@ var MonitorHandle = class {
1726
1845
  "track refine requires at least one field to change (schedule / targetUrls / extractionSchema / triggerDsl / stopConditionDsl / notifyChannel)"
1727
1846
  );
1728
1847
  }
1729
- return camelizeRecord(await this.ops.update({ action: "refine", patch: body }));
1848
+ return normalizeMonitorRecord(await this.ops.update({ action: "refine", patch: body }));
1730
1849
  }
1731
1850
  /** Trigger an immediate tick outside the schedule. */
1732
1851
  async runNow() {
1733
- return camelizeRecord(await this.ops.runNow());
1852
+ return normalizeMonitorRecord(await this.ops.runNow());
1734
1853
  }
1735
1854
  /**
1736
1855
  * Stream semantic monitor events. Monitors are resident, so the stream has
@@ -1751,7 +1870,7 @@ var MonitorHandle = class {
1751
1870
  interactionHandle(interaction) {
1752
1871
  return new InteractionHandle(
1753
1872
  interaction,
1754
- (action, body) => this.ops.postAction(action, body)
1873
+ (action, body, options) => this.ops.postAction(action, body, options)
1755
1874
  );
1756
1875
  }
1757
1876
  /**
@@ -1856,17 +1975,15 @@ function createTrackNamespace(transport) {
1856
1975
  update: async (input) => asRecord3((await api.updateMonitor({ token, monitorId, ...input })).data),
1857
1976
  runNow: async () => asRecord3((await api.runNow({ token, monitorId })).data),
1858
1977
  streamEvents: (opts) => api.events({ token, monitorId, lastEventId: opts.lastEventId, signal: opts.signal }),
1859
- postAction: async (action, body) => {
1860
- await transport.webAgentJson(
1861
- action.method || "POST",
1862
- action.endpoint,
1863
- token,
1864
- {
1865
- ...body !== void 0 ? { body } : {},
1866
- requiredScopes: ["webagent.track:manage"]
1867
- }
1868
- );
1869
- },
1978
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1979
+ action.method || "POST",
1980
+ action.endpoint,
1981
+ token,
1982
+ {
1983
+ ...body !== void 0 ? { body } : {},
1984
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.track:manage")
1985
+ }
1986
+ )).data,
1870
1987
  listRuns: async (opts) => asRecord3((await api.listRuns({ token, monitorId, ...opts })).data),
1871
1988
  getRun: async (runId) => asRecord3((await api.getRun({ token, monitorId, runId })).data),
1872
1989
  delete: async () => {
@@ -1881,10 +1998,12 @@ function createTrackNamespace(transport) {
1881
1998
  if (!prompt || typeof prompt !== "string") {
1882
1999
  throw new QoniValidationError("track.create requires a prompt string");
1883
2000
  }
2001
+ const body = snakeifyKeys(rest);
2002
+ if (body.schedule !== void 0) body.schedule = normalizeSchedule(body.schedule);
1884
2003
  const created = await api.createMonitor({
1885
2004
  token,
1886
2005
  intent: prompt,
1887
- ...snakeifyKeys(rest)
2006
+ ...body
1888
2007
  });
1889
2008
  const monitorId = created.data.id || created.data.monitor_id;
1890
2009
  if (!monitorId) {
@@ -1902,7 +2021,7 @@ function createTrackNamespace(transport) {
1902
2021
  };
1903
2022
  }
1904
2023
  function normalizeMonitorRun(item) {
1905
- const normalized = camelizeRecord(item);
2024
+ const normalized = normalizeMonitorRecord(item);
1906
2025
  if (typeof normalized.id !== "string" || !normalized.id) {
1907
2026
  const id = normalized.runId ?? normalized.snapshotId;
1908
2027
  if (typeof id === "string" && id) normalized.id = id;
@@ -1912,11 +2031,44 @@ function normalizeMonitorRun(item) {
1912
2031
  function normalizeTrackInterveneInput(input) {
1913
2032
  return renameKeys4(input, { requestId: "request_id" });
1914
2033
  }
2034
+ var USER_KEYED_FIELDS = /* @__PURE__ */ new Set(["extraction_schema", "baseline_extracted", "extracted"]);
2035
+ var USER_KEYED_DIFF_FIELDS = /* @__PURE__ */ new Set(["before", "after", "added", "removed", "sample_unchanged"]);
2036
+ function normalizeMonitorRecord(value) {
2037
+ const out = {};
2038
+ for (const [key, item] of Object.entries(value)) {
2039
+ if (USER_KEYED_FIELDS.has(key)) out[camelKey2(key)] = item;
2040
+ else if (key === "diffs" && Array.isArray(item)) out.diffs = item.map(normalizeDiff);
2041
+ else out[camelKey2(key)] = camelizeKeys(item);
2042
+ }
2043
+ return out;
2044
+ }
2045
+ function normalizeDiff(entry) {
2046
+ if (!isJsonObject(entry)) return camelizeKeys(entry);
2047
+ const out = {};
2048
+ for (const [key, item] of Object.entries(entry)) {
2049
+ out[camelKey2(key)] = USER_KEYED_DIFF_FIELDS.has(key) ? item : camelizeKeys(item);
2050
+ }
2051
+ return out;
2052
+ }
2053
+ function camelKey2(key) {
2054
+ return key.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase());
2055
+ }
1915
2056
  function normalizeSchedule(schedule) {
2057
+ if (!isJsonObject(schedule)) return schedule;
1916
2058
  if (schedule.kind === "interval") {
1917
- return { kind: "interval", interval_seconds: schedule.intervalSeconds };
2059
+ const { intervalSeconds, interval_seconds, ...rest } = schedule;
2060
+ const seconds = intervalSeconds ?? interval_seconds;
2061
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
2062
+ throw new QoniValidationError(
2063
+ 'track interval schedule needs a positive number: { kind: "interval", intervalSeconds }'
2064
+ );
2065
+ }
2066
+ return { ...rest, interval_seconds: seconds };
1918
2067
  }
1919
- return { kind: "cron", expr: schedule.expr };
2068
+ if (schedule.kind === "cron" && (typeof schedule.expr !== "string" || !schedule.expr.trim())) {
2069
+ throw new QoniValidationError('track cron schedule needs an expression: { kind: "cron", expr }');
2070
+ }
2071
+ return schedule;
1920
2072
  }
1921
2073
  function snakeifyKeys(value) {
1922
2074
  const out = {};
@@ -2103,7 +2255,8 @@ var Qoni = class {
2103
2255
  webAgentSSE: (...args) => this.webAgentSSE(...args)
2104
2256
  };
2105
2257
  this.genauth = createGenAuthNamespace(transport, {
2106
- adminTokenFor: () => this.genauthAdminTokenFor()
2258
+ adminTokenFor: () => this.genauthAdminTokenFor(),
2259
+ delegateTokenFor: (input) => this.delegateToken(input)
2107
2260
  });
2108
2261
  this.qoni = createQoniNamespace(
2109
2262
  transport,
@@ -2816,19 +2969,43 @@ function toRecord(value) {
2816
2969
  }
2817
2970
  function normalizeDelegateTokenInput(input) {
2818
2971
  const body = toRecord(input);
2972
+ if (input.deniedScopes !== void 0) {
2973
+ throw new QoniValidationError(
2974
+ "delegateToken: deniedScopes is not supported by the delegation service. Do not rely on this field to prevent mailbox or browser actions."
2975
+ );
2976
+ }
2977
+ if (input.agent !== void 0 && input.agentKey !== void 0 && input.agent !== input.agentKey) {
2978
+ throw new QoniValidationError("delegateToken: agent and agentKey must identify the same Agent");
2979
+ }
2819
2980
  if (!body.userId && isJsonObject(input.user) && typeof input.user.id === "string") {
2820
2981
  body.userId = input.user.id;
2821
2982
  }
2822
2983
  if (input.mode !== "interactive" && !body.userId) {
2823
2984
  throw new QoniValidationError(
2824
- '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.'
2985
+ 'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the top-level `userId`). Interactive mode (`mode: "interactive"`) does not need one.'
2825
2986
  );
2826
2987
  }
2827
- body.agent = input.agent ?? "sdk";
2988
+ body.agent = input.agent ?? input.agentKey ?? "sdk";
2989
+ delete body.agentKey;
2990
+ if (input.expiresIn !== void 0) body.expiresIn = delegationDurationSeconds(input.expiresIn);
2828
2991
  body.scopes = resolveDelegationScopes(input.scopes, input.products);
2829
2992
  delete body.products;
2830
2993
  return body;
2831
2994
  }
2995
+ function delegationDurationSeconds(value) {
2996
+ let seconds = value;
2997
+ if (typeof value === "string") {
2998
+ const duration = /^(\d+)(s|m|h|d)?$/.exec(value);
2999
+ const units = { s: 1, m: 60, h: 3600, d: 86400 };
3000
+ seconds = duration ? Number(duration[1]) * units[duration[2] ?? "s"] : NaN;
3001
+ }
3002
+ if (typeof seconds !== "number" || !Number.isInteger(seconds) || seconds < 60 || seconds > 86400) {
3003
+ throw new QoniValidationError(
3004
+ 'delegateToken: expiresIn must be 60\u201386400 integer seconds or a whole-number duration such as "15m"'
3005
+ );
3006
+ }
3007
+ return seconds;
3008
+ }
2832
3009
  function resolveDelegationScopes(scopes, products) {
2833
3010
  const resolved = /* @__PURE__ */ new Set();
2834
3011
  for (const product of products ?? []) {
@@ -2852,6 +3029,11 @@ function resolveDelegationScopes(scopes, products) {
2852
3029
  return [...resolved];
2853
3030
  }
2854
3031
  function validateScopeFormat(scope) {
3032
+ if (scope.startsWith("mailbox.") || scope === "web.search" || scope === "web.extract") {
3033
+ throw new QoniValidationError(
3034
+ `delegateToken: legacy action scope "${scope}" has no enforced mapping in this delegation service. It will not be expanded into broader product permissions.`
3035
+ );
3036
+ }
2855
3037
  if (KNOWN_SCOPES.includes(scope)) return;
2856
3038
  const prefixed = KNOWN_SCOPES.find((known) => known.endsWith(`.${scope}`) || known === `webagent.${scope}`);
2857
3039
  if (prefixed) {