tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

fledge-util.sub.js (50013B)


      1 "use strict";
      2 
      3 const BASE_URL = document.baseURI.substring(0, document.baseURI.lastIndexOf('/') + 1);
      4 const BASE_PATH = (new URL(BASE_URL)).pathname;
      5 
      6 // Allow overriding to allow other repositories to use these utility functions.
      7 let RESOURCE_PATH = `${BASE_PATH}resources/`
      8 
      9 const DEFAULT_INTEREST_GROUP_NAME = 'default name';
     10 
     11 // Unlike other URLs, trusted signals URLs can't have query strings
     12 // that are set by tests, since FLEDGE controls it entirely, so tests that
     13 // exercise them use a fixed URL string. Note that FLEDGE adds query
     14 // params when requesting these URLs, and the python scripts use these
     15 // to construct the response.
     16 const TRUSTED_BIDDING_SIGNALS_URL =
     17    `${BASE_URL}resources/trusted-bidding-signals.py`;
     18 const TRUSTED_SCORING_SIGNALS_URL =
     19    `${BASE_URL}resources/trusted-scoring-signals.py`;
     20 
     21 // Other origins that should all be distinct from the main frame origin
     22 // that the tests start with.
     23 const OTHER_ORIGIN1 = 'https://{{hosts[alt][]}}:{{ports[https][0]}}';
     24 const OTHER_ORIGIN2 = 'https://{{hosts[alt][]}}:{{ports[https][1]}}';
     25 const OTHER_ORIGIN3 = 'https://{{hosts[][]}}:{{ports[https][1]}}';
     26 const OTHER_ORIGIN4 = 'https://{{hosts[][www]}}:{{ports[https][0]}}';
     27 const OTHER_ORIGIN5 = 'https://{{hosts[][www]}}:{{ports[https][1]}}';
     28 const OTHER_ORIGIN6 = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}';
     29 const OTHER_ORIGIN7 = 'https://{{hosts[alt][www]}}:{{ports[https][1]}}';
     30 
     31 // Trusted signals hosted on OTHER_ORIGIN1
     32 const CROSS_ORIGIN_TRUSTED_BIDDING_SIGNALS_URL = OTHER_ORIGIN1 + BASE_PATH +
     33    'resources/trusted-bidding-signals.py';
     34 const CROSS_ORIGIN_TRUSTED_SCORING_SIGNALS_URL = OTHER_ORIGIN1 + BASE_PATH +
     35    'resources/trusted-scoring-signals.py';
     36 
     37 // Creates a URL that will be sent to the URL request tracker script.
     38 // `uuid` is used to identify the stash shard to use.
     39 // `dispatch` affects what the tracker script does.
     40 // `id` can be used to uniquely identify tracked requests. It has no effect
     41 //     on behavior of the script; it only serves to make the URL unique.
     42 // `id` will always be the last query parameter.
     43 function createTrackerURL(origin, uuid, dispatch, id = null) {
     44  let url = new URL(`${origin}${RESOURCE_PATH}request-tracker.py`);
     45  let search = `uuid=${uuid}&dispatch=${dispatch}`;
     46  if (id)
     47    search += `&id=${id}`;
     48  url.search = search;
     49  return url.toString();
     50 }
     51 
     52 // Create a URL that when fetches clears tracked URLs. Note that the origin
     53 // doesn't matter - it will clean up all tracked URLs with the provided uuid,
     54 // regardless of origin they were fetched from.
     55 function createCleanupURL(uuid) {
     56  return createTrackerURL(window.location.origin, uuid, 'clean_up');
     57 }
     58 
     59 // Create tracked bidder/seller URLs. The only difference is the prefix added
     60 // to the `id` passed to createTrackerURL. The optional `id` field allows
     61 // multiple bidder/seller report URLs to be distinguishable from each other.
     62 // `id` will always be the last query parameter.
     63 function createBidderReportURL(uuid, id = '1', origin = window.location.origin) {
     64  return createTrackerURL(origin, uuid, `track_get`, `bidder_report_${id}`);
     65 }
     66 function createSellerReportURL(uuid, id = '1', origin = window.location.origin) {
     67  return createTrackerURL(origin, uuid, `track_get`, `seller_report_${id}`);
     68 }
     69 
     70 function createHighestScoringOtherBidReportURL(uuid, highestScoringOtherBid) {
     71  return createSellerReportURL(uuid) + '&highestScoringOtherBid=' + Math.round(highestScoringOtherBid);
     72 }
     73 
     74 // Much like above ReportURL methods, except designed for beacons, which
     75 // are expected to be POSTs.
     76 function createBidderBeaconURL(uuid, id = '1', origin = window.location.origin) {
     77  return createTrackerURL(origin, uuid, `track_post`, `bidder_beacon_${id}`);
     78 }
     79 function createSellerBeaconURL(uuid, id = '1', origin = window.location.origin) {
     80  return createTrackerURL(origin, uuid, `track_post`, `seller_beacon_${id}`);
     81 }
     82 
     83 function createDirectFromSellerSignalsURL(origin = window.location.origin) {
     84  let url = new URL(`${origin}${RESOURCE_PATH}direct-from-seller-signals.py`);
     85  return url.toString();
     86 }
     87 
     88 function createUpdateURL(params = {}) {
     89  let origin = window.location.origin;
     90  let url = new URL(`${origin}${RESOURCE_PATH}update-url.py`);
     91  url.searchParams.append('body', params.body);
     92  url.searchParams.append('uuid', params.uuid);
     93 
     94  return url.toString();
     95 }
     96 
     97 // Generates a UUID and registers a cleanup method with the test fixture to
     98 // request a URL from the request tracking script that clears all data
     99 // associated with the generated uuid when requested.
    100 function generateUuid(test) {
    101  let uuid = token();
    102  test.add_cleanup(async () => {
    103    let response = await fetch(createCleanupURL(uuid),
    104                               { credentials: 'omit', mode: 'cors' });
    105    assert_equals(await response.text(), 'cleanup complete',
    106                  `Sever state cleanup failed`);
    107  });
    108  return uuid;
    109 }
    110 
    111 // Helper to fetch "tracked_data" URL to fetch all data recorded by the
    112 // tracker URL associated with "uuid". Throws on error, including if
    113 // the retrieved object's errors field is non-empty.
    114 async function fetchTrackedData(uuid) {
    115  let trackedRequestsURL = createTrackerURL(window.location.origin, uuid,
    116                                            'tracked_data');
    117  let response = await fetch(trackedRequestsURL,
    118                             { credentials: 'omit', mode: 'cors' });
    119  let trackedData = await response.json();
    120 
    121  // Fail on fetch error.
    122  if (trackedData.error) {
    123    throw trackedRequestsURL + ' fetch failed:' + JSON.stringify(trackedData);
    124  }
    125 
    126  // Fail on errors reported by the tracker script.
    127  if (trackedData.errors.length > 0) {
    128    throw 'Errors reported by request-tracker.py:' +
    129        JSON.stringify(trackedData.errors);
    130  }
    131 
    132  return trackedData;
    133 }
    134 
    135 // Repeatedly requests "tracked_data" URL until exactly the entries in
    136 // "expectedRequests" have been observed by the request tracker script (in
    137 // any order, since report URLs are not guaranteed to be sent in any order).
    138 //
    139 // Elements of `expectedRequests` should either be URLs, in the case of GET
    140 // requests, or "<URL>, body: <body>" in the case of POST requests.
    141 //
    142 // `filter` will be applied to the array of tracked requests.
    143 //
    144 // If any other strings are received from the tracking script, or the tracker
    145 // script reports an error, fails the test.
    146 async function waitForObservedRequests(uuid, expectedRequests, filter) {
    147  // Sort array for easier comparison, as observed request order does not
    148  // matter, and replace UUID to print consistent errors on failure.
    149  expectedRequests = expectedRequests.map((url) => url.replace(uuid, '<uuid>')).sort();
    150 
    151  while (true) {
    152    let trackedData = await fetchTrackedData(uuid);
    153 
    154    // Clean up "trackedRequests" in same manner as "expectedRequests".
    155    let trackedRequests = trackedData.trackedRequests.map(
    156                              (url) => url.replace(uuid, '<uuid>')).sort();
    157 
    158    if (filter) {
    159      trackedRequests = trackedRequests.filter(filter);
    160    }
    161 
    162    // If fewer than total number of expected requests have been observed,
    163    // compare what's been received so far, to have a greater chance to fail
    164    // rather than hang on error.
    165    for (const trackedRequest of trackedRequests) {
    166      assert_in_array(trackedRequest, expectedRequests);
    167    }
    168 
    169    // If expected number of requests have been observed, compare with list of
    170    // all expected requests and exit. This check was previously before the for loop,
    171    // but was swapped in order to avoid flakiness with failing tests and their
    172    // respective *-expected.txt.
    173    if (trackedRequests.length >= expectedRequests.length) {
    174      assert_array_equals(trackedRequests, expectedRequests);
    175      break;
    176    }
    177  }
    178 }
    179 
    180 
    181 // Similar to waitForObservedRequests, but ignore forDebuggingOnly reports.
    182 async function waitForObservedRequestsIgnoreDebugOnlyReports(
    183    uuid, expectedRequests) {
    184  return waitForObservedRequests(
    185      uuid,
    186      expectedRequests,
    187      request => !request.includes('forDebuggingOnly'));
    188 }
    189 
    190 // Creates a bidding script with the provided code in the method bodies. The
    191 // bidding script's generateBid() method will return a bid of 9 for the first
    192 // ad, after the passed in code in the "generateBid" input argument has been
    193 // run, unless it returns something or throws.
    194 //
    195 // The default reportWin() method is empty.
    196 function createBiddingScriptURL(params = {}) {
    197  let origin = params.origin ? params.origin : new URL(BASE_URL).origin;
    198  let url = new URL(`${origin}${RESOURCE_PATH}bidding-logic.sub.py`);
    199  // These checks use "!=" to ignore null and not provided arguments, while
    200  // treating '' as a valid argument.
    201  if (params.generateBid != null)
    202    url.searchParams.append('generateBid', params.generateBid);
    203  if (params.reportWin != null)
    204    url.searchParams.append('reportWin', params.reportWin);
    205  if (params.reportAdditionalBidWin != null)
    206    url.searchParams.append('reportAdditionalBidWin', params.reportAdditionalBidWin);
    207  if (params.error != null)
    208    url.searchParams.append('error', params.error);
    209  if (params.bid != null)
    210    url.searchParams.append('bid', params.bid);
    211  if (params.bidCurrency != null)
    212    url.searchParams.append('bidCurrency', params.bidCurrency);
    213  if (params.allowComponentAuction != null)
    214    url.searchParams.append('allowComponentAuction', JSON.stringify(params.allowComponentAuction))
    215  return url.toString();
    216 }
    217 
    218 // TODO: Make this return a valid WASM URL.
    219 function createBiddingWasmHelperURL(params = {}) {
    220  let origin = params.origin ? params.origin : new URL(BASE_URL).origin;
    221  return `${origin}${RESOURCE_PATH}bidding-wasmlogic.wasm`;
    222 }
    223 
    224 // Creates a decision script with the provided code in the method bodies. The
    225 // decision script's scoreAd() method will reject ads with renderURLs that
    226 // don't ends with "uuid", and will return a score equal to the bid, after the
    227 // passed in code in the "scoreAd" input argument has been run, unless it
    228 // returns something or throws.
    229 //
    230 // The default reportResult() method is empty.
    231 function createDecisionScriptURL(uuid, params = {}) {
    232  let origin = params.origin ? params.origin : new URL(BASE_URL).origin;
    233  let url = new URL(`${origin}${RESOURCE_PATH}decision-logic.sub.py`);
    234  url.searchParams.append('uuid', uuid);
    235  // These checks use "!=" to ignore null and not provided arguments, while
    236  // treating '' as a valid argument.
    237  if (params.scoreAd != null)
    238    url.searchParams.append('scoreAd', params.scoreAd);
    239  if (params.reportResult != null)
    240    url.searchParams.append('reportResult', params.reportResult);
    241  if (params.error != null)
    242    url.searchParams.append('error', params.error);
    243  if (params.permitCrossOriginTrustedSignals != null) {
    244    url.searchParams.append('permit-cross-origin-trusted-signals',
    245                            params.permitCrossOriginTrustedSignals);
    246  }
    247  return url.toString();
    248 }
    249 
    250 // Creates a renderURL for an ad that runs the passed in "script". "uuid" has
    251 // no effect, beyond making the URL distinct between tests, and being verified
    252 // by the decision logic script before accepting a bid. "uuid" is expected to
    253 // be last.  "signalsParams" also has no effect, but is used by
    254 // trusted-scoring-signals.py to affect the response.
    255 function createRenderURL(uuid, script, signalsParams, origin) {
    256  // These checks use "==" and "!=" to ignore null and not provided
    257  // arguments, while treating '' as a valid argument.
    258  if (origin == null)
    259    origin = new URL(BASE_URL).origin;
    260  let url = new URL(`${origin}${RESOURCE_PATH}fenced-frame.sub.py`);
    261  if (script != null)
    262    url.searchParams.append('script', script);
    263  if (signalsParams != null)
    264    url.searchParams.append('signalsParams', signalsParams);
    265  url.searchParams.append('uuid', uuid);
    266  return url.toString();
    267 }
    268 
    269 // Creates an interest group owned by "origin" with a bidding logic URL located
    270 // on "origin" as well. Uses standard render and report URLs, which are not
    271 // necessarily on "origin". "interestGroupOverrides" may be used to override any
    272 // field of the created interest group.
    273 function createInterestGroupForOrigin(uuid, origin,
    274                                      interestGroupOverrides = {}) {
    275  return {
    276    owner: origin,
    277    name: DEFAULT_INTEREST_GROUP_NAME,
    278    biddingLogicURL: createBiddingScriptURL(
    279        { origin: origin,
    280          reportWin: `sendReportTo('${createBidderReportURL(uuid)}');` }),
    281    ads: [{ renderURL: createRenderURL(uuid) }],
    282    ...interestGroupOverrides
    283  };
    284 }
    285 
    286 // Waits for the join command to complete. Adds cleanup command to `test` to
    287 // leave the interest group when the test completes.
    288 async function joinInterestGroupWithoutDefaults(test, interestGroup,
    289                                                durationSeconds = 60) {
    290  await navigator.joinAdInterestGroup(interestGroup, durationSeconds);
    291  await makeInterestGroupKAnonymous(interestGroup);
    292  test.add_cleanup(
    293    async () => { await navigator.leaveAdInterestGroup(interestGroup); });
    294 }
    295 
    296 // Joins an interest group that, by default, is owned by the current frame's
    297 // origin, is named DEFAULT_INTEREST_GROUP_NAME, has a bidding script that
    298 // issues a bid of 9 with a renderURL of "https://not.checked.test/${uuid}",
    299 // and sends a report to createBidderReportURL(uuid) if it wins. Waits for the
    300 // join command to complete. Adds cleanup command to `test` to leave the
    301 // interest group when the test completes.
    302 //
    303 // `interestGroupOverrides` may be used to override fields in the joined
    304 // interest group.
    305 async function joinInterestGroup(test, uuid, interestGroupOverrides = {},
    306                                 durationSeconds = 60) {
    307  await joinInterestGroupWithoutDefaults(
    308      test, createInterestGroupForOrigin(
    309          uuid, window.location.origin, interestGroupOverrides),
    310      durationSeconds);
    311 }
    312 
    313 // Joins a negative interest group with the specified owner, name, and
    314 // additionalBidKey. Because these are the only valid fields for a negative
    315 // interest groups, this function doesn't expose an 'overrides' parameter.
    316 // Adds cleanup command to `test` to leave the interest group when the test
    317 // completes.
    318 async function joinNegativeInterestGroup(
    319    test, owner, name, additionalBidKey) {
    320  let interestGroup = {
    321    owner: owner,
    322    name: name,
    323    additionalBidKey: additionalBidKey
    324  };
    325  if (owner !== window.location.origin) {
    326    let iframe = await createIframe(test, owner, 'join-ad-interest-group');
    327    await runInFrame(
    328      test, iframe,
    329      `await joinInterestGroupWithoutDefaults(` +
    330          `test_instance, ${JSON.stringify(interestGroup)})`);
    331  } else {
    332    await joinInterestGroupWithoutDefaults(test, interestGroup);
    333  }
    334 }
    335 
    336 // Similar to joinInterestGroup, but leaves the interest group instead.
    337 // Generally does not need to be called manually when using
    338 // "joinInterestGroup()".
    339 async function leaveInterestGroup(interestGroupOverrides = {}) {
    340  let interestGroup = {
    341    owner: window.location.origin,
    342    name: DEFAULT_INTEREST_GROUP_NAME,
    343    ...interestGroupOverrides
    344  };
    345 
    346  await navigator.leaveAdInterestGroup(interestGroup);
    347 }
    348 
    349 // Runs a FLEDGE auction and returns the result. By default, the seller is the
    350 // current frame's origin, and the only buyer is as well. The seller script
    351 // rejects bids for URLs that don't contain "uuid" (to avoid running into issues
    352 // with any interest groups from other tests), and reportResult() sends a report
    353 // to createSellerReportURL(uuid).
    354 //
    355 // `auctionConfigOverrides` may be used to override fields in the auction
    356 // configuration.
    357 async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides = {}) {
    358  let auctionConfig = {
    359    seller: window.location.origin,
    360    decisionLogicURL: createDecisionScriptURL(
    361        uuid,
    362        { reportResult: `sendReportTo('${createSellerReportURL(uuid)}');` }),
    363    interestGroupBuyers: [window.location.origin],
    364    resolveToConfig: true,
    365    ...auctionConfigOverrides
    366  };
    367  return await navigator.runAdAuction(auctionConfig);
    368 }
    369 
    370 // Checks that await'ed return value of runAdAuction() denotes a successful
    371 // auction with a winner.
    372 function expectSuccess(config) {
    373  assert_true(config !== null, `Auction unexpectedly had no winner`);
    374  assert_true(
    375      config instanceof FencedFrameConfig,
    376      `Wrong value type returned from auction: ${config.constructor.type}`);
    377 }
    378 
    379 // Checks that await'ed return value of runAdAuction() denotes an auction
    380 // without a winner (but no fatal error).
    381 function expectNoWinner(result) {
    382  assert_true(result === null, 'Auction unexpectedly had a winner');
    383 }
    384 
    385 // Wrapper around runBasicFledgeAuction() that runs an auction with the specified
    386 // arguments, expecting the auction to have a winner. Returns the FencedFrameConfig
    387 // from the auction.
    388 async function runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides = {}) {
    389  let config = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
    390  expectSuccess(config);
    391  return config;
    392 }
    393 
    394 // Wrapper around runBasicFledgeAuction() that runs an auction with the specified
    395 // arguments, expecting the auction to have no winner.
    396 async function runBasicFledgeTestExpectingNoWinner(
    397    test, uuid, auctionConfigOverrides = {}) {
    398  let result = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
    399  expectNoWinner(result);
    400 }
    401 
    402 // Creates a fenced frame and applies fencedFrameConfig to it. Also adds a cleanup
    403 // method to destroy the fenced frame at the end of the current test.
    404 function createAndNavigateFencedFrame(test, fencedFrameConfig) {
    405  let fencedFrame = document.createElement('fencedframe');
    406  fencedFrame.mode = 'opaque-ads';
    407  fencedFrame.config = fencedFrameConfig;
    408  document.body.appendChild(fencedFrame);
    409  test.add_cleanup(() => { document.body.removeChild(fencedFrame); });
    410 }
    411 
    412 // Calls runBasicFledgeAuction(), expecting the auction to have a winner.
    413 // Creates a fenced frame that will be destroyed on completion of "test", and
    414 // navigates it to the URN URL returned by the auction. Does not wait for the
    415 // fenced frame to finish loading, since there's no API that can do that.
    416 async function runBasicFledgeAuctionAndNavigate(test, uuid,
    417                                                auctionConfigOverrides = {}) {
    418  let config = await runBasicFledgeTestExpectingWinner(test, uuid,
    419                                                       auctionConfigOverrides);
    420  createAndNavigateFencedFrame(test, config);
    421 }
    422 
    423 // Joins an interest group and runs an auction, expecting a winner to be
    424 // returned. "testConfig" can optionally modify the uuid, interest group or
    425 // auctionConfig.
    426 async function joinGroupAndRunBasicFledgeTestExpectingWinner(test, testConfig = {}) {
    427  const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test);
    428  await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
    429  await runBasicFledgeTestExpectingWinner(test, uuid, testConfig.auctionConfigOverrides);
    430 }
    431 
    432 // Joins an interest group and runs an auction, expecting no winner to be
    433 // returned. "testConfig" can optionally modify the uuid, interest group or
    434 // auctionConfig.
    435 async function joinGroupAndRunBasicFledgeTestExpectingNoWinner(test, testConfig = {}) {
    436  const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test);
    437  await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
    438  await runBasicFledgeTestExpectingNoWinner(test, uuid, testConfig.auctionConfigOverrides);
    439 }
    440 
    441 // Test helper for report phase of auctions that lets the caller specify the
    442 // body of reportResult() and reportWin(). Passing in null will cause there
    443 // to be no reportResult() or reportWin() method.
    444 //
    445 // If the "SuccessCondition" fields are non-null and evaluate to false in
    446 // the corresponding reporting method, the report is sent to an error URL.
    447 // Otherwise, the corresponding 'reportResult' / 'reportWin' values are run.
    448 //
    449 // `codeToInsert` is a JS object that contains the following fields to control
    450 // the code generated for the auction worklet:
    451 // scoreAd - function body for scoreAd() seller worklet function
    452 // reportResultSuccessCondition - Success condition to trigger reportResult()
    453 // reportResult - function body for reportResult() seller worklet function
    454 // generateBid - function body for generateBid() buyer worklet function
    455 // reportWinSuccessCondition - Success condition to trigger reportWin()
    456 // decisionScriptURLOrigin - Origin of decision script URL
    457 // reportWin - function body for reportWin() buyer worklet function
    458 //
    459 // Additionally the following fields can be added to check for errors during the
    460 // execution of the corresponding worklets:
    461 // reportWinSuccessCondition - boolean condition added to reportWin() in the
    462 // buyer worklet that triggers a sendReportTo() to an 'error' URL if not met.
    463 // reportResultSuccessCondition - boolean condition added to reportResult() in
    464 // the seller worklet that triggers a sendReportTo() to an 'error' URL if not
    465 // met.
    466 //
    467 // `renderURLOverride` allows the ad URL of the joined InterestGroup to
    468 // to be set by the caller.
    469 //
    470 // `auctionConfigOverrides` may be used to override fields in the auction
    471 // configuration.
    472 //
    473 // Requesting error report URLs causes waitForObservedRequests() to throw
    474 // rather than hang.
    475 async function runReportTest(test, uuid, codeToInsert, expectedReportURLs,
    476    renderURLOverride, auctionConfigOverrides) {
    477  let scoreAd = codeToInsert.scoreAd;
    478  let reportResultSuccessCondition = codeToInsert.reportResultSuccessCondition;
    479  let reportResult = codeToInsert.reportResult;
    480  let generateBid = codeToInsert.generateBid;
    481  let reportWinSuccessCondition = codeToInsert.reportWinSuccessCondition;
    482  let reportWin = codeToInsert.reportWin;
    483  let decisionScriptURLOrigin = codeToInsert.decisionScriptURLOrigin;
    484 
    485  if (reportResultSuccessCondition) {
    486    reportResult = `if (!(${reportResultSuccessCondition})) {
    487                      sendReportTo('${createSellerReportURL(uuid, 'error')}');
    488                      return false;
    489                    }
    490                    ${reportResult}`;
    491  }
    492  let decisionScriptURLParams = {};
    493 
    494  if (scoreAd !== undefined) {
    495    decisionScriptURLParams.scoreAd = scoreAd;
    496  }
    497 
    498  if (reportResult !== null)
    499    decisionScriptURLParams.reportResult = reportResult;
    500  else
    501    decisionScriptURLParams.error = 'no-reportResult';
    502 
    503  if (decisionScriptURLOrigin !== undefined) {
    504    decisionScriptURLParams.origin = decisionScriptURLOrigin;
    505  }
    506 
    507  if (reportWinSuccessCondition) {
    508    reportWin = `if (!(${reportWinSuccessCondition})) {
    509                   sendReportTo('${createBidderReportURL(uuid, 'error')}');
    510                   return false;
    511                 }
    512                 ${reportWin}`;
    513  }
    514  let biddingScriptURLParams = {};
    515 
    516  if (generateBid !== undefined) {
    517    biddingScriptURLParams.generateBid = generateBid;
    518  }
    519 
    520  if (reportWin !== null)
    521    biddingScriptURLParams.reportWin = reportWin;
    522  else
    523    biddingScriptURLParams.error = 'no-reportWin';
    524 
    525  let interestGroupOverrides =
    526      { biddingLogicURL: createBiddingScriptURL(biddingScriptURLParams) };
    527  if (renderURLOverride)
    528    interestGroupOverrides.ads = [{ renderURL: renderURLOverride }]
    529 
    530  await joinInterestGroup(test, uuid, interestGroupOverrides);
    531 
    532  if (auctionConfigOverrides === undefined) {
    533    auctionConfigOverrides =
    534        { decisionLogicURL: createDecisionScriptURL(uuid, decisionScriptURLParams) };
    535  } else if (auctionConfigOverrides.decisionLogicURL === undefined) {
    536    auctionConfigOverrides.decisionLogicURL =
    537        createDecisionScriptURL(uuid, decisionScriptURLParams);
    538  }
    539 
    540  await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
    541  await waitForObservedRequests(uuid, expectedReportURLs);
    542 }
    543 
    544 // Helper function for running a standard test of the additional bid and
    545 // negative targeting features. This helper verifies that the auction produces a
    546 // winner. It takes the following arguments:
    547 // - test/uuid: the test object and uuid from the test case (see generateUuid)
    548 // - buyers: array of strings, each a domain for a buyer participating in this
    549 //       auction
    550 // - auctionNonce: string, the auction nonce for this auction, typically
    551 //       retrieved from a prior call to navigator.createAuctionNonce
    552 // - additionalBidsPromise: promise resolving to undefined, to be resolved when
    553 //       the additional bids have been retrieved with fetch().
    554 // - highestScoringOtherBid: the amount of the second-highest bid,
    555 //       or zero if there's no second-highest bid
    556 // - winningAdditionalBidId: the label of the winning bid
    557 async function runAdditionalBidTest(test, uuid, buyers, auctionNonce,
    558                                    additionalBidsPromise,
    559                                    highestScoringOtherBid,
    560                                    winningAdditionalBidId) {
    561  await runBasicFledgeAuctionAndNavigate(
    562      test, uuid,
    563      { interestGroupBuyers: buyers,
    564        auctionNonce: auctionNonce,
    565        additionalBids: additionalBidsPromise,
    566        decisionLogicURL: createDecisionScriptURL(
    567            uuid,
    568            { reportResult: `sendReportTo("${createSellerReportURL(uuid)}&highestScoringOtherBid=" + Math.round(browserSignals.highestScoringOtherBid));` })});
    569 
    570  await waitForObservedRequests(
    571      uuid, [createHighestScoringOtherBidReportURL(uuid, highestScoringOtherBid),
    572             createBidderReportURL(uuid, winningAdditionalBidId)]);
    573 }
    574 
    575 // Similar to runAdditionalBidTest(), but expects no winner. It takes the
    576 // following arguments:
    577 // - test/uuid: the test object and uuid from the test case (see generateUuid)
    578 // - buyers: array of strings, each a domain for a buyer participating in this
    579 //       auction
    580 // - auctionNonce: string, the auction nonce for this auction, typically
    581 //       retrieved from a prior call to navigator.createAuctionNonce
    582 // - additionalBidsPromise: promise resolving to undefined, to be resolved when
    583 //       the additional bids have been retrieved with fetch().
    584 async function runAdditionalBidTestNoWinner(
    585    test, uuid, buyers, auctionNonce, additionalBidsPromise) {
    586  await runBasicFledgeTestExpectingNoWinner(test, uuid, {
    587    interestGroupBuyers: buyers,
    588    auctionNonce: auctionNonce,
    589    additionalBids: additionalBidsPromise,
    590    decisionLogicURL: createDecisionScriptURL(uuid)
    591  });
    592 }
    593 
    594 // Runs "script" in "child_window" via an eval call. The "child_window" must
    595 // have been created by calling "createFrame()" below. "param" is passed to the
    596 // context "script" is run in, so can be used to pass objects that
    597 // "script" references that can't be serialized to a string, like
    598 // fencedFrameConfigs.
    599 async function runInFrame(test, child_window, script, param) {
    600  const messageUuid = generateUuid(test);
    601  let receivedResponse = {};
    602 
    603  let promise = new Promise(function(resolve, reject) {
    604    function WaitForMessage(event) {
    605      if (event.data.messageUuid !== messageUuid)
    606        return;
    607      receivedResponse = event.data;
    608      if (event.data.result === 'success') {
    609        resolve();
    610      } else {
    611        reject(event.data.result);
    612      }
    613    }
    614    window.addEventListener('message', WaitForMessage);
    615    child_window.postMessage(
    616        {messageUuid: messageUuid, script: script, param: param}, '*');
    617  });
    618  await promise;
    619  return receivedResponse.returnValue;
    620 }
    621 
    622 // Creates an frame and navigates it to a URL on "origin", and waits for the URL
    623 // to finish loading by waiting for the frame to send an event. Then returns
    624 // the frame's Window object. Depending on the value of "is_iframe", the created
    625 // frame will either be a new iframe, or a new top-level main frame. In the iframe
    626 // case, its "allow" field will be set to "permissions".
    627 //
    628 // Also adds a cleanup callback to "test", which runs all cleanup functions
    629 // added within the frame and waits for them to complete, and then destroys the
    630 // iframe or closes the window.
    631 async function createFrame(test, origin, is_iframe = true, permissions = null) {
    632  const frameUuid = generateUuid(test);
    633  const frameURL =
    634      `${origin}${RESOURCE_PATH}subordinate-frame.sub.html?uuid=${frameUuid}`;
    635  let promise = new Promise(function(resolve, reject) {
    636    function WaitForMessage(event) {
    637      if (event.data.messageUuid !== frameUuid)
    638        return;
    639      if (event.data.result === 'load complete') {
    640        resolve();
    641      } else {
    642        reject(event.data.result);
    643      }
    644    }
    645    window.addEventListener('message', WaitForMessage);
    646  });
    647 
    648  if (is_iframe) {
    649    let iframe = document.createElement('iframe');
    650    if (permissions)
    651      iframe.allow = permissions;
    652    iframe.src = frameURL;
    653    document.body.appendChild(iframe);
    654 
    655    test.add_cleanup(async () => {
    656      await runInFrame(test, iframe.contentWindow, "await test_instance.do_cleanup();");
    657      document.body.removeChild(iframe);
    658    });
    659 
    660    await promise;
    661    return iframe.contentWindow;
    662  }
    663 
    664  let child_window = window.open(frameURL);
    665  test.add_cleanup(async () => {
    666    await runInFrame(test, child_window, "await test_instance.do_cleanup();");
    667    child_window.close();
    668  });
    669 
    670  await promise;
    671  return child_window;
    672 }
    673 
    674 // Wrapper around createFrame() that creates an iframe and optionally sets
    675 // permissions.
    676 async function createIframe(test, origin, permissions = null) {
    677  return await createFrame(test, origin, /*is_iframe=*/true, permissions);
    678 }
    679 
    680 // Wrapper around createFrame() that creates a top-level window.
    681 async function createTopLevelWindow(test, origin) {
    682  return await createFrame(test, origin, /*is_iframe=*/false);
    683 }
    684 
    685 // Joins a cross-origin interest group. Currently does this by joining the
    686 // interest group in an iframe, though it may switch to using a .well-known
    687 // fetch to allow the cross-origin join, when support for that is added
    688 // to these tests, so callers should not assume that's the mechanism in use.
    689 async function joinCrossOriginInterestGroup(test, uuid, origin, interestGroupOverrides = {}) {
    690  let interestGroup = JSON.stringify(
    691      createInterestGroupForOrigin(uuid, origin, interestGroupOverrides));
    692 
    693  let iframe = await createIframe(test, origin, 'join-ad-interest-group');
    694  await runInFrame(test, iframe,
    695                   `await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`);
    696 }
    697 
    698 // Leaves a cross-origin interest group, by running a leave in an iframe.
    699 async function leaveCrossOriginInterestGroup(test, uuid, origin, interestGroupOverrides = {}) {
    700  let interestGroup = JSON.stringify(
    701      createInterestGroupForOrigin(uuid, origin, interestGroupOverrides));
    702 
    703  let iframe = await createIframe(test, origin, 'join-ad-interest-group');
    704  await runInFrame(test, iframe,
    705                   `await leaveInterestGroup(${interestGroup})`);
    706 }
    707 
    708 // Joins an interest group in a top-level window, which has the same origin
    709 // as the joined interest group.
    710 async function joinInterestGroupInTopLevelWindow(
    711    test, uuid, origin, interestGroupOverrides = {}) {
    712  let interestGroup = JSON.stringify(
    713      createInterestGroupForOrigin(uuid, origin, interestGroupOverrides));
    714 
    715  let topLevelWindow = await createTopLevelWindow(test, origin);
    716  await runInFrame(test, topLevelWindow,
    717                   `await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`);
    718 }
    719 
    720 // Opens a top-level window and calls joinCrossOriginInterestGroup() in it.
    721 async function joinCrossOriginInterestGroupInTopLevelWindow(
    722    test, uuid, windowOrigin, interestGroupOrigin, interestGroupOverrides = {}) {
    723  let interestGroup = JSON.stringify(
    724      createInterestGroupForOrigin(uuid, interestGroupOrigin, interestGroupOverrides));
    725 
    726  let topLevelWindow = await createTopLevelWindow(test, windowOrigin);
    727  await runInFrame(test, topLevelWindow,
    728                  `await joinCrossOriginInterestGroup(
    729                        test_instance, "${uuid}", "${interestGroupOrigin}", ${interestGroup})`);
    730 }
    731 
    732 // Fetch directFromSellerSignals from seller and check header
    733 // 'Ad-Auction-Signals' is hidden from documents.
    734 async function fetchDirectFromSellerSignals(headers_content, origin) {
    735  const response = await fetch(
    736      createDirectFromSellerSignalsURL(origin),
    737      { adAuctionHeaders: true, headers: headers_content });
    738 
    739  if (!('Negative-Test-Option' in headers_content)) {
    740    assert_equals(
    741        response.status,
    742        200,
    743        'Failed to fetch directFromSellerSignals: ' + await response.text());
    744  }
    745  assert_false(
    746      response.headers.has('Ad-Auction-Signals'),
    747      'Header "Ad-Auction-Signals" should be hidden from documents.');
    748 }
    749 
    750 // Generate directFromSellerSignals evaluation code for different worklets and
    751 // pass to `runReportTest()` as `codeToInsert`.
    752 function directFromSellerSignalsValidatorCode(uuid, expectedSellerSignals,
    753    expectedAuctionSignals, expectedPerBuyerSignals) {
    754  expectedSellerSignals = JSON.stringify(expectedSellerSignals);
    755  expectedAuctionSignals = JSON.stringify(expectedAuctionSignals);
    756  expectedPerBuyerSignals = JSON.stringify(expectedPerBuyerSignals);
    757 
    758  return {
    759    // Seller worklets
    760    scoreAd:
    761      `if (directFromSellerSignals == null ||
    762           directFromSellerSignals.sellerSignals !== ${expectedSellerSignals} ||
    763           directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} ||
    764           Object.keys(directFromSellerSignals).length !== 2) {
    765              throw 'Failed to get expected directFromSellerSignals in scoreAd(): ' +
    766                JSON.stringify(directFromSellerSignals);
    767          }`,
    768    reportResultSuccessCondition:
    769      `directFromSellerSignals != null &&
    770           directFromSellerSignals.sellerSignals === ${expectedSellerSignals} &&
    771           directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} &&
    772           Object.keys(directFromSellerSignals).length === 2`,
    773    reportResult:
    774      `sendReportTo("${createSellerReportURL(uuid)}");`,
    775 
    776    // Bidder worklets
    777    generateBid:
    778      `if (directFromSellerSignals == null ||
    779           directFromSellerSignals.perBuyerSignals !== ${expectedPerBuyerSignals} ||
    780           directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} ||
    781           Object.keys(directFromSellerSignals).length !== 2) {
    782              throw 'Failed to get expected directFromSellerSignals in generateBid(): ' +
    783                JSON.stringify(directFromSellerSignals);
    784        }`,
    785    reportWinSuccessCondition:
    786      `directFromSellerSignals != null &&
    787           directFromSellerSignals.perBuyerSignals === ${expectedPerBuyerSignals} &&
    788           directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} &&
    789           Object.keys(directFromSellerSignals).length === 2`,
    790    reportWin:
    791      `sendReportTo("${createBidderReportURL(uuid)}");`,
    792  };
    793 }
    794 
    795 let additionalBidHelper = function() {
    796 
    797  // Creates an additional bid with the given parameters. This additional bid
    798  // specifies a biddingLogicURL that provides an implementation of
    799  // reportAdditionalBidWin that triggers a sendReportTo() to the bidder report
    800  // URL of the winning additional bid. Additional bids are described in more
    801  // detail at
    802  // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#6-additional-bids.
    803  // Returned bids have an additional `testMetadata` field that's modified by
    804  // several of the other helper functions defined below and is consumed by
    805  // `fetchAdditionalBids()`. Created additional bids must be used only once,
    806  // as `fetchAdditionalBids()` consumes and discards the `testMetadata` field.
    807  function createAdditionalBid(uuid, seller, buyer, interestGroupName, bidAmount) {
    808    return {
    809      interestGroup: {
    810        name: interestGroupName,
    811        biddingLogicURL: createBiddingScriptURL({
    812          origin: buyer,
    813          reportAdditionalBidWin: `sendReportTo("${
    814              createBidderReportURL(uuid, interestGroupName)}");`
    815        }),
    816        owner: buyer
    817      },
    818      bid: {ad: ['metadata'], bid: bidAmount, render: createRenderURL(uuid)},
    819      seller: seller,
    820      testMetadata: {}
    821    };
    822  }
    823 
    824  // Sets the auction nonce that will be included by the server on the
    825  // 'Ad-Auction-Additional-Bid' response header for this bid. All valid
    826  // additional bids should have an auctionNonce in the header, so this
    827  // should be called by most tests.
    828  function setAuctionNonceInHeader(additionalBid, auctionNonce) {
    829    additionalBid.testMetadata.auctionNonce = auctionNonce;
    830  }
    831 
    832  // Sets the seller nonce that will be included by the server on the
    833  // 'Ad-Auction-Additional-Bid' response header for this bid.
    834  function setSellerNonceInHeader(additionalBid, sellerNonce) {
    835    additionalBid.testMetadata.sellerNonce = sellerNonce;
    836  }
    837 
    838  // Tells `fetchAdditionalBids` to correctly sign the additional bid with
    839  // the given secret keys before returning that as a signed additional bid.
    840  // The signatures aren't computed yet because `additionalBid` - whose string
    841  // representation is signed - may still change between when this is called
    842  // and when `fetchAdditionalBids` is called.
    843  function signWithSecretKeys(additionalBid, secretKeys) {
    844    additionalBid.testMetadata.secretKeysForValidSignatures = secretKeys;
    845  }
    846 
    847  // Tells the additional bid endpoint to incorrectly sign the additional bid
    848  // with the given secret keys before returning that as a signed additional
    849  // bid. This is used for testing the behavior when the auction encounters an
    850  // invalid signature. The signatures aren't computed yet because
    851  // `additionalBid` - whose string representation is signed - may still change
    852  // between when this is called and when `fetchAdditionalBids` is called.
    853  function incorrectlySignWithSecretKeys(additionalBid, secretKeys) {
    854    additionalBid.testMetadata.secretKeysForInvalidSignatures = secretKeys;
    855  }
    856 
    857  // Takes the auctionNonce and sellerNonce as strings, and combines them with
    858  // SHA256, returning the result as a base64 string.
    859  async function computeBidNonce(auctionNonce, sellerNonce) {
    860    // Compute the bidNonce as hashed bytes.
    861    const combined_utf8 = new TextEncoder().encode(auctionNonce + sellerNonce);
    862    const hashed = await crypto.subtle.digest('SHA-256',combined_utf8);
    863 
    864    // Convert the hashed bytes to base64.
    865    return btoa(String.fromCharCode(...new Uint8Array(hashed)));
    866  }
    867 
    868  // Adds a single negative interest group to an additional bid, as described at:
    869  // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups
    870  function addNegativeInterestGroup(additionalBid, negativeInterestGroup) {
    871    additionalBid.negativeInterestGroup = negativeInterestGroup;
    872  }
    873 
    874  // Adds multiple negative interest groups to an additional bid, as described at:
    875  // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups
    876  function addNegativeInterestGroups(
    877      additionalBid, negativeInterestGroups, joiningOrigin) {
    878    additionalBid.negativeInterestGroups = {
    879      joiningOrigin: joiningOrigin,
    880      interestGroupNames: negativeInterestGroups
    881    };
    882  }
    883 
    884  const _ed25519ModulePromise =
    885      import('../third_party/noble-ed25519/noble-ed25519.js');
    886 
    887  // Returns a signature entry for a signed additional bid.
    888  //
    889  // `message` is the additional bid text (or other text if generating an
    890  // invalid signature) to sign.
    891  //
    892  // `base64EncodedSecretKey` is the base64-encoded Ed25519 key with which to
    893  // sign the message. From this secret key, the public key can be deduced,
    894  // which becomes part of the signature entry.
    895  async function _generateSignature(message, base64EncodedSecretKey) {
    896    const ed25519 = await _ed25519ModulePromise;
    897    const secretKey =
    898        Uint8Array.from(atob(base64EncodedSecretKey), c => c.charCodeAt(0));
    899    const [publicKey, signature] = await Promise.all([
    900      ed25519.getPublicKeyAsync(secretKey),
    901      ed25519.signAsync(new TextEncoder().encode(message), secretKey)
    902    ]);
    903 
    904    return {
    905      'key': btoa(String.fromCharCode(...publicKey)),
    906      'signature': btoa(String.fromCharCode(...signature))
    907    };
    908  }
    909 
    910  // Returns a signed additional bid given an additional bid and secret keys.
    911  // `additionalBid` is the additional bid to sign. It must not contain a
    912  // `testMetadata` - that should have been removed prior to calling this.
    913  //
    914  // `secretKeysForValidSignatures` is a list of strings, each a base64-encoded
    915  // Ed25519 secret key with which to sign the additional bid, whereas
    916  // `secretKeysForInvalidSignatures` is a list of strings, each a
    917  // base64-encoded Ed25519 secret key with which to *incorrectly* sign the
    918  // additional bid.
    919  async function _signAdditionalBid(
    920      additionalBid, secretKeysForValidSignatures,
    921      secretKeysForInvalidSignatures) {
    922    async function _signString(string, secretKeys) {
    923      if (!secretKeys) {
    924        return [];
    925      }
    926      return await Promise.all(secretKeys.map(
    927             async secretKey => await _generateSignature(
    928                  string, secretKey)));
    929    }
    930 
    931    assert_not_own_property(
    932        additionalBid, 'testMetadata',
    933        'testMetadata should be removed from additionalBid before signing');
    934    const additionalBidString = JSON.stringify(additionalBid);
    935    let [validSignatures, invalidSignatures] = await Promise.all([
    936        _signString(additionalBidString, secretKeysForValidSignatures),
    937 
    938        // For invalid signatures, we use the correct secret key to sign a
    939        // different message - the additional bid prepended by 'invalid' - so
    940        // that the signature is a structually valid signature with the correct
    941        // (public) key, but can't be used to verify the additional bid.
    942        _signString('invalid' + additionalBidString, secretKeysForInvalidSignatures)
    943    ]);
    944    return {
    945        'bid': additionalBidString,
    946        'signatures': validSignatures.concat(invalidSignatures)
    947    };
    948  }
    949 
    950  // Given an additionalBid object, this returns a string to be used as the
    951  // value of the `Ad-Auction-Additional-Bid` response header. To produce this
    952  // header, this signs the signing the `additionalBid` with the signatures
    953  // specified by prior calls to `signWithSecretKeys` and
    954  // `incorrectlySignWithSecretKeys` above; base64-encodes the stringified
    955  // `signedAdditionalBid`; and then prepends that with the `auctionNonce` and/or
    956  // `sellerNonce` specified by prior calls to `setAuctionNonceInHeader` and
    957  // `setSellerNonceInHeader` above, respectively.
    958  async function _convertAdditionalBidToResponseHeader(additionalBid) {
    959    const testMetadata = additionalBid.testMetadata;
    960    delete additionalBid.testMetadata;
    961 
    962    const signedAdditionalBid = await _signAdditionalBid(
    963        additionalBid,
    964        testMetadata.secretKeysForValidSignatures,
    965        testMetadata.secretKeysForInvalidSignatures);
    966 
    967 
    968    return [
    969        testMetadata.auctionNonce,
    970        testMetadata.sellerNonce,
    971        btoa(JSON.stringify(signedAdditionalBid))
    972    ].filter(k => k !== undefined).join(':');
    973  }
    974 
    975  // Fetch some number of fully prepared additional bid from a seller and verify
    976  // that the `Ad-Auction-Additional-Bid` header is not visible in this
    977  // JavaScript context. The `additionalBids` parameter is a list of additional
    978  // bids objects created by `createAdditionalBid` and modified by other
    979  // functions on this helper. Once passed to this method, additional bids may
    980  // not be reused in a future call to `fetchAdditionalBids()`, since this
    981  // mothod consumes and destroys their `testMetadata` field.
    982  async function fetchAdditionalBids(seller, additionalBids) {
    983    let additionalBidHeaderValues = await Promise.all(additionalBids.map(
    984        async additionalBid =>
    985            await _convertAdditionalBidToResponseHeader(additionalBid)));
    986 
    987    const url = new URL(`${seller}${RESOURCE_PATH}additional-bids.py`);
    988    url.searchParams.append(
    989        'additionalBidHeaderValues', JSON.stringify(additionalBidHeaderValues));
    990    const response = await fetch(url.href, {adAuctionHeaders: true});
    991 
    992    assert_equals(response.status, 200, 'Failed to fetch additional bid: ' + await response.text());
    993    assert_false(
    994        response.headers.has('Ad-Auction-Additional-Bid'),
    995        'Header "Ad-Auction-Additional-Bid" should not be available in JavaScript context.');
    996  }
    997 
    998  return {
    999    createAdditionalBid: createAdditionalBid,
   1000    setAuctionNonceInHeader: setAuctionNonceInHeader,
   1001    setSellerNonceInHeader: setSellerNonceInHeader,
   1002    signWithSecretKeys: signWithSecretKeys,
   1003    incorrectlySignWithSecretKeys: incorrectlySignWithSecretKeys,
   1004    computeBidNonce: computeBidNonce,
   1005    addNegativeInterestGroup: addNegativeInterestGroup,
   1006    addNegativeInterestGroups: addNegativeInterestGroups,
   1007    fetchAdditionalBids: fetchAdditionalBids
   1008  };
   1009 }();
   1010 
   1011 
   1012 // DeprecatedRenderURLReplacements helper function.
   1013 // Returns an object containing sample strings both before and after the
   1014 // replacements in 'replacements' have been applied by
   1015 // deprecatedRenderURLReplacements. All substitution strings will appear
   1016 // only once in the output strings.
   1017 function createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements) {
   1018  let beforeReplacements = '';
   1019  let afterReplacements = '';
   1020  if(deprecatedRenderURLReplacements){
   1021    for (const [match, replacement] of Object.entries(deprecatedRenderURLReplacements)) {
   1022      beforeReplacements += match + "/";
   1023      afterReplacements += replacement + "/";
   1024    }
   1025  }
   1026  return { beforeReplacements, afterReplacements };
   1027 }
   1028 
   1029 // Delete all cookies. Separate function so that can be replaced with
   1030 // something else for testing outside of a WPT environment.
   1031 async function deleteAllCookies() {
   1032  await test_driver.delete_all_cookies();
   1033 }
   1034 
   1035 // Deletes all cookies (to avoid pre-existing cookies causing inconsistent
   1036 // output on failure) and sets a cookie with name "cookie" and a value of
   1037 // "cookie". Adds a cleanup task to delete all cookies again when the test
   1038 // is done.
   1039 async function setCookie(test) {
   1040  await deleteAllCookies();
   1041  document.cookie = 'cookie=cookie; path=/'
   1042  test.add_cleanup(deleteAllCookies);
   1043 }
   1044 
   1045 async function makeInterestGroupKAnonymous(passedInterestGroup) {
   1046  // Make a copy so we can sanitize fields without affecting the tests.
   1047  let interestGroup = structuredClone(passedInterestGroup);
   1048  const ownerURL = new URL(interestGroup.owner);
   1049  interestGroup.owner = ownerURL.origin;
   1050  interestGroup.name = String(interestGroup.name).toWellFormed();
   1051  interestGroup.biddingLogicURL =
   1052      (new URL(interestGroup.biddingLogicURL, BASE_URL)).toString();
   1053 
   1054  function b64(array) {
   1055    return btoa(String.fromCharCode.apply(null, array));
   1056  }
   1057  let hashes = [];
   1058  if (Array.isArray(interestGroup.ads)) {
   1059    for (const ad of interestGroup.ads) {
   1060      hashes.push(b64(await computeKeyHashOfAd(interestGroup, ad)));
   1061      hashes.push(
   1062          b64(await computeKeyHashOfReportingId(interestGroup, ad, null)));
   1063      if (Array.isArray(ad.selectableBuyerAndSellerReportingIds)) {
   1064        for (const id of ad.selectableBuyerAndSellerReportingIds) {
   1065          hashes.push(
   1066              b64(await computeKeyHashOfReportingId(interestGroup, ad, id)));
   1067        }
   1068      }
   1069    }
   1070  }
   1071  if (Array.isArray(interestGroup.adComponents)) {
   1072    for (const ad of interestGroup.adComponents) {
   1073      hashes.push(b64(await computeKeyHashOfComponentAd(interestGroup, ad)));
   1074    }
   1075  }
   1076  await test_driver.set_protected_audience_k_anonymity(
   1077      interestGroup.owner, interestGroup.name, hashes);
   1078 }
   1079 
   1080 async function computeKeyHashOfAd(ig, ad) {
   1081  const encoder = new TextEncoder();
   1082  const kAnonKey = encoder.encode(
   1083      `AdBid\n${ig.owner}/\n${ig.biddingLogicURL}\n${ad.renderURL}`);
   1084  return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey));
   1085 }
   1086 
   1087 async function computeKeyHashOfReportingId(ig, ad, selectedReportingId = null) {
   1088  const encoder = new TextEncoder();
   1089  let kAnonKey = null;
   1090  if (!selectedReportingId) {
   1091    if (ad.buyerAndSellerReportingId) {
   1092      kAnonKey = encoder.encode(
   1093          `BuyerAndSellerReportId\n${ig.owner}/\n${ig.biddingLogicURL}\n${
   1094              ad.renderURL}\n${ad.buyerAndSellerReportingId}`);
   1095    } else if (ad.buyerReportingId) {
   1096      kAnonKey = encoder.encode(`BuyerReportId\n${ig.owner}/\n${
   1097          ig.biddingLogicURL}\n${ad.renderURL}\n${ad.buyerReportingId}`);
   1098    } else {
   1099      kAnonKey = encoder.encode(`NameReport\n${ig.owner}/\n${
   1100          ig.biddingLogicURL}\n${ad.renderURL}\n${ig.name}`);
   1101    }
   1102  } else {
   1103    function encodeKeyPartInto(part, array) {
   1104      array[0] = 0x0a;
   1105      if (!part) {
   1106        for (let i = 1; i < 6; i++) {
   1107          array[i] = 0x00;
   1108        }
   1109        return 6;
   1110      }
   1111      const len = part.length;
   1112      array[1] = 0x01;
   1113      array[2] = (len >> 24) % 256
   1114      array[3] = (len >> 16) % 256
   1115      array[4] = (len >> 8) % 256
   1116      array[5] = len % 256;
   1117      encoder.encodeInto(part, array.subarray(6));
   1118      return 1 + 5 + len;
   1119    }
   1120    const baseText = `SelectedBuyerAndSellerReportId\n${ig.owner}/\n${
   1121        ig.biddingLogicURL}\n${ad.renderURL}`;
   1122    const selectedReportingIdLen =
   1123        1 + 5 + (selectedReportingId ? selectedReportingId.length : 0);
   1124    const buyerAndSellerReportingIdLen = 1 + 5 +
   1125        (ad.buyerAndSellerReportingId ? ad.buyerAndSellerReportingId.length : 0)
   1126    const buyerReportingIdLen =
   1127        1 + 5 + (ad.buyerReportingId ? ad.buyerReportingId.length : 0)
   1128    const expectedLen = baseText.length + selectedReportingIdLen +
   1129        buyerAndSellerReportingIdLen + buyerReportingIdLen;
   1130    kAnonKey = new Uint8Array(expectedLen);
   1131    let actualLen = 0;
   1132    actualLen += encoder.encodeInto(baseText, kAnonKey).written;
   1133    actualLen +=
   1134        encodeKeyPartInto(selectedReportingId, kAnonKey.subarray(actualLen));
   1135    actualLen += encodeKeyPartInto(
   1136        ad.buyerAndSellerReportingId, kAnonKey.subarray(actualLen));
   1137    actualLen +=
   1138        encodeKeyPartInto(ad.buyerReportingId, kAnonKey.subarray(actualLen));
   1139  }
   1140  return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey));
   1141 }
   1142 
   1143 async function computeKeyHashOfComponentAd(ig, ad) {
   1144  const encoder = new TextEncoder();
   1145  const kAnonKey = encoder.encode(`ComponentBid\n${ad.renderURL}`);
   1146  return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey));
   1147 }