diff --git a/spec.bs b/spec.bs index 740ec174d..ab6271c2d 100644 --- a/spec.bs +++ b/spec.bs @@ -26,12 +26,17 @@ urlPrefix: https://fetch.spec.whatwg.org/; spec: Fetch text: task destination; for: fetch params; url: fetch-params-task-destination urlPrefix: https://www.ietf.org/rfc/rfc4122.txt type: dfn; text: urn uuid + type: dfn; text: string representation + type: dfn; text: version 4 UUID urlPrefix: https://github.com/WICG/turtledove/blob/main/FLEDGE_k_anonymity_server.md type: dfn; text: k-anonymity; url: what-is-k-anonymity urlPrefix: https://developer.chrome.com/en/docs/privacy-sandbox/glossary/ type: dfn; text: ad creative; url: ad-creative spec: RFC6234; urlPrefix: https://www.ietf.org/rfc/rfc6234.txt type: dfn; text: SHA-256 +urlPrefix: https://datatracker.ietf.org/doc/html/rfc8032 + type: dfn + text: verify; url: section-5.1.7 spec: html; urlPrefix: https://html.spec.whatwg.org/C type: dfn text: create an agent; url: create-an-agent @@ -120,7 +125,7 @@ spec: Shared Storage API; urlPrefix: https://wicg.github.io/shared-storage # Introduction # {#intro} -This section is non-normative +*This section is non-normative.* The Protected Audience API facilitates selecting an advertisement to display to a user based on a previous interaction with the advertiser or advertising network. @@ -129,13 +134,15 @@ When a user's interactions with an advertiser indicate an interest in something, ask the browser to record this interest on-device by calling {{Window/navigator}}.{{Navigator/joinAdInterestGroup()}}. Later, when a website wants to select an advertisement to show to the user, the website can call -{{Window/navigator}}.{{Navigator/runAdAuction()}} to ask the browser to conduct an auction where -each of these on-device recorded interests are given the chance to calculate a bid to display their -advertisement. +{{Window/navigator}}.{{Navigator/runAdAuction()}} to ask the browser to conduct an +auction where each of these on-device recorded interests are given the chance to +calculate a bid to display their advertisement.

Joining Interest Groups

