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 }