@validate.qa/runner 1.0.101 → 1.0.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -0
  2. package/dist/{chunk-SN3PN4G4.mjs → chunk-36AWDKPL.mjs} +6 -2
  3. package/dist/{chunk-CYWHPXNZ.mjs → chunk-7SH3COMQ.mjs} +1 -1
  4. package/dist/{chunk-BBTK6OX6.mjs → chunk-BECCGMU4.mjs} +383 -11
  5. package/dist/{chunk-4SZC5ZX3.mjs → chunk-CG75I3ZZ.mjs} +25477 -5374
  6. package/dist/{chunk-TPU42VH2.mjs → chunk-CMM4DOXC.mjs} +10 -5
  7. package/dist/{chunk-M3VZGHZQ.mjs → chunk-NNJYKR3O.mjs} +22 -11
  8. package/dist/{chunk-IGXGSDE3.mjs → chunk-OTKJZIOD.mjs} +1298 -384
  9. package/dist/{chunk-CO5KHIVX.mjs → chunk-TK5KEGNW.mjs} +1 -1
  10. package/dist/{chunk-FQUH2PU2.mjs → chunk-WVLBQDFP.mjs} +140 -7
  11. package/dist/{chunk-2OKZY3WN.mjs → chunk-YI3ZC2XI.mjs} +2 -2
  12. package/dist/{chunk-XZUUBMP3.mjs → chunk-YIM746WO.mjs} +1 -1
  13. package/dist/cli.js +140692 -118903
  14. package/dist/cli.mjs +362 -108
  15. package/dist/credential-mcp-server.js +1402 -350
  16. package/dist/credential-mcp-server.mjs +3 -3
  17. package/dist/discovery-mcp-server.js +1330 -401
  18. package/dist/discovery-mcp-server.mjs +4 -4
  19. package/dist/dist-2G7YTNH5.mjs +17 -0
  20. package/dist/inbox-mcp-server.js +1676 -629
  21. package/dist/inbox-mcp-server.mjs +3 -3
  22. package/dist/route-seed-worker.js +1244 -330
  23. package/dist/route-seed-worker.mjs +4 -4
  24. package/dist/run-write-guard-preload.js +1622 -332
  25. package/dist/run-write-guard-preload.mjs +4 -4
  26. package/dist/run-write-guard.js +1622 -332
  27. package/dist/run-write-guard.mjs +3 -3
  28. package/dist/spec-driver.js +1 -1
  29. package/dist/spec-driver.mjs +1 -1
  30. package/package.json +3 -2
  31. package/dist/dist-VTPBY2C4.mjs +0 -17
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Local test runner for the **[validate.qa](https://validate.qa)** AI testing platform.
4
4
 
5
+ > **Not sure this is the package you need?** See [Which one do I need?](https://validate.qa/docs/getting-started).
6
+
5
7
  This package enables you to execute Playwright and Appium tests generated from your voice narrations directly on your local machine, securely connecting to your validate.qa cloud project.
6
8
 
7
9
  **Features:**
@@ -373,6 +375,7 @@ Credentials are stored in `~/.validate.qa/credentials.json`:
373
375
  | `POLL_INTERVAL_MS` | Poll interval | `2000` |
374
376
  | `RUNS_DIR` | Directory for test run artifacts | `~/.validate.qa/runs` |
375
377
  | `CHROME_USER_DATA_DIR` | Chrome profile path | Auto-detected |
378
+ | `RUNNER_RESULT_RETRY_BUDGET_MS` | How long a result post keeps retrying while the server is unreachable or answers 5xx (a redeploy); at least 10 s | `150000` |
376
379
 
377
380
  ### Example: Custom Configuration
378
381
 
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  dist_exports,
3
3
  init_dist
4
- } from "./chunk-IGXGSDE3.mjs";
4
+ } from "./chunk-OTKJZIOD.mjs";
5
5
  import {
6
6
  __commonJS,
7
7
  __require,
@@ -25,6 +25,7 @@ var require_survey_network_guard = __commonJS({
25
25
  exports.isTokenRefreshWrite = isTokenRefreshWrite;
26
26
  exports.isSignInPathWrite = isSignInPathWrite;
27
27
  exports.graphqlUrlCarriesQuery = graphqlUrlCarriesQuery;
28
+ exports.graphqlUrlQueriesAreReads = graphqlUrlQueriesAreReads;
28
29
  exports.graphqlGetMayWrite = graphqlGetMayWrite;
29
30
  exports.writeShapedGet = writeShapedGet;
30
31
  exports.decideSurveyRequest = decideSurveyRequest;
@@ -583,6 +584,9 @@ var require_survey_network_guard = __commonJS({
583
584
  function graphqlUrlCarriesQuery(params) {
584
585
  return params.has("query");
585
586
  }
587
+ function graphqlUrlQueriesAreReads(params) {
588
+ return params.getAll("query").every((doc) => doc.trim() !== "" && graphqlDocumentIsQueryOnly(doc));
589
+ }
586
590
  function graphqlGetMayWrite(url, body) {
587
591
  const docs = url.searchParams.getAll("query");
588
592
  return docs.length > 0 && (typeof body === "string" && body.length > 0 || docs.some((doc) => !graphqlDocumentIsQueryOnly(doc)));
@@ -826,7 +830,7 @@ var require_survey_network_guard = __commonJS({
826
830
  return { action: "allow", rule: "extra-pattern", reason: extra.reason, record };
827
831
  if (graphqlGet)
828
832
  return { action: "block", rule: "app-write", record };
829
- if (method === "POST" && isGraphqlEndpoint(url, s) && !graphqlUrlCarriesQuery(url.searchParams) && isGraphqlQueryOnly(req.postData))
833
+ if (method === "POST" && isGraphqlEndpoint(url, s) && graphqlUrlQueriesAreReads(url.searchParams) && isGraphqlQueryOnly(req.postData))
830
834
  return { action: "allow", rule: "graphql-query", record };
831
835
  const passive = s.lastNavigationAt > 0 && (s.lastActionAt === 0 || s.lastNavigationAt >= s.lastActionAt) && s.now - s.lastNavigationAt <= s.passiveWindowMs;
832
836
  const signature = learnableSignature(req, url, record, s);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  require_route_manifest
3
- } from "./chunk-CO5KHIVX.mjs";
3
+ } from "./chunk-TK5KEGNW.mjs";
4
4
  import {
5
5
  __commonJS,
6
6
  __require
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  require_survey_network_guard
3
- } from "./chunk-SN3PN4G4.mjs";
3
+ } from "./chunk-36AWDKPL.mjs";
4
4
  import {
5
5
  dist_exports,
6
6
  init_dist
7
- } from "./chunk-IGXGSDE3.mjs";
7
+ } from "./chunk-OTKJZIOD.mjs";
8
8
  import {
9
9
  __commonJS,
10
10
  __require,
@@ -348,7 +348,7 @@ var require_run_write_guard = __commonJS({
348
348
  "../runner-core/dist/services/run-write-guard.js"(exports) {
349
349
  "use strict";
350
350
  Object.defineProperty(exports, "__esModule", { value: true });
351
- exports.RUN_WRITE_RELAYED_LINE = exports.WRITE_GUARD_UNAVAILABLE_ERROR_PREFIX = exports.WRITE_HELD_ERROR_PREFIX = exports.RUN_WRITE_UNAVAILABLE_LINE = exports.RUN_WRITE_SAW_LINE = exports.RUN_WRITE_HELD_LINE = exports.RUN_WRITE_GUARD_LINE_PREFIX = exports.RUN_WRITE_SIGNIN_VARS_ENV = exports.RUN_WRITE_POLICY_ENV = exports.NO_KEEPALIVE_INIT_SCRIPT = exports.RUN_WRITE_LEAVING_EVENT = exports.RUN_WRITE_RELAY_KEY = exports.RUN_WRITE_DROPPED_BINDING = exports.RUN_WRITE_BLOCK_CONTEXT_OPTIONS = exports.WEBSOCKET_NOT_GUARDED = exports.RunWriteGuard = exports.BROKER_TOKEN_TTL_MS = exports.MAX_RUN_WRITE_RECORDS = exports.SHARED_SUFFIX_PATTERN_REASON = exports.FIREBASE_AUTH_HANDLER_PATH = exports.SIGN_IN_SECRET_VARIABLE = exports.MAX_ALLOW_PATTERN_CHARS = exports.MAX_ALLOW_PATTERNS = exports.DEFAULT_RECORD_ONLY_RULES = exports.RECORD_ONLY_CAPABLE_RULES = exports.RUN_WRITE_POLICY_VERSION = void 0;
351
+ exports.RUN_WRITE_RELAYED_LINE = exports.WRITE_GUARD_UNAVAILABLE_ERROR_PREFIX = exports.WRITE_HELD_ERROR_PREFIX = exports.RUN_WRITE_UNAVAILABLE_LINE = exports.RUN_WRITE_SAW_LINE = exports.RUN_WRITE_HELD_LINE = exports.RUN_WRITE_GUARD_LINE_PREFIX = exports.RUN_WRITE_SIGNIN_VARS_ENV = exports.RUN_WRITE_POLICY_ENV = exports.NO_KEEPALIVE_INIT_SCRIPT = exports.RUN_WRITE_LEAVING_EVENT = exports.RUN_WRITE_RELAY_KEY = exports.RUN_WRITE_DROPPED_BINDING = exports.RUN_WRITE_BLOCK_CONTEXT_OPTIONS = exports.WEBSOCKET_NOT_GUARDED = exports.RunWriteGuard = exports.BROKER_TOKEN_TTL_MS = exports.MAX_RUN_WRITE_RECORDS = exports.SHARED_SUFFIX_PATTERN_REASON = exports.FIREBASE_AUTH_HANDLER_PATH = exports.SIGN_IN_SECRET_VARIABLE = exports.MAX_ALLOW_PATTERN_CHARS = exports.MAX_ALLOW_PATTERNS = exports.MAX_IDP_HOSTS = exports.DEFAULT_RECORD_ONLY_RULES = exports.RECORD_ONLY_CAPABLE_RULES = exports.RUN_WRITE_POLICY_VERSION = void 0;
352
352
  exports.parseRunWritePolicy = parseRunWritePolicy;
353
353
  exports.signInSecretsOf = signInSecretsOf;
354
354
  exports.withRunWritePolicyScope = withRunWritePolicyScope;
@@ -357,6 +357,8 @@ var require_run_write_guard = __commonJS({
357
357
  exports.bodyHasSignInSecret = bodyHasSignInSecret;
358
358
  exports.compileAllowPattern = compileAllowPattern;
359
359
  exports.allowPatternProblem = allowPatternProblem;
360
+ exports.normaliseIdpHost = normaliseIdpHost;
361
+ exports.idpHostProblem = idpHostProblem;
360
362
  exports.runWritePatternNotes = runWritePatternNotes;
361
363
  exports.decideRunWrite = decideRunWrite;
362
364
  exports.isGoogleBrokerToken = isGoogleBrokerToken;
@@ -389,6 +391,8 @@ var require_run_write_guard = __commonJS({
389
391
  exports.RECORD_ONLY_CAPABLE_RULES = ["write-shaped-get"];
390
392
  exports.DEFAULT_RECORD_ONLY_RULES = ["write-shaped-get"];
391
393
  var MAX_APP_SITES = 16;
394
+ exports.MAX_IDP_HOSTS = 4;
395
+ var MAX_IDP_HINTS = 4;
392
396
  exports.MAX_ALLOW_PATTERNS = 50;
393
397
  exports.MAX_ALLOW_PATTERN_CHARS = 200;
394
398
  var MAX_REASON_CHARS = 300;
@@ -416,6 +420,11 @@ var require_run_write_guard = __commonJS({
416
420
  const e = x;
417
421
  return e && typeof e.pattern === "string" && typeof e.reason === "string" ? [{ pattern: e.pattern.slice(0, exports.MAX_ALLOW_PATTERN_CHARS), reason: e.reason.slice(0, MAX_REASON_CHARS) }] : [];
418
422
  }) : [];
423
+ const idpHosts = Array.isArray(v.idpHosts) ? [...new Set(v.idpHosts.filter((h) => typeof h === "string" && normaliseIdpHost(h) === h && idpHostProblem(h) === null))].slice(0, exports.MAX_IDP_HOSTS) : [];
424
+ const ignoredIdpHosts = Array.isArray(v.ignoredIdpHosts) ? v.ignoredIdpHosts.slice(0, exports.MAX_ALLOW_PATTERNS).flatMap((x) => {
425
+ const e = x;
426
+ return e && typeof e.pattern === "string" && typeof e.reason === "string" ? [{ pattern: e.pattern.slice(0, exports.MAX_ALLOW_PATTERN_CHARS), reason: e.reason.slice(0, MAX_REASON_CHARS) }] : [];
427
+ }) : [];
419
428
  const recordOnlyRules = Array.isArray(v.recordOnlyRules) ? exports.RECORD_ONLY_CAPABLE_RULES.filter((rule) => v.recordOnlyRules.includes(rule)) : [...exports.DEFAULT_RECORD_ONLY_RULES];
420
429
  if (typeof v.reason !== "string")
421
430
  return null;
@@ -428,6 +437,9 @@ var require_run_write_guard = __commonJS({
428
437
  appSites: appSites.map((s) => s.toLowerCase()),
429
438
  allowPatterns,
430
439
  ignoredAllowPatterns,
440
+ // Only when set: a policy without them parses exactly as before.
441
+ ...idpHosts.length > 0 ? { idpHosts } : {},
442
+ ...ignoredIdpHosts.length > 0 ? { ignoredIdpHosts } : {},
431
443
  recordOnlyRules,
432
444
  reason: v.reason.slice(0, MAX_REASON_CHARS),
433
445
  ...v.pwAgentsGuard === "off" ? { pwAgentsGuard: "off" } : {}
@@ -629,8 +641,153 @@ var require_run_write_guard = __commonJS({
629
641
  return named.size + Math.min(bare.size, 2) >= 2;
630
642
  }
631
643
  var GRAPHQL_SIGN_IN_NAME = /^(?:log_?in|sign_?in|authenticate|login\w{0,40}|signin\w{0,40})$/i;
644
+ var GRAPHQL_SIGN_IN_VERBS = ["login", "signin", "authenticate"];
645
+ var GRAPHQL_SIGN_IN_PHRASE_WORDS = /* @__PURE__ */ new Set(["with", "via", "by", "using"]);
646
+ var GRAPHQL_SIGN_IN_TAIL_WORDS = /* @__PURE__ */ new Set([
647
+ "user",
648
+ "mutation",
649
+ "mfa",
650
+ "otp",
651
+ "totp",
652
+ "sso",
653
+ "anonymous",
654
+ "anonymously",
655
+ "email",
656
+ "password",
657
+ "phone",
658
+ "number",
659
+ "code",
660
+ "token",
661
+ "refresh",
662
+ "custom",
663
+ "api",
664
+ "key",
665
+ "magic",
666
+ "credentials",
667
+ "username",
668
+ "credential",
669
+ "sms",
670
+ "pin",
671
+ "jwt",
672
+ "mobile",
673
+ "session",
674
+ "ticket",
675
+ "qr",
676
+ "biometric",
677
+ "biometrics",
678
+ "wallet",
679
+ "identifier",
680
+ "address",
681
+ "local",
682
+ "login",
683
+ "userid",
684
+ "passkey",
685
+ "webauthn",
686
+ "idp",
687
+ "oauth",
688
+ "oidc",
689
+ "saml",
690
+ "ldap",
691
+ "google",
692
+ "apple",
693
+ "github",
694
+ "gitlab",
695
+ "facebook",
696
+ "microsoft",
697
+ "azure",
698
+ "twitter",
699
+ "linkedin",
700
+ "discord",
701
+ "slack",
702
+ "twitch",
703
+ "keycloak",
704
+ "okta",
705
+ "ethereum",
706
+ "firebase",
707
+ "cognito",
708
+ "apikey",
709
+ "appleid",
710
+ "auth0",
711
+ "faceid",
712
+ "touchid",
713
+ "twofactor",
714
+ "backupcode",
715
+ "recoverycode",
716
+ "rememberme"
717
+ ]);
718
+ var GRAPHQL_SIGN_IN_SUBJECT_WORDS = /* @__PURE__ */ new Set(["user", "customer", "admin", "member", "staff", "account"]);
719
+ var GRAPHQL_SIGN_IN_JOINERS = /* @__PURE__ */ new Set(["and", "or"]);
720
+ var GRAPHQL_SIGN_IN_JOINED_KINDS = /* @__PURE__ */ new Set([
721
+ "github",
722
+ "gitlab",
723
+ "linkedin",
724
+ "webauthn",
725
+ "passkey",
726
+ "apikey",
727
+ "idp",
728
+ "appleid",
729
+ "auth0",
730
+ "faceid",
731
+ "touchid",
732
+ "twofactor",
733
+ "backupcode",
734
+ "recoverycode",
735
+ "rememberme",
736
+ "username",
737
+ "userid"
738
+ ]);
739
+ function joinSplitKinds(words) {
740
+ const out = [];
741
+ for (let i = 0; i < words.length; i++) {
742
+ let joined = 0;
743
+ for (let n = Math.min(3, words.length - i); n >= 2; n--) {
744
+ const part = words.slice(i, i + n);
745
+ if (!part.some((w) => GRAPHQL_SIGN_IN_JOINERS.has(w)) && GRAPHQL_SIGN_IN_JOINED_KINDS.has(part.join(""))) {
746
+ joined = n;
747
+ break;
748
+ }
749
+ }
750
+ if (joined) {
751
+ out.push(words.slice(i, i + joined).join(""));
752
+ i += joined - 1;
753
+ } else
754
+ out.push(words[i]);
755
+ }
756
+ return out;
757
+ }
758
+ var GRAPHQL_SIGN_IN_LINK_KINDS = /* @__PURE__ */ new Set(["magic", "email"]);
759
+ function graphqlSignInTail(name) {
760
+ const words = wordsOf(name.replace(/(?<=[A-Z0-9][A-Z0-9])(?=[A-Z][a-z])/g, " "));
761
+ const head = words[0] === "log" || words[0] === "sign" ? words[1] === "in" ? [`${words[0]}in`, ...words.slice(2)] : null : words;
762
+ if (!head || head.length === 0)
763
+ return null;
764
+ const verb = GRAPHQL_SIGN_IN_VERBS.find((v) => head[0].startsWith(v));
765
+ if (!verb)
766
+ return null;
767
+ const glued = head[0].slice(verb.length);
768
+ return glued ? [glued, ...head.slice(1)] : head.slice(1);
769
+ }
770
+ function isGraphqlSignInTail(tail) {
771
+ const words = joinSplitKinds(tail);
772
+ let start = 0;
773
+ while (start < words.length && GRAPHQL_SIGN_IN_SUBJECT_WORDS.has(words[start]))
774
+ start++;
775
+ if (start === words.length)
776
+ return true;
777
+ let kinds = words.slice(start);
778
+ const phrase = kinds.findIndex((w) => GRAPHQL_SIGN_IN_PHRASE_WORDS.has(w));
779
+ if (phrase >= 0) {
780
+ if (phrase === kinds.length - 1 || phrase > 0 && GRAPHQL_SIGN_IN_JOINERS.has(kinds[phrase - 1]) || GRAPHQL_SIGN_IN_JOINERS.has(kinds[phrase + 1]))
781
+ return false;
782
+ kinds = [...kinds.slice(0, phrase), ...kinds.slice(phrase + 1)];
783
+ }
784
+ if (kinds.length === 0)
785
+ return false;
786
+ const isKind = (w, before) => GRAPHQL_SIGN_IN_TAIL_WORDS.has(w) || /^v\d{1,3}$/.test(w) || /^\d?fa$/.test(w) || w === "link" && before !== void 0 && GRAPHQL_SIGN_IN_LINK_KINDS.has(before);
787
+ return kinds.every((w, i) => GRAPHQL_SIGN_IN_JOINERS.has(w) ? i > 0 && i < kinds.length - 1 && !GRAPHQL_SIGN_IN_JOINERS.has(kinds[i - 1]) : isKind(w, i === phrase ? void 0 : kinds[i - 1]));
788
+ }
632
789
  function wordsOf(segment) {
633
- return segment.replace(/([a-z])(?=[A-Z0-9])/g, "$1 ").toLowerCase().split(/[\s._/-]+/).filter(Boolean);
790
+ return segment.replace(/([a-z])(?=[A-Z0-9])/g, "$1 ").toLowerCase().split(/[\s.,_/-]+/).filter(Boolean);
634
791
  }
635
792
  function rawSegmentsOf(pathname) {
636
793
  return pathname.split("/").map((seg) => {
@@ -651,7 +808,10 @@ var require_run_write_guard = __commonJS({
651
808
  return words.some((w) => ACCOUNT_CHANGE_WORDS.has(w)) && words.some((w) => ACCOUNT_CHANGE_CONTEXT.has(w));
652
809
  }
653
810
  function isGraphqlSignInName(name) {
654
- return GRAPHQL_SIGN_IN_NAME.test(name) && !changesSomething([name], { passwordIsCredential: true });
811
+ if (!GRAPHQL_SIGN_IN_NAME.test(name))
812
+ return false;
813
+ const tail = graphqlSignInTail(name);
814
+ return tail !== null && isGraphqlSignInTail(tail) && !changesSomething([name], { passwordIsCredential: true });
655
815
  }
656
816
  function graphqlSignIn(body) {
657
817
  let parsed;
@@ -687,6 +847,173 @@ var require_run_write_guard = __commonJS({
687
847
  }
688
848
  return signIns > 0 ? "document" : null;
689
849
  }
850
+ var NOT_A_BROKER_SIGN_IN_WORDS = /* @__PURE__ */ new Set([
851
+ "add",
852
+ "book",
853
+ "booking",
854
+ "bookings",
855
+ "order",
856
+ "orders",
857
+ "reserve",
858
+ "reservation",
859
+ "reservations",
860
+ "submit",
861
+ "send",
862
+ "invite",
863
+ "invites",
864
+ "invitation",
865
+ "invitations",
866
+ "checkout",
867
+ "purchase",
868
+ "purchases",
869
+ "buy",
870
+ "pay",
871
+ "payment",
872
+ "payments",
873
+ "cart",
874
+ "subscribe",
875
+ "subscription",
876
+ "subscriptions",
877
+ "publish",
878
+ "upload",
879
+ "charge",
880
+ "charges",
881
+ "refund",
882
+ "refunds"
883
+ ]);
884
+ var BROKER_MAKE_VERBS = /* @__PURE__ */ new Set(["create", "generate"]);
885
+ var BROKER_CREDENTIAL_WORDS = /* @__PURE__ */ new Set(["session", "sessions", "login", "logins", "signin", "sign", "token", "tokens", "jwt", "jwts"]);
886
+ var BROKER_CREDENTIAL_QUALIFIERS = /* @__PURE__ */ new Set([
887
+ "access",
888
+ "refresh",
889
+ "id",
890
+ "auth",
891
+ "oauth",
892
+ "bearer",
893
+ "in",
894
+ "new",
895
+ "user",
896
+ "customer",
897
+ "with",
898
+ "from",
899
+ "via",
900
+ "for",
901
+ "by",
902
+ "and",
903
+ "mutation",
904
+ "google",
905
+ "apple",
906
+ "facebook",
907
+ "github",
908
+ "microsoft",
909
+ "sso",
910
+ "social",
911
+ "oidc",
912
+ "firebase"
913
+ ]);
914
+ function namesOnlyACredential(words) {
915
+ const rest = words.filter((w) => !BROKER_MAKE_VERBS.has(w));
916
+ return rest.some((w) => BROKER_CREDENTIAL_WORDS.has(w)) && rest.every((w) => BROKER_CREDENTIAL_WORDS.has(w) || BROKER_CREDENTIAL_QUALIFIERS.has(w));
917
+ }
918
+ function actsBesidesBrokerSignIn(segments) {
919
+ return segments.some((seg, i) => {
920
+ const words = wordsOf(seg);
921
+ if (words.some((w) => NOT_A_BROKER_SIGN_IN_WORDS.has(w)) || NOT_A_BROKER_SIGN_IN_WORDS.has(words.join("")))
922
+ return true;
923
+ if (!words.some((w) => BROKER_MAKE_VERBS.has(w)))
924
+ return false;
925
+ if (namesOnlyACredential(words))
926
+ return false;
927
+ return !(words.length === 1 && i > 0 && i === segments.length - 1 && namesOnlyACredential(wordsOf(segments[i - 1])));
928
+ });
929
+ }
930
+ function brokerRefuses(segments) {
931
+ return changesSomething(segments) || actsBesidesBrokerSignIn(segments);
932
+ }
933
+ var BROKER_SIGN_IN_NAME_WORDS = /* @__PURE__ */ new Set(["login", "signin", "sign", "auth", "authenticate", "google", "oauth", "sso", "social", "session"]);
934
+ function isBrokerSignInName(name) {
935
+ return typeof name === "string" && wordsOf(name).some((w) => BROKER_SIGN_IN_NAME_WORDS.has(w)) && !brokerRefuses([name]);
936
+ }
937
+ function graphqlBrokerSignIn(body) {
938
+ let parsed;
939
+ try {
940
+ parsed = JSON.parse(body);
941
+ } catch {
942
+ parsed = { query: body };
943
+ }
944
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
945
+ let mutationRoots = 0;
946
+ for (const entry of entries) {
947
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
948
+ return false;
949
+ const { query, operationName } = entry;
950
+ const record = entry;
951
+ const ids = PERSISTED_ID_KEYS.map((key) => record[key]).filter((n) => n !== void 0);
952
+ const names = operationName === void 0 ? ids : [operationName, ...ids];
953
+ if (ids.length > 0 && !names.every(isBrokerSignInName))
954
+ return false;
955
+ if (typeof query !== "string") {
956
+ return !Array.isArray(parsed) && query === void 0 && names.length > 0 && names.every(isBrokerSignInName);
957
+ }
958
+ const ops = (0, survey_network_guard_js_1.graphqlOperationRoots)(query);
959
+ if (!ops)
960
+ return false;
961
+ for (const op of ops) {
962
+ if (op.kind === "query")
963
+ continue;
964
+ if (typeof operationName === "string" && brokerRefuses([operationName]))
965
+ return false;
966
+ const roots = op.roots.filter((root) => root !== "__typename");
967
+ if (roots.some((root) => brokerRefuses([root])))
968
+ return false;
969
+ mutationRoots += roots.length;
970
+ }
971
+ }
972
+ return mutationRoots === 1;
973
+ }
974
+ var GRAPHQL_DOCUMENT_START = /^[\s,]*(?:[{#]|(?:query|mutation|subscription|fragment)\b)/;
975
+ function looksLikeGraphqlDocument(text) {
976
+ return GRAPHQL_DOCUMENT_START.test(text) || (0, survey_network_guard_js_1.graphqlOperationRoots)(text) !== null;
977
+ }
978
+ var PERSISTED_ID_KEYS = ["id", "documentId", "doc_id", "queryId"];
979
+ var GRAPHQL_ENVELOPE_FIELDS = ["variables", "operationName", "documentId", "doc_id", "queryId", "sha256Hash", "hash", "extensions", "operations"];
980
+ function hasGraphqlEnvelope(has, queryIsString) {
981
+ return queryIsString || GRAPHQL_ENVELOPE_FIELDS.some(has);
982
+ }
983
+ function carriesGraphql(search, body, contentType) {
984
+ if (search.getAll("query").some(looksLikeGraphqlDocument))
985
+ return true;
986
+ if (/^\s*application\/graphql\b/i.test(contentType ?? ""))
987
+ return true;
988
+ if (/multipart\//i.test(contentType ?? "") || /^\s*--/.test(body)) {
989
+ const parts = new Set([...body.matchAll(/\bname\s*=\s*"?([\w-]+)/gi)].map((m) => m[1]));
990
+ return hasGraphqlEnvelope((field) => parts.has(field), parts.has("query"));
991
+ }
992
+ let parsed;
993
+ try {
994
+ parsed = JSON.parse(body);
995
+ } catch {
996
+ if (looksLikeGraphqlDocument(body))
997
+ return true;
998
+ const form = new URLSearchParams(body);
999
+ return hasGraphqlEnvelope((field) => form.has(field), form.has("query"));
1000
+ }
1001
+ if (Array.isArray(parsed))
1002
+ return true;
1003
+ if (!parsed || typeof parsed !== "object")
1004
+ return false;
1005
+ const record = parsed;
1006
+ return hasGraphqlEnvelope((field) => record[field] !== void 0, typeof record.query === "string");
1007
+ }
1008
+ function isBrokerSignInShape(pathname, search, postData, contentType) {
1009
+ if (brokerRefuses(rawSegmentsOf(pathname)))
1010
+ return false;
1011
+ if (!isGraphqlPath(pathname) && !(typeof postData === "string" && carriesGraphql(search, postData, contentType)))
1012
+ return true;
1013
+ if ((0, survey_network_guard_js_1.graphqlUrlCarriesQuery)(search) || PERSISTED_ID_KEYS.some((key) => search.has(key)) || typeof postData !== "string" || postData.length > MAX_BODY_CHARS)
1014
+ return false;
1015
+ return graphqlBrokerSignIn(postData);
1016
+ }
690
1017
  function isCredentialSignInShape(pathname, search, postData, contentType) {
691
1018
  if (typeof postData === "string" && postData.length > MAX_BODY_CHARS)
692
1019
  return false;
@@ -900,6 +1227,26 @@ var require_run_write_guard = __commonJS({
900
1227
  }).join("");
901
1228
  return { method, host, path: new RegExp(`^${body}/?$`, "i") };
902
1229
  }
1230
+ function normaliseIdpHost(raw) {
1231
+ return raw.trim().toLowerCase().replace(/\.$/, "");
1232
+ }
1233
+ var WILDCARD_DNS_RE = /(?:^|\.)(?:nip\.io|sslip\.io|xip\.io|localtest\.me|lvh\.me|vcap\.me|traefik\.me|localho\.st|localhost\.direct|local\.gd|fuf\.me)$/;
1234
+ function idpHostProblem(host) {
1235
+ if (host.length > 253 || !/^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/.test(host))
1236
+ return "not a host name (no scheme, port, path or wildcard)";
1237
+ if (/^[0-9.]+$/.test(host))
1238
+ return "an IP address";
1239
+ if (/(?:^|\.)(?:localhost|local|internal)$/.test(host))
1240
+ return "a local name";
1241
+ if (WILDCARD_DNS_RE.test(host))
1242
+ return "a wildcard-DNS name (it can point at any address)";
1243
+ if ((0, shared_1.isSharedSuffix)(host))
1244
+ return exports.SHARED_SUFFIX_PATTERN_REASON;
1245
+ return null;
1246
+ }
1247
+ function isIdpHost(url, host, policy) {
1248
+ return url.protocol === "https:" && url.port === "" && (policy.idpHosts ?? []).includes(host);
1249
+ }
903
1250
  function matchesAllowPatterns(method, host, pathname, policy, compiled) {
904
1251
  const patterns = compiled ?? policy.allowPatterns.map(compileAllowPattern).filter((p) => p !== null);
905
1252
  return patterns.some((p) => {
@@ -923,7 +1270,10 @@ var require_run_write_guard = __commonJS({
923
1270
  return reason ? [{ pattern, reason }] : [];
924
1271
  })
925
1272
  ];
926
- return ignored.map(({ pattern, reason }) => `[run-write-guard] SAFE_WRITE_PATTERNS entry ${JSON.stringify(pattern)} ignored: ${reason}`);
1273
+ return [
1274
+ ...ignored.map(({ pattern, reason }) => `[run-write-guard] SAFE_WRITE_PATTERNS entry ${JSON.stringify(pattern)} ignored: ${reason}`),
1275
+ ...(policy.ignoredIdpHosts ?? []).map(({ pattern, reason }) => `[run-write-guard] AUTH_PROVIDER_HOSTS entry ${JSON.stringify(pattern)} ignored: ${reason}`)
1276
+ ];
927
1277
  }
928
1278
  function describe(facts, url, rule) {
929
1279
  return {
@@ -975,6 +1325,8 @@ var require_run_write_guard = __commonJS({
975
1325
  return { action: "write", rule: "sign-up", record: describe(facts, url, "sign-up") };
976
1326
  if (isAuthProviderWrite(method, host, pathname, facts.amzTarget, url.searchParams))
977
1327
  return { action: "allow", rule: "auth-provider" };
1328
+ if (isIdpHost(url, host, policy) && isAuth0CustomDomainSignIn(method, pathname, url.searchParams))
1329
+ return { action: "allow", rule: "auth-provider" };
978
1330
  const appSite = isAppSite(host, policy);
979
1331
  if (facts.registeredAuthHandler && pathname === exports.FIREBASE_AUTH_HANDLER_PATH)
980
1332
  return { action: "allow", rule: "auth-provider" };
@@ -982,7 +1334,7 @@ var require_run_write_guard = __commonJS({
982
1334
  return { action: "allow", rule: "sign-in" };
983
1335
  if (appSite && isAuth0CustomDomainSignIn(method, pathname, url.searchParams))
984
1336
  return { action: "allow", rule: "sign-in" };
985
- if (appSite && method === "POST" && facts.brokerTokenInBody && !changesSomething(rawSegmentsOf(pathname))) {
1337
+ if (appSite && method === "POST" && facts.brokerTokenInBody && isBrokerSignInShape(pathname, url.searchParams, facts.postData, facts.contentType)) {
986
1338
  return { action: "allow", rule: "broker-sign-in" };
987
1339
  }
988
1340
  if (appSite && isEvidencedTokenRefresh(method, pathname, facts.postData))
@@ -990,7 +1342,7 @@ var require_run_write_guard = __commonJS({
990
1342
  const signIn = appSite && method === "POST" && facts.signInSecretInBody ? isCredentialSignInShape(pathname, url.searchParams, facts.postData, facts.contentType) : false;
991
1343
  if (signIn)
992
1344
  return { action: "allow", rule: "credential-sign-in", ...signIn === "persisted" ? { persisted: true } : {} };
993
- if (method === "POST" && isGraphqlPath(pathname) && !(0, survey_network_guard_js_1.graphqlUrlCarriesQuery)(url.searchParams) && isGraphqlQueryOnlyBody(facts.postData))
1345
+ if (method === "POST" && isGraphqlPath(pathname) && (0, survey_network_guard_js_1.graphqlUrlQueriesAreReads)(url.searchParams) && isGraphqlQueryOnlyBody(facts.postData))
994
1346
  return { action: "allow", rule: "graphql-query" };
995
1347
  if (method === "POST" && hostIs(host, SEARCH_POST_HOSTS))
996
1348
  return { action: "allow", rule: "search-post" };
@@ -1047,6 +1399,8 @@ var require_run_write_guard = __commonJS({
1047
1399
  relayRejected = 0;
1048
1400
  /** Credential sign-ins already said in the run log (observe mode; method, host and path shape). */
1049
1401
  signInsLogged = /* @__PURE__ */ new Set();
1402
+ /** Hosts already named in the run log as a possible AUTH_PROVIDER_HOSTS entry (at most MAX_IDP_HINTS). */
1403
+ idpHintsLogged = /* @__PURE__ */ new Set();
1050
1404
  constructor(policy, opts = {}) {
1051
1405
  this.policy = policy;
1052
1406
  this.compiled = policy.allowPatterns.map(compileAllowPattern).filter((p) => p !== null);
@@ -1144,6 +1498,22 @@ var require_run_write_guard = __commonJS({
1144
1498
  const verb = held ? "held" : "saw";
1145
1499
  const why = record.writeShaped ? `${record.rule}, ${record.writeShaped}` : record.rule;
1146
1500
  this.log?.(`[run-write-guard] ${verb} ${record.method} ${record.host}${record.path}${record.keys.length ? ` (${record.keys.join(", ")})` : ""} [${why}]`);
1501
+ this.hintIdpHost(record);
1502
+ }
1503
+ /**
1504
+ * An Auth0 sign-in step (by path) posted to another site was a third-party write: the tenant may
1505
+ * answer on a custom domain of its own. Said once per host, for at most MAX_IDP_HINTS hosts, so a
1506
+ * person knows AUTH_PROVIDER_HOSTS exists; the guard never names the host itself.
1507
+ */
1508
+ hintIdpHost(record) {
1509
+ if (record.rule !== "third-party-write" || record.method !== "POST")
1510
+ return;
1511
+ if (this.idpHintsLogged.has(record.host) || this.idpHintsLogged.size >= MAX_IDP_HINTS)
1512
+ return;
1513
+ if ((this.policy.idpHosts ?? []).includes(record.host) || !AUTH0_AUTH_RE.test(record.path.toLowerCase()))
1514
+ return;
1515
+ this.idpHintsLogged.add(record.host);
1516
+ this.log?.(`[run-write-guard] note: ${record.host} looks like an Auth0 sign-in on another site; if it is the app's own sign-in, a project admin can name it in AUTH_PROVIDER_HOSTS in Test Data`);
1147
1517
  }
1148
1518
  /**
1149
1519
  * A page sent a message over a WebSocket to the app's site. The guard cannot tell a read from a
@@ -1951,8 +2321,9 @@ var require_run_write_guard = __commonJS({
1951
2321
  };
1952
2322
  Object.assign(context, {
1953
2323
  route: async (url, handler, options) => {
1954
- await route(url, handler, options);
2324
+ const disposable = await route(url, handler, options);
1955
2325
  others.push({ url, handler });
2326
+ return disposable;
1956
2327
  },
1957
2328
  unroute: async (url, handler) => {
1958
2329
  if (handler === void 0 && sameRouteUrl(url, GUARD_ROUTE)) {
@@ -2285,6 +2656,7 @@ var require_run_write_guard = __commonJS({
2285
2656
  }
2286
2657
  function buildRunWriteGuardHook(policy, modulePath, signInSecretNames = [], opts = {}) {
2287
2658
  const withContext = opts.browserContext !== false;
2659
+ const hookTest = opts.hookTest ?? "test";
2288
2660
  const names = signInSecretNames.filter((n) => /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(n)).slice(0, 16);
2289
2661
  return `
2290
2662
  import { createRequire as __qaWriteGuardRequire } from 'node:module';
@@ -2307,7 +2679,7 @@ try {
2307
2679
  } catch (err) {
2308
2680
  __qaWriteGuardError = err;
2309
2681
  }
2310
- test.beforeEach(async (${withContext ? "{ context }" : ""}) => {
2682
+ ${hookTest}.beforeEach(async (${withContext ? "{ context }" : ""}) => {
2311
2683
  try {
2312
2684
  if (__qaWriteGuardError) throw __qaWriteGuardError;
2313
2685
  ${withContext ? "await __qaWriteGuard.installRunWriteGuard(context, __qaWriteGuardPolicy, { onLog: (line) => console.log(line), signInSecrets: __qaWriteGuardSecrets });" : "// an @api test: no browser context, only Node's fetch is guarded"}
@@ -2316,7 +2688,7 @@ test.beforeEach(async (${withContext ? "{ context }" : ""}) => {
2316
2688
  ${policy.mode === "block" ? "throw err;" : "// observe: never fail the test"}
2317
2689
  }
2318
2690
  });${withContext && policy.mode === "block" ? `
2319
- test.afterEach(async ({ context }) => {
2691
+ ${hookTest}.afterEach(async ({ context }) => {
2320
2692
  try { if (__qaWriteGuard) await __qaWriteGuard.clearRunWriteRelay(context); } catch { /* the context closes next */ }
2321
2693
  });` : ""}
2322
2694
  `;