@qoniai/qoni 0.7.0 → 0.9.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/CHANGELOG.md +126 -3
- package/README.md +146 -71
- package/README.zh-CN.md +98 -44
- package/dist/index.cjs +395 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -44
- package/dist/index.d.ts +176 -44
- package/dist/index.js +395 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/qoni-sdk/SKILL.md +42 -20
package/dist/index.js
CHANGED
|
@@ -518,8 +518,22 @@ var QoniScopes = {
|
|
|
518
518
|
// interaction). Not part of any product bundle: the AccessKey policy must
|
|
519
519
|
// allow it, and the delegation must request it explicitly.
|
|
520
520
|
SITE_LOGIN_REQUEST: "webagent.site_login:request",
|
|
521
|
-
// Persist that login into the browser profile (`
|
|
522
|
-
|
|
521
|
+
// Persist that login into the browser profile (`InteractionHandle.confirmSignedIn()`
|
|
522
|
+
// after `openLogin()`).
|
|
523
|
+
SITE_LOGIN_CONFIRM: "webagent.site_login:confirm",
|
|
524
|
+
// The delegating user's own data and the Agent's own mailbox (Personal
|
|
525
|
+
// Agent guide). Not part of any product bundle: request them explicitly;
|
|
526
|
+
// the AccessKey policy must allow them.
|
|
527
|
+
// Read the user's Profile (name, phone, shipping address, card brand + last 4).
|
|
528
|
+
USER_PROFILE_READ: "user.profile:read",
|
|
529
|
+
// Write answers back into the user's Profile.
|
|
530
|
+
USER_PROFILE_WRITE: "user.profile:write",
|
|
531
|
+
// Use the user's saved payment card once the user approves a payment.
|
|
532
|
+
USER_PAYMENT_USE: "user.payment:use",
|
|
533
|
+
// Read mail received by the Agent's own mailbox (for example verification codes).
|
|
534
|
+
AGENT_MAIL_READ: "agent.mail:read",
|
|
535
|
+
// Send mail from the Agent's own mailbox.
|
|
536
|
+
AGENT_MAIL_SEND: "agent.mail:send"
|
|
523
537
|
};
|
|
524
538
|
var QoniProductScopes = {
|
|
525
539
|
doAnything: [
|
|
@@ -572,6 +586,15 @@ var QoniScopeBundles = {
|
|
|
572
586
|
};
|
|
573
587
|
|
|
574
588
|
// src/interactions.ts
|
|
589
|
+
var InteractionTypes2 = {
|
|
590
|
+
SiteLogin: "site_login",
|
|
591
|
+
TakeControl: "take_control",
|
|
592
|
+
AskUser: "ask_user",
|
|
593
|
+
Confirmation: "confirmation",
|
|
594
|
+
Wait: "wait",
|
|
595
|
+
/** @deprecated `clarification` is presented as `ask_user`; use `InteractionTypes.AskUser`. */
|
|
596
|
+
Clarification: "ask_user"
|
|
597
|
+
};
|
|
575
598
|
function asString2(value) {
|
|
576
599
|
return typeof value === "string" && value ? value : void 0;
|
|
577
600
|
}
|
|
@@ -600,6 +623,49 @@ function normalizeEvidence(c) {
|
|
|
600
623
|
const artifactId = asString2(c.evidenceArtifactId) ?? (isJsonObject(c.evidence) ? asString2(c.evidence.artifactId) : void 0);
|
|
601
624
|
return artifactId ? { artifactId } : void 0;
|
|
602
625
|
}
|
|
626
|
+
var ANSWER_TYPES = /* @__PURE__ */ new Set([
|
|
627
|
+
"text",
|
|
628
|
+
"number",
|
|
629
|
+
"boolean",
|
|
630
|
+
"single_choice",
|
|
631
|
+
"multiple_choice"
|
|
632
|
+
]);
|
|
633
|
+
function normalizeOptions(value) {
|
|
634
|
+
if (!Array.isArray(value)) return [];
|
|
635
|
+
return value.flatMap((raw) => {
|
|
636
|
+
if (typeof raw === "string") return [{ value: raw, label: raw }];
|
|
637
|
+
if (!isJsonObject(raw) || typeof raw.value !== "string") return [];
|
|
638
|
+
return [{ value: raw.value, label: typeof raw.label === "string" ? raw.label : raw.value }];
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function isAnswerType(value) {
|
|
642
|
+
return value !== void 0 && ANSWER_TYPES.has(value);
|
|
643
|
+
}
|
|
644
|
+
function askUserPayload(c) {
|
|
645
|
+
const options = normalizeOptions(c.options);
|
|
646
|
+
const native = asString2(c.answerType);
|
|
647
|
+
const hint = asString2(c.inputType);
|
|
648
|
+
let answerType;
|
|
649
|
+
if (isAnswerType(native)) answerType = native;
|
|
650
|
+
else if (isAnswerType(hint) && hint !== "single_choice" && hint !== "multiple_choice") answerType = hint;
|
|
651
|
+
else answerType = options.length > 0 ? hint === "multiple_choice" ? "multiple_choice" : "single_choice" : "text";
|
|
652
|
+
const payload = { question: asString2(c.question) ?? "", answerType };
|
|
653
|
+
if (options.length > 0) payload.options = options;
|
|
654
|
+
return payload;
|
|
655
|
+
}
|
|
656
|
+
var BASE_FIELDS = /* @__PURE__ */ new Set([
|
|
657
|
+
"id",
|
|
658
|
+
"type",
|
|
659
|
+
"status",
|
|
660
|
+
"createdAt",
|
|
661
|
+
"resolvedAt",
|
|
662
|
+
"expiresAt",
|
|
663
|
+
"title",
|
|
664
|
+
"prompt",
|
|
665
|
+
"evidence",
|
|
666
|
+
"evidenceArtifactId",
|
|
667
|
+
"actions"
|
|
668
|
+
]);
|
|
603
669
|
function shapePayload(type, c) {
|
|
604
670
|
switch (type) {
|
|
605
671
|
case "site_login": {
|
|
@@ -616,14 +682,10 @@ function shapePayload(type, c) {
|
|
|
616
682
|
const payload = { sites };
|
|
617
683
|
const monitorId = asString2(c.monitorId);
|
|
618
684
|
if (monitorId) payload.monitorId = monitorId;
|
|
619
|
-
return payload;
|
|
620
|
-
}
|
|
621
|
-
case "clarification": {
|
|
622
|
-
const payload = { question: asString2(c.question) ?? "" };
|
|
623
|
-
const inputType = asString2(c.inputType);
|
|
624
|
-
if (inputType) payload.inputType = inputType;
|
|
625
|
-
return payload;
|
|
685
|
+
return { ...payload };
|
|
626
686
|
}
|
|
687
|
+
case "ask_user":
|
|
688
|
+
return { ...askUserPayload(c) };
|
|
627
689
|
case "confirmation":
|
|
628
690
|
return { summary: asString2(c.summary) ?? "" };
|
|
629
691
|
case "take_control": {
|
|
@@ -632,7 +694,7 @@ function shapePayload(type, c) {
|
|
|
632
694
|
const reason = asString2(c.reason);
|
|
633
695
|
if (surface) payload.surface = surface;
|
|
634
696
|
if (reason) payload.reason = reason;
|
|
635
|
-
return payload;
|
|
697
|
+
return { ...payload };
|
|
636
698
|
}
|
|
637
699
|
case "wait": {
|
|
638
700
|
const payload = {
|
|
@@ -642,14 +704,20 @@ function shapePayload(type, c) {
|
|
|
642
704
|
const retryable = asBool(c.retryable);
|
|
643
705
|
if (until) payload.until = until;
|
|
644
706
|
if (retryable !== void 0) payload.retryable = retryable;
|
|
707
|
+
return { ...payload };
|
|
708
|
+
}
|
|
709
|
+
default: {
|
|
710
|
+
const payload = {};
|
|
711
|
+
for (const [key, value] of Object.entries(c)) if (!BASE_FIELDS.has(key)) payload[key] = value;
|
|
645
712
|
return payload;
|
|
646
713
|
}
|
|
647
714
|
}
|
|
648
715
|
}
|
|
649
716
|
function normalizeInteraction(payload) {
|
|
650
717
|
const c = camelizeRecord(payload);
|
|
651
|
-
const
|
|
652
|
-
if (!
|
|
718
|
+
const wireType = asString2(c.type);
|
|
719
|
+
if (!wireType) return null;
|
|
720
|
+
const type = wireType === "clarification" ? "ask_user" : wireType;
|
|
653
721
|
const interaction = {
|
|
654
722
|
id: asString2(c.id) ?? "",
|
|
655
723
|
type,
|
|
@@ -669,15 +737,109 @@ function normalizeInteraction(payload) {
|
|
|
669
737
|
if (evidence) interaction.evidence = evidence;
|
|
670
738
|
return interaction;
|
|
671
739
|
}
|
|
740
|
+
var SETTLED_INTERACTION_STATUSES = /* @__PURE__ */ new Set(["resolved", "expired", "canceled"]);
|
|
741
|
+
function isSettlingShell(next) {
|
|
742
|
+
if (!SETTLED_INTERACTION_STATUSES.has(next.status) || next.actions.length > 0) return false;
|
|
743
|
+
switch (next.type) {
|
|
744
|
+
case "ask_user":
|
|
745
|
+
return next.payload.question === "";
|
|
746
|
+
case "site_login":
|
|
747
|
+
return next.payload.sites.every((site) => !site.siteId);
|
|
748
|
+
case "confirmation":
|
|
749
|
+
return next.payload.summary === "";
|
|
750
|
+
default:
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function foldInteraction(previous, next) {
|
|
755
|
+
if (!previous || previous.id !== next.id || !isSettlingShell(next)) return next;
|
|
756
|
+
const folded = { ...previous, status: next.status, actions: next.actions };
|
|
757
|
+
if (next.resolvedAt) folded.resolvedAt = next.resolvedAt;
|
|
758
|
+
return folded;
|
|
759
|
+
}
|
|
672
760
|
function interactionActionScopes(action, productManageScope) {
|
|
673
761
|
return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
|
|
674
762
|
}
|
|
763
|
+
function loginSlotIdle(slot) {
|
|
764
|
+
return slot.inflight === 0 && (!slot.session || slot.ended === true || slot.session.completed === true);
|
|
765
|
+
}
|
|
766
|
+
function markLoginEnded(store, interaction) {
|
|
767
|
+
if (!SETTLED_INTERACTION_STATUSES.has(interaction.status)) return;
|
|
768
|
+
const slot = store.get(interaction.id);
|
|
769
|
+
if (slot) slot.ended = true;
|
|
770
|
+
}
|
|
771
|
+
var SAVE_RETRY_PROBES = /* @__PURE__ */ new Set(["release_failed"]);
|
|
772
|
+
function isNoPendingLogin(err) {
|
|
773
|
+
return err instanceof QoniError && err.status === 409 && err.code === "no_pending_login";
|
|
774
|
+
}
|
|
775
|
+
var INTERACTION_MEMORY_LIMIT = 256;
|
|
776
|
+
function rememberBounded(map, key, value, limit = INTERACTION_MEMORY_LIMIT, evictable = () => true) {
|
|
777
|
+
map.delete(key);
|
|
778
|
+
map.set(key, value);
|
|
779
|
+
if (map.size <= limit) return;
|
|
780
|
+
for (const [candidate, entry] of map) {
|
|
781
|
+
if (map.size <= limit) break;
|
|
782
|
+
if (candidate !== key && evictable(entry)) map.delete(candidate);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function interactionSettled(interaction) {
|
|
786
|
+
return SETTLED_INTERACTION_STATUSES.has(interaction.status);
|
|
787
|
+
}
|
|
788
|
+
function controlSession(response) {
|
|
789
|
+
const raw = isJsonObject(response) ? response : {};
|
|
790
|
+
const fields = camelizeRecord(raw);
|
|
791
|
+
const session = { liveUrl: asString2(fields.standaloneControlUrl) ?? asString2(fields.liveUrl) ?? "", raw };
|
|
792
|
+
const expiresAt = asString2(fields.controlTokenExpiresAt) ?? asString2(fields.expiresAt);
|
|
793
|
+
const status = asString2(fields.status);
|
|
794
|
+
if (expiresAt) session.expiresAt = expiresAt;
|
|
795
|
+
if (status) session.status = status;
|
|
796
|
+
return session;
|
|
797
|
+
}
|
|
798
|
+
function describeOptions(payload) {
|
|
799
|
+
return (payload.options ?? []).map((option) => option.value).join(", ");
|
|
800
|
+
}
|
|
801
|
+
function checkAnswer(payload, value) {
|
|
802
|
+
const fail = (expected) => {
|
|
803
|
+
throw new QoniValidationError(`answer() for a ${payload.answerType} question expects ${expected}`);
|
|
804
|
+
};
|
|
805
|
+
const values = new Set((payload.options ?? []).map((option) => option.value));
|
|
806
|
+
switch (payload.answerType) {
|
|
807
|
+
case "text":
|
|
808
|
+
if (typeof value !== "string") fail("a string");
|
|
809
|
+
return;
|
|
810
|
+
case "number":
|
|
811
|
+
if (typeof value !== "number" || !Number.isFinite(value)) fail("a finite number");
|
|
812
|
+
return;
|
|
813
|
+
case "boolean":
|
|
814
|
+
if (typeof value !== "boolean") fail("a boolean");
|
|
815
|
+
return;
|
|
816
|
+
case "single_choice":
|
|
817
|
+
if (typeof value !== "string" || values.size > 0 && !values.has(value)) {
|
|
818
|
+
fail(`one option value: ${describeOptions(payload)}`);
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
case "multiple_choice":
|
|
822
|
+
if (!Array.isArray(value)) fail(`an array of option values: ${describeOptions(payload)}`);
|
|
823
|
+
if (value.some((item) => typeof item !== "string" || values.size > 0 && !values.has(item))) {
|
|
824
|
+
fail(`option values only: ${describeOptions(payload)}`);
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
675
829
|
var InteractionHandle = class {
|
|
676
|
-
|
|
830
|
+
/**
|
|
831
|
+
* @param loginSessions Login sessions opened on interactions, by interaction
|
|
832
|
+
* id. A RunHandle / MonitorHandle shares one store across the handles it
|
|
833
|
+
* hands out, so `confirmSignedIn()` finds the login `openLogin()` opened
|
|
834
|
+
* even on the handle of a later event.
|
|
835
|
+
*/
|
|
836
|
+
constructor(interaction, post, loginSessions = /* @__PURE__ */ new Map()) {
|
|
677
837
|
this.post = post;
|
|
838
|
+
this.loginSessions = loginSessions;
|
|
678
839
|
this.interaction = interaction;
|
|
679
840
|
}
|
|
680
841
|
post;
|
|
842
|
+
loginSessions;
|
|
681
843
|
interaction;
|
|
682
844
|
get id() {
|
|
683
845
|
return this.interaction.id;
|
|
@@ -695,9 +857,18 @@ var InteractionHandle = class {
|
|
|
695
857
|
can(kind) {
|
|
696
858
|
return this.interaction.actions.some((a) => a.kind === kind);
|
|
697
859
|
}
|
|
698
|
-
/**
|
|
699
|
-
|
|
700
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Answer an `ask_user` question. The value follows `payload.answerType`:
|
|
862
|
+
* `text` → string, `number` → number, `boolean` → boolean, `single_choice` →
|
|
863
|
+
* one option `value`, `multiple_choice` → an array of option values. A value
|
|
864
|
+
* that does not fit throws `QoniValidationError` before anything is sent.
|
|
865
|
+
*/
|
|
866
|
+
async answer(value) {
|
|
867
|
+
const action = this.#declaredAction(ActionKinds.answer);
|
|
868
|
+
if (this.interaction.type === "ask_user") {
|
|
869
|
+
checkAnswer(this.interaction.payload, value);
|
|
870
|
+
}
|
|
871
|
+
await this.post(action, { response: value });
|
|
701
872
|
}
|
|
702
873
|
/** Skip / dismiss the interaction (proceed without acting on it). */
|
|
703
874
|
async skip() {
|
|
@@ -715,8 +886,8 @@ var InteractionHandle = class {
|
|
|
715
886
|
* Start an interactive login session for a `site_login` interaction. The
|
|
716
887
|
* backend needs the site to sign in to: pass `siteId`, or omit it when the
|
|
717
888
|
* interaction lists exactly one site. Show `liveUrl` to the user; when they
|
|
718
|
-
*
|
|
719
|
-
*
|
|
889
|
+
* say they are signed in, call {@link confirmSignedIn}, which saves the login
|
|
890
|
+
* and lets the agent continue.
|
|
720
891
|
* The delegation must include `QoniScopes.SITE_LOGIN_REQUEST` and
|
|
721
892
|
* `QoniScopes.SITE_LOGIN_CONFIRM`; the AccessKey policy must allow both.
|
|
722
893
|
*/
|
|
@@ -729,57 +900,82 @@ var InteractionHandle = class {
|
|
|
729
900
|
}
|
|
730
901
|
body.profile_id = options.profileId;
|
|
731
902
|
}
|
|
732
|
-
const
|
|
903
|
+
const slot = this.#loginSlot();
|
|
904
|
+
const call = ++slot.calls;
|
|
905
|
+
slot.inflight += 1;
|
|
906
|
+
let response;
|
|
907
|
+
try {
|
|
908
|
+
response = await this.invoke(ActionKinds.openLogin, body);
|
|
909
|
+
} finally {
|
|
910
|
+
slot.inflight -= 1;
|
|
911
|
+
}
|
|
733
912
|
const raw = isJsonObject(response) ? response : {};
|
|
734
913
|
const fields = camelizeRecord(raw);
|
|
735
914
|
const profileId = asString2(fields.profileId) ?? "";
|
|
736
915
|
const siteId = asString2(fields.siteId) ?? String(body.site_id);
|
|
916
|
+
const sessionId = asString2(fields.sessionId) ?? "";
|
|
917
|
+
const current = slot.session;
|
|
918
|
+
const reused = current !== void 0 && sessionId !== "" && current.sessionId === sessionId && current.profileId === profileId && current.siteId === siteId;
|
|
919
|
+
const session = reused ? current : { profileId, siteId, sessionId, call };
|
|
920
|
+
if (reused) {
|
|
921
|
+
current.call = Math.max(current.call, call);
|
|
922
|
+
} else if (profileId && (current?.call ?? 0) < call) {
|
|
923
|
+
if (current) current.replaced = true;
|
|
924
|
+
slot.session = session;
|
|
925
|
+
} else {
|
|
926
|
+
session.replaced = true;
|
|
927
|
+
}
|
|
737
928
|
return {
|
|
738
929
|
profileId,
|
|
739
930
|
siteId,
|
|
740
|
-
sessionId
|
|
931
|
+
sessionId,
|
|
741
932
|
liveUrl: asString2(fields.liveUrl) ?? "",
|
|
742
933
|
raw,
|
|
743
934
|
confirm: async (confirmOptions = {}) => {
|
|
744
935
|
if (!profileId) {
|
|
745
936
|
throw new QoniValidationError("openLogin response did not include a profileId to confirm");
|
|
746
937
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
};
|
|
938
|
+
if (session.replaced) {
|
|
939
|
+
throw new QoniValidationError(
|
|
940
|
+
"this login session was replaced by a newer openLogin(); confirm the newer one (or call confirmSignedIn(), which saves the current session)"
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return this.#saveOnce(session, confirmOptions);
|
|
769
944
|
}
|
|
770
945
|
};
|
|
771
946
|
}
|
|
772
|
-
/**
|
|
947
|
+
/**
|
|
948
|
+
* The user finished signing in through the live browser `openLogin()`
|
|
949
|
+
* opened. Saves that login (so later tasks reuse it), then tells the agent
|
|
950
|
+
* to recheck the site and continue. If the user is not signed in after all,
|
|
951
|
+
* the agent raises a new `site_login` interaction.
|
|
952
|
+
*
|
|
953
|
+
* Saving needs `QoniScopes.SITE_LOGIN_CONFIRM` on the delegation; without
|
|
954
|
+
* `openLogin()` on this interaction, only the recheck is sent.
|
|
955
|
+
*/
|
|
773
956
|
async confirmSignedIn() {
|
|
774
|
-
|
|
957
|
+
const action = this.#declaredAction(ActionKinds.confirmSignedIn);
|
|
958
|
+
const session = this.loginSessions.get(this.id)?.session;
|
|
959
|
+
if (!session) {
|
|
960
|
+
await this.post(action, void 0);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
session.done ??= this.#saveThenRecheck(session, action).catch((err) => {
|
|
964
|
+
session.done = void 0;
|
|
965
|
+
throw err;
|
|
966
|
+
});
|
|
967
|
+
await session.done;
|
|
775
968
|
}
|
|
776
|
-
/**
|
|
969
|
+
/**
|
|
970
|
+
* Take over the live browser for a take_control interaction. Resolves to the
|
|
971
|
+
* signed control URL to show the user (`liveUrl`) and when it expires.
|
|
972
|
+
*/
|
|
777
973
|
async connectControl() {
|
|
778
|
-
await this.invoke(ActionKinds.connectControl);
|
|
974
|
+
return controlSession(await this.invoke(ActionKinds.connectControl));
|
|
779
975
|
}
|
|
780
|
-
/**
|
|
976
|
+
/** Get a fresh control URL when the current one expired. */
|
|
781
977
|
async refreshControl() {
|
|
782
|
-
await this.invoke(ActionKinds.refreshControl);
|
|
978
|
+
return controlSession(await this.invoke(ActionKinds.refreshControl));
|
|
783
979
|
}
|
|
784
980
|
/** Hand control back to the agent. */
|
|
785
981
|
async releaseControl() {
|
|
@@ -793,6 +989,81 @@ var InteractionHandle = class {
|
|
|
793
989
|
async switchProfile() {
|
|
794
990
|
await this.invoke(ActionKinds.switchProfile);
|
|
795
991
|
}
|
|
992
|
+
/** `POST /profiles/{profileId}/confirm` — persist the login of `session`. */
|
|
993
|
+
async #confirmLogin(session, options = {}) {
|
|
994
|
+
const confirmBody = { site_id: session.siteId };
|
|
995
|
+
if (options.displayLabel !== void 0) confirmBody.display_label = options.displayLabel;
|
|
996
|
+
const response = await this.post(
|
|
997
|
+
{
|
|
998
|
+
kind: ActionKinds.openLogin,
|
|
999
|
+
label: "Confirm login",
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
endpoint: `/profiles/${encodeURIComponent(session.profileId)}/confirm`
|
|
1002
|
+
},
|
|
1003
|
+
confirmBody,
|
|
1004
|
+
{ requiredScopes: [QoniScopes.SITE_LOGIN_CONFIRM] }
|
|
1005
|
+
);
|
|
1006
|
+
const raw = isJsonObject(response) ? response : {};
|
|
1007
|
+
const fields = camelizeRecord(raw);
|
|
1008
|
+
const state = asString2(fields.state) ?? "";
|
|
1009
|
+
return {
|
|
1010
|
+
profileId: asString2(fields.profileId) ?? session.profileId,
|
|
1011
|
+
active: state === "active",
|
|
1012
|
+
state,
|
|
1013
|
+
probeResult: asString2(fields.probeResult) ?? "",
|
|
1014
|
+
raw
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/** The save for `session`, started once; a failed attempt is forgotten so a retry saves again. */
|
|
1018
|
+
#saveOnce(session, options) {
|
|
1019
|
+
session.save ??= this.#confirmLogin(session, options).then(
|
|
1020
|
+
(result) => {
|
|
1021
|
+
if (SAVE_RETRY_PROBES.has(result.probeResult)) session.save = void 0;
|
|
1022
|
+
return result;
|
|
1023
|
+
},
|
|
1024
|
+
(err) => {
|
|
1025
|
+
if (!isNoPendingLogin(err)) session.save = void 0;
|
|
1026
|
+
throw err;
|
|
1027
|
+
}
|
|
1028
|
+
);
|
|
1029
|
+
return session.save;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Save the login, then let the agent recheck. A user who is not signed in
|
|
1033
|
+
* yet still gets a recheck (the agent asks again), and so does 409
|
|
1034
|
+
* `no_pending_login`. A save the backend could not complete (`release_failed`)
|
|
1035
|
+
* throws a retryable error instead of telling the agent to go on.
|
|
1036
|
+
*/
|
|
1037
|
+
async #saveThenRecheck(session, action) {
|
|
1038
|
+
let result;
|
|
1039
|
+
try {
|
|
1040
|
+
result = await this.#saveOnce(session);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
if (!isNoPendingLogin(err)) throw err;
|
|
1043
|
+
}
|
|
1044
|
+
if (result && SAVE_RETRY_PROBES.has(result.probeResult)) {
|
|
1045
|
+
throw new QoniError(
|
|
1046
|
+
`the login could not be saved yet (probeResult "${result.probeResult}"); the login browser is kept \u2014 call confirmSignedIn() again to retry`,
|
|
1047
|
+
{ code: "site_login.save_failed", retryable: true }
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (session.replaced) {
|
|
1051
|
+
throw new QoniError(
|
|
1052
|
+
"the login was reopened while it was being saved; call confirmSignedIn() again to save the newer login",
|
|
1053
|
+
{ code: "site_login.session_replaced", retryable: true }
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
await this.post(action, void 0);
|
|
1057
|
+
session.completed = true;
|
|
1058
|
+
}
|
|
1059
|
+
#loginSlot() {
|
|
1060
|
+
let slot = this.loginSessions.get(this.id);
|
|
1061
|
+
if (!slot) {
|
|
1062
|
+
slot = { calls: 0, inflight: 0 };
|
|
1063
|
+
rememberBounded(this.loginSessions, this.id, slot, INTERACTION_MEMORY_LIMIT, loginSlotIdle);
|
|
1064
|
+
}
|
|
1065
|
+
return slot;
|
|
1066
|
+
}
|
|
796
1067
|
/** Resolve the site for `openLogin`: explicit, or the only listed site. */
|
|
797
1068
|
#loginSiteId(requested) {
|
|
798
1069
|
const payload = this.interaction.payload;
|
|
@@ -848,6 +1119,10 @@ var RunHandle = class {
|
|
|
848
1119
|
/** Pass back to `run({ session })` to reuse the same backend session. */
|
|
849
1120
|
sessionRef;
|
|
850
1121
|
capture;
|
|
1122
|
+
/** Login sessions opened on this run's interactions — see {@link interactionHandle}. */
|
|
1123
|
+
#loginSessions = /* @__PURE__ */ new Map();
|
|
1124
|
+
/** Latest card per interaction id, so lifecycle updates fold onto it. */
|
|
1125
|
+
#interactions = /* @__PURE__ */ new Map();
|
|
851
1126
|
/** Refresh and return the run's current state. */
|
|
852
1127
|
async status() {
|
|
853
1128
|
const detail = await this.ops.getDetail();
|
|
@@ -861,8 +1136,14 @@ var RunHandle = class {
|
|
|
861
1136
|
async *events(opts = {}) {
|
|
862
1137
|
for await (const wire of this.ops.streamEvents(opts)) {
|
|
863
1138
|
await opts.onWireEvent?.(wire);
|
|
864
|
-
|
|
1139
|
+
let event = normalizeRunEvent(wire, { topRunId: this.id });
|
|
865
1140
|
if (!event) continue;
|
|
1141
|
+
if (event.type === "interaction") {
|
|
1142
|
+
const card = foldInteraction(this.#interactions.get(event.data.id), event.data);
|
|
1143
|
+
rememberBounded(this.#interactions, card.id, card, INTERACTION_MEMORY_LIMIT, interactionSettled);
|
|
1144
|
+
markLoginEnded(this.#loginSessions, card);
|
|
1145
|
+
event = { ...event, data: card };
|
|
1146
|
+
}
|
|
866
1147
|
if (event.type === "screenshot" && !this.capture.screenshots) continue;
|
|
867
1148
|
if (isVideoFrame(wire) && !this.capture.videoFrames) continue;
|
|
868
1149
|
yield event;
|
|
@@ -935,12 +1216,16 @@ var RunHandle = class {
|
|
|
935
1216
|
* Wrap an {@link Interaction} (from `event.data` of an `interaction` event)
|
|
936
1217
|
* in a typed {@link InteractionHandle} bound to this run. The handle's action
|
|
937
1218
|
* methods (`answer` / `confirmSignedIn` / `releaseControl` / …) post to the
|
|
938
|
-
* endpoint the backend declared for that action.
|
|
1219
|
+
* endpoint the backend declared for that action. Handles this RunHandle
|
|
1220
|
+
* hands out share the login sessions `openLogin()` opens, so
|
|
1221
|
+
* `confirmSignedIn()` on the handle of a later event still saves that login.
|
|
1222
|
+
* A RunHandle from another `attach()` (or process) does not see them.
|
|
939
1223
|
*/
|
|
940
1224
|
interactionHandle(interaction) {
|
|
941
1225
|
return new InteractionHandle(
|
|
942
1226
|
interaction,
|
|
943
|
-
(action, body, options) => this.ops.postAction(action, body, options)
|
|
1227
|
+
(action, body, options) => this.ops.postAction(action, body, options),
|
|
1228
|
+
this.#loginSessions
|
|
944
1229
|
);
|
|
945
1230
|
}
|
|
946
1231
|
/**
|
|
@@ -1564,7 +1849,14 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
|
|
|
1564
1849
|
|
|
1565
1850
|
// src/genauth.ts
|
|
1566
1851
|
function createGenAuthNamespace(transport, options = {}) {
|
|
1852
|
+
async function delegateAgent(input) {
|
|
1853
|
+
if (!options.delegateTokenFor) {
|
|
1854
|
+
throw new QoniValidationError("GenAuth delegateAgent requires a Qoni delegation provider");
|
|
1855
|
+
}
|
|
1856
|
+
return (await options.delegateTokenFor(input)).data;
|
|
1857
|
+
}
|
|
1567
1858
|
return {
|
|
1859
|
+
delegateAgent,
|
|
1568
1860
|
userInfo: (input) => transport.genauthJson("GET", "/oidc/me", input.accessToken),
|
|
1569
1861
|
jwks: () => transport.genauthJson("GET", "/oidc/.well-known/jwks.json"),
|
|
1570
1862
|
discovery: () => transport.genauthJson("GET", "/oidc/.well-known/openid-configuration"),
|
|
@@ -1758,6 +2050,10 @@ var MonitorHandle = class {
|
|
|
1758
2050
|
}
|
|
1759
2051
|
ops;
|
|
1760
2052
|
id;
|
|
2053
|
+
/** Login sessions opened on this monitor's interactions — see {@link interactionHandle}. */
|
|
2054
|
+
#loginSessions = /* @__PURE__ */ new Map();
|
|
2055
|
+
/** Latest card per interaction id, so settling shells fold onto it. */
|
|
2056
|
+
#interactions = /* @__PURE__ */ new Map();
|
|
1761
2057
|
/** Refresh and return the monitor (normalized camelCase fields). */
|
|
1762
2058
|
async get() {
|
|
1763
2059
|
return normalizeMonitorRecord(await this.ops.get());
|
|
@@ -1802,7 +2098,13 @@ var MonitorHandle = class {
|
|
|
1802
2098
|
*/
|
|
1803
2099
|
async *events(opts = {}) {
|
|
1804
2100
|
for await (const wire of this.ops.streamEvents(opts)) {
|
|
1805
|
-
|
|
2101
|
+
let event = normalizeRunEvent(wire);
|
|
2102
|
+
if (event?.type === "interaction") {
|
|
2103
|
+
const card = foldInteraction(this.#interactions.get(event.data.id), event.data);
|
|
2104
|
+
rememberBounded(this.#interactions, card.id, card, INTERACTION_MEMORY_LIMIT, interactionSettled);
|
|
2105
|
+
markLoginEnded(this.#loginSessions, card);
|
|
2106
|
+
event = { ...event, data: card };
|
|
2107
|
+
}
|
|
1806
2108
|
if (event) yield event;
|
|
1807
2109
|
}
|
|
1808
2110
|
}
|
|
@@ -1810,12 +2112,14 @@ var MonitorHandle = class {
|
|
|
1810
2112
|
* Wrap an {@link Interaction} (from `event.data` of an `interaction` event on
|
|
1811
2113
|
* the monitor stream — e.g. a `site_login` to unlock a `needs_reauth` park)
|
|
1812
2114
|
* in a typed {@link InteractionHandle}. The handle's action methods post to
|
|
1813
|
-
* the endpoint the backend declared for that action.
|
|
2115
|
+
* the endpoint the backend declared for that action. Handles of one
|
|
2116
|
+
* MonitorHandle share the login sessions `openLogin()` opens.
|
|
1814
2117
|
*/
|
|
1815
2118
|
interactionHandle(interaction) {
|
|
1816
2119
|
return new InteractionHandle(
|
|
1817
2120
|
interaction,
|
|
1818
|
-
(action, body, options) => this.ops.postAction(action, body, options)
|
|
2121
|
+
(action, body, options) => this.ops.postAction(action, body, options),
|
|
2122
|
+
this.#loginSessions
|
|
1819
2123
|
);
|
|
1820
2124
|
}
|
|
1821
2125
|
/**
|
|
@@ -2200,7 +2504,8 @@ var Qoni = class {
|
|
|
2200
2504
|
webAgentSSE: (...args) => this.webAgentSSE(...args)
|
|
2201
2505
|
};
|
|
2202
2506
|
this.genauth = createGenAuthNamespace(transport, {
|
|
2203
|
-
adminTokenFor: () => this.genauthAdminTokenFor()
|
|
2507
|
+
adminTokenFor: () => this.genauthAdminTokenFor(),
|
|
2508
|
+
delegateTokenFor: (input) => this.delegateToken(input)
|
|
2204
2509
|
});
|
|
2205
2510
|
this.qoni = createQoniNamespace(
|
|
2206
2511
|
transport,
|
|
@@ -2913,19 +3218,43 @@ function toRecord(value) {
|
|
|
2913
3218
|
}
|
|
2914
3219
|
function normalizeDelegateTokenInput(input) {
|
|
2915
3220
|
const body = toRecord(input);
|
|
3221
|
+
if (input.deniedScopes !== void 0) {
|
|
3222
|
+
throw new QoniValidationError(
|
|
3223
|
+
"delegateToken: deniedScopes is not supported by the delegation service. Do not rely on this field to prevent mailbox or browser actions."
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
if (input.agent !== void 0 && input.agentKey !== void 0 && input.agent !== input.agentKey) {
|
|
3227
|
+
throw new QoniValidationError("delegateToken: agent and agentKey must identify the same Agent");
|
|
3228
|
+
}
|
|
2916
3229
|
if (!body.userId && isJsonObject(input.user) && typeof input.user.id === "string") {
|
|
2917
3230
|
body.userId = input.user.id;
|
|
2918
3231
|
}
|
|
2919
3232
|
if (input.mode !== "interactive" && !body.userId) {
|
|
2920
3233
|
throw new QoniValidationError(
|
|
2921
|
-
'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the
|
|
3234
|
+
'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the top-level `userId`). Interactive mode (`mode: "interactive"`) does not need one.'
|
|
2922
3235
|
);
|
|
2923
3236
|
}
|
|
2924
|
-
body.agent = input.agent ?? "sdk";
|
|
3237
|
+
body.agent = input.agent ?? input.agentKey ?? "sdk";
|
|
3238
|
+
delete body.agentKey;
|
|
3239
|
+
if (input.expiresIn !== void 0) body.expiresIn = delegationDurationSeconds(input.expiresIn);
|
|
2925
3240
|
body.scopes = resolveDelegationScopes(input.scopes, input.products);
|
|
2926
3241
|
delete body.products;
|
|
2927
3242
|
return body;
|
|
2928
3243
|
}
|
|
3244
|
+
function delegationDurationSeconds(value) {
|
|
3245
|
+
let seconds = value;
|
|
3246
|
+
if (typeof value === "string") {
|
|
3247
|
+
const duration = /^(\d+)(s|m|h|d)?$/.exec(value);
|
|
3248
|
+
const units = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
3249
|
+
seconds = duration ? Number(duration[1]) * units[duration[2] ?? "s"] : NaN;
|
|
3250
|
+
}
|
|
3251
|
+
if (typeof seconds !== "number" || !Number.isInteger(seconds) || seconds < 60 || seconds > 86400) {
|
|
3252
|
+
throw new QoniValidationError(
|
|
3253
|
+
'delegateToken: expiresIn must be 60\u201386400 integer seconds or a whole-number duration such as "15m"'
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
return seconds;
|
|
3257
|
+
}
|
|
2929
3258
|
function resolveDelegationScopes(scopes, products) {
|
|
2930
3259
|
const resolved = /* @__PURE__ */ new Set();
|
|
2931
3260
|
for (const product of products ?? []) {
|
|
@@ -2949,11 +3278,16 @@ function resolveDelegationScopes(scopes, products) {
|
|
|
2949
3278
|
return [...resolved];
|
|
2950
3279
|
}
|
|
2951
3280
|
function validateScopeFormat(scope) {
|
|
3281
|
+
if (scope.startsWith("mailbox.") || scope === "web.search" || scope === "web.extract") {
|
|
3282
|
+
throw new QoniValidationError(
|
|
3283
|
+
`delegateToken: legacy action scope "${scope}" has no enforced mapping in this delegation service. It will not be expanded into broader product permissions.`
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
2952
3286
|
if (KNOWN_SCOPES.includes(scope)) return;
|
|
2953
|
-
const prefixed = KNOWN_SCOPES.
|
|
2954
|
-
if (prefixed) {
|
|
3287
|
+
const prefixed = KNOWN_SCOPES.filter((known) => known.endsWith(`.${scope}`));
|
|
3288
|
+
if (prefixed.length > 0) {
|
|
2955
3289
|
throw new QoniValidationError(
|
|
2956
|
-
`delegateToken: scope "${scope}" is missing its service prefix \u2014 write "${
|
|
3290
|
+
`delegateToken: scope "${scope}" is missing its service prefix \u2014 write ` + prefixed.map((known) => `"${known}"`).join(" or ")
|
|
2957
3291
|
);
|
|
2958
3292
|
}
|
|
2959
3293
|
if (!/^[a-z0-9_]+(\.[a-z0-9_]+)+:[a-z0-9_]+$/.test(scope)) {
|
|
@@ -3175,7 +3509,7 @@ export {
|
|
|
3175
3509
|
ActionKinds,
|
|
3176
3510
|
InteractionHandle,
|
|
3177
3511
|
InteractionStatuses,
|
|
3178
|
-
InteractionTypes,
|
|
3512
|
+
InteractionTypes2 as InteractionTypes,
|
|
3179
3513
|
MonitorHandle,
|
|
3180
3514
|
Qoni,
|
|
3181
3515
|
QoniAuthError,
|