+*This first introductory paragraph is non-normative.* + When a user's interactions with a website indicate that the user may have a particular interest, an advertiser or someone working on behalf of the advertiser (e.g. a demand side platform, DSP) can ask the user's browser to record this interest on-device by calling @@ -182,6 +189,7 @@ dictionary GenerateBidInterestGroup { dictionary AuctionAdInterestGroup : GenerateBidInterestGroup { double priority = 0.0; record prioritySignalsOverrides; + DOMString additionalBidKey; }; @@ -315,6 +323,16 @@ This is detectable because it can change the set of fields that are read from th a {{TypeError}}. 1. Set |igAd|'s [=interest group ad/allowed reporting origins=] to |allowedReportingOrigins|. 1. [=list/Append=] |igAd| to |interestGroup|'s |interestGroupField|. + 1. If |group|["{{AuctionAdInterestGroup/additionalBidKey}}"] [=map/exists=]: + 1. Let |decodedKey| be the result of running [=forgiving-base64 decode=] with + |group|["{{AuctionAdInterestGroup/additionalBidKey}}"]. + 1. [=exception/Throw=] a {{TypeError}} if any of the following conditions hold: + * |decodedKey| is a failure; + * |decodedKey|'s [=byte sequence/length=] is not 32; + * |group|["{{GenerateBidInterestGroup/ads}}"] [=map/exists=]; + + * |group|["{{GenerateBidInterestGroup/updateURL}}"] [=map/exists=]. + 1. Set |interestGroup|'s [=interest group/additional bid key=] to |decodedKey|. 1. If |interestGroup|'s [=interest group/estimated size=] is greater than 50 KB, then [=exception/throw=] a {{TypeError}}. 1. Let |p| be [=a new promise=]. @@ -347,17 +365,17 @@ The estimated size of an [=interest group=] |ig| 1. The [=string/length=] of the [=serialization of an origin|serialization=] of |ig|'s [=interest group/owner=]. 1. The [=string/length=] of |ig|'s [=interest group/name=]. -1. 8 bytes, which is the size of |ig|'s [=interest group/priority=]. +1. 8, which is the size of |ig|'s [=interest group/priority=]. 1. The [=string/length=] of |ig|'s [=interest group/execution mode=]. -1. 2 bytes, which is the size of |ig|'s [=interest group/enable bidding signals prioritization=]. +1. 2, which is the size of |ig|'s [=interest group/enable bidding signals prioritization=]. 1. If |ig|'s [=interest group/priority vector=] is not null, [=map/for each=] |key| → |value| of [=interest group/priority vector=]: 1. The [=string/length=] of |key|. - 1. 8 bytes, which is the size of |value|. + 1. 8, which is the size of |value|. 1. If |ig|'s [=interest group/priority signals overrides=] is not null, [=map/for each=] |key| → |value| of [=interest group/priority signals overrides=]: 1. The [=string/length=] of |key|. - 1. 8 bytes, which is the size of |value|. + 1. 8, which is the size of |value|. 1. The [=string/length=] of the [=URL serializer|serialization=] of |ig|'s [=interest group/bidding url=], if the field is not null. 1. The [=string/length=] of the [=URL serializer|serialization=] of |ig|'s @@ -383,6 +401,8 @@ The estimated size of an [=interest group=] |ig| 1. The [=string/length=] of the [=URL serializer|serialization=] of |ad|'s [=interest group ad/render url=]. 1. The [=string/length=] of |ad|'s [=interest group ad/metadata=] if the field is not null. +1. If |ig|'s [=interest group/additional bid key=] is not null: + 1. 32, which is its size (number of bytes). @@ -454,6 +474,8 @@ To build an interest group permissions url given a [=origin=] |ownerO

Leaving Interest Groups

+*This first introductory paragraph is non-normative.* + {{Window/navigator}}.{{Navigator/leaveAdInterestGroup()}} removes a user from a particular interest group. @@ -516,6 +538,8 @@ are:

Running Ad Auctions

+*This first introductory paragraph is non-normative.* + When a website or someone working on behalf of the website (e.g. a supply side platform, SSP) wants to conduct an auction to select an advertisement to display to the user, they can call the {{Window/navigator}}.{{Navigator/runAdAuction()}} function, providing an auction configuration that @@ -548,6 +572,8 @@ dictionary AuctionAdConfig { record> perBuyerPrioritySignals; Promise> perBuyerCurrencies; sequence componentAuctions = []; + Promise additionalBids; + DOMString auctionNonce; AbortSignal? signal; Promise resolveToConfig; }; @@ -930,6 +956,29 @@ To validate and convert auction ad config given an {{AuctionAdConfig} * To parse the value |result|, set |auctionConfig|'s [=auction config/seller signals=] to the result of [=serializing a JavaScript value to a JSON string=], given |result|. * To handle an error, set |auctionConfig|'s [=auction config/seller signals=] to failure. +1. If |config|["{{AuctionAdConfig/auctionNonce}}"] [=map/exists=], then [=map/set=] |auctionConfig|'s + + [=auction config/auction nonce=] to the result of running [=get uuid from string=] with + |config|["{{AuctionAdConfig/auctionNonce}}"]. +1. If |config|["{{AuctionAdConfig/additionalBids}}"] [=map/exists=]: + 1. [=exception/Throw=] a {{TypeError}} if any of the following conditions hold: + * |config|["{{AuctionAdConfig/auctionNonce}}"] does not [=map/exist=]; + * |config|["{{AuctionAdConfig/interestGroupBuyers}}"] does not [=map/exist=], or is [=list/empty=]; + * |config|["{{AuctionAdConfig/componentAuctions}}"] is not [=list/empty=]. + 1. Set |auctionConfig|'s [=auction config/expects additional bids=] to true. + 1. [=Handle an input promise in configuration=] given |auctionConfig| and + |config|["{{AuctionAdConfig/additionalBids}}"]: + + Note: The JavaScript code calling {{Navigator/runAdAuction()}} is responsible for not resolving + |config|["{{AuctionAdConfig/additionalBids}}"] until an associated [=request=] whose + [=request/initiator type=] is `"fetch"` and the {{RequestInit/adAuctionHeaders}} option set to + `true` returns a response, or alternatively, not calling {{Navigator/runAdAuction()}} until the + [=request=] returns a response. + + * To parse the value |result|: + 1. Set |auctionConfig|'s [=auction config/expects additional bids=] to false. + * To handle an error: + 1. Set |auctionConfig|'s [=auction config/expects additional bids=] to failure. 1. If |config|["{{AuctionAdConfig/directFromSellerSignalsHeaderAdSlot}}"] [=map/exists=]: 1. [=Handle an input promise in configuration=] given |auctionConfig| and |config|["{{AuctionAdConfig/directFromSellerSignalsHeaderAdSlot}}"]: @@ -1107,9 +1156,17 @@ To parse an https origin given a [=string=] |input|: To build bid generators map given an [=auction config=] |auctionConfig|: 1. Let |bidGenerators| be a new [=ordered map=] whose [=map/keys=] are [=origins=] and whose [=map/values=] are [=per buyer bid generators=]. +1. Let |negativeTargetInfo| be a new [=negative target info=]. 1. [=list/For each=] |buyer| in |auctionConfig|'s [=auction config/interest group buyers=]: 1. [=list/For each=] |ig| of the user agent's [=interest group set=] whose [=interest group/owner=] is |buyer|: + 1. Let |igName| be |ig|'s [=interest group/name=]. + 1. If |ig|'s [=interest group/additional bid key=] is not null: + 1. [=map/Set=] |negativeTargetInfo|[(|buyer|, |igName|)] to (|ig|'s + [=interest group/joining origin=], |ig|'s [=interest group/additional bid key=]). + 1. [=iteration/Continue=] if any of the following conditions hold: + * |ig|'s [=interest group/bidding url=] is null; + * |ig|'s [=interest group/ads=] is null, or [=list/is empty=]. 1. Let |signalsUrl| be |ig|'s [=interest group/trusted bidding signals url=]. 1. Let |joiningOrigin| be |ig|'s [=interest group/joining origin=]. 1. If |bidGenerators| does not [=map/contain=] |buyer|: @@ -1131,7 +1188,7 @@ To build bid generators map given an [=auction config=] |auctionConfi 1. If |perSignalsUrlGenerator| does not [=map/contain=] |joiningOrigin|, then [=map/set=] |perSignalsUrlGenerator|[|joiningOrigin|] to « |ig| ». 1. Otherwise, [=list/append=] |ig| to |perSignalsUrlGenerator|[|joiningOrigin|]. -1. Return |bidGenerators|. +1. Return « |bidGenerators|, |negativeTargetInfo| ». @@ -1190,7 +1247,8 @@ To generate and score bids given an [=auction config=] |auctionConfig 1. Let |decisionLogicScript| be the result of [=fetching script=] with |auctionConfig|'s [=auction config/decision logic url=]. 1. If |decisionLogicScript| is failure, return null. -1. Let |bidGenerators| be the result of running [=build bid generators map=] with |auctionConfig|. +1. Let « |bidGenerators|, |negativeTargetInfo| » be the result of running + [=build bid generators map=] with |auctionConfig|. 1. Let |leadingBidInfo| be a new [=leading bid info=]. 1. Let |queue| be the result of [=starting a new parallel queue=]. 1. Let |capturedAuctionHeaders| be |global|'s [=associated Document's=] [=node navigable's=] @@ -1276,6 +1334,14 @@ To generate and score bids given an [=auction config=] |auctionConfig 1. Set |componentAuctionExpectedCurrency| to the result of [=looking up per-buyer currency=] with |topLevelAuctionConfig| and |auctionConfig|'s [=auction config/seller=]. 1. Let |pendingBuyers| be the [=map/size=] of |bidGenerators|. +1. Let |additionalBids| be the result of running [=validate and convert additional bids=] with + |auctionConfig|, |topLevelAuctionConfig|, |negativeTargetInfo| and |global|. +1. Let |pendingAdditionalBids| be the [=list/size=] of |additionalBids|. +1. [=list/For each=] |additionalBid| of |additionalBids|, run the following steps [=in parallel=]: + 1. [=Score and rank a bid=] with |auctionConfig|, |additionalBid|, |leadingBidInfo|, + |decisionLogicScript|, null, |auctionLevel|, |componentAuctionExpectedCurrency|, and + |settings|'s [=environment/top-level origin=]. + 1. Decrement |pendingAdditionalBids| by 1. 1. [=map/For each=] |buyer| → |perBuyerGenerator| of |bidGenerators|, [=parallel queue/enqueue steps|enqueue the following steps=] to |queue|: 1. Let |buyerExperimentGroupId| be |allBuyersExperimentGroupId|. @@ -1371,7 +1437,7 @@ To generate and score bids given an [=auction config=] |auctionConfig |decisionLogicScript|, |directFromSellerSignalsForSeller|, |dataVersion|, |auctionLevel|, |componentAuctionExpectedCurrency|, and |settings|'s [=environment/top-level origin=]. 1. Decrement |pendingBuyers| by 1. -1. Wait until |pendingBuyers| is 0. +1. Wait until both |pendingBuyers| and |pendingAdditionalBids| are 0. 1. If |leadingBidInfo|'s [=leading bid info/leading bid=] is null, return null. 1. If |topLevelAuctionConfig| is null: 1. Let « |sellerSignals|, |reportResultBrowserSignals| » be the result of running @@ -1615,7 +1681,7 @@ The Ad-Auction-Allowed HTTP response header
To validate fetching response given a [=response=] |response|, null, failure, or a -[=byte sequence=]|responseBody|, and a [=string=] |mimeType|: +[=byte sequence=] |responseBody|, and a [=string=] |mimeType|: 1. If |responseBody| is null or failure, return false. 1. If [=header list/getting a structured field value|getting=] [:Ad-Auction-Allowed:] and @@ -2074,6 +2140,9 @@ a {{ReportingBrowserSignals}} |browserSignals|, and a [=direct from seller signa to |winner|'s [=generated bid/interest group=] [=interest group/name=]. 1. Let |buyerReportingScript| be the result of [=fetching script=] with |winner|'s [=generated bid/interest group=]'s [=interest group/bidding url=]. + 1. Let |reportFunctionName| be "`reportWin`". + 1. If |winner|'s [=generated bid/provided as additional bid=] is true: + 1. Set |reportFunctionName| be "`reportAdditionalBidWin`". 1. Let « ignored, |resultUrl|, |reportingBeaconMap|, |reportingMacroMap| » be the result of [=evaluating a reporting script=] with |buyerReportingScript|, "`reportWin`", and « |leadingBidInfo|'s [=leading bid info/auction config=]'s [=auction config/config idl=]'s @@ -2091,6 +2160,411 @@ a {{ReportingBrowserSignals}} |browserSignals|, and a [=direct from seller signa :: |reportingMacroMap|
+# Additional Bids and Negative Targeting # {#additional-bids-and-negative-targeting} + +## createAuctionNonce() ## {#create-auction-nonce} + +*This first introductory paragraph is non-normative.* + +{{Window/navigator}}.{{Navigator/createAuctionNonce()}} creates an auction nonce, a +one-time [=version 4 UUID=] uniquely associated with a single call to +{{Window/navigator}}.{{Navigator/runAdAuction()}}. For multi-seller auctions, this ID is uniquely +associated with all {{AuctionAdConfig/componentAuctions}}. This nonce will need to be passed back in +via a subsequent call to {{Window/navigator}}.{{Navigator/runAdAuction()}} via the +{{AuctionAdConfig}}. This is currently only needed for [=auctions=] that use [=additional bids=], +for which the [=auction nonce=] will be included in each [=additional bid=] as a way of ensuring +that those bids are only used in the [=auctions=] for which they were intended. + + +[SecureContext] +partial interface Navigator { + Promise<DOMString> createAuctionNonce(); +}; + + +
+The createAuctionNonce() method steps are: + + 1. Let |p| be [=a new promise=]. + 1. Run the following steps [=in parallel=]: + 1. Let |nonce| be the [=string representation=] of a [=version 4 UUID=]. + +
+ Because we're going in parallel: + * There is no guarantee that the promise will be resolved before other tasks get queued on the + main thread; + * ...which gives browsers the freedom to generate this UUID in another process, and + asynchronously send it back to the main thread at an arbitrary future time. +
+ 1. Let |global| be [=this=]'s [=relevant global object=]. + 1. [=Queue a global task=] on [=DOM manipulation task source=], given |global|, to [=resolve=] + |p| with |nonce|. + 1. Return |p|. +
+ +## Additional Bids ## {#additional-bids-section} + +*This first introductory paragraph is non-normative.* + +In addition to [=generate a bid|bids generated by interest groups=], sellers can enable buyers to +introduce bids generated outside of the auction, which are called additional bids. +[=Additional bids=] are commonly triggered using contextual signals. Buyers compute the +[=additional bids=], likely as part of a contextual auction. Buyers need to package up each +[=additional bid=] using a new data structure that encapsulates all of the information needed for +the [=additional bid=] to compete against other bids in a Protected Audience [=auction=]. + +
+

Each additional bid is expressed using the following JSON data structure:

+
+  const additionalBid = {
+    "bid": {
+      "ad": 'ad-metadata',
+      "adCost": 2.99,
+      "bid": 1.99,
+      "bidCurrency": "USD",
+      "render": "https://www.example-dsp.com/ad/123.jpg",
+      "adComponents": [adComponent1, adComponent2],
+      "allowComponentAuction": true,
+      "modelingSignals": 123,
+    },
+    "interestGroup": {
+      "owner": "https://www.example-dsp.com"
+      "name": "campaign123",
+      "biddingLogicURL": "https://www.example-dsp.com/bid_logic.js"
+    },
+    "negativeInterestGroups": {
+      joiningOrigin: "https://www.example-advertiser.com",
+      interestGroupNames: [
+        "example_advertiser_negative_interest_group_a",
+        "example_advertiser_negative_interest_group_b",
+      ]
+    },
+    "auctionNonce": "12345678-90ab-cdef-fedcba09876543210",
+    "seller": "https://www.example-ssp.com",
+    "topLevelSeller": "https://www.another-ssp.com"
+  }
+  
+
+ +
+To validate and convert additional bids given an [=auction config=] |auctionConfig|, an +[=auction config=]-or-null |topLevelAuctionConfig|, a [=negative target info=] |negativeTargetInfo|, +and a [=global object=] |global|: + + 1. [=Assert=] that these steps are running [=in parallel=]. + 1. [=Assert=] that |auctionConfig|'s [=auction config/auction nonce=] is not null. + 1. Let |auctionNonce| be the [=string representation=] of |auctionConfig|'s + [=auction config/auction nonce=]. + 1. Let |capturedAdditionalBidsHeaders| be |global|'s [=associated Document's=] + [=node navigable's=] [=traversable navigable's=] [=captured additional bids headers=]. + 1. Let |additionalBids| be a new [=list=] of [=decoded additional bids=]. + 1. [=list/For each=] |encodedSignedAdditionalBid| of |capturedAdditionalBidsHeaders|[|auctionNonce|]: + 1. Let |signedAdditionalBid| be the result of running [=forgiving-base64 decode=] with + |encodedSignedAdditionalBid|. + 1. If |signedAdditionalBid| is failure, then [=iteration/continue=]. + 1. Let |additionalBid| be the result of running [=parse a signed additional bid=] given + |signedAdditionalBid|, |auctionConfig|, |topLevelAuctionConfig|, and |negativeTargetInfo|. + 1. If |additionalBid| is not null, then [=list/append=] |additionalBid| to |additionalBids|. + 1. Return |additionalBids|. +
+ +
+To parse a signed additional bid given a [=byte sequence=] |signedAdditionalBid|, an +[=auction config=] |auctionConfig|, an [=auction config=]-or-null |topLevelAuctionConfig|, and a +[=negative target info=] |negativeTargetInfo|: + + 1. [=Assert=] that these steps are running [=in parallel=]. + 1. Let |parsedSignedAdditionalBid| be the result of running [=parse a JSON string to an infra value=] + given |signedAdditionalBid|. + 1. Return null if any of the following conditions hold: + * |parsedSignedAdditionalBid| is not a [=map=]; + * |parsedSignedAdditionalBid|["bid"] does not [=map/exist=], or is not a [=string=]; + * |parsedSignedAdditionalBid|["signatures"] does not [=map/exist=], or is not a [=list=]. + 1. Let |signatures| be a new [=list=] of [=signed additional bid signatures=]. + 1. Let |decodeSignatureFailed| be false. + 1. [=list/For each=] |sig| of |parsedSignedAdditionalBid|["signatures"]: + 1. Set |decodeSignatureFailed| to true and [=iteration/break=] if any of the following + conditions hold: + * |sig| is not a [=map=]; + * |sig|["key"] does not [=map/exist=], or is not a [=string=]; + * |sig|["signature"] does not [=map/exist=], or is not a [=string=]. + 1. Let |maybeKey| be the result of running [=forgiving-base64 decode=] with |sig|["key"]. + 1. Let |maybeSignature| be the result of running [=forgiving-base64 decode=] with + |sig|["signature"]. + 1. Set |decodeSignatureFailed| to true and [=iteration/break=] if any of the following + conditions hold: + * |maybeKey| is failure, or its [=byte sequence/length=] is not 32; + * |maybeSignature| is failure, or its [=byte sequence/length=] is not 64; + 1. Let |signature| be a [=signed additional bid signatures=], whose + [=signed additional bid signature/key=] is |maybeKey|, and + [=signed additional bid signature/signature=] is |maybeSignature|. + 1. [=list/Append=] |signature| to |signatures|. + 1. If |decodeSignatureFailed| is true, then return null. + 1. Let |decodedAdditionalBid| be the result of [=decode an additional bid json=] given + |parsedSignedAdditionalBid|["bid"], |auctionConfig| and |topLevelAuctionConfig|. + 1. Return null if any of the following conditions hold: + * |decodedAdditionalBid| is failure; + * The result of [=checking a currency tag=] with |decodedAdditionalBid|'s + [=decoded additional bid/bid=]'s [=generated bid/bid=]'s [=bid with currency/currency=] and + the result of running [=look up per-buyer currency=] with |auctionConfig|. + 1. Let |verifiedSignatureKeys| be a new [=set=] of [=byte sequences=]. + 1. [=list/For each=] |signature| of |signatures|: + 1. Let |isSignatureValid| be the result of running [=verify=] |signature|'s + [=signed additional bid signature/signature=] on message |parsedSignedAdditionalBid|["bid"] + using |signature|'s [=signed additional bid signature/key=], with 0 for Ed25519ctx. + 1. If |isSignatureValid| is true, then [=set/append=] |signature|'s + [=signed additional bid signature/key=] to |verifiedSignatureKeys|. + 1. If the result of [=checking whether negative targeted=] given |decodedAdditionalBid|, + |verifiedSignatureKeys| and |negativeTargetInfo| is true, then return null. + 1. Return |decodedAdditionalBid|. +
+ +
+To decode an additional bid json given a [=string=] |additionalBidJson|, an +[=auction config=] |auctionConfig|, and an [=auction config=]-or-null |topLevelAuctionConfig|: + + 1. [=Assert=] that these steps are running [=in parallel=]. + 1. Let |parsedAdditionalBid| be the result of [=parse a JSON string to an infra value=] given + |additionalBidJson|. + 1. If |parsedAdditionalBid| is not a [=map=], then return failure. + 1. Let |result| be a new [=decoded additional bid=]. + 1. Return failure if any of the following conditions hold: + * |parsedAdditionalBid|["auctionNonce"] does not [=map/exist=]; + * |parsedAdditionalBid|["auctionNonce"] is not the [=string representation=] of + |auctionConfig|'s [=auction config/auction nonce=]; + * |parsedAdditionalBid|["seller"] does not [=map/exist=]; + * The result of running the [=parse an https origin=] with |parsedAdditionalBid|["seller"] is + failure, or not [=same origin=] with |auctionConfig|'s [=auction config/seller=]. + 1. If |topLevelAuctionConfig| is null: + 1. If |parsedAdditionalBid|["topLevelSeller"] [=map/exists=], then return failure. + 1. Otherwise: + 1. If |parsedAdditionalBid|["topLevelSeller"] does not [=map/exist=], then return failure. + 1. Let |bidTopLevelSeller| be the result of running the [=parse an https origin=] with + |parsedAdditionalBid|["topLevelSeller"]. + 1. If |bidTopLevelSeller| is failure, or |bidTopLevelSeller| is not [=same origin=] with + |topLevelAuctionConfig|'s [=auction config/seller=], then return failure. + 1. If |parsedAdditionalBid|["interestGroup"] does not [=map/exist=], then return failure. + 1. Let |igMap| be |parsedAdditionalBid|["interestGroup"]. + 1. Return failure if any the following conditions hold: + * |igMap| is not a [=map=]; + * |igMap|["name"] does not [=map/exist=], or is not a [=string=]; + * |igMap|["biddingLogicURL"] does not [=map/exist=], or is not a [=string=]; + * |igMap|["owner"] does not [=map/exist=], or is not a [=string=]; + 1. Let |igOwner| be the result of running [=parse an https origin=] given |igMap|["owner"]. + 1. Let |igName| be |igMap|["name"]. + 1. Let |igBiddingUrl| be the result of running [=url parser=] on |igMap|["biddingLogicURL"]. + 1. Return failure if any of the following conditions hold: + * |igOwner| is failure; + * |auctionConfig|'s [=auction config/interest group buyers=] does not [=list/contain=] |igOwner|; + * |igBiddingUrl| is failure; + * |igOwner| is not [=same origin=] with |igBiddingUrl|. + 1. Let |ig| be a new [=interest group=] with the following properties: + : [=interest group/owner=] + :: |igOwner| + : [=interest group/name=] + :: |igName| + : [=interest group/bidding url=] + :: |igBiddingUrl| + 1. If |parsedAdditionalBid|["bid"] does not [=map/exist=], or is not a [=map=], return failure. + 1. Let |bidMap| be |parsedAdditionalBid|["bid"]. + 1. If |bidMap|["render"] does not [=map/exist=] or is not a [=string=], then return failure. + 1. Let |renderUrl| be the result of running [=URL parser=] on |bidMap|["render"]. + + 1. If |renderUrl| is failure, then return failure. + 1. Let |ad| be a new [=interest group ad=] whose [=interest group ad/render url=] is |renderUrl|. + 1. Set |ig|'s [=interest group/ads=] to « |ad| ». + 1. Let |bidVal| be |bidMap|["bid"] if it [=map/exists=], otherwise return failure. + 1. If |bidVal| is not a {{double}}, or is less than or equal to 0, then return failure. + 1. Let |adMetadata| be "null". + 1. If |bidMap|["ad"] [=map/exists=]: + 1. Set |adMetadata| to the result of running [=serialize an Infra value to a JSON string=] with + |bidMap|["ad"]. + 1. Let |bidCurrency| be null. + 1. If |bidMap|["bidCurrency"] [=map/exists=]: + 1. If |bidMap|["bidCurrency"] is not a [=string=], or the result of [=checking whether a string + is a valid currency tag=] is failure, then return failure. + 1. Set |bidCurrency| to |bidMap|["bidCurrency"]. + 1. Let |adCost| be null. + 1. If |bidMap|["adCost"] [=map/exists=]: + 1. If |bidMap|["adCost"] is not a {{double}}, then return failure. + 1. Set |adCost| to |bidMap|["adCost"]. + 1. Let |modelingSignals| be null. + 1. If |bidMap|["modelingSignals"] [=map/exists=]: + 1. If |bidMap|["modelingSignals"] is not a {{double}}, then return failure. + 1. If |bidMap|["modelingSignals"] ≥ 0, and < 4096, then set |modelingSignals| to + |bidMap|["modelingSignals"]. + 1. Let |adComponents| be a new [=list=] of [=ad descriptors=]. + 1. If |bidMap|["adComponents"] [=map/exists=]: + 1. If |bidMap|["adComponents"] is not a [=list=], then return failure. + 1. [=list/For each=] |component| of |bidMap|["adComponents"]: + 1. If |component| is not a [=string=], then return failure. + 1. Let |componentUrl| be the result of running [=URL parser=] on |component|. + 1. If |componentUrl| is failure, then return failure. + 1. Let |componentDescriptor| be a new [=ad descriptor=] whose [=ad descriptor/url=] is + |componentUrl|. + 1. [=list/Append=] |componentDescriptor| to |adComponents|. + 1. Set |ig|'s [=interest group/ad components=] to |adComponents|. + 1. If |parsedAdditionalBid|["negativeInterestGroup"] [=map/exists=]: + 1. If |parsedAdditionalBid|["negativeInterestGroups"] [=map/exists=], or + |parsedAdditionalBid|["negativeInterestGroup"] is not a [=string=], then return failure. + 1. [=list/Append=] |parsedAdditionalBid|["negativeInterestGroup"] to |result|'s + [=decoded additional bid/negative target interest group names=]. + 1. If |parsedAdditionalBid|["negativeInterestGroups"] [=map/exists=]: + 1. Let |multipleNegativeIg| be |parsedAdditionalBid|["negativeInterestGroups"]. + 1. Return failure if any of the following conditions hold: + * |multipleNegativeIg| is not a [=map=]; + * |multipleNegativeIg|["joiningOrigin"] does not [=map/exist=], or is not a [=string=]; + * |multipleNegativeIg|["interestGroupNames"] does not [=map/exist=], or is not a [=list=]. + 1. Let |joiningOrigin| be the result of running [=parse an https origin=] with + |multipleNegativeIg|["joiningOrigin"]. + 1. If |joiningOrigin| is failure, then return failure. + 1. Set |result|'s [=decoded additional bid/negative target joining origin=] to |joiningOrigin|. + 1. [=list/For each=] |igName| of |multipleNegativeIg|["interestGroupNames"]: + 1. If |igName| is not a [=string=], then return failure. + 1. [=list/Append=] |igName| to |result|'s + [=decoded additional bid/negative target interest group names=]. + 1. Set |result|'s [=decoded additional bid/bid=] to a new [=generated bid=] with the following + properties: + : [=generated bid/bid=] + :: A [=bid with currency=] whose [=bid with currency/value=] is |bidVal|, and + [=bid with currency/currency=] is |bidCurrency| + : [=generated bid/ad=] + :: |adMetadata| + : [=generated bid/ad descriptor=] + :: An [=ad descriptor=] whose [=ad descriptor/url=] is |renderUrl| + : [=generated bid/ad component descriptors=] + :: |adComponents| + : [=generated bid/ad cost=] + :: |adCost| + : [=generated bid/modeling signals=] + :: |modelingSignals| + : [=generated bid/interest group=] + :: |ig| + : [=generated bid/bid ad=] + :: A [=interest group ad=] whose [=interest group ad/render url=] is |renderUrl|, and + [=interest group ad/metadata=] is |adMetadata| + : [=generated bid/provided as additional bid=] + :: true + 1. Return |result|. +
+ +A signed additional bid signature is a [=struct=] with the following [=struct/items=]: +
+ : key + :: A [=byte sequence=] of length 32. + : signature + :: A [=byte sequence=] of length 64. +
+ +A decoded additional bid is a [=struct=] with the following [=struct/items=]: +
+ : bid + :: A [=generated bid=]. Fields analogous to those returned by `generateBid()`. + : negative target interest group names + :: A [=list=] of [=strings=]. + : negative target joining origin + :: Null or an [=origin=]. Required if there is more than one entry in + [=decoded additional bid/negative target interest group names=]. +
+ +Each [=traversable navigable=] has a captured additional bids headers, which is a [=map=] +whose [=map/keys=] are [=strings=] for [=auction nonces=], and whose values are [=list=] of +[=strings=] for encoded additional bids. + +## Negative Targeting ## {#negative-targeting-section} + +*This first introductory paragraph is non-normative.* + +In online ad auctions for ad space, it’s sometimes useful to prevent showing an ad to certain +audiences, a concept known as negative targeting. To facilitate [=negative targeting=] in +Protected Audience [=auctions=], each [=additional bid=] is allowed to identify one or more +[=negative interest groups=]. If the user has been joined to any of the identified +[=negative interest groups=], the [=additional bid=] is dropped; otherwise it participates in the +[=auction=], competing alongside bids created by calls to `generateBid()`. An [=additional bid=] +that specifies no [=negative interest groups=] is always accepted into the [=auction=]. + +
+To check whether negative targeted given an [=decoded additional bid=] |additionalBid|, a +[=set=] of [=byte sequences=] |verifiedSignatureKeys|, and a [=negative target info=] +|negativeTargetInfo|: + + 1. [=Assert=] that these steps are running [=in parallel=]. + 1. Let |negativeTargeted| be false. + 1. Let |additionalBidBuyer| be |additionalBid|'s [=decoded additional bid/bid=]'s + [=generated bid/interest group=]'s [=interest group/owner=]. + 1. [=list/For each=] |igName| of |additionalBid|'s + [=decoded additional bid/negative target interest group names=]: + 1. If |negativeTargetInfo|[(|additionalBidBuyer|, |igName|)] [=map/exists=]: + 1. Let (|joiningOrigin|, |additionalBidKey|) be + |negativeTargetInfo|[(|additionalBidBuyer|, |igName|)]. + 1. If |verifiedSignatureKeys| [=list/contains=] |additionalBidKey|: + 1. If |joiningOrigin| is not null: + 1. If |joiningOrigin| is not [=same origin=] with |additionalBid|'s + [=decoded additional bid/negative target joining origin=], then [=iteration/continue=]. + 1. Set |negativeTargeted| to true, and [=iteration/break=]. + + Note: If the signature doesn't verify successfully, the [=additional bid=] proceeds as if the + [=negative interest group=] is not present. This ensures that only the + [=interest group/owner=] of the [=negative interest group=], who created the + {{AuctionAdInterestGroup/additionalBidKey}}, is allowed to + [=negative targeting|negatively target=] the interest group, and that nobody else can learn + whether the [=interest group set=] [=list/contains=] the interest group. + 1. Return |negativeTargeted|. +
+ +A negative target info is a [=map=]. Its [=map/keys=] are [=tuples=] consisting of an +[=origin=] for [=interest group/owner=] and a [=string=] for [=interest group/name=]. +Its [=map/values=] are [=tuples=] consisting of an [=origin=] for [=interest group/joining origin=] +and a [=byte sequence=] for [=interest group/additional bid key=]. + +### Negative Interest Groups ### {#negative-interest-groups} + +*This section is non-normative.* + +Though [=negative interest groups=] are joined using the same {{Navigator/joinAdInterestGroup()}} +API as [=regular interest groups=], they remain distinct from one another. Only +[=negative interest groups=]'s [=interest group/additional bid key=] can be non-null, while only +[=regular interest groups=]'s [=interest group/ads=] can be non-null. Because the subset of fields +used by a [=negative interest group=] cannot be meaningfully updated, a [=negative interest group=]'s +[=interest group/update url=] must be null, otherwise a {{TypeError}} will be [=exception/thrown=] by +{{Navigator/joinAdInterestGroup()}} API. + +[=Additional bids=] specify the [=negative interest groups=] they're [=negatively targeting=] +against using at most one of the following two fields in their JSON data structure: + * negativeInterestGroup, for a single negative interest group; + * negativeInterestGroups, for more than one negative interest groups. + +If an [=additional bid=] needs to specify more than one [=negative interest groups=], all of those +[=negative interest groups=] must be joined from the [=same origin=], and that [=origin=] must be +identified ahead of time in the [=additional bid=]'s `joiningOrigin` field. Any +[=negative interest group=] that wasn't joined from that identified [=origin=] is ignored for +[=negative targeting=]. + +
+

Use `negativeInterestGroup` in additional bid's JSON:

+
+  const additionalBid = {
+    ...
+    "negativeInterestGroup": "example_advertiser_negative_interest_group",
+    ...
+  }
+  
+

Use `negativeInterestGroups` in additional bid's JSON:

+
+  const additionalBid = {
+    ...
+    "negativeInterestGroups": {
+      joiningOrigin: "https://example-advertiser.com",
+      interestGroupNames: [
+        "example_advertiser_negative_interest_group_a",
+        "example_advertiser_negative_interest_group_b",
+      ]
+    },
+    ...
+  }
+  
+
+ # K-anonymity # {#k-anonymity} Two goals of this specification rely on applying [=k-anonymity=] thresholds: @@ -2436,7 +2910,7 @@ of the following global objects: 1. Let |reportURL| be |global|'s [=InterestGroupReportingScriptRunnerGlobalScope/report url=] 1. If |reportURL| is failure, set |reportURL| to null. 1. Let |macroMap| be |global|'s [=InterestGroupReportingScriptRunnerGlobalScope/reporting macro map=] - if |functionName| is "`reportWin`", null otherwise. + if |functionName| is "`reportWin`" or "`reportAdditionalBidWin`", null otherwise. 1. Return « |resultJSON|, |reportURL|, |global|'s [=InterestGroupReportingScriptRunnerGlobalScope/reporting beacon map=], |macroMap| ». @@ -2822,6 +3296,8 @@ Each {{InterestGroupReportingScriptRunnerGlobalScope}} has a # Interest Group Updates # {#interest-group-updates} +*This first introductory paragraph is non-normative.* + [=Interest groups=] have an [=interest group/update url=] field that allows updating the interest group definition stored on disk with information periodically retrieved from the [=interest group/update url=]. The [=interest group update=] steps are triggered during @@ -2988,6 +3464,7 @@ The updateAdInterestGroups() method steps are: 1. If |key| is not |groupMember|, [=iteration/continue=]. 1. If |value| is not a [=list=] of {{AuctionAd}}, jump to the step labeled Abort update. + 1. Let |igAds| be a new [=list=] of [=interest group ads=]. 1. [=list/For each=] |ad| of |value|: 1. Let |igAd| be a new [=interest group ad=]. 1. Let |renderURL| be the result of running the [=URL parser=] on @@ -3007,21 +3484,58 @@ The updateAdInterestGroups() method steps are: 1. If |ad|["{{AuctionAd/buyerReportingId}}"] [=map/exists=] then set |igAd|'s [=interest group ad/buyer reporting ID=] to it. 1. If |ad|["{{AuctionAd/buyerAndSellerReportingId}}"] [=map/exists=] - then set |igAd|'s [=interest group ad/buyer and seller - reporting ID=] to it. - 1. [=list/Append=] |igAd| to |ig|'s |interestGroupField|. + then set |igAd|'s [=interest group ad/buyer and seller reporting ID=] to it. + 1. [=list/Append=] |igAd| to |igAds|. + 1. If |igAds| is not [=list/is empty=]: + 1. Set |ig|'s |interestGroupField| to |igAds|. - +
"`additionalBidKey`" +
+ 1. Let |decodedKey| be the result of running [=forgiving-base64 decode=] with |value|. + 1. Jump to the step labeled Abort update if any of the + following conditions hold: + * |decodedKey| is a failure; + * |decodedKey|'s [=byte sequence/length=] is not 32; + 1. Set |ig|'s [=interest group/additional bid key=] to |decodedKey|. + + 1. Jump to the step labeled Abort update if any of the + following conditions hold: + * |ig|'s [=interest group/ads=] is not null, and |ig|'s [=interest group/additional bid key=] + is not null; + * |ig|'s [=interest group/estimated size=] is greater than 50 KB. 1. Set |ig|'s [=interest group/next update after=] to the [=current wall time=] plus 24 hours. 1. [=list/Replace=] the [=interest group=] that has |ig|'s [=interest group/owner=] and [=interest group/name=] in the browser’s [=interest group set=] with |ig|. 1. Abort update: We jump here if some part of the - [=interest group=] update failed. [=iteration/Continue=] to the next [=interest group=] update. + [=interest group=] update failed. [=iteration/Continue=] to the next [=interest group=] update. +# Common Algorithms # {#common-algorithms} + +The following algorithms are some helper methods used in this document. + +
+ To get uuid from string given a [=string=] |input|: + + 1. If |input|'s [=string/length=] is not 36, return an empty [=string=]. + 1. Let |uuidStr| be an empty [=string=]. + 1. [=list/For each=] |i| in [=the range=] from 1 to 36, inclusive: + 1. Let |unit| be |input|'s |i|th [=code unit=]. + 1. If « 8, 13, 18, 23 » [=list/contains=] |i|: + 1. If |unit| is not 0x002D (-): + + 1. Return an empty [=string=]. + 1. Otherwise: + 1. If |unit| is not an [=ASCII lower hex digit=]: + 1. Return an empty [=string=]. + 1. Append |unit| to the end of |uuidStr|. + 1. Return [=ASCII encoded=] |uuidStr|. +
+ + # Permissions Policy integration # {#permissions-policy-integration} This specification defines two [=policy-controlled features=] identified by the strings @@ -3145,6 +3659,14 @@ response header The \`Ad-Auction-Signals\` response header provides value of a JSON array of dictionaries, each with an `adSlot` key. Protected Audience non-component, component, and top-level auctions may specify which signals to load by the `adSlot` key. + +

The \`Ad-Auction-Additional-Bid\` +HTTP response header.

+ +The \`Ad-Auction-Additional-Bid\` response header provides value +of a string in the format of `:`, which +corresponds to a single additional bid. The response may include more than one additional bid by +specifying multiple instances of the [:Ad-Auction-Additional-Bid:] response header.
@@ -3433,7 +3955,8 @@ An interest group is a [=struct=] with the following [=struct/items=]:

: update url :: Null or a [=URL=]. Provides a mechanism for the group's owner to periodically update the - attributes of the interest group. See [interest group updates](#interest-group-updates). + attributes of the interest group. See [[#interest-group-updates]]. Must be null if + [=interest group/additional bid key=] is not null.

When non-null, the [=interest group/update url=]'s [=origin=] will always be [=same origin=] with [=interest group/owner=]. @@ -3451,11 +3974,16 @@ An interest group is a [=struct=] with the following [=struct/items=]: :: Null or a [=string=]. Additional metadata that the owner can use during on-device bidding. : ads :: Null or a [=list=] of [=interest group ad=]. Contains various ads that the interest group might - show. + show. Must be null if [=interest group/additional bid key=] is not null. : ad components :: Null or a [=list=] of [=interest group ad=]. Contains various ad components (or "products") that can be used to construct ads composed of multiple pieces — a top-level ad template "container" which includes some slots that can be filled in with specific "products". +: additional bid key +:: Null or a [=byte sequence=] of length 32. Must be null if [=interest group/ads=] or + [=interest group/update url=] is not null. The Ed25519 public key (a 256-bit EdDSA public key) + used to guarantee that this [=interest group=], if used by an additional bid for a negative + targeting, can only be used by its [=interest group/owner=]. : joining origin :: An [=origin=]. The top level page origin from where the interest group was joined. : join counts @@ -3478,6 +4006,12 @@ An interest group is a [=struct=] with the following [=struct/items=]: +A regular interest group is an [=interest group=] whose +[=interest group/additional bid key=] is null. + +A negative interest group is an [=interest group=] whose +[=interest group/additional bid key=] is not null. +

Interest group ad

An interest group ad is a [=struct=] with the following [=struct/items=]: @@ -3627,10 +4161,12 @@ An auction config is a [=struct=] with the following items: Optional identifier for an experiment group to support coordinated experiments with buyers' trusted servers for buyers without a specified experiment group. : pending promise count -:: An integer, initially 0. The number of [=auction config/auction signals=], +:: An integer, initially 0. The number of things that are pending that are needed to score + everything. It includes waiting for {{Promise}}s [=auction config/auction signals=], [=auction config/per buyer signals=], [=auction config/per buyer currencies=], [=auction config/per buyer timeouts=], [=auction config/direct from seller signals header ad slot=], - or [=auction config/seller signals=] whose {{Promise}}s are not yet resolved. + [=auction config/seller signals=], or {{AuctionAdConfig/additionalBids}} whose {{Promise}}s are not + yet resolved. : config idl :: {{AuctionAdConfig}}. : resolve to config @@ -3639,19 +4175,29 @@ An auction config is a [=struct=] with the following items: : seller currency :: A [=currency tag=]. Specifies the currency bids returned by `scoreAd()` are expected to use, and which reporting for this auction will agree on. - : per buyer currencies :: A {{Promise}} or failure or an [=ordered map=] whose [=map/keys=] are [=origins=] and whose [=map/values=] are [=currency tags=]. Specifies the currency bids returned by `generateBid()` or `scoreAd()` in component auctions are expected to use. The initial value is an empty map. - : all buyers currency :: A [=currency tag=]. Specifies the currency bids returned by `generateBid()` or `scoreAd()` in component auctions are expected to use if [=auction config/per buyer currencies=] does not specify a particular value. - : direct from seller signals header ad slot :: Null, a [=string=], a {{Promise}}, or failure. Initially null. +: auction nonce +:: Null or a [=version 4 UUID=], initially null. + A unique identifier associated with this and only this invocation of + {{Window/navigator}}.{{Navigator/runAdAuction()}}. For + multi-seller auctions, this ID is uniquely associated with all {{AuctionAdConfig/componentAuctions}}. + This must come from a prior call to {{Window/navigator}}.{{Navigator/createAuctionNonce()}}. This + is only required for auctions that provide additional bids, and each of those additional bids must + use the same auction nonce to ensure that each of those additional bids was intended for this and + only this auction. +: expects additional bids +:: A [=boolean=] or failure, initially false. + Specifies whether some bids will be provided as signed exchanges. Sets to failure if the + {{AuctionAdConfig/additionalBids}} {{Promise}} is [=rejected=]. @@ -3661,7 +4207,7 @@ To wait until configuration input promises resolve given an [=auction 1. [=Assert=] |auctionConfig|'s [=auction config/auction signals=], [=auction config/seller signals=], [=auction config/per buyer signals=], [=auction config/per buyer currencies=], [=auction config/per buyer timeouts=], and [=auction config/direct from seller signals header ad slot=] - are not {{Promise}}s. + are not {{Promise}}s, and [=auction config/expects additional bids=] is false. 1. If |auctionConfig|'s [=auction config/auction signals=], [=auction config/seller signals=], [=auction config/per buyer signals=], [=auction config/per buyer currencies=], [=auction config/per buyer timeouts=], or [=auction config/direct from seller signals header ad slot=] @@ -3674,8 +4220,8 @@ To wait until configuration input promises resolve given an [=auction To recursively wait until configuration input promises resolve given an [=auction config=] |auctionConfig|: 1. [=list/For each=] |componentAuctionConfig| in |auctionConfig|'s [=auction config/component auctions=]: - 1. If the result of [=waiting until configuration input promises resolve=] given - |componentAuctionConfig| is failure, return failure. + 1. If the result of [=waiting until configuration input promises resolve=] given |componentAuctionConfig| is + failure, return failure. 1. Return the result of [=waiting until configuration input promises resolve=] given |auctionConfig|.
@@ -3744,8 +4290,9 @@ Numeric value of a bid and the currency it is in.

Generated bid

-The output of running a Protected Audience `generateBid()` script, which needs to be scored by -the seller. +A bid that needs to be scored by the seller. The bid is either the output of running a Protected +Audience `generateBid()` script, or an additional bid provided by the [:Ad-Auction-Additional-Bid:] +response headers.
: bid @@ -3756,11 +4303,15 @@ the seller. original bid if the currency already matched, or a conversion provided by `scoreAd()`. : ad :: A [=string=]. JSON string to be passed to the scoring function. + + Issue: TODO: Check whether [=generated bid/ad descriptor=] can be moved to + [=generated bid/bid ad=] to avoid duplication + (WICG/turtledove#868). : ad descriptor :: An [=ad descriptor=]. Render URL and size of the bid's ad. : ad component descriptors :: Null or a [=list=] of [=ad descriptors=]. Ad components associated with bid, if any. May have at - most 20 URLs. Must be null if the interest group making this bid has a null + most 20 [=list/items=]. Must be null if the interest group making this bid has a null [=interest group/ad components=] field. : ad cost :: Null or a {{double}}. Advertiser click or conversion cost passed from `generateBid()` to @@ -3769,7 +4320,8 @@ the seller. : modeling signals :: Null or an {{unsigned short}}. A 0-4095 integer (12-bits) passed to `reportWin()`, with noising. : interest group -:: An [=interest group=], whose `generateBid()` invocation generated this bid. +:: An [=interest group=], whose `generateBid()` invocation generated this bid, or specified by the + additional bid. : bid ad :: The [=interest group ad=] within [=generated bid/interest group=] to display. : modified bid @@ -3777,6 +4329,9 @@ the seller. The bid value a component auction's `scoreAd()` script returns. : bid duration :: A [=duration=] in milliseconds. How long it took to run `generateBid()`. +: provided as additional bid +:: A [=boolean=], initially false. +