4141from pydantic import BaseModel , TypeAdapter
4242
4343from adcp ._deferred_adapters import deferred_adapter
44+ from adcp .signing .constants import WEBHOOK_ACCEPTED_ADCP_USES
4445from adcp .signing .crypto import (
4546 ALG_ED25519 ,
4647 ALG_ES256 ,
6465 WholesaleFeedEvent ,
6566 WholesaleFeedWebhook ,
6667)
68+ from adcp .types .base import AdCPBaseModel
6769from adcp .types .domains .core .async_response_data import AdcpAsyncResponseData
6870from adcp .webhook_auth import (
6971 AdcpLegacyHmacStrategy ,
@@ -389,26 +391,21 @@ def from_jwk(
389391 ) -> WebhookSender :
390392 """Construct from a JWK that includes the private scalar.
391393
392- The JWK MUST have ``adcp_use == "webhook-signing"`` — the sender
393- doesn't validate this (you're signing with your own key; validation
394- happens at the receiver), but a key whose adcp_use is wrong will be
395- rejected by every conformant verifier.
394+ The JWK must identify a purpose accepted by the AdCP webhook profile:
395+ ``request-signing`` or ``webhook-signing``.
396396
397397 ``allow_private_destinations`` and ``allowed_destination_ports``
398398 forward to :meth:`__init__` — see that signature for semantics.
399399 """
400400 # Snapshot the mapping once — a live Mapping could otherwise return
401401 # different values across the adcp_use / kid / d / alg reads.
402402 jwk_snapshot = dict (jwk )
403- if jwk_snapshot .get ("adcp_use" ) != "webhook-signing" :
403+ adcp_use = jwk_snapshot .get ("adcp_use" )
404+ if adcp_use not in WEBHOOK_ACCEPTED_ADCP_USES :
405+ accepted_uses = ", " .join (sorted (WEBHOOK_ACCEPTED_ADCP_USES ))
404406 raise ValueError (
405- f"WebhookSender requires a JWK with adcp_use='webhook-signing' "
406- f"(got { jwk_snapshot .get ('adcp_use' )!r} ). Webhook-signing and "
407- f"request-signing keys MUST be distinct so a signature from one "
408- f"surface cannot be replayed as the other. Generate a separate "
409- f"key with adcp_use='webhook-signing' and publish it in your "
410- f"adagents.json alongside your request-signing key. See "
411- f"https://adcontextprotocol.org/docs/building/implementation/security"
407+ "WebhookSender requires a JWK with an AdCP webhook signing purpose "
408+ f"({ accepted_uses } ); got { adcp_use !r} "
412409 )
413410 alg = jwk_snapshot .get ("alg" )
414411 if alg == "EdDSA" :
@@ -445,7 +442,7 @@ def from_pem(
445442 ) -> WebhookSender :
446443 """Load a private key from a PEM file and bind it as a webhook sender.
447444
448- Companion to ``adcp-keygen --purpose webhook -signing``, which writes
445+ Companion to ``adcp-keygen --purpose request -signing``, which writes
449446 the PEM and prints the public JWK. The JWK is published at your
450447 ``jwks_uri``; the PEM holds the private key material. ``from_pem``
451448 reads the PEM, constructs the right ``PrivateKey`` type for ``alg``,
@@ -1643,13 +1640,17 @@ async def send_webhook_challenge(
16431640 url : str ,
16441641 account_id : str ,
16451642 subscriber_id : str ,
1643+ seller_agent_url : str ,
1644+ event_types : Sequence [str ],
1645+ authentication : AdCPBaseModel | Mapping [str , Any ] | None = None ,
16461646 challenge : str | None = None ,
16471647 extra_headers : Mapping [str , str ] | None = None ,
16481648 ) -> WebhookDeliveryResult :
16491649 """POST a signed durable-subscription proof-of-control challenge.
16501650
16511651 The body matches the durable ``notification_configs[]`` challenge
1652- shape and intentionally does not inject ``idempotency_key``:
1652+ shape, binds the pending delivery authentication and event set, and
1653+ intentionally does not inject ``idempotency_key``:
16531654
16541655 ``{"type":"webhook.challenge","challenge":"...", ...}``
16551656
@@ -1658,9 +1659,15 @@ async def send_webhook_challenge(
16581659 want URL validation and response echo checking in one call.
16591660 """
16601661
1662+ if not self .signs_with_rfc9421 :
1663+ raise ValueError ("webhook challenges require RFC 9421 signing" )
1664+
16611665 payload = create_webhook_challenge_payload (
16621666 account_id = account_id ,
16631667 subscriber_id = subscriber_id ,
1668+ seller_agent_url = seller_agent_url ,
1669+ event_types = event_types ,
1670+ authentication = authentication ,
16641671 challenge = challenge ,
16651672 )
16661673 challenge_value = str (payload ["challenge" ])
0 commit comments