@qoniai/qoni 0.8.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 +102 -2
- package/README.md +119 -72
- package/README.zh-CN.md +73 -44
- package/dist/index.cjs +355 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -42
- package/dist/index.d.ts +163 -42
- package/dist/index.js +355 -58
- 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
|
/**
|
|
@@ -1765,6 +2050,10 @@ var MonitorHandle = class {
|
|
|
1765
2050
|
}
|
|
1766
2051
|
ops;
|
|
1767
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();
|
|
1768
2057
|
/** Refresh and return the monitor (normalized camelCase fields). */
|
|
1769
2058
|
async get() {
|
|
1770
2059
|
return normalizeMonitorRecord(await this.ops.get());
|
|
@@ -1809,7 +2098,13 @@ var MonitorHandle = class {
|
|
|
1809
2098
|
*/
|
|
1810
2099
|
async *events(opts = {}) {
|
|
1811
2100
|
for await (const wire of this.ops.streamEvents(opts)) {
|
|
1812
|
-
|
|
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
|
+
}
|
|
1813
2108
|
if (event) yield event;
|
|
1814
2109
|
}
|
|
1815
2110
|
}
|
|
@@ -1817,12 +2112,14 @@ var MonitorHandle = class {
|
|
|
1817
2112
|
* Wrap an {@link Interaction} (from `event.data` of an `interaction` event on
|
|
1818
2113
|
* the monitor stream — e.g. a `site_login` to unlock a `needs_reauth` park)
|
|
1819
2114
|
* in a typed {@link InteractionHandle}. The handle's action methods post to
|
|
1820
|
-
* 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.
|
|
1821
2117
|
*/
|
|
1822
2118
|
interactionHandle(interaction) {
|
|
1823
2119
|
return new InteractionHandle(
|
|
1824
2120
|
interaction,
|
|
1825
|
-
(action, body, options) => this.ops.postAction(action, body, options)
|
|
2121
|
+
(action, body, options) => this.ops.postAction(action, body, options),
|
|
2122
|
+
this.#loginSessions
|
|
1826
2123
|
);
|
|
1827
2124
|
}
|
|
1828
2125
|
/**
|
|
@@ -2987,10 +3284,10 @@ function validateScopeFormat(scope) {
|
|
|
2987
3284
|
);
|
|
2988
3285
|
}
|
|
2989
3286
|
if (KNOWN_SCOPES.includes(scope)) return;
|
|
2990
|
-
const prefixed = KNOWN_SCOPES.
|
|
2991
|
-
if (prefixed) {
|
|
3287
|
+
const prefixed = KNOWN_SCOPES.filter((known) => known.endsWith(`.${scope}`));
|
|
3288
|
+
if (prefixed.length > 0) {
|
|
2992
3289
|
throw new QoniValidationError(
|
|
2993
|
-
`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 ")
|
|
2994
3291
|
);
|
|
2995
3292
|
}
|
|
2996
3293
|
if (!/^[a-z0-9_]+(\.[a-z0-9_]+)+:[a-z0-9_]+$/.test(scope)) {
|
|
@@ -3212,7 +3509,7 @@ export {
|
|
|
3212
3509
|
ActionKinds,
|
|
3213
3510
|
InteractionHandle,
|
|
3214
3511
|
InteractionStatuses,
|
|
3215
|
-
InteractionTypes,
|
|
3512
|
+
InteractionTypes2 as InteractionTypes,
|
|
3216
3513
|
MonitorHandle,
|
|
3217
3514
|
Qoni,
|
|
3218
3515
|
QoniAuthError,
|