| "use strict"; |
| |
| const BASE_URL = document.baseURI.substring(0, document.baseURI.lastIndexOf('/') + 1); |
| const BASE_PATH = (new URL(BASE_URL)).pathname; |
| |
| // Allow overriding to allow other repositories to use these utility functions. |
| let RESOURCE_PATH = `${BASE_PATH}resources/` |
| |
| const DEFAULT_INTEREST_GROUP_NAME = 'default name'; |
| |
| // Unlike other URLs, trusted signals URLs can't have query strings |
| // that are set by tests, since FLEDGE controls it entirely, so tests that |
| // exercise them use a fixed URL string. Note that FLEDGE adds query |
| // params when requesting these URLs, and the python scripts use these |
| // to construct the response. |
| const TRUSTED_BIDDING_SIGNALS_URL = |
| `${BASE_URL}resources/trusted-bidding-signals.py`; |
| const TRUSTED_SCORING_SIGNALS_URL = |
| `${BASE_URL}resources/trusted-scoring-signals.py`; |
| |
| // Other origins that should all be distinct from the main frame origin |
| // that the tests start with. |
| const OTHER_ORIGIN1 = 'https://{{hosts[alt][]}}:{{ports[https][0]}}'; |
| const OTHER_ORIGIN2 = 'https://{{hosts[alt][]}}:{{ports[https][1]}}'; |
| const OTHER_ORIGIN3 = 'https://{{hosts[][]}}:{{ports[https][1]}}'; |
| const OTHER_ORIGIN4 = 'https://{{hosts[][www]}}:{{ports[https][0]}}'; |
| const OTHER_ORIGIN5 = 'https://{{hosts[][www]}}:{{ports[https][1]}}'; |
| const OTHER_ORIGIN6 = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}'; |
| const OTHER_ORIGIN7 = 'https://{{hosts[alt][www]}}:{{ports[https][1]}}'; |
| |
| // Trusted signals hosted on OTHER_ORIGIN1 |
| const CROSS_ORIGIN_TRUSTED_BIDDING_SIGNALS_URL = OTHER_ORIGIN1 + BASE_PATH + |
| 'resources/trusted-bidding-signals.py'; |
| const CROSS_ORIGIN_TRUSTED_SCORING_SIGNALS_URL = OTHER_ORIGIN1 + BASE_PATH + |
| 'resources/trusted-scoring-signals.py'; |
| |
| // Creates a URL that will be sent to the URL request tracker script. |
| // `uuid` is used to identify the stash shard to use. |
| // `dispatch` affects what the tracker script does. |
| // `id` can be used to uniquely identify tracked requests. It has no effect |
| // on behavior of the script; it only serves to make the URL unique. |
| // `id` will always be the last query parameter. |
| function createTrackerURL(origin, uuid, dispatch, id = null) { |
| let url = new URL(`${origin}${RESOURCE_PATH}request-tracker.py`); |
| let search = `uuid=${uuid}&dispatch=${dispatch}`; |
| if (id) |
| search += `&id=${id}`; |
| url.search = search; |
| return url.toString(); |
| } |
| |
| // Create a URL that when fetches clears tracked URLs. Note that the origin |
| // doesn't matter - it will clean up all tracked URLs with the provided uuid, |
| // regardless of origin they were fetched from. |
| function createCleanupURL(uuid) { |
| return createTrackerURL(window.location.origin, uuid, 'clean_up'); |
| } |
| |
| // Create tracked bidder/seller URLs. The only difference is the prefix added |
| // to the `id` passed to createTrackerURL. The optional `id` field allows |
| // multiple bidder/seller report URLs to be distinguishable from each other. |
| // `id` will always be the last query parameter. |
| function createBidderReportURL(uuid, id = '1', origin = window.location.origin) { |
| return createTrackerURL(origin, uuid, `track_get`, `bidder_report_${id}`); |
| } |
| function createSellerReportURL(uuid, id = '1', origin = window.location.origin) { |
| return createTrackerURL(origin, uuid, `track_get`, `seller_report_${id}`); |
| } |
| |
| function createHighestScoringOtherBidReportURL(uuid, highestScoringOtherBid) { |
| return createSellerReportURL(uuid) + '&highestScoringOtherBid=' + Math.round(highestScoringOtherBid); |
| } |
| |
| // Much like above ReportURL methods, except designed for beacons, which |
| // are expected to be POSTs. |
| function createBidderBeaconURL(uuid, id = '1', origin = window.location.origin) { |
| return createTrackerURL(origin, uuid, `track_post`, `bidder_beacon_${id}`); |
| } |
| function createSellerBeaconURL(uuid, id = '1', origin = window.location.origin) { |
| return createTrackerURL(origin, uuid, `track_post`, `seller_beacon_${id}`); |
| } |
| |
| function createDirectFromSellerSignalsURL(origin = window.location.origin) { |
| let url = new URL(`${origin}${RESOURCE_PATH}direct-from-seller-signals.py`); |
| return url.toString(); |
| } |
| |
| function createUpdateURL(params = {}) { |
| let origin = window.location.origin; |
| let url = new URL(`${origin}${RESOURCE_PATH}update-url.py`); |
| url.searchParams.append('body', params.body); |
| url.searchParams.append('uuid', params.uuid); |
| |
| return url.toString(); |
| } |
| |
| // Generates a UUID and registers a cleanup method with the test fixture to |
| // request a URL from the request tracking script that clears all data |
| // associated with the generated uuid when requested. |
| function generateUuid(test) { |
| let uuid = token(); |
| test.add_cleanup(async () => { |
| let response = await fetch(createCleanupURL(uuid), |
| { credentials: 'omit', mode: 'cors' }); |
| assert_equals(await response.text(), 'cleanup complete', |
| `Sever state cleanup failed`); |
| }); |
| return uuid; |
| } |
| |
| // Helper to fetch "tracked_data" URL to fetch all data recorded by the |
| // tracker URL associated with "uuid". Throws on error, including if |
| // the retrieved object's errors field is non-empty. |
| async function fetchTrackedData(uuid) { |
| let trackedRequestsURL = createTrackerURL(window.location.origin, uuid, |
| 'tracked_data'); |
| let response = await fetch(trackedRequestsURL, |
| { credentials: 'omit', mode: 'cors' }); |
| let trackedData = await response.json(); |
| |
| // Fail on fetch error. |
| if (trackedData.error) { |
| throw trackedRequestsURL + ' fetch failed:' + JSON.stringify(trackedData); |
| } |
| |
| // Fail on errors reported by the tracker script. |
| if (trackedData.errors.length > 0) { |
| throw 'Errors reported by request-tracker.py:' + |
| JSON.stringify(trackedData.errors); |
| } |
| |
| return trackedData; |
| } |
| |
| // Repeatedly requests "tracked_data" URL until exactly the entries in |
| // "expectedRequests" have been observed by the request tracker script (in |
| // any order, since report URLs are not guaranteed to be sent in any order). |
| // |
| // Elements of `expectedRequests` should either be URLs, in the case of GET |
| // requests, or "<URL>, body: <body>" in the case of POST requests. |
| // |
| // `filter` will be applied to the array of tracked requests. |
| // |
| // If any other strings are received from the tracking script, or the tracker |
| // script reports an error, fails the test. |
| async function waitForObservedRequests(uuid, expectedRequests, filter) { |
| // Sort array for easier comparison, as observed request order does not |
| // matter, and replace UUID to print consistent errors on failure. |
| expectedRequests = expectedRequests.map((url) => url.replace(uuid, '<uuid>')).sort(); |
| |
| while (true) { |
| let trackedData = await fetchTrackedData(uuid); |
| |
| // Clean up "trackedRequests" in same manner as "expectedRequests". |
| let trackedRequests = trackedData.trackedRequests.map( |
| (url) => url.replace(uuid, '<uuid>')).sort(); |
| |
| if (filter) { |
| trackedRequests = trackedRequests.filter(filter); |
| } |
| |
| // If fewer than total number of expected requests have been observed, |
| // compare what's been received so far, to have a greater chance to fail |
| // rather than hang on error. |
| for (const trackedRequest of trackedRequests) { |
| assert_in_array(trackedRequest, expectedRequests); |
| } |
| |
| // If expected number of requests have been observed, compare with list of |
| // all expected requests and exit. This check was previously before the for loop, |
| // but was swapped in order to avoid flakiness with failing tests and their |
| // respective *-expected.txt. |
| if (trackedRequests.length >= expectedRequests.length) { |
| assert_array_equals(trackedRequests, expectedRequests); |
| break; |
| } |
| } |
| } |
| |
| |
| // Similar to waitForObservedRequests, but ignore forDebuggingOnly reports. |
| async function waitForObservedRequestsIgnoreDebugOnlyReports( |
| uuid, expectedRequests) { |
| return waitForObservedRequests( |
| uuid, |
| expectedRequests, |
| request => !request.includes('forDebuggingOnly')); |
| } |
| |
| // Creates a bidding script with the provided code in the method bodies. The |
| // bidding script's generateBid() method will return a bid of 9 for the first |
| // ad, after the passed in code in the "generateBid" input argument has been |
| // run, unless it returns something or throws. |
| // |
| // The default reportWin() method is empty. |
| function createBiddingScriptURL(params = {}) { |
| let origin = params.origin ? params.origin : new URL(BASE_URL).origin; |
| let url = new URL(`${origin}${RESOURCE_PATH}bidding-logic.sub.py`); |
| // These checks use "!=" to ignore null and not provided arguments, while |
| // treating '' as a valid argument. |
| if (params.generateBid != null) |
| url.searchParams.append('generateBid', params.generateBid); |
| if (params.reportWin != null) |
| url.searchParams.append('reportWin', params.reportWin); |
| if (params.reportAdditionalBidWin != null) |
| url.searchParams.append('reportAdditionalBidWin', params.reportAdditionalBidWin); |
| if (params.error != null) |
| url.searchParams.append('error', params.error); |
| if (params.bid != null) |
| url.searchParams.append('bid', params.bid); |
| if (params.bidCurrency != null) |
| url.searchParams.append('bidCurrency', params.bidCurrency); |
| if (params.allowComponentAuction != null) |
| url.searchParams.append('allowComponentAuction', JSON.stringify(params.allowComponentAuction)) |
| return url.toString(); |
| } |
| |
| // TODO: Make this return a valid WASM URL. |
| function createBiddingWasmHelperURL(params = {}) { |
| let origin = params.origin ? params.origin : new URL(BASE_URL).origin; |
| return `${origin}${RESOURCE_PATH}bidding-wasmlogic.wasm`; |
| } |
| |
| // Creates a decision script with the provided code in the method bodies. The |
| // decision script's scoreAd() method will reject ads with renderURLs that |
| // don't ends with "uuid", and will return a score equal to the bid, after the |
| // passed in code in the "scoreAd" input argument has been run, unless it |
| // returns something or throws. |
| // |
| // The default reportResult() method is empty. |
| function createDecisionScriptURL(uuid, params = {}) { |
| let origin = params.origin ? params.origin : new URL(BASE_URL).origin; |
| let url = new URL(`${origin}${RESOURCE_PATH}decision-logic.sub.py`); |
| url.searchParams.append('uuid', uuid); |
| // These checks use "!=" to ignore null and not provided arguments, while |
| // treating '' as a valid argument. |
| if (params.scoreAd != null) |
| url.searchParams.append('scoreAd', params.scoreAd); |
| if (params.reportResult != null) |
| url.searchParams.append('reportResult', params.reportResult); |
| if (params.error != null) |
| url.searchParams.append('error', params.error); |
| if (params.permitCrossOriginTrustedSignals != null) { |
| url.searchParams.append('permit-cross-origin-trusted-signals', |
| params.permitCrossOriginTrustedSignals); |
| } |
| return url.toString(); |
| } |
| |
| // Creates a renderURL for an ad that runs the passed in "script". "uuid" has |
| // no effect, beyond making the URL distinct between tests, and being verified |
| // by the decision logic script before accepting a bid. "uuid" is expected to |
| // be last. "signalsParams" also has no effect, but is used by |
| // trusted-scoring-signals.py to affect the response. |
| function createRenderURL(uuid, script, signalsParams, origin) { |
| // These checks use "==" and "!=" to ignore null and not provided |
| // arguments, while treating '' as a valid argument. |
| if (origin == null) |
| origin = new URL(BASE_URL).origin; |
| let url = new URL(`${origin}${RESOURCE_PATH}fenced-frame.sub.py`); |
| if (script != null) |
| url.searchParams.append('script', script); |
| if (signalsParams != null) |
| url.searchParams.append('signalsParams', signalsParams); |
| url.searchParams.append('uuid', uuid); |
| return url.toString(); |
| } |
| |
| // Creates an interest group owned by "origin" with a bidding logic URL located |
| // on "origin" as well. Uses standard render and report URLs, which are not |
| // necessarily on "origin". "interestGroupOverrides" may be used to override any |
| // field of the created interest group. |
| function createInterestGroupForOrigin(uuid, origin, |
| interestGroupOverrides = {}) { |
| return { |
| owner: origin, |
| name: DEFAULT_INTEREST_GROUP_NAME, |
| biddingLogicURL: createBiddingScriptURL( |
| { origin: origin, |
| reportWin: `sendReportTo('${createBidderReportURL(uuid)}');` }), |
| ads: [{ renderURL: createRenderURL(uuid) }], |
| ...interestGroupOverrides |
| }; |
| } |
| |
| // Waits for the join command to complete. Adds cleanup command to `test` to |
| // leave the interest group when the test completes. |
| async function joinInterestGroupWithoutDefaults(test, interestGroup, |
| durationSeconds = 60) { |
| await navigator.joinAdInterestGroup(interestGroup, durationSeconds); |
| await makeInterestGroupKAnonymous(interestGroup); |
| test.add_cleanup( |
| async () => { await navigator.leaveAdInterestGroup(interestGroup); }); |
| } |
| |
| // Joins an interest group that, by default, is owned by the current frame's |
| // origin, is named DEFAULT_INTEREST_GROUP_NAME, has a bidding script that |
| // issues a bid of 9 with a renderURL of "https://not.checked.test/${uuid}", |
| // and sends a report to createBidderReportURL(uuid) if it wins. Waits for the |
| // join command to complete. Adds cleanup command to `test` to leave the |
| // interest group when the test completes. |
| // |
| // `interestGroupOverrides` may be used to override fields in the joined |
| // interest group. |
| async function joinInterestGroup(test, uuid, interestGroupOverrides = {}, |
| durationSeconds = 60) { |
| await joinInterestGroupWithoutDefaults( |
| test, createInterestGroupForOrigin( |
| uuid, window.location.origin, interestGroupOverrides), |
| durationSeconds); |
| } |
| |
| // Joins a negative interest group with the specified owner, name, and |
| // additionalBidKey. Because these are the only valid fields for a negative |
| // interest groups, this function doesn't expose an 'overrides' parameter. |
| // Adds cleanup command to `test` to leave the interest group when the test |
| // completes. |
| async function joinNegativeInterestGroup( |
| test, owner, name, additionalBidKey) { |
| let interestGroup = { |
| owner: owner, |
| name: name, |
| additionalBidKey: additionalBidKey |
| }; |
| if (owner !== window.location.origin) { |
| let iframe = await createIframe(test, owner, 'join-ad-interest-group'); |
| await runInFrame( |
| test, iframe, |
| `await joinInterestGroupWithoutDefaults(` + |
| `test_instance, ${JSON.stringify(interestGroup)})`); |
| } else { |
| await joinInterestGroupWithoutDefaults(test, interestGroup); |
| } |
| } |
| |
| // Similar to joinInterestGroup, but leaves the interest group instead. |
| // Generally does not need to be called manually when using |
| // "joinInterestGroup()". |
| async function leaveInterestGroup(interestGroupOverrides = {}) { |
| let interestGroup = { |
| owner: window.location.origin, |
| name: DEFAULT_INTEREST_GROUP_NAME, |
| ...interestGroupOverrides |
| }; |
| |
| await navigator.leaveAdInterestGroup(interestGroup); |
| } |
| |
| // Runs a FLEDGE auction and returns the result. By default, the seller is the |
| // current frame's origin, and the only buyer is as well. The seller script |
| // rejects bids for URLs that don't contain "uuid" (to avoid running into issues |
| // with any interest groups from other tests), and reportResult() sends a report |
| // to createSellerReportURL(uuid). |
| // |
| // `auctionConfigOverrides` may be used to override fields in the auction |
| // configuration. |
| async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides = {}) { |
| let auctionConfig = { |
| seller: window.location.origin, |
| decisionLogicURL: createDecisionScriptURL( |
| uuid, |
| { reportResult: `sendReportTo('${createSellerReportURL(uuid)}');` }), |
| interestGroupBuyers: [window.location.origin], |
| resolveToConfig: true, |
| ...auctionConfigOverrides |
| }; |
| return await navigator.runAdAuction(auctionConfig); |
| } |
| |
| // Checks that await'ed return value of runAdAuction() denotes a successful |
| // auction with a winner. |
| function expectSuccess(config) { |
| assert_true(config !== null, `Auction unexpectedly had no winner`); |
| assert_true( |
| config instanceof FencedFrameConfig, |
| `Wrong value type returned from auction: ${config.constructor.type}`); |
| } |
| |
| // Checks that await'ed return value of runAdAuction() denotes an auction |
| // without a winner (but no fatal error). |
| function expectNoWinner(result) { |
| assert_true(result === null, 'Auction unexpectedly had a winner'); |
| } |
| |
| // Wrapper around runBasicFledgeAuction() that runs an auction with the specified |
| // arguments, expecting the auction to have a winner. Returns the FencedFrameConfig |
| // from the auction. |
| async function runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides = {}) { |
| let config = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); |
| expectSuccess(config); |
| return config; |
| } |
| |
| // Wrapper around runBasicFledgeAuction() that runs an auction with the specified |
| // arguments, expecting the auction to have no winner. |
| async function runBasicFledgeTestExpectingNoWinner( |
| test, uuid, auctionConfigOverrides = {}) { |
| let result = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); |
| expectNoWinner(result); |
| } |
| |
| // Creates a fenced frame and applies fencedFrameConfig to it. Also adds a cleanup |
| // method to destroy the fenced frame at the end of the current test. |
| function createAndNavigateFencedFrame(test, fencedFrameConfig) { |
| let fencedFrame = document.createElement('fencedframe'); |
| fencedFrame.mode = 'opaque-ads'; |
| fencedFrame.config = fencedFrameConfig; |
| document.body.appendChild(fencedFrame); |
| test.add_cleanup(() => { document.body.removeChild(fencedFrame); }); |
| } |
| |
| // Calls runBasicFledgeAuction(), expecting the auction to have a winner. |
| // Creates a fenced frame that will be destroyed on completion of "test", and |
| // navigates it to the URN URL returned by the auction. Does not wait for the |
| // fenced frame to finish loading, since there's no API that can do that. |
| async function runBasicFledgeAuctionAndNavigate(test, uuid, |
| auctionConfigOverrides = {}) { |
| let config = await runBasicFledgeTestExpectingWinner(test, uuid, |
| auctionConfigOverrides); |
| createAndNavigateFencedFrame(test, config); |
| } |
| |
| // Joins an interest group and runs an auction, expecting a winner to be |
| // returned. "testConfig" can optionally modify the uuid, interest group or |
| // auctionConfig. |
| async function joinGroupAndRunBasicFledgeTestExpectingWinner(test, testConfig = {}) { |
| const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test); |
| await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides); |
| await runBasicFledgeTestExpectingWinner(test, uuid, testConfig.auctionConfigOverrides); |
| } |
| |
| // Joins an interest group and runs an auction, expecting no winner to be |
| // returned. "testConfig" can optionally modify the uuid, interest group or |
| // auctionConfig. |
| async function joinGroupAndRunBasicFledgeTestExpectingNoWinner(test, testConfig = {}) { |
| const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test); |
| await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides); |
| await runBasicFledgeTestExpectingNoWinner(test, uuid, testConfig.auctionConfigOverrides); |
| } |
| |
| // Test helper for report phase of auctions that lets the caller specify the |
| // body of reportResult() and reportWin(). Passing in null will cause there |
| // to be no reportResult() or reportWin() method. |
| // |
| // If the "SuccessCondition" fields are non-null and evaluate to false in |
| // the corresponding reporting method, the report is sent to an error URL. |
| // Otherwise, the corresponding 'reportResult' / 'reportWin' values are run. |
| // |
| // `codeToInsert` is a JS object that contains the following fields to control |
| // the code generated for the auction worklet: |
| // scoreAd - function body for scoreAd() seller worklet function |
| // reportResultSuccessCondition - Success condition to trigger reportResult() |
| // reportResult - function body for reportResult() seller worklet function |
| // generateBid - function body for generateBid() buyer worklet function |
| // reportWinSuccessCondition - Success condition to trigger reportWin() |
| // decisionScriptURLOrigin - Origin of decision script URL |
| // reportWin - function body for reportWin() buyer worklet function |
| // |
| // Additionally the following fields can be added to check for errors during the |
| // execution of the corresponding worklets: |
| // reportWinSuccessCondition - boolean condition added to reportWin() in the |
| // buyer worklet that triggers a sendReportTo() to an 'error' URL if not met. |
| // reportResultSuccessCondition - boolean condition added to reportResult() in |
| // the seller worklet that triggers a sendReportTo() to an 'error' URL if not |
| // met. |
| // |
| // `renderURLOverride` allows the ad URL of the joined InterestGroup to |
| // to be set by the caller. |
| // |
| // `auctionConfigOverrides` may be used to override fields in the auction |
| // configuration. |
| // |
| // Requesting error report URLs causes waitForObservedRequests() to throw |
| // rather than hang. |
| async function runReportTest(test, uuid, codeToInsert, expectedReportURLs, |
| renderURLOverride, auctionConfigOverrides) { |
| let scoreAd = codeToInsert.scoreAd; |
| let reportResultSuccessCondition = codeToInsert.reportResultSuccessCondition; |
| let reportResult = codeToInsert.reportResult; |
| let generateBid = codeToInsert.generateBid; |
| let reportWinSuccessCondition = codeToInsert.reportWinSuccessCondition; |
| let reportWin = codeToInsert.reportWin; |
| let decisionScriptURLOrigin = codeToInsert.decisionScriptURLOrigin; |
| |
| if (reportResultSuccessCondition) { |
| reportResult = `if (!(${reportResultSuccessCondition})) { |
| sendReportTo('${createSellerReportURL(uuid, 'error')}'); |
| return false; |
| } |
| ${reportResult}`; |
| } |
| let decisionScriptURLParams = {}; |
| |
| if (scoreAd !== undefined) { |
| decisionScriptURLParams.scoreAd = scoreAd; |
| } |
| |
| if (reportResult !== null) |
| decisionScriptURLParams.reportResult = reportResult; |
| else |
| decisionScriptURLParams.error = 'no-reportResult'; |
| |
| if (decisionScriptURLOrigin !== undefined) { |
| decisionScriptURLParams.origin = decisionScriptURLOrigin; |
| } |
| |
| if (reportWinSuccessCondition) { |
| reportWin = `if (!(${reportWinSuccessCondition})) { |
| sendReportTo('${createBidderReportURL(uuid, 'error')}'); |
| return false; |
| } |
| ${reportWin}`; |
| } |
| let biddingScriptURLParams = {}; |
| |
| if (generateBid !== undefined) { |
| biddingScriptURLParams.generateBid = generateBid; |
| } |
| |
| if (reportWin !== null) |
| biddingScriptURLParams.reportWin = reportWin; |
| else |
| biddingScriptURLParams.error = 'no-reportWin'; |
| |
| let interestGroupOverrides = |
| { biddingLogicURL: createBiddingScriptURL(biddingScriptURLParams) }; |
| if (renderURLOverride) |
| interestGroupOverrides.ads = [{ renderURL: renderURLOverride }] |
| |
| await joinInterestGroup(test, uuid, interestGroupOverrides); |
| |
| if (auctionConfigOverrides === undefined) { |
| auctionConfigOverrides = |
| { decisionLogicURL: createDecisionScriptURL(uuid, decisionScriptURLParams) }; |
| } else if (auctionConfigOverrides.decisionLogicURL === undefined) { |
| auctionConfigOverrides.decisionLogicURL = |
| createDecisionScriptURL(uuid, decisionScriptURLParams); |
| } |
| |
| await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides); |
| await waitForObservedRequests(uuid, expectedReportURLs); |
| } |
| |
| // Helper function for running a standard test of the additional bid and |
| // negative targeting features. This helper verifies that the auction produces a |
| // winner. It takes the following arguments: |
| // - test/uuid: the test object and uuid from the test case (see generateUuid) |
| // - buyers: array of strings, each a domain for a buyer participating in this |
| // auction |
| // - auctionNonce: string, the auction nonce for this auction, typically |
| // retrieved from a prior call to navigator.createAuctionNonce |
| // - additionalBidsPromise: promise resolving to undefined, to be resolved when |
| // the additional bids have been retrieved with fetch(). |
| // - highestScoringOtherBid: the amount of the second-highest bid, |
| // or zero if there's no second-highest bid |
| // - winningAdditionalBidId: the label of the winning bid |
| async function runAdditionalBidTest(test, uuid, buyers, auctionNonce, |
| additionalBidsPromise, |
| highestScoringOtherBid, |
| winningAdditionalBidId) { |
| await runBasicFledgeAuctionAndNavigate( |
| test, uuid, |
| { interestGroupBuyers: buyers, |
| auctionNonce: auctionNonce, |
| additionalBids: additionalBidsPromise, |
| decisionLogicURL: createDecisionScriptURL( |
| uuid, |
| { reportResult: `sendReportTo("${createSellerReportURL(uuid)}&highestScoringOtherBid=" + Math.round(browserSignals.highestScoringOtherBid));` })}); |
| |
| await waitForObservedRequests( |
| uuid, [createHighestScoringOtherBidReportURL(uuid, highestScoringOtherBid), |
| createBidderReportURL(uuid, winningAdditionalBidId)]); |
| } |
| |
| // Similar to runAdditionalBidTest(), but expects no winner. It takes the |
| // following arguments: |
| // - test/uuid: the test object and uuid from the test case (see generateUuid) |
| // - buyers: array of strings, each a domain for a buyer participating in this |
| // auction |
| // - auctionNonce: string, the auction nonce for this auction, typically |
| // retrieved from a prior call to navigator.createAuctionNonce |
| // - additionalBidsPromise: promise resolving to undefined, to be resolved when |
| // the additional bids have been retrieved with fetch(). |
| async function runAdditionalBidTestNoWinner( |
| test, uuid, buyers, auctionNonce, additionalBidsPromise) { |
| await runBasicFledgeTestExpectingNoWinner(test, uuid, { |
| interestGroupBuyers: buyers, |
| auctionNonce: auctionNonce, |
| additionalBids: additionalBidsPromise, |
| decisionLogicURL: createDecisionScriptURL(uuid) |
| }); |
| } |
| |
| // Runs "script" in "child_window" via an eval call. The "child_window" must |
| // have been created by calling "createFrame()" below. "param" is passed to the |
| // context "script" is run in, so can be used to pass objects that |
| // "script" references that can't be serialized to a string, like |
| // fencedFrameConfigs. |
| async function runInFrame(test, child_window, script, param) { |
| const messageUuid = generateUuid(test); |
| let receivedResponse = {}; |
| |
| let promise = new Promise(function(resolve, reject) { |
| function WaitForMessage(event) { |
| if (event.data.messageUuid !== messageUuid) |
| return; |
| receivedResponse = event.data; |
| if (event.data.result === 'success') { |
| resolve(); |
| } else { |
| reject(event.data.result); |
| } |
| } |
| window.addEventListener('message', WaitForMessage); |
| child_window.postMessage( |
| {messageUuid: messageUuid, script: script, param: param}, '*'); |
| }); |
| await promise; |
| return receivedResponse.returnValue; |
| } |
| |
| // Creates an frame and navigates it to a URL on "origin", and waits for the URL |
| // to finish loading by waiting for the frame to send an event. Then returns |
| // the frame's Window object. Depending on the value of "is_iframe", the created |
| // frame will either be a new iframe, or a new top-level main frame. In the iframe |
| // case, its "allow" field will be set to "permissions". |
| // |
| // Also adds a cleanup callback to "test", which runs all cleanup functions |
| // added within the frame and waits for them to complete, and then destroys the |
| // iframe or closes the window. |
| async function createFrame(test, origin, is_iframe = true, permissions = null) { |
| const frameUuid = generateUuid(test); |
| const frameURL = |
| `${origin}${RESOURCE_PATH}subordinate-frame.sub.html?uuid=${frameUuid}`; |
| let promise = new Promise(function(resolve, reject) { |
| function WaitForMessage(event) { |
| if (event.data.messageUuid !== frameUuid) |
| return; |
| if (event.data.result === 'load complete') { |
| resolve(); |
| } else { |
| reject(event.data.result); |
| } |
| } |
| window.addEventListener('message', WaitForMessage); |
| }); |
| |
| if (is_iframe) { |
| let iframe = document.createElement('iframe'); |
| if (permissions) |
| iframe.allow = permissions; |
| iframe.src = frameURL; |
| document.body.appendChild(iframe); |
| |
| test.add_cleanup(async () => { |
| await runInFrame(test, iframe.contentWindow, "await test_instance.do_cleanup();"); |
| document.body.removeChild(iframe); |
| }); |
| |
| await promise; |
| return iframe.contentWindow; |
| } |
| |
| let child_window = window.open(frameURL); |
| test.add_cleanup(async () => { |
| await runInFrame(test, child_window, "await test_instance.do_cleanup();"); |
| child_window.close(); |
| }); |
| |
| await promise; |
| return child_window; |
| } |
| |
| // Wrapper around createFrame() that creates an iframe and optionally sets |
| // permissions. |
| async function createIframe(test, origin, permissions = null) { |
| return await createFrame(test, origin, /*is_iframe=*/true, permissions); |
| } |
| |
| // Wrapper around createFrame() that creates a top-level window. |
| async function createTopLevelWindow(test, origin) { |
| return await createFrame(test, origin, /*is_iframe=*/false); |
| } |
| |
| // Joins a cross-origin interest group. Currently does this by joining the |
| // interest group in an iframe, though it may switch to using a .well-known |
| // fetch to allow the cross-origin join, when support for that is added |
| // to these tests, so callers should not assume that's the mechanism in use. |
| async function joinCrossOriginInterestGroup(test, uuid, origin, interestGroupOverrides = {}) { |
| let interestGroup = JSON.stringify( |
| createInterestGroupForOrigin(uuid, origin, interestGroupOverrides)); |
| |
| let iframe = await createIframe(test, origin, 'join-ad-interest-group'); |
| await runInFrame(test, iframe, |
| `await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`); |
| } |
| |
| // Leaves a cross-origin interest group, by running a leave in an iframe. |
| async function leaveCrossOriginInterestGroup(test, uuid, origin, interestGroupOverrides = {}) { |
| let interestGroup = JSON.stringify( |
| createInterestGroupForOrigin(uuid, origin, interestGroupOverrides)); |
| |
| let iframe = await createIframe(test, origin, 'join-ad-interest-group'); |
| await runInFrame(test, iframe, |
| `await leaveInterestGroup(${interestGroup})`); |
| } |
| |
| // Joins an interest group in a top-level window, which has the same origin |
| // as the joined interest group. |
| async function joinInterestGroupInTopLevelWindow( |
| test, uuid, origin, interestGroupOverrides = {}) { |
| let interestGroup = JSON.stringify( |
| createInterestGroupForOrigin(uuid, origin, interestGroupOverrides)); |
| |
| let topLevelWindow = await createTopLevelWindow(test, origin); |
| await runInFrame(test, topLevelWindow, |
| `await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`); |
| } |
| |
| // Opens a top-level window and calls joinCrossOriginInterestGroup() in it. |
| async function joinCrossOriginInterestGroupInTopLevelWindow( |
| test, uuid, windowOrigin, interestGroupOrigin, interestGroupOverrides = {}) { |
| let interestGroup = JSON.stringify( |
| createInterestGroupForOrigin(uuid, interestGroupOrigin, interestGroupOverrides)); |
| |
| let topLevelWindow = await createTopLevelWindow(test, windowOrigin); |
| await runInFrame(test, topLevelWindow, |
| `await joinCrossOriginInterestGroup( |
| test_instance, "${uuid}", "${interestGroupOrigin}", ${interestGroup})`); |
| } |
| |
| // Fetch directFromSellerSignals from seller and check header |
| // 'Ad-Auction-Signals' is hidden from documents. |
| async function fetchDirectFromSellerSignals(headers_content, origin) { |
| const response = await fetch( |
| createDirectFromSellerSignalsURL(origin), |
| { adAuctionHeaders: true, headers: headers_content }); |
| |
| if (!('Negative-Test-Option' in headers_content)) { |
| assert_equals( |
| response.status, |
| 200, |
| 'Failed to fetch directFromSellerSignals: ' + await response.text()); |
| } |
| assert_false( |
| response.headers.has('Ad-Auction-Signals'), |
| 'Header "Ad-Auction-Signals" should be hidden from documents.'); |
| } |
| |
| // Generate directFromSellerSignals evaluation code for different worklets and |
| // pass to `runReportTest()` as `codeToInsert`. |
| function directFromSellerSignalsValidatorCode(uuid, expectedSellerSignals, |
| expectedAuctionSignals, expectedPerBuyerSignals) { |
| expectedSellerSignals = JSON.stringify(expectedSellerSignals); |
| expectedAuctionSignals = JSON.stringify(expectedAuctionSignals); |
| expectedPerBuyerSignals = JSON.stringify(expectedPerBuyerSignals); |
| |
| return { |
| // Seller worklets |
| scoreAd: |
| `if (directFromSellerSignals == null || |
| directFromSellerSignals.sellerSignals !== ${expectedSellerSignals} || |
| directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} || |
| Object.keys(directFromSellerSignals).length !== 2) { |
| throw 'Failed to get expected directFromSellerSignals in scoreAd(): ' + |
| JSON.stringify(directFromSellerSignals); |
| }`, |
| reportResultSuccessCondition: |
| `directFromSellerSignals != null && |
| directFromSellerSignals.sellerSignals === ${expectedSellerSignals} && |
| directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} && |
| Object.keys(directFromSellerSignals).length === 2`, |
| reportResult: |
| `sendReportTo("${createSellerReportURL(uuid)}");`, |
| |
| // Bidder worklets |
| generateBid: |
| `if (directFromSellerSignals == null || |
| directFromSellerSignals.perBuyerSignals !== ${expectedPerBuyerSignals} || |
| directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} || |
| Object.keys(directFromSellerSignals).length !== 2) { |
| throw 'Failed to get expected directFromSellerSignals in generateBid(): ' + |
| JSON.stringify(directFromSellerSignals); |
| }`, |
| reportWinSuccessCondition: |
| `directFromSellerSignals != null && |
| directFromSellerSignals.perBuyerSignals === ${expectedPerBuyerSignals} && |
| directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} && |
| Object.keys(directFromSellerSignals).length === 2`, |
| reportWin: |
| `sendReportTo("${createBidderReportURL(uuid)}");`, |
| }; |
| } |
| |
| let additionalBidHelper = function() { |
| |
| // Creates an additional bid with the given parameters. This additional bid |
| // specifies a biddingLogicURL that provides an implementation of |
| // reportAdditionalBidWin that triggers a sendReportTo() to the bidder report |
| // URL of the winning additional bid. Additional bids are described in more |
| // detail at |
| // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#6-additional-bids. |
| // Returned bids have an additional `testMetadata` field that's modified by |
| // several of the other helper functions defined below and is consumed by |
| // `fetchAdditionalBids()`. Created additional bids must be used only once, |
| // as `fetchAdditionalBids()` consumes and discards the `testMetadata` field. |
| function createAdditionalBid(uuid, seller, buyer, interestGroupName, bidAmount) { |
| return { |
| interestGroup: { |
| name: interestGroupName, |
| biddingLogicURL: createBiddingScriptURL({ |
| origin: buyer, |
| reportAdditionalBidWin: `sendReportTo("${ |
| createBidderReportURL(uuid, interestGroupName)}");` |
| }), |
| owner: buyer |
| }, |
| bid: {ad: ['metadata'], bid: bidAmount, render: createRenderURL(uuid)}, |
| seller: seller, |
| testMetadata: {} |
| }; |
| } |
| |
| // Sets the auction nonce that will be included by the server on the |
| // 'Ad-Auction-Additional-Bid' response header for this bid. All valid |
| // additional bids should have an auctionNonce in the header, so this |
| // should be called by most tests. |
| function setAuctionNonceInHeader(additionalBid, auctionNonce) { |
| additionalBid.testMetadata.auctionNonce = auctionNonce; |
| } |
| |
| // Sets the seller nonce that will be included by the server on the |
| // 'Ad-Auction-Additional-Bid' response header for this bid. |
| function setSellerNonceInHeader(additionalBid, sellerNonce) { |
| additionalBid.testMetadata.sellerNonce = sellerNonce; |
| } |
| |
| // Tells `fetchAdditionalBids` to correctly sign the additional bid with |
| // the given secret keys before returning that as a signed additional bid. |
| // The signatures aren't computed yet because `additionalBid` - whose string |
| // representation is signed - may still change between when this is called |
| // and when `fetchAdditionalBids` is called. |
| function signWithSecretKeys(additionalBid, secretKeys) { |
| additionalBid.testMetadata.secretKeysForValidSignatures = secretKeys; |
| } |
| |
| // Tells the additional bid endpoint to incorrectly sign the additional bid |
| // with the given secret keys before returning that as a signed additional |
| // bid. This is used for testing the behavior when the auction encounters an |
| // invalid signature. The signatures aren't computed yet because |
| // `additionalBid` - whose string representation is signed - may still change |
| // between when this is called and when `fetchAdditionalBids` is called. |
| function incorrectlySignWithSecretKeys(additionalBid, secretKeys) { |
| additionalBid.testMetadata.secretKeysForInvalidSignatures = secretKeys; |
| } |
| |
| // Takes the auctionNonce and sellerNonce as strings, and combines them with |
| // SHA256, returning the result as a base64 string. |
| async function computeBidNonce(auctionNonce, sellerNonce) { |
| // Compute the bidNonce as hashed bytes. |
| const combined_utf8 = new TextEncoder().encode(auctionNonce + sellerNonce); |
| const hashed = await crypto.subtle.digest('SHA-256',combined_utf8); |
| |
| // Convert the hashed bytes to base64. |
| return btoa(String.fromCharCode(...new Uint8Array(hashed))); |
| } |
| |
| // Adds a single negative interest group to an additional bid, as described at: |
| // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups |
| function addNegativeInterestGroup(additionalBid, negativeInterestGroup) { |
| additionalBid.negativeInterestGroup = negativeInterestGroup; |
| } |
| |
| // Adds multiple negative interest groups to an additional bid, as described at: |
| // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups |
| function addNegativeInterestGroups( |
| additionalBid, negativeInterestGroups, joiningOrigin) { |
| additionalBid.negativeInterestGroups = { |
| joiningOrigin: joiningOrigin, |
| interestGroupNames: negativeInterestGroups |
| }; |
| } |
| |
| const _ed25519ModulePromise = |
| import('../third_party/noble-ed25519/noble-ed25519.js'); |
| |
| // Returns a signature entry for a signed additional bid. |
| // |
| // `message` is the additional bid text (or other text if generating an |
| // invalid signature) to sign. |
| // |
| // `base64EncodedSecretKey` is the base64-encoded Ed25519 key with which to |
| // sign the message. From this secret key, the public key can be deduced, |
| // which becomes part of the signature entry. |
| async function _generateSignature(message, base64EncodedSecretKey) { |
| const ed25519 = await _ed25519ModulePromise; |
| const secretKey = |
| Uint8Array.from(atob(base64EncodedSecretKey), c => c.charCodeAt(0)); |
| const [publicKey, signature] = await Promise.all([ |
| ed25519.getPublicKeyAsync(secretKey), |
| ed25519.signAsync(new TextEncoder().encode(message), secretKey) |
| ]); |
| |
| return { |
| 'key': btoa(String.fromCharCode(...publicKey)), |
| 'signature': btoa(String.fromCharCode(...signature)) |
| }; |
| } |
| |
| // Returns a signed additional bid given an additional bid and secret keys. |
| // `additionalBid` is the additional bid to sign. It must not contain a |
| // `testMetadata` - that should have been removed prior to calling this. |
| // |
| // `secretKeysForValidSignatures` is a list of strings, each a base64-encoded |
| // Ed25519 secret key with which to sign the additional bid, whereas |
| // `secretKeysForInvalidSignatures` is a list of strings, each a |
| // base64-encoded Ed25519 secret key with which to *incorrectly* sign the |
| // additional bid. |
| async function _signAdditionalBid( |
| additionalBid, secretKeysForValidSignatures, |
| secretKeysForInvalidSignatures) { |
| async function _signString(string, secretKeys) { |
| if (!secretKeys) { |
| return []; |
| } |
| return await Promise.all(secretKeys.map( |
| async secretKey => await _generateSignature( |
| string, secretKey))); |
| } |
| |
| assert_not_own_property( |
| additionalBid, 'testMetadata', |
| 'testMetadata should be removed from additionalBid before signing'); |
| const additionalBidString = JSON.stringify(additionalBid); |
| let [validSignatures, invalidSignatures] = await Promise.all([ |
| _signString(additionalBidString, secretKeysForValidSignatures), |
| |
| // For invalid signatures, we use the correct secret key to sign a |
| // different message - the additional bid prepended by 'invalid' - so |
| // that the signature is a structually valid signature with the correct |
| // (public) key, but can't be used to verify the additional bid. |
| _signString('invalid' + additionalBidString, secretKeysForInvalidSignatures) |
| ]); |
| return { |
| 'bid': additionalBidString, |
| 'signatures': validSignatures.concat(invalidSignatures) |
| }; |
| } |
| |
| // Given an additionalBid object, this returns a string to be used as the |
| // value of the `Ad-Auction-Additional-Bid` response header. To produce this |
| // header, this signs the signing the `additionalBid` with the signatures |
| // specified by prior calls to `signWithSecretKeys` and |
| // `incorrectlySignWithSecretKeys` above; base64-encodes the stringified |
| // `signedAdditionalBid`; and then prepends that with the `auctionNonce` and/or |
| // `sellerNonce` specified by prior calls to `setAuctionNonceInHeader` and |
| // `setSellerNonceInHeader` above, respectively. |
| async function _convertAdditionalBidToResponseHeader(additionalBid) { |
| const testMetadata = additionalBid.testMetadata; |
| delete additionalBid.testMetadata; |
| |
| const signedAdditionalBid = await _signAdditionalBid( |
| additionalBid, |
| testMetadata.secretKeysForValidSignatures, |
| testMetadata.secretKeysForInvalidSignatures); |
| |
| |
| return [ |
| testMetadata.auctionNonce, |
| testMetadata.sellerNonce, |
| btoa(JSON.stringify(signedAdditionalBid)) |
| ].filter(k => k !== undefined).join(':'); |
| } |
| |
| // Fetch some number of fully prepared additional bid from a seller and verify |
| // that the `Ad-Auction-Additional-Bid` header is not visible in this |
| // JavaScript context. The `additionalBids` parameter is a list of additional |
| // bids objects created by `createAdditionalBid` and modified by other |
| // functions on this helper. Once passed to this method, additional bids may |
| // not be reused in a future call to `fetchAdditionalBids()`, since this |
| // mothod consumes and destroys their `testMetadata` field. |
| async function fetchAdditionalBids(seller, additionalBids) { |
| let additionalBidHeaderValues = await Promise.all(additionalBids.map( |
| async additionalBid => |
| await _convertAdditionalBidToResponseHeader(additionalBid))); |
| |
| const url = new URL(`${seller}${RESOURCE_PATH}additional-bids.py`); |
| url.searchParams.append( |
| 'additionalBidHeaderValues', JSON.stringify(additionalBidHeaderValues)); |
| const response = await fetch(url.href, {adAuctionHeaders: true}); |
| |
| assert_equals(response.status, 200, 'Failed to fetch additional bid: ' + await response.text()); |
| assert_false( |
| response.headers.has('Ad-Auction-Additional-Bid'), |
| 'Header "Ad-Auction-Additional-Bid" should not be available in JavaScript context.'); |
| } |
| |
| return { |
| createAdditionalBid: createAdditionalBid, |
| setAuctionNonceInHeader: setAuctionNonceInHeader, |
| setSellerNonceInHeader: setSellerNonceInHeader, |
| signWithSecretKeys: signWithSecretKeys, |
| incorrectlySignWithSecretKeys: incorrectlySignWithSecretKeys, |
| computeBidNonce: computeBidNonce, |
| addNegativeInterestGroup: addNegativeInterestGroup, |
| addNegativeInterestGroups: addNegativeInterestGroups, |
| fetchAdditionalBids: fetchAdditionalBids |
| }; |
| }(); |
| |
| |
| // DeprecatedRenderURLReplacements helper function. |
| // Returns an object containing sample strings both before and after the |
| // replacements in 'replacements' have been applied by |
| // deprecatedRenderURLReplacements. All substitution strings will appear |
| // only once in the output strings. |
| function createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements) { |
| let beforeReplacements = ''; |
| let afterReplacements = ''; |
| if(deprecatedRenderURLReplacements){ |
| for (const [match, replacement] of Object.entries(deprecatedRenderURLReplacements)) { |
| beforeReplacements += match + "/"; |
| afterReplacements += replacement + "/"; |
| } |
| } |
| return { beforeReplacements, afterReplacements }; |
| } |
| |
| // Delete all cookies. Separate function so that can be replaced with |
| // something else for testing outside of a WPT environment. |
| async function deleteAllCookies() { |
| await test_driver.delete_all_cookies(); |
| } |
| |
| // Deletes all cookies (to avoid pre-existing cookies causing inconsistent |
| // output on failure) and sets a cookie with name "cookie" and a value of |
| // "cookie". Adds a cleanup task to delete all cookies again when the test |
| // is done. |
| async function setCookie(test) { |
| await deleteAllCookies(); |
| document.cookie = 'cookie=cookie; path=/' |
| test.add_cleanup(deleteAllCookies); |
| } |
| |
| async function makeInterestGroupKAnonymous(passedInterestGroup) { |
| // Make a copy so we can sanitize fields without affecting the tests. |
| let interestGroup = structuredClone(passedInterestGroup); |
| const ownerURL = new URL(interestGroup.owner); |
| interestGroup.owner = ownerURL.origin; |
| interestGroup.name = String(interestGroup.name).toWellFormed(); |
| interestGroup.biddingLogicURL = |
| (new URL(interestGroup.biddingLogicURL, BASE_URL)).toString(); |
| |
| function b64(array) { |
| return btoa(String.fromCharCode.apply(null, array)); |
| } |
| let hashes = []; |
| if (Array.isArray(interestGroup.ads)) { |
| for (const ad of interestGroup.ads) { |
| hashes.push(b64(await computeKeyHashOfAd(interestGroup, ad))); |
| hashes.push( |
| b64(await computeKeyHashOfReportingId(interestGroup, ad, null))); |
| if (Array.isArray(ad.selectableBuyerAndSellerReportingIds)) { |
| for (const id of ad.selectableBuyerAndSellerReportingIds) { |
| hashes.push( |
| b64(await computeKeyHashOfReportingId(interestGroup, ad, id))); |
| } |
| } |
| } |
| } |
| if (Array.isArray(interestGroup.adComponents)) { |
| for (const ad of interestGroup.adComponents) { |
| hashes.push(b64(await computeKeyHashOfComponentAd(interestGroup, ad))); |
| } |
| } |
| await test_driver.set_protected_audience_k_anonymity( |
| interestGroup.owner, interestGroup.name, hashes); |
| } |
| |
| async function computeKeyHashOfAd(ig, ad) { |
| const encoder = new TextEncoder(); |
| const kAnonKey = encoder.encode( |
| `AdBid\n${ig.owner}/\n${ig.biddingLogicURL}\n${ad.renderURL}`); |
| return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey)); |
| } |
| |
| async function computeKeyHashOfReportingId(ig, ad, selectedReportingId = null) { |
| const encoder = new TextEncoder(); |
| let kAnonKey = null; |
| if (!selectedReportingId) { |
| if (ad.buyerAndSellerReportingId) { |
| kAnonKey = encoder.encode( |
| `BuyerAndSellerReportId\n${ig.owner}/\n${ig.biddingLogicURL}\n${ |
| ad.renderURL}\n${ad.buyerAndSellerReportingId}`); |
| } else if (ad.buyerReportingId) { |
| kAnonKey = encoder.encode(`BuyerReportId\n${ig.owner}/\n${ |
| ig.biddingLogicURL}\n${ad.renderURL}\n${ad.buyerReportingId}`); |
| } else { |
| kAnonKey = encoder.encode(`NameReport\n${ig.owner}/\n${ |
| ig.biddingLogicURL}\n${ad.renderURL}\n${ig.name}`); |
| } |
| } else { |
| function encodeKeyPartInto(part, array) { |
| array[0] = 0x0a; |
| if (!part) { |
| for (let i = 1; i < 6; i++) { |
| array[i] = 0x00; |
| } |
| return 6; |
| } |
| const len = part.length; |
| array[1] = 0x01; |
| array[2] = (len >> 24) % 256 |
| array[3] = (len >> 16) % 256 |
| array[4] = (len >> 8) % 256 |
| array[5] = len % 256; |
| encoder.encodeInto(part, array.subarray(6)); |
| return 1 + 5 + len; |
| } |
| const baseText = `SelectedBuyerAndSellerReportId\n${ig.owner}/\n${ |
| ig.biddingLogicURL}\n${ad.renderURL}`; |
| const selectedReportingIdLen = |
| 1 + 5 + (selectedReportingId ? selectedReportingId.length : 0); |
| const buyerAndSellerReportingIdLen = 1 + 5 + |
| (ad.buyerAndSellerReportingId ? ad.buyerAndSellerReportingId.length : 0) |
| const buyerReportingIdLen = |
| 1 + 5 + (ad.buyerReportingId ? ad.buyerReportingId.length : 0) |
| const expectedLen = baseText.length + selectedReportingIdLen + |
| buyerAndSellerReportingIdLen + buyerReportingIdLen; |
| kAnonKey = new Uint8Array(expectedLen); |
| let actualLen = 0; |
| actualLen += encoder.encodeInto(baseText, kAnonKey).written; |
| actualLen += |
| encodeKeyPartInto(selectedReportingId, kAnonKey.subarray(actualLen)); |
| actualLen += encodeKeyPartInto( |
| ad.buyerAndSellerReportingId, kAnonKey.subarray(actualLen)); |
| actualLen += |
| encodeKeyPartInto(ad.buyerReportingId, kAnonKey.subarray(actualLen)); |
| } |
| return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey)); |
| } |
| |
| async function computeKeyHashOfComponentAd(ig, ad) { |
| const encoder = new TextEncoder(); |
| const kAnonKey = encoder.encode(`ComponentBid\n${ad.renderURL}`); |
| return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey)); |
| } |