@validate.qa/runner 1.0.100 → 1.0.102

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-MBKGTELT.mjs → chunk-3JM6XTSZ.mjs} +1862 -530
  3. package/dist/{chunk-2XD63NU5.mjs → chunk-4WLP3RWN.mjs} +941 -86
  4. package/dist/{chunk-MHFP3QXU.mjs → chunk-5DFBEL72.mjs} +1 -1
  5. package/dist/{chunk-II7QW7MZ.mjs → chunk-BXLEPT4M.mjs} +31 -18
  6. package/dist/{chunk-MAO6ADAO.mjs → chunk-CBVHPYVJ.mjs} +62 -2
  7. package/dist/{chunk-5P4ANVDY.mjs → chunk-DN3O6EYT.mjs} +43073 -23357
  8. package/dist/{chunk-GIIIVO2W.mjs → chunk-E2RRAE27.mjs} +155 -11
  9. package/dist/chunk-E2VEMBQQ.mjs +291 -0
  10. package/dist/{chunk-HMUC3DKS.mjs → chunk-HJ6FKHMM.mjs} +2281 -2
  11. package/dist/{chunk-WDXLAZ2J.mjs → chunk-PWFWMVZU.mjs} +3 -2
  12. package/dist/{chunk-6NIT2CCH.mjs → chunk-ZZYBRIBT.mjs} +18 -12
  13. package/dist/cli.js +139723 -114754
  14. package/dist/cli.mjs +536 -247
  15. package/dist/credential-mcp-server.js +1761 -358
  16. package/dist/credential-mcp-server.mjs +3 -3
  17. package/dist/discovery-mcp-server.js +1874 -379
  18. package/dist/discovery-mcp-server.mjs +4 -4
  19. package/dist/dist-52M5RDZ6.mjs +17 -0
  20. package/dist/inbox-mcp-server.js +1902 -505
  21. package/dist/inbox-mcp-server.mjs +3 -3
  22. package/dist/route-seed-worker.js +3976 -356
  23. package/dist/route-seed-worker.mjs +14 -7
  24. package/dist/run-write-guard-preload.js +2908 -572
  25. package/dist/run-write-guard-preload.mjs +5 -285
  26. package/dist/run-write-guard.js +2908 -572
  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 +4 -3
  31. package/dist/dist-DFNLMH6L.mjs +0 -16
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  require_survey_network_guard
3
- } from "./chunk-GIIIVO2W.mjs";
3
+ } from "./chunk-E2RRAE27.mjs";
4
4
  import {
5
5
  dist_exports,
6
6
  init_dist
7
- } from "./chunk-MBKGTELT.mjs";
7
+ } from "./chunk-3JM6XTSZ.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;
@@ -365,6 +367,7 @@ var require_run_write_guard = __commonJS({
365
367
  exports.addRunWriteAuthHandler = addRunWriteAuthHandler;
366
368
  exports.runWriteContextOptions = runWriteContextOptions;
367
369
  exports.clearRunWriteRelay = clearRunWriteRelay;
370
+ exports.runWriteGuardHopBytesInFlight = runWriteGuardHopBytesInFlight;
368
371
  exports.installRunWriteGuard = installRunWriteGuard;
369
372
  exports.installNodeFetchGuard = installNodeFetchGuard;
370
373
  exports.runWriteGuardRefusal = runWriteGuardRefusal;
@@ -376,7 +379,11 @@ var require_run_write_guard = __commonJS({
376
379
  exports.safeWritePatternFor = safeWritePatternFor;
377
380
  var node_async_hooks_1 = __require("async_hooks");
378
381
  var node_crypto_1 = __require("crypto");
382
+ var node_http_1 = __require("http");
383
+ var node_https_1 = __require("https");
379
384
  var node_path_1 = __require("path");
385
+ var node_stream_1 = __require("stream");
386
+ var node_zlib_1 = __require("zlib");
380
387
  var shared_1 = (init_dist(), __toCommonJS(dist_exports));
381
388
  var survey_network_guard_js_1 = require_survey_network_guard();
382
389
  var bounded_output_js_1 = require_bounded_output();
@@ -384,6 +391,8 @@ var require_run_write_guard = __commonJS({
384
391
  exports.RECORD_ONLY_CAPABLE_RULES = ["write-shaped-get"];
385
392
  exports.DEFAULT_RECORD_ONLY_RULES = ["write-shaped-get"];
386
393
  var MAX_APP_SITES = 16;
394
+ exports.MAX_IDP_HOSTS = 4;
395
+ var MAX_IDP_HINTS = 4;
387
396
  exports.MAX_ALLOW_PATTERNS = 50;
388
397
  exports.MAX_ALLOW_PATTERN_CHARS = 200;
389
398
  var MAX_REASON_CHARS = 300;
@@ -411,6 +420,11 @@ var require_run_write_guard = __commonJS({
411
420
  const e = x;
412
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) }] : [];
413
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
+ }) : [];
414
428
  const recordOnlyRules = Array.isArray(v.recordOnlyRules) ? exports.RECORD_ONLY_CAPABLE_RULES.filter((rule) => v.recordOnlyRules.includes(rule)) : [...exports.DEFAULT_RECORD_ONLY_RULES];
415
429
  if (typeof v.reason !== "string")
416
430
  return null;
@@ -423,13 +437,17 @@ var require_run_write_guard = __commonJS({
423
437
  appSites: appSites.map((s) => s.toLowerCase()),
424
438
  allowPatterns,
425
439
  ignoredAllowPatterns,
440
+ // Only when set: a policy without them parses exactly as before.
441
+ ...idpHosts.length > 0 ? { idpHosts } : {},
442
+ ...ignoredIdpHosts.length > 0 ? { ignoredIdpHosts } : {},
426
443
  recordOnlyRules,
427
- reason: v.reason.slice(0, MAX_REASON_CHARS)
444
+ reason: v.reason.slice(0, MAX_REASON_CHARS),
445
+ ...v.pwAgentsGuard === "off" ? { pwAgentsGuard: "off" } : {}
428
446
  };
429
447
  }
430
448
  var policyScope = new node_async_hooks_1.AsyncLocalStorage();
431
449
  var MIN_SIGN_IN_SECRET_CHARS = 6;
432
- exports.SIGN_IN_SECRET_VARIABLE = /PASS(?:WORD|CODE|PHRASE)?$/i;
450
+ exports.SIGN_IN_SECRET_VARIABLE = /(?:PASS(?:WORD|CODE|PHRASE)?|[A-Z0-9]_PASSWD|[A-Z0-9]_PWD)$/i;
433
451
  function signInSecretsOf(vars) {
434
452
  if (!vars || typeof vars !== "object")
435
453
  return [];
@@ -446,6 +464,7 @@ var require_run_write_guard = __commonJS({
446
464
  }
447
465
  var READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
448
466
  var MAX_BODY_CHARS = 262144;
467
+ var MAX_SIGN_IN_LOG_LINES = 20;
449
468
  var CAPTCHA_HOSTS = ["recaptcha.net", "hcaptcha.com", "challenges.cloudflare.com"];
450
469
  var SEARCH_POST_HOSTS = ["algolia.net", "algolianet.com", "algolia.io"];
451
470
  var PAYMENT_HOSTS = [
@@ -471,7 +490,7 @@ var require_run_write_guard = __commonJS({
471
490
  var SEARCH_LAST_SEGMENTS = /* @__PURE__ */ new Set(["search", "autocomplete", "suggest", "suggestions", "typeahead"]);
472
491
  var PROVIDER_SIGN_UP_PATH = /(?:\/accounts:signup|\/dbconnections\/signup|\/auth\/v1\/signup|\/v1\/client\/sign_ups)(?:[/?]|$)/i;
473
492
  var FIRST_PARTY_SIGN_UP_SEGMENTS = /* @__PURE__ */ new Set(["signup", "sign-up", "sign_up", "register", "registration", "create-account", "createaccount", "sign_ups"]);
474
- var NOT_A_SIGN_IN_SEGMENTS = /* @__PURE__ */ new Set([
493
+ var NOT_A_SIGN_IN_WORDS = /* @__PURE__ */ new Set([
475
494
  "delete",
476
495
  "destroy",
477
496
  "remove",
@@ -480,19 +499,29 @@ var require_run_write_guard = __commonJS({
480
499
  "cancel",
481
500
  "password",
482
501
  "passwords",
483
- "change-password",
484
- "change_password",
485
502
  "reset",
486
- "reset-password",
487
503
  "update",
488
504
  "logout",
489
505
  "signout",
490
- "sign-out",
491
506
  "revoke",
492
507
  "transfer",
493
- "withdraw"
508
+ "withdraw",
509
+ "disable",
510
+ "enable",
511
+ "unlink",
512
+ "change",
513
+ "erase",
514
+ "purge",
515
+ "wipe",
516
+ "terminate",
517
+ "suspend",
518
+ "leave",
519
+ "disconnect",
520
+ "deauthorize"
494
521
  ]);
495
- var SIGN_IN_SHAPED_SEGMENTS = /* @__PURE__ */ new Set([
522
+ var ACCOUNT_CHANGE_WORDS = /* @__PURE__ */ new Set(["email", "mfa", "2fa", "totp", "otp"]);
523
+ var ACCOUNT_CHANGE_CONTEXT = /* @__PURE__ */ new Set(["account", "owner", "ownership", "invite"]);
524
+ var SIGN_IN_LAST_SEGMENTS = /* @__PURE__ */ new Set([
496
525
  "session",
497
526
  "sessions",
498
527
  "login",
@@ -502,25 +531,511 @@ var require_run_write_guard = __commonJS({
502
531
  "signin",
503
532
  "sign-in",
504
533
  "sign_in",
505
- "auth",
506
534
  "authenticate",
507
535
  "authentication",
536
+ "auth"
537
+ ]);
538
+ var SIGN_IN_VERB_SEGMENTS = /* @__PURE__ */ new Set(["login", "log-in", "log_in", "signin", "sign-in", "sign_in"]);
539
+ var SIGN_IN_CREDENTIAL_KINDS = /* @__PURE__ */ new Set(["email", "username", "phone", "password"]);
540
+ var SIGN_IN_TAILS = /* @__PURE__ */ new Set(["callback/credentials", "auth/local", "session/new", "sessions/new"]);
541
+ var PASSWORD_GRANT = /(?:^|[&{,\s"])grant_type"?\s*[=:]\s*"?password(?=["&,}\s]|$)/;
542
+ var NEW_CREDENTIAL_STEM = "(?:pass(?:word|wd)?|pwd)";
543
+ var NEW_CREDENTIAL_WORD = `(?:(?:new|old|current|confirm|repeat|verify|retype)[_-]?(?:${NEW_CREDENTIAL_STEM}|email)|${NEW_CREDENTIAL_STEM}[_-]?(?:new|old|current|confirm(?:ation)?|repeat(?:ed)?|verify|verification|again|retype|check|confirmed|2)|email[_-]?confirm(?:ation)?)`;
544
+ var NEW_CREDENTIAL_NAME = new RegExp(`^${NEW_CREDENTIAL_WORD}$`, "i");
545
+ var NEW_CREDENTIAL_KEY = new RegExp(`(?:^|["&{,\\s[])${NEW_CREDENTIAL_WORD}(?:["\\]]?\\s*[:=]|"\\s*(?:;|$))`, "im");
546
+ var PASSWORD_KEY_WORDS = /* @__PURE__ */ new Set(["password", "passwd", "pwd", "pass"]);
547
+ var PASSWORD_KEY_QUALIFIERS = /* @__PURE__ */ new Set([
548
+ "new",
549
+ "old",
550
+ "current",
551
+ "confirm",
552
+ "confirmed",
553
+ "confirmation",
554
+ "repeat",
555
+ "repeated",
556
+ "verify",
557
+ "verification",
558
+ "again",
559
+ "retype",
560
+ "check",
561
+ "2"
562
+ ]);
563
+ function isPasswordKey(key) {
564
+ const words = wordsOf(key);
565
+ return words.some((w) => PASSWORD_KEY_WORDS.has(w)) && words.every((w) => PASSWORD_KEY_WORDS.has(w) || PASSWORD_KEY_QUALIFIERS.has(w));
566
+ }
567
+ function couldBePassword(value) {
568
+ if (value === void 0)
569
+ return true;
570
+ if (typeof value === "number")
571
+ return Number.isFinite(value);
572
+ if (typeof value !== "string")
573
+ return false;
574
+ const v = value.trim();
575
+ return v !== "" && !/^(?:true|false|on|off|yes|no|null)$/i.test(v);
576
+ }
577
+ var MAX_CREDENTIAL_KEYS = 200;
578
+ function credentialKeysOf(body, contentType) {
579
+ const type = (contentType ?? "").toLowerCase();
580
+ const keys = [];
581
+ try {
582
+ if (type.includes("json") || /^\s*[[{]/.test(body)) {
583
+ const stack = [{ value: JSON.parse(body), under: null }];
584
+ while (stack.length > 0 && keys.length < MAX_CREDENTIAL_KEYS) {
585
+ const { value, under } = stack.pop();
586
+ if (Array.isArray(value))
587
+ stack.push(...value.slice(0, MAX_CREDENTIAL_KEYS).map((inner) => ({ value: inner, under: null })));
588
+ else if (value && typeof value === "object") {
589
+ for (const [key, inner] of Object.entries(value).slice(0, MAX_CREDENTIAL_KEYS)) {
590
+ const name = under === null ? key : `${under}_${key}`;
591
+ keys.push({ name, value: inner });
592
+ if (Array.isArray(inner) && isPasswordKey(name)) {
593
+ for (const item of inner.slice(0, MAX_CREDENTIAL_KEYS))
594
+ if (item === null || typeof item !== "object")
595
+ keys.push({ name, value: item });
596
+ }
597
+ const object = inner !== null && typeof inner === "object" && !Array.isArray(inner);
598
+ stack.push({ value: inner, under: object && isPasswordKey(name) ? name : null });
599
+ }
600
+ }
601
+ }
602
+ } else if (type.includes("multipart/form-data")) {
603
+ for (const m of body.matchAll(/(?:^|[;\s])name="([^"]{1,200})"/g))
604
+ if (keys.push({ name: m[1], value: void 0 }) >= MAX_CREDENTIAL_KEYS)
605
+ break;
606
+ } else if (type.includes("x-www-form-urlencoded")) {
607
+ for (const [name, value] of new URLSearchParams(body))
608
+ if (keys.push({ name, value }) >= MAX_CREDENTIAL_KEYS)
609
+ break;
610
+ }
611
+ } catch {
612
+ }
613
+ return keys.slice(0, MAX_CREDENTIAL_KEYS).map(({ name, value }) => {
614
+ const bare = name.replace(/\[\]$/, "");
615
+ const parts = bare.match(/[^[\]]+/g);
616
+ if (bare.endsWith("]") && parts && parts.length > 1) {
617
+ const inner = parts[parts.length - 1];
618
+ const outer = parts[parts.length - 2];
619
+ if (isPasswordKey(outer)) {
620
+ if (/^\d+$/.test(inner))
621
+ return { name: outer, value };
622
+ if (isPasswordKey(inner) || NEW_CREDENTIAL_NAME.test(inner))
623
+ return { name: inner, value };
624
+ return { name: `${outer}_${inner}`, value };
625
+ }
626
+ }
627
+ return { name: /\[([^[\]]*)\]$/.exec(bare)?.[1] ?? bare, value };
628
+ });
629
+ }
630
+ function namesNewCredential(body, contentType) {
631
+ if (NEW_CREDENTIAL_KEY.test(body))
632
+ return true;
633
+ const type = contentType ?? (/^\s*[[{]/.test(body) ? null : "application/x-www-form-urlencoded");
634
+ const keys = credentialKeysOf(body, type);
635
+ if (keys.some(({ name }) => NEW_CREDENTIAL_NAME.test(name)))
636
+ return true;
637
+ const found = keys.filter(({ name, value }) => isPasswordKey(name) && couldBePassword(value));
638
+ const isBare = (name) => wordsOf(name).every((w) => PASSWORD_KEY_WORDS.has(w));
639
+ const named = new Set(found.filter(({ name }) => !isBare(name)).map(({ name }) => name.toLowerCase()));
640
+ const bare = new Set(found.filter(({ name }) => isBare(name)).map(({ name, value }) => value === void 0 ? `name:${name.toLowerCase()}` : `value:${String(value)}`));
641
+ return named.size + Math.min(bare.size, 2) >= 2;
642
+ }
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",
508
660
  "token",
509
- "tokens",
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
+ }
789
+ function wordsOf(segment) {
790
+ return segment.replace(/([a-z])(?=[A-Z0-9])/g, "$1 ").toLowerCase().split(/[\s.,_/-]+/).filter(Boolean);
791
+ }
792
+ function rawSegmentsOf(pathname) {
793
+ return pathname.split("/").map((seg) => {
794
+ const raw = seg.split(";")[0];
795
+ try {
796
+ return decodeURIComponent(raw);
797
+ } catch {
798
+ return raw;
799
+ }
800
+ }).filter(Boolean);
801
+ }
802
+ function changesSomething(segments, opts = {}) {
803
+ const words = segments.flatMap(wordsOf);
804
+ const joined = segments.map((seg) => wordsOf(seg).join(""));
805
+ const hard = (w) => NOT_A_SIGN_IN_WORDS.has(w) && !(opts.passwordIsCredential && (w === "password" || w === "passwords"));
806
+ if (words.some(hard) || joined.some(hard))
807
+ return true;
808
+ return words.some((w) => ACCOUNT_CHANGE_WORDS.has(w)) && words.some((w) => ACCOUNT_CHANGE_CONTEXT.has(w));
809
+ }
810
+ function isGraphqlSignInName(name) {
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 });
815
+ }
816
+ function graphqlSignIn(body) {
817
+ let parsed;
818
+ try {
819
+ parsed = JSON.parse(body);
820
+ } catch {
821
+ parsed = { query: body };
822
+ }
823
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
824
+ let signIns = 0;
825
+ for (const entry of entries) {
826
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
827
+ return null;
828
+ const { query, operationName } = entry;
829
+ if (typeof query !== "string") {
830
+ if (!Array.isArray(parsed) && query === void 0 && typeof operationName === "string" && isGraphqlSignInName(operationName))
831
+ return "persisted";
832
+ return null;
833
+ }
834
+ const ops = (0, survey_network_guard_js_1.graphqlOperationRoots)(query);
835
+ if (!ops)
836
+ return null;
837
+ for (const op of ops) {
838
+ if (op.kind === "query")
839
+ continue;
840
+ if (typeof operationName === "string" && changesSomething([operationName], { passwordIsCredential: true }))
841
+ return null;
842
+ const roots = op.roots.filter((root) => root !== "__typename");
843
+ if (roots.length === 0 || !roots.every(isGraphqlSignInName))
844
+ return null;
845
+ signIns += roots.length;
846
+ }
847
+ }
848
+ return signIns > 0 ? "document" : null;
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",
510
891
  "oauth",
511
- "oauth2",
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",
512
909
  "sso",
513
- "identity",
514
- "credentials"
910
+ "social",
911
+ "oidc",
912
+ "firebase"
515
913
  ]);
516
- var GRAPHQL_SIGN_IN = /"operationName"\s*:\s*"(?:log_?in|sign_?in|authenticate|login\w{0,40}|signin\w{0,40})"|\bmutation\b[^{]{0,200}\{\s*(?:log_?in|sign_?in|authenticate|login\w{0,40}|signin\w{0,40})\s*[({]/i;
517
- function isCredentialSignInShape(pathname, postData) {
518
- const segs = segmentsOf(pathname);
519
- if (segs.some((seg) => NOT_A_SIGN_IN_SEGMENTS.has(seg)))
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
+ }
1017
+ function isCredentialSignInShape(pathname, search, postData, contentType) {
1018
+ if (typeof postData === "string" && postData.length > MAX_BODY_CHARS)
1019
+ return false;
1020
+ const body = typeof postData === "string" && postData.length <= MAX_BODY_CHARS ? postData : null;
1021
+ if (body !== null && namesNewCredential(body, contentType))
520
1022
  return false;
521
- if (segs.some((seg) => SIGN_IN_SHAPED_SEGMENTS.has(seg)))
1023
+ const raw = rawSegmentsOf(pathname);
1024
+ const segs = raw.map((seg) => seg.toLowerCase());
1025
+ const last = segs[segs.length - 1] ?? "";
1026
+ const prev = segs[segs.length - 2] ?? "";
1027
+ if (SIGN_IN_VERB_SEGMENTS.has(prev) && SIGN_IN_CREDENTIAL_KINDS.has(last))
1028
+ return !changesSomething(raw, { passwordIsCredential: true });
1029
+ if (changesSomething(raw))
1030
+ return false;
1031
+ if (SIGN_IN_LAST_SEGMENTS.has(last) || SIGN_IN_TAILS.has(`${prev}/${last}`))
522
1032
  return true;
523
- return isGraphqlPath(pathname) && typeof postData === "string" && postData.length <= MAX_BODY_CHARS && GRAPHQL_SIGN_IN.test(postData);
1033
+ if (last === "token")
1034
+ return search.get("grant_type") === "password" || body !== null && PASSWORD_GRANT.test(body);
1035
+ if (!isGraphqlPath(pathname) || body === null || (0, survey_network_guard_js_1.graphqlUrlCarriesQuery)(search))
1036
+ return false;
1037
+ const graphql = graphqlSignIn(body);
1038
+ return graphql === "persisted" ? graphql : graphql === "document";
524
1039
  }
525
1040
  function bodyHasSignInSecret(body, secrets) {
526
1041
  if (!body || body.length > MAX_BODY_CHARS || secrets.length === 0)
@@ -548,7 +1063,7 @@ var require_run_write_guard = __commonJS({
548
1063
  var COGNITO_HOSTED_UI_RE = /^\/(?:oauth2\/(?:token|authorize|userinfo|idpresponse)|saml2\/idpresponse|login|logincontinue)$/;
549
1064
  var CLERK_FAPI_RE = /^\/v1\/(?:client(?:\/(?:sign_ins(?:\/[^/]+\/(?:prepare_first_factor|attempt_first_factor|prepare_second_factor|attempt_second_factor))?|sessions\/[^/]+\/(?:touch|tokens(?:\/[^/]+)?)|handshake|sync|link))?|dev_browser|environment|oauth_callback)$/;
550
1065
  var SUPABASE_AUTH_RE = /^\/auth\/v1\/(?:token|otp|magiclink|verify|authorize|callback|sso|saml\/acs|reauthenticate|factors\/[^/]+\/(?:challenge|verify))$/;
551
- var AUTH0_AUTH_RE = /^\/(?:oauth\/(?:token|ro)|authorize(?:\/resume)?|co\/authenticate|usernamepassword\/(?:login|challenge)|login\/(?:callback|identifier|password)|passwordless\/(?:start|verify)|userinfo|mfa\/challenge|u\/(?:login(?:\/[^/]+)?|mfa-(?:otp|sms|email|voice|phone|push|webauthn-platform|webauthn-roaming|recovery-code)-challenge(?:-push|-new-code)?|mfa-login-options|mfa-detect-browser-capabilities|mfa-country-codes|consent))$/;
1066
+ var AUTH0_AUTH_RE = /^\/(?:oauth\/(?:token|ro)|authorize(?:\/resume)?|co\/authenticate|usernamepassword\/(?:login|challenge)|login\/(?:callback|identifier|password)|passwordless\/(?:start|verify)|userinfo|mfa\/challenge|u\/(?:login(?:\/[^/]+)?|mfa-(?:otp|sms|email|voice|phone|push|webauthn-platform|webauthn-roaming|recovery-code)-challenge(?:-push|-new-code)?|mfa-(?:sms|push|email)-list|mfa-login-options|mfa-detect-browser-capabilities|mfa-country-codes|consent))$/;
552
1067
  var OKTA_AUTH_RE = /^\/(?:api\/v1\/(?:authn(?:\/(?:factors\/[^/]+\/verify|skip|cancel))?|sessions(?:\/me\/lifecycle\/refresh)?)|oauth2\/(?:[^/]+\/)?v1\/(?:token|authorize|introspect|userinfo|device\/authorize)|idp\/idx\/(?:introspect|identify(?:\/select)?|challenge(?:\/answer|\/poll)?|skip|cancel)|login\/[^/]+(?:\/[^/]+)?)$/;
553
1068
  var MICROSOFT_AUTH_RE = /\/(?:oauth2\/(?:v2\.0\/)?(?:token|authorize|devicecode)|login|kmsi|getcredentialtype|sas\/(?:processauth|beginauth|endauth)|selfasserted|api\/combinedsigninandsignup\/[^/]+|resume|reprocess|federation\/oauth2)$/;
554
1069
  var GOOGLE_ACCOUNTS_RE = /^\/(?:o\/oauth2\/(?:token|auth|v2\/auth|iframerpc|postmessagerelay|approval)|gsi\/[^/]+(?:\/[^/]+)?|_\/(?:signin|lookup|identitysigninui|accountssigninui)\/.+|v3\/signin\/.+|signin\/.+|servicelogin|checkcookie|accountchooser)$/;
@@ -614,14 +1129,26 @@ var require_run_write_guard = __commonJS({
614
1129
  return GENERIC_IDP_AUTH_RE.test(path);
615
1130
  }
616
1131
  function isAuthProviderWrite(method, host, pathname, amzTarget, search) {
1132
+ if (!isPlainPost(method, search))
1133
+ return false;
1134
+ if (!(0, shared_1.isAuthProviderHost)(host))
1135
+ return false;
1136
+ return isAuthProviderSignIn(host.toLowerCase(), pathname, amzTarget, search);
1137
+ }
1138
+ function isPlainPost(method, search) {
617
1139
  if (method.toUpperCase() !== "POST")
618
1140
  return false;
619
1141
  const tunnelled = search.get("_method");
620
- if (tunnelled !== null && tunnelled.toUpperCase() !== "POST")
1142
+ return tunnelled === null || tunnelled.toUpperCase() === "POST";
1143
+ }
1144
+ function isAuth0CustomDomainSignIn(method, pathname, search) {
1145
+ if (!isPlainPost(method, search))
621
1146
  return false;
622
- if (!(0, shared_1.isAuthProviderHost)(host))
1147
+ const path = pathname.toLowerCase();
1148
+ if (!AUTH0_AUTH_RE.test(path))
623
1149
  return false;
624
- return isAuthProviderSignIn(host.toLowerCase(), pathname, amzTarget, search);
1150
+ const needsState = path.startsWith("/u/") || path === "/authorize" || path === "/authorize/resume";
1151
+ return !needsState || (search.get("state") ?? "") !== "";
625
1152
  }
626
1153
  function isSignUp(host, pathname, amzTarget) {
627
1154
  if (PROVIDER_SIGN_UP_PATH.test(pathname))
@@ -700,6 +1227,26 @@ var require_run_write_guard = __commonJS({
700
1227
  }).join("");
701
1228
  return { method, host, path: new RegExp(`^${body}/?$`, "i") };
702
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
+ }
703
1250
  function matchesAllowPatterns(method, host, pathname, policy, compiled) {
704
1251
  const patterns = compiled ?? policy.allowPatterns.map(compileAllowPattern).filter((p) => p !== null);
705
1252
  return patterns.some((p) => {
@@ -723,7 +1270,10 @@ var require_run_write_guard = __commonJS({
723
1270
  return reason ? [{ pattern, reason }] : [];
724
1271
  })
725
1272
  ];
726
- 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
+ ];
727
1277
  }
728
1278
  function describe(facts, url, rule) {
729
1279
  return {
@@ -747,8 +1297,7 @@ var require_run_write_guard = __commonJS({
747
1297
  const host = url.hostname.toLowerCase().replace(/\.$/, "");
748
1298
  const pathname = url.pathname;
749
1299
  if (READ_METHODS.has(method)) {
750
- const doc = method === "GET" && isGraphqlPath(pathname) ? url.searchParams.get("query") : null;
751
- if (doc !== null && !(0, survey_network_guard_js_1.graphqlDocumentIsQueryOnly)(doc)) {
1300
+ if (method === "GET" && isGraphqlPath(pathname) && (0, survey_network_guard_js_1.graphqlGetMayWrite)(url, facts.postData)) {
752
1301
  return { action: "write", rule: "graphql-get-mutation", record: describe(facts, url, "graphql-get-mutation") };
753
1302
  }
754
1303
  if (method !== "OPTIONS" && isAppSite(host, policy)) {
@@ -776,20 +1325,24 @@ var require_run_write_guard = __commonJS({
776
1325
  return { action: "write", rule: "sign-up", record: describe(facts, url, "sign-up") };
777
1326
  if (isAuthProviderWrite(method, host, pathname, facts.amzTarget, url.searchParams))
778
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" };
779
1330
  const appSite = isAppSite(host, policy);
780
1331
  if (facts.registeredAuthHandler && pathname === exports.FIREBASE_AUTH_HANDLER_PATH)
781
1332
  return { action: "allow", rule: "auth-provider" };
782
1333
  if (appSite && (0, survey_network_guard_js_1.isSignInPathWrite)(method, pathname))
783
1334
  return { action: "allow", rule: "sign-in" };
784
- if (appSite && method === "POST" && facts.brokerTokenInBody && !segmentsOf(pathname).some((seg) => NOT_A_SIGN_IN_SEGMENTS.has(seg))) {
1335
+ if (appSite && isAuth0CustomDomainSignIn(method, pathname, url.searchParams))
1336
+ return { action: "allow", rule: "sign-in" };
1337
+ if (appSite && method === "POST" && facts.brokerTokenInBody && isBrokerSignInShape(pathname, url.searchParams, facts.postData, facts.contentType)) {
785
1338
  return { action: "allow", rule: "broker-sign-in" };
786
1339
  }
787
1340
  if (appSite && isEvidencedTokenRefresh(method, pathname, facts.postData))
788
1341
  return { action: "allow", rule: "token-refresh" };
789
- if (appSite && method === "POST" && facts.signInSecretInBody && isCredentialSignInShape(pathname, facts.postData)) {
790
- return { action: "allow", rule: "credential-sign-in" };
791
- }
792
- if (method === "POST" && isGraphqlPath(pathname) && isGraphqlQueryOnlyBody(facts.postData))
1342
+ const signIn = appSite && method === "POST" && facts.signInSecretInBody ? isCredentialSignInShape(pathname, url.searchParams, facts.postData, facts.contentType) : false;
1343
+ if (signIn)
1344
+ return { action: "allow", rule: "credential-sign-in", ...signIn === "persisted" ? { persisted: true } : {} };
1345
+ if (method === "POST" && isGraphqlPath(pathname) && (0, survey_network_guard_js_1.graphqlUrlQueriesAreReads)(url.searchParams) && isGraphqlQueryOnlyBody(facts.postData))
793
1346
  return { action: "allow", rule: "graphql-query" };
794
1347
  if (method === "POST" && hostIs(host, SEARCH_POST_HOSTS))
795
1348
  return { action: "allow", rule: "search-post" };
@@ -834,6 +1387,8 @@ var require_run_write_guard = __commonJS({
834
1387
  recordOnly;
835
1388
  log;
836
1389
  signInSecrets;
1390
+ /** The deadline of one request the guard sends itself, all its hops together (sendCheckingRedirect). */
1391
+ redirectCheckTimeoutMs;
837
1392
  /** Tokens the Google sign-in broker handed this context, each until its expiry (epoch ms). */
838
1393
  brokerTokens = [];
839
1394
  /** Origins of the Firebase Auth handlers the broker registered for this context. */
@@ -842,12 +1397,18 @@ var require_run_write_guard = __commonJS({
842
1397
  relayIds = /* @__PURE__ */ new Set();
843
1398
  relayed = 0;
844
1399
  relayRejected = 0;
1400
+ /** Credential sign-ins already said in the run log (observe mode; method, host and path shape). */
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();
845
1404
  constructor(policy, opts = {}) {
846
1405
  this.policy = policy;
847
1406
  this.compiled = policy.allowPatterns.map(compileAllowPattern).filter((p) => p !== null);
848
1407
  this.recordOnly = new Set((policy.recordOnlyRules ?? exports.DEFAULT_RECORD_ONLY_RULES).filter((rule) => exports.RECORD_ONLY_CAPABLE_RULES.includes(rule)));
849
1408
  this.log = opts.onLog;
850
1409
  this.signInSecrets = (opts.signInSecrets ?? []).filter((v) => typeof v === "string" && v.length >= MIN_SIGN_IN_SECRET_CHARS);
1410
+ const ms = opts.redirectCheckTimeoutMs;
1411
+ this.redirectCheckTimeoutMs = typeof ms === "number" && ms > 0 ? Math.min(ms, REDIRECT_CHECK_TIMEOUT_MS) : REDIRECT_CHECK_TIMEOUT_MS;
851
1412
  }
852
1413
  decide(facts) {
853
1414
  const signInSecretInBody = facts.signInSecretInBody ?? bodyHasSignInSecret(facts.postData, this.signInSecrets);
@@ -870,8 +1431,28 @@ var require_run_write_guard = __commonJS({
870
1431
  consumedBrokerTokens.add(brokerToken.value);
871
1432
  this.brokerTokens = this.brokerTokens.filter((t) => t !== brokerToken);
872
1433
  }
1434
+ if (decision.rule === "credential-sign-in" && this.policy.mode === "observe")
1435
+ this.noteCredentialSignIn(facts, decision.persisted === true);
873
1436
  return decision;
874
1437
  }
1438
+ /**
1439
+ * Observe mode says where each credential sign-in went, once per path and kind (the lead): the
1440
+ * method, host and masked path only, never the body, so the rule can be checked against real runs
1441
+ * before a site is blocked. A GraphQL persisted query is marked unverified.
1442
+ */
1443
+ noteCredentialSignIn(facts, persisted) {
1444
+ let host = "";
1445
+ try {
1446
+ host = new URL(facts.url).hostname.toLowerCase();
1447
+ } catch {
1448
+ return;
1449
+ }
1450
+ const line = `${facts.method.toUpperCase()} ${host}${(0, survey_network_guard_js_1.maskedPath)(facts.url)} [credential-sign-in${persisted ? " (persisted, unverified)" : ""}]`;
1451
+ if (this.signInsLogged.has(line) || this.signInsLogged.size >= MAX_SIGN_IN_LOG_LINES)
1452
+ return;
1453
+ this.signInsLogged.add(line);
1454
+ this.log?.(`[run-write-guard] allowed ${line}`);
1455
+ }
875
1456
  /**
876
1457
  * A code or ID token the Google sign-in broker hands this context: the first POST that carries it
877
1458
  * to the app in the next BROKER_TOKEN_TTL_MS is the sign-in. Anything not shaped like a Google code
@@ -917,6 +1498,22 @@ var require_run_write_guard = __commonJS({
917
1498
  const verb = held ? "held" : "saw";
918
1499
  const why = record.writeShaped ? `${record.rule}, ${record.writeShaped}` : record.rule;
919
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`);
920
1517
  }
921
1518
  /**
922
1519
  * A page sent a message over a WebSocket to the app's site. The guard cannot tell a read from a
@@ -931,8 +1528,9 @@ var require_run_write_guard = __commonJS({
931
1528
  }
932
1529
  /**
933
1530
  * A hop the guard fetched itself (sendCheckingRedirect) failed, so the request fails in the page.
934
- * Said in the run log, with the timeout when it was that: a slow allowed upload or stream is cut
935
- * at REDIRECT_CHECK_TIMEOUT_MS, and the page only sees a network error.
1531
+ * Said in the run log, with the reason when it was the guard's own limit: a slow allowed upload or
1532
+ * stream is cut at its deadline, an answer over MAX_GUARD_HOP_BYTES as it streams in, and the page
1533
+ * only sees a network error.
936
1534
  */
937
1535
  noteRedirectCheckFailed(facts, err, timeoutMs) {
938
1536
  let host = "";
@@ -940,8 +1538,8 @@ var require_run_write_guard = __commonJS({
940
1538
  host = new URL(facts.url).hostname.toLowerCase();
941
1539
  } catch {
942
1540
  }
943
- const timedOut = err instanceof Error && (err.name === "TimeoutError" || /\btimeout\b/i.test(err.message));
944
- const why = timedOut ? `the guard's redirect check timed out after ${Math.round(timeoutMs / 1e3)} s (REDIRECT_CHECK_TIMEOUT_MS)` : "the guard's redirect check failed";
1541
+ const reason = err instanceof GuardHopError ? err.reason : null;
1542
+ const why = reason === "timeout" ? `the guard's redirect check timed out after ${Math.round(timeoutMs / 1e3)} s (REDIRECT_CHECK_TIMEOUT_MS)` : reason === "over-cap" ? `the answer was over ${MAX_GUARD_HOP_BYTES / (1024 * 1024)} MB (MAX_GUARD_HOP_BYTES), cut as it came in` : "the guard's redirect check failed";
945
1543
  this.log?.(`[run-write-guard] note: ${facts.method.toUpperCase()} ${host}${(0, survey_network_guard_js_1.maskedPath)(facts.url)} failed in the page: ${why}`);
946
1544
  }
947
1545
  /** A request still undecided when its page closed (see guardPageClose): it is failed in the page, so the browser never sends it. */
@@ -1389,66 +1987,321 @@ var require_run_write_guard = __commonJS({
1389
1987
  }
1390
1988
  }
1391
1989
  var MAX_GUARD_REDIRECTS = 5;
1392
- var REDIRECT_CHECK_TIMEOUT_MS = 6e4;
1990
+ var REDIRECT_CHECK_TIMEOUT_MS = 3e4;
1991
+ var MAX_GUARD_HOP_BYTES = 16 * 1024 * 1024;
1992
+ var MAX_GUARD_BUFFERED_BYTES = 128 * 1024 * 1024;
1993
+ var SEND_HTTP = node_http_1.request;
1994
+ var SEND_HTTPS = node_https_1.request;
1995
+ var HOP_ONLY_HEADERS = /* @__PURE__ */ new Set([
1996
+ "connection",
1997
+ "keep-alive",
1998
+ "proxy-connection",
1999
+ "transfer-encoding",
2000
+ "te",
2001
+ "trailer",
2002
+ "upgrade",
2003
+ "proxy-authorization",
2004
+ "proxy-authenticate",
2005
+ "host",
2006
+ "content-length"
2007
+ ]);
2008
+ var hopBytesInFlight = 0;
2009
+ var hopsInFlight = /* @__PURE__ */ new Set();
2010
+ var hopBudgetWaiters = [];
2011
+ function releaseHopBytes(hop, bytes) {
2012
+ hopsInFlight.delete(hop);
2013
+ hopBytesInFlight -= bytes;
2014
+ for (const wake of hopBudgetWaiters.splice(0))
2015
+ wake();
2016
+ }
2017
+ function runWriteGuardHopBytesInFlight() {
2018
+ return hopBytesInFlight;
2019
+ }
2020
+ var GuardHopError = class extends Error {
2021
+ reason;
2022
+ constructor(reason, message) {
2023
+ super(message);
2024
+ this.reason = reason;
2025
+ this.name = "GuardHopError";
2026
+ }
2027
+ };
2028
+ function sendGuardHop(hop) {
2029
+ return new Promise((resolve, reject) => {
2030
+ let url;
2031
+ try {
2032
+ url = new URL(hop.url);
2033
+ } catch (err) {
2034
+ reject(err);
2035
+ return;
2036
+ }
2037
+ const send = url.protocol === "https:" ? SEND_HTTPS : url.protocol === "http:" ? SEND_HTTP : null;
2038
+ if (!send) {
2039
+ reject(new Error(`the guard sends only http(s), not ${url.protocol}`));
2040
+ return;
2041
+ }
2042
+ const id = /* @__PURE__ */ Symbol("guard hop");
2043
+ hopsInFlight.add(id);
2044
+ let held = 0;
2045
+ let settled = false;
2046
+ let req = null;
2047
+ const chunks = [];
2048
+ const fail = (err) => {
2049
+ if (settled)
2050
+ return;
2051
+ settled = true;
2052
+ clearTimeout(timer);
2053
+ req?.destroy();
2054
+ releaseHopBytes(id, held);
2055
+ held = 0;
2056
+ reject(err);
2057
+ };
2058
+ const overCap = () => new GuardHopError("over-cap", `answer over ${MAX_GUARD_HOP_BYTES} bytes`);
2059
+ const timer = setTimeout(() => fail(new GuardHopError("timeout", "the guard hop passed its deadline")), Math.max(0, hop.deadline - Date.now()));
2060
+ try {
2061
+ req = send(url, { method: hop.method, headers: hop.headers, rejectUnauthorized: !hop.ignoreHTTPSErrors, agent: false }, (res) => {
2062
+ const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
2063
+ const decoder = encoding === "gzip" || encoding === "x-gzip" ? (0, node_zlib_1.createGunzip)({ flush: node_zlib_1.constants.Z_SYNC_FLUSH, finishFlush: node_zlib_1.constants.Z_SYNC_FLUSH }) : encoding === "br" ? (0, node_zlib_1.createBrotliDecompress)({ flush: node_zlib_1.constants.BROTLI_OPERATION_FLUSH, finishFlush: node_zlib_1.constants.BROTLI_OPERATION_FLUSH }) : encoding === "deflate" ? (0, node_zlib_1.createInflate)() : null;
2064
+ const body = decoder ? (0, node_stream_1.pipeline)(res, decoder, (err) => {
2065
+ if (err)
2066
+ fail(err);
2067
+ }) : res;
2068
+ const parked = [];
2069
+ let paused = false;
2070
+ let ended = false;
2071
+ const take = (chunk) => {
2072
+ if (settled)
2073
+ return;
2074
+ if (paused) {
2075
+ parked.push(chunk);
2076
+ return;
2077
+ }
2078
+ if (held + chunk.length > MAX_GUARD_HOP_BYTES) {
2079
+ fail(overCap());
2080
+ return;
2081
+ }
2082
+ const oldest = hopsInFlight.values().next().value === id;
2083
+ const room = oldest ? MAX_GUARD_BUFFERED_BYTES : MAX_GUARD_BUFFERED_BYTES - MAX_GUARD_HOP_BYTES;
2084
+ if (hopBytesInFlight + chunk.length > room) {
2085
+ paused = true;
2086
+ body.pause();
2087
+ parked.push(chunk);
2088
+ hopBudgetWaiters.push(drain);
2089
+ return;
2090
+ }
2091
+ hopBytesInFlight += chunk.length;
2092
+ held += chunk.length;
2093
+ chunks.push(chunk);
2094
+ };
2095
+ const drain = () => {
2096
+ paused = false;
2097
+ const queued = parked.splice(0);
2098
+ for (let i = 0; i < queued.length; i++) {
2099
+ take(queued[i]);
2100
+ if (settled)
2101
+ return;
2102
+ if (paused) {
2103
+ parked.push(...queued.slice(i + 1));
2104
+ return;
2105
+ }
2106
+ }
2107
+ if (ended)
2108
+ finish();
2109
+ else
2110
+ body.resume();
2111
+ };
2112
+ body.on("data", take);
2113
+ body.on("error", (err) => fail(err));
2114
+ res.on("error", (err) => fail(err));
2115
+ body.on("end", () => {
2116
+ ended = true;
2117
+ if (!paused)
2118
+ finish();
2119
+ });
2120
+ const finish = () => {
2121
+ if (settled)
2122
+ return;
2123
+ settled = true;
2124
+ clearTimeout(timer);
2125
+ const kept = held;
2126
+ let released = false;
2127
+ const headers = [];
2128
+ for (let i = 0; i + 1 < res.rawHeaders.length; i += 2)
2129
+ headers.push([res.rawHeaders[i], res.rawHeaders[i + 1]]);
2130
+ resolve({
2131
+ status: res.statusCode ?? 0,
2132
+ headers,
2133
+ body: Buffer.concat(chunks, kept),
2134
+ decoded: decoder !== null,
2135
+ release: () => {
2136
+ if (!released) {
2137
+ released = true;
2138
+ releaseHopBytes(id, kept);
2139
+ }
2140
+ }
2141
+ });
2142
+ };
2143
+ });
2144
+ req.on("error", (err) => fail(err));
2145
+ req.end(hop.body ?? void 0);
2146
+ } catch (err) {
2147
+ fail(err instanceof Error ? err : new Error(String(err)));
2148
+ }
2149
+ });
2150
+ }
2151
+ function contextIgnoresHttpsErrors(context) {
2152
+ try {
2153
+ return context._options?.ignoreHTTPSErrors === true;
2154
+ } catch {
2155
+ return false;
2156
+ }
2157
+ }
2158
+ function hopRequestHeaders(pageHeaders, cookie) {
2159
+ const out = { accept: "*/*" };
2160
+ for (const [name, value] of Object.entries(pageHeaders)) {
2161
+ const key = name.toLowerCase();
2162
+ if (key.startsWith(":") || HOP_ONLY_HEADERS.has(key) || key === "cookie")
2163
+ continue;
2164
+ out[key] = value;
2165
+ }
2166
+ out["accept-encoding"] = "gzip, deflate, br";
2167
+ if (cookie)
2168
+ out.cookie = cookie;
2169
+ return out;
2170
+ }
2171
+ function cookiePairs(header) {
2172
+ return header.split(";").flatMap((part) => {
2173
+ const eq = part.indexOf("=");
2174
+ return eq > 0 ? [[part.slice(0, eq).trim(), part.slice(eq + 1).trim()]] : [];
2175
+ });
2176
+ }
2177
+ function hopCookie(pageCookie, contextCookies, setCookies) {
2178
+ const jar = /* @__PURE__ */ new Map();
2179
+ if (pageCookie)
2180
+ for (const [name, value] of cookiePairs(pageCookie))
2181
+ jar.set(name, value);
2182
+ else
2183
+ for (const c of contextCookies)
2184
+ jar.set(c.name, c.value);
2185
+ for (const line of setCookies) {
2186
+ const [pair, ...attrs] = line.split(";");
2187
+ const eq = pair.indexOf("=");
2188
+ if (eq <= 0)
2189
+ continue;
2190
+ const name = pair.slice(0, eq).trim();
2191
+ const gone = attrs.some((a) => {
2192
+ const [k, v = ""] = a.split("=");
2193
+ const key = k.trim().toLowerCase();
2194
+ return key === "max-age" && Number(v.trim()) <= 0 || key === "expires" && Date.parse(v.trim()) < Date.now();
2195
+ });
2196
+ if (gone)
2197
+ jar.delete(name);
2198
+ else
2199
+ jar.set(name, pair.slice(eq + 1).trim());
2200
+ }
2201
+ return [...jar].map(([name, value]) => `${name}=${value}`).join("; ");
2202
+ }
2203
+ function setCookiesOf(answer) {
2204
+ return answer.headers.filter(([name]) => name.toLowerCase() === "set-cookie").map(([, value]) => value);
2205
+ }
2206
+ function fulfillHeaders(answer, earlierSetCookies) {
2207
+ const out = {};
2208
+ const cookies = [...earlierSetCookies];
2209
+ for (const [name, value] of answer.headers) {
2210
+ const key = name.toLowerCase();
2211
+ if (HOP_ONLY_HEADERS.has(key) || answer.decoded && key === "content-encoding")
2212
+ continue;
2213
+ if (key === "set-cookie") {
2214
+ cookies.push(value);
2215
+ continue;
2216
+ }
2217
+ out[key] = key in out ? `${out[key]}, ${value}` : value;
2218
+ }
2219
+ if (cookies.length > 0)
2220
+ out["set-cookie"] = cookies.join("\n");
2221
+ return out;
2222
+ }
1393
2223
  var cutAtClose = /* @__PURE__ */ new WeakSet();
1394
- async function sendCheckingRedirect(route, guard) {
1395
- const first = factsOf(route.request());
2224
+ async function sendCheckingRedirect(route, guard, context) {
2225
+ const request = route.request();
2226
+ const first = factsOf(request);
2227
+ const deadline = Date.now() + guard.redirectCheckTimeoutMs;
1396
2228
  let facts = first;
2229
+ let pageHeaders = {};
2230
+ let postBody = null;
2231
+ const setCookies = [];
1397
2232
  for (let hop = 0; ; hop++) {
1398
- let response;
2233
+ let answer;
1399
2234
  try {
1400
- response = await route.fetch(hop === 0 ? { maxRedirects: 0, timeout: REDIRECT_CHECK_TIMEOUT_MS } : { url: facts.url, maxRedirects: 0, timeout: REDIRECT_CHECK_TIMEOUT_MS });
2235
+ if (hop === 0) {
2236
+ pageHeaders = request.headers();
2237
+ postBody = request.postDataBuffer();
2238
+ }
2239
+ const pageCookie = hop === 0 ? Object.entries(pageHeaders).find(([name]) => name.toLowerCase() === "cookie")?.[1] : void 0;
2240
+ const cookie = hopCookie(pageCookie, pageCookie ? [] : await context.cookies(facts.url), setCookies);
2241
+ answer = await sendGuardHop({
2242
+ url: facts.url,
2243
+ method: facts.method,
2244
+ headers: hopRequestHeaders(pageHeaders, cookie),
2245
+ body: postBody,
2246
+ ignoreHTTPSErrors: contextIgnoresHttpsErrors(context),
2247
+ deadline
2248
+ });
1401
2249
  } catch (err) {
1402
2250
  if (cutAtClose.has(route))
1403
2251
  return;
1404
- guard.noteRedirectCheckFailed(facts, err, REDIRECT_CHECK_TIMEOUT_MS);
2252
+ guard.noteRedirectCheckFailed(facts, err, guard.redirectCheckTimeoutMs);
1405
2253
  await route.abort("failed").catch(() => {
1406
2254
  });
1407
2255
  return;
1408
2256
  }
1409
- if (cutAtClose.has(route))
1410
- return;
1411
- const status = response.status();
1412
- const location = response.headers()["location"];
1413
- const next = REDIRECT_STATUSES.has(status) && location ? redirectFacts(facts, status, location) : null;
1414
- if (!next) {
1415
- await route.fulfill({ response }).catch(() => {
1416
- });
1417
- return;
1418
- }
1419
- let decision;
1420
2257
  try {
1421
- decision = guard.decide(next);
1422
- } catch {
1423
- decision = { action: "write", rule: "decision-error", record: { method: next.method, host: "", path: (0, survey_network_guard_js_1.maskedPath)(next.url), keys: [], rule: "decision-error" } };
1424
- }
1425
- if (decision.action === "write") {
1426
- if (decision.record)
1427
- guard.note(decision.record);
1428
- if (guard.holds(decision.rule)) {
2258
+ if (cutAtClose.has(route))
2259
+ return;
2260
+ const status = answer.status;
2261
+ const location = answer.headers.find(([name]) => name.toLowerCase() === "location")?.[1];
2262
+ const next = REDIRECT_STATUSES.has(status) && location ? redirectFacts(facts, status, location) : null;
2263
+ if (!next) {
2264
+ await route.fulfill({ status, headers: fulfillHeaders(answer, setCookies), body: answer.body }).catch(() => {
2265
+ });
2266
+ return;
2267
+ }
2268
+ let decision;
2269
+ try {
2270
+ decision = guard.decide(next);
2271
+ } catch {
2272
+ decision = { action: "write", rule: "decision-error", record: { method: next.method, host: "", path: (0, survey_network_guard_js_1.maskedPath)(next.url), keys: [], rule: "decision-error" } };
2273
+ }
2274
+ if (decision.action === "write") {
2275
+ if (decision.record)
2276
+ guard.note(decision.record);
2277
+ if (guard.holds(decision.rule)) {
2278
+ await route.abort("blockedbyclient").catch(() => {
2279
+ });
2280
+ return;
2281
+ }
2282
+ }
2283
+ if (READ_METHODS.has(next.method.toUpperCase())) {
2284
+ await route.fulfill({ status, headers: { ...fulfillHeaders(answer, setCookies), location: next.url }, body: answer.body }).catch(() => {
2285
+ });
2286
+ return;
2287
+ }
2288
+ if (!sameOrigin(next.url, first.url)) {
2289
+ const target = new URL(next.url);
2290
+ guard.note(describe(next, target, isAppSite(target.hostname.toLowerCase(), guard.policy) ? "app-write" : "third-party-write"));
1429
2291
  await route.abort("blockedbyclient").catch(() => {
1430
2292
  });
1431
2293
  return;
1432
2294
  }
2295
+ if (hop + 1 >= MAX_GUARD_REDIRECTS) {
2296
+ await route.abort("failed").catch(() => {
2297
+ });
2298
+ return;
2299
+ }
2300
+ setCookies.push(...setCookiesOf(answer));
2301
+ facts = next;
2302
+ } finally {
2303
+ answer.release();
1433
2304
  }
1434
- if (READ_METHODS.has(next.method.toUpperCase())) {
1435
- await route.fulfill({ response, headers: { ...response.headers(), location: next.url } }).catch(() => {
1436
- });
1437
- return;
1438
- }
1439
- if (!sameOrigin(next.url, first.url)) {
1440
- const target = new URL(next.url);
1441
- guard.note(describe(next, target, isAppSite(target.hostname.toLowerCase(), guard.policy) ? "app-write" : "third-party-write"));
1442
- await route.abort("blockedbyclient").catch(() => {
1443
- });
1444
- return;
1445
- }
1446
- if (hop + 1 >= MAX_GUARD_REDIRECTS) {
1447
- await route.abort("failed").catch(() => {
1448
- });
1449
- return;
1450
- }
1451
- facts = next;
1452
2305
  }
1453
2306
  }
1454
2307
  function sameRouteUrl(a, b) {
@@ -1468,8 +2321,9 @@ var require_run_write_guard = __commonJS({
1468
2321
  };
1469
2322
  Object.assign(context, {
1470
2323
  route: async (url, handler, options) => {
1471
- await route(url, handler, options);
2324
+ const disposable = await route(url, handler, options);
1472
2325
  others.push({ url, handler });
2326
+ return disposable;
1473
2327
  },
1474
2328
  unroute: async (url, handler) => {
1475
2329
  if (handler === void 0 && sameRouteUrl(url, GUARD_ROUTE)) {
@@ -1662,7 +2516,7 @@ var require_run_write_guard = __commonJS({
1662
2516
  }
1663
2517
  }
1664
2518
  if (decision.action === "allow" && !READ_METHODS.has(request.method().toUpperCase()) && checksRedirect(decision.rule, request.url(), guard.policy)) {
1665
- await sendCheckingRedirect(route, guard);
2519
+ await sendCheckingRedirect(route, guard, context);
1666
2520
  return;
1667
2521
  }
1668
2522
  await route.fallback().catch(() => {
@@ -1802,6 +2656,7 @@ var require_run_write_guard = __commonJS({
1802
2656
  }
1803
2657
  function buildRunWriteGuardHook(policy, modulePath, signInSecretNames = [], opts = {}) {
1804
2658
  const withContext = opts.browserContext !== false;
2659
+ const hookTest = opts.hookTest ?? "test";
1805
2660
  const names = signInSecretNames.filter((n) => /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(n)).slice(0, 16);
1806
2661
  return `
1807
2662
  import { createRequire as __qaWriteGuardRequire } from 'node:module';
@@ -1824,7 +2679,7 @@ try {
1824
2679
  } catch (err) {
1825
2680
  __qaWriteGuardError = err;
1826
2681
  }
1827
- test.beforeEach(async (${withContext ? "{ context }" : ""}) => {
2682
+ ${hookTest}.beforeEach(async (${withContext ? "{ context }" : ""}) => {
1828
2683
  try {
1829
2684
  if (__qaWriteGuardError) throw __qaWriteGuardError;
1830
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"}
@@ -1833,7 +2688,7 @@ test.beforeEach(async (${withContext ? "{ context }" : ""}) => {
1833
2688
  ${policy.mode === "block" ? "throw err;" : "// observe: never fail the test"}
1834
2689
  }
1835
2690
  });${withContext && policy.mode === "block" ? `
1836
- test.afterEach(async ({ context }) => {
2691
+ ${hookTest}.afterEach(async ({ context }) => {
1837
2692
  try { if (__qaWriteGuard) await __qaWriteGuard.clearRunWriteRelay(context); } catch { /* the context closes next */ }
1838
2693
  });` : ""}
1839
2694
  `;