Skip to content

[codex] Clarify notification config semantics#4982

Merged
bokelley merged 1 commit into
mainfrom
bokelley/notification-configs-semantics
May 24, 2026
Merged

[codex] Clarify notification config semantics#4982
bokelley merged 1 commit into
mainfrom
bokelley/notification-configs-semantics

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • Clarifies sync_accounts.accounts[].notification_configs[] replacement/idempotency semantics, including omission, empty arrays, subscriber_id keying, response echoing, and failure atomicity.
  • Documents that durable account-level configs do not currently define account.* lifecycle events; account status remains list_accounts polling or one-shot sync_accounts.push_notification_config.
  • Adds RFC 9421-signed webhook proof-of-control challenge/response schemas and hardens active/paused activation semantics.

Validation

  • npm run build:schemas
  • npm run test:schemas
  • npm run test:json-schema
  • npm run test:examples
  • npm run lint:schema-links -- --check
  • npm run test:schema-links
  • npm run test:oneof-discriminators
  • git diff --check
  • precommit hook: image quality, test:unit, test:test-dynamic-imports, test:callapi-state-change, typecheck
  • pre-push hook: version sync, schema link convention, Mintlify broken-links check

@gitguardian

gitguardian Bot commented May 24, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@bokelley bokelley marked this pull request as ready for review May 24, 2026 01:19
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 24, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. Right shape for a pre-GA clarification: schema, MDX, and changeset all agree on the four load-bearing invariants — whole-set atomicity, tuple-scoped proof reuse, paused-config write-time SSRF, and dry_run MUST NOT touch the network.

Things I checked

  • Changeset level. minor is correct. notification_configs[] first landed in 3.1.0-beta.x (post-#4730), no static/schemas/v3/... materialization exists, so generalizing the activation challenge from "wholesale feed subscribers" to "any new or changed active subscriber" is in-beta tightening, not a v3 break.
  • XOR on webhook-challenge-response.json. anyOf + not: {required:[challenge,token]} correctly expresses exactly-one. additionalProperties: false closes the surface. webhook-challenge-response.json:23-40.
  • Conditional on delivery_auth.mode. if/then/allOf requires credential_fingerprint iff mode is Bearer/HMAC-SHA256, forbids it for rfc9421. additionalProperties: false closes the object; enum gates future modes. webhook-challenge.json:56-100.
  • No new undiscriminated oneOf. Challenge uses if/then; response uses anyOf/not. Audit walker untouched.
  • Schema-vs-docs coherence. Atomicity (sync_accounts.mdx:193), tuple scope (sync_accounts.mdx:207), paused write-time checks (notification-config.json:230), and dry_run no-network rule (sync_accounts.mdx:239) match between schema descriptions and MDX prose.
  • Echo on response. sync-accounts-response.json:148-155 correctly $refs notification-config.json and notes credentials redacted.
  • index.json registration. Both new schemas wired under core at index.json:417-424.

Follow-ups (non-blocking — file as issues)

  • Receiver-side match list omits account_id and subscriber_id. sync_accounts.mdx:207 and security.mdx:149 require the receiver to reject unless seller_agent_url, delivery_auth, and event_types match the pending registration. A receiver registered at multiple sellers, or facing a malicious upstream, can be cross-account-confused into echoing a challenge whose account_id/subscriber_id don't match its expectation. Both fields are required in webhook-challenge.json:412-420; add them to the receiver's MUST-match list.
  • Tuple binding is in prose, not on the wire. The response echoes only challenge (or token). Sellers keying their pending-challenge store by the random value alone are vulnerable to in-flight replay across tuples at the same seller. Either tighten the seller-side language to "MUST look up the pending challenge by the full tuple, not by challenge value" or require the response to echo account_id + subscriber_id.
  • Paused-then-activated language. "Reactivation MUST repeat full SSRF validation … for any tuple without current valid proof" (notification-config.json:230) is correct but reads ambiguously for the never-active path: an active: false registration that flips to active: true for the first time has no prior proof, and a buggy reader might interpret "current valid proof" as not required. Tighten to "any tuple that has never received a successful proof, or whose last proof has expired, MUST be re-challenged before becoming active: true."
  • Pin /schemas/latest/ URLs at next release. The JSX comment at sync_accounts.mdx:209-210 flags this. Better as a tracked issue than an inline TODO that ages with the file.
  • Duplicated normative prose. The notification_configs description in sync-accounts-request.json:63 is now ~3.5KB of normative text duplicated in the MDX. Currently coherent, but two copies of the same MUSTs is a drift hazard.

Minor nits (non-blocking)

  1. delivery_auth.mode casing. webhook-challenge.json:43-47 lists "Bearer" and "HMAC-SHA256" capitalized. Confirm this matches the existing auth-scheme.json / notification-config.authentication.scheme literals so receivers can string-compare without a transform.
  2. token alias parser differential. webhook-challenge-response.json:23-40 accepts either field. The schema correctly forbids both, but lenient hand-rolled validators that short-circuit on anyOf could accept both with different values. The aliases are unreleased per the same inline comment — easiest fix is to drop token before anyone deploys against it; otherwise require byte-equality when both are present.

The webhook.challenge type const is a notable departure from the standard task-envelope discriminator pattern, but acceptable here — this is an out-of-band proof payload, not a task surface.

LGTM. Follow-ups noted above.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 24, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The proof-of-control shape is right: tuple-scoped to (account_id, subscriber_id, normalized url, auth mode/credential binding, normalized event_types), challenge POST signed with the seller's RFC 9421 key regardless of delivery_auth.mode, write-time SSRF enforced even for active: false. Closes the public-endpoint hijack class that SSRF validation alone doesn't cover.

Things I checked

  • Schema-vs-docs coherence across all eight touched surfaces — the same normative claims (omit/empty/non-empty replace, subscriber_id keying, failure atomicity, RFC 9421 signing of the challenge POST, paused-config carve-out, no account.* lifecycle events) land identically in sync_accounts.mdx, list_accounts.mdx, security.mdx, notification-config.json, account.json, sync-accounts-request.json, sync-accounts-response.json, and notification-type.json.
  • delivery_auth.mode casing (Bearer, HMAC-SHA256) matches static/schemas/source/enums/auth-scheme.json — no wire-level enum drift. The new rfc9421 value is intentionally not in auth-scheme.json (absence-of-block has historically encoded 9421); the challenge surface lifts that to an explicit scalar so the receiver can verify intent.
  • webhook-challenge-response.json XOR encoding is correct draft-07: anyOf(challenge|token) + not.required:[challenge,token] + additionalProperties: false. JSON-Schema-validating receivers get the XOR for free.
  • Pattern regexes: challenge ^[A-Za-z0-9_.:-]{32,255}$ gives ~192 bits at the floor — fine for single-use. ^[a-f0-9]{64}$ is strict lowercase SHA-256 hex.
  • index.json registration follows the same shape as the wholesale-feed-webhook sibling.
  • Anchors: #endpoint-proof-of-control resolves against ### Endpoint proof of control in sync_accounts.mdx; security.mdx cross-link points at the right slug.
  • audit-oneof — no new undiscriminated oneOf. delivery_auth uses allOf + if/then keyed on mode. Response XOR uses anyOf + not.
  • Changeset categorization: new optional schemas, no existing-field renames or required-flips, prose-only tightening elsewhere → minor is right.
  • [codex] PR title prefix and 4977-… changeset filename match the repo conventions visible in recent commits.

Follow-ups (non-blocking — file as issues)

  1. delivery_auth.credential_fingerprint is a salt-free SHA-256 of a counterparty-known secret. static/schemas/source/core/webhook-challenge.json:50-54. For credentials at the notification-config.json:credentials.minLength: 32 floor (or drawn from any recognizable generator format), the hash is offline-brute-forceable. It's also a stable cross-seller deanonymizer for buyers that reuse one Bearer token across multiple sellers — a hostile or breached seller log can correlate "subscriber on seller A using credential H" with "subscriber on seller B using credential H" without ever seeing the secret. The seller already knows the binding from the signed registration; the receiver does not need a derivative of its own secret echoed back. Cleanest fix: drop the field and replace with an opaque seller-generated binding_id (random nonce stable for the proof's lifetime). Fallback: keyed HMAC with a per-receiver salt (HMAC(brand_domain, credential)), so the same credential reused across sellers produces different fingerprints. Worth resolving before the first conforming seller emits a legacy-mode challenge — hard to remove once it's in counterparty logs in the wild.

  2. delivery_auth.mode is not in required. static/schemas/source/core/webhook-challenge.json:37-100. {"delivery_auth": {}} passes structural validation because both if/then clauses guard on required: ["mode"] — with mode missing, neither fires and the credential_fingerprint enforcement is silently bypassable. Add "required": ["mode"] to the delivery_auth subschema.

  3. token alias deserves a sunset note or a citation. static/schemas/source/core/webhook-challenge-response.json:15-21 and sync_accounts.mdx:229. Two field names for the same wire value is cruft every implementation has to accept forever. If there is real shipped seller traffic using token (Slack url_verification precedent is the likely lineage), cite it in the changeset. Otherwise mark token with x-deprecated: true and a 4.0 removal note, mirroring the legacy-auth deprecation cadence in notification-config.json:31.

  4. Lift the response XOR into normative prose. sync_accounts.mdx:229 currently says "Sellers MUST also accept the backward-compatible alias" without saying "MUST reject when both are present." Hand-rolled parsers will skip the XOR. Add: "Sellers MUST reject responses that include both challenge and token, and MUST treat a missing-or-empty echo field as proof failure."

  5. Re-challenge cadence is unbounded. sync_accounts.mdx:207 says "Sellers MAY re-challenge on their own proof-expiration policy" with no floor. Worth a normative SHOULD-NOT-more-often-than-N-hours (24h is a reasonable default) plus a 429-is-transient clause for receiver-side rate-limiting.

  6. Receiver-verify field list under-specified. sync_accounts.mdx:207 says the receiver MUST reject unless seller_agent_url, delivery_auth, and event_types match — but the proof-scope tuple also includes account_id and subscriber_id, both of which the schema requires on the challenge. Expand the list to all five, or split the section into explicit "Seller obligations" and "Receiver obligations" subsections so SDK authors implementing the receiver side don't ship a bare echo.

  7. State that the challenge body is covered by the RFC 9421 signature. sync_accounts.mdx:207 says the POST is signed but doesn't restate that the body is in content-digest / @signature-params coverage per the AdCP webhook profile. One sentence closes a theoretical body-rewriting attack on a non-conforming verifier.

Minor nits (non-blocking)

  1. RFC 2119 casing drift on the paused-config carve-out. sync_accounts.mdx:201 uses MAY skip; notification-config.json:56, sync-accounts-request.json activation-proof block, and security.mdx:102 use may skip. Same permission, different casing across four surfaces — pick one.

  2. SSRF write-time checklist phrasing. sync_accounts.mdx:201 says "HTTPS parsing, hostname normalization, and reserved-range rejection on write"; security.mdx:102 and the schemas say "URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time". Same intent, different checklist — standardize on the four-item form.

  3. dry_run carve-out is page-only. sync_accounts.mdx:237 documents dry_run: true MUST NOT send network challenges; no schema description carries it. Agents reading sync-accounts-request.json alone won't pick this up. One-line addition to the request-schema description covers it.

Approving on the strength of the proof-of-control design plus the cross-surface coherence. The credential_fingerprint design is the one item worth resolving before the first seller ships against this; the schema required: ["mode"] is a clean one-line fix.

@bokelley bokelley force-pushed the bokelley/notification-configs-semantics branch from fed40b9 to f5193c3 Compare May 24, 2026 01:46

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. The schema/docs prose is internally consistent, the new webhook-challenge.json and webhook-challenge-response.json slot cleanly under core/, and the changeset is wired up.

Things I checked

  • New webhook-challenge.json and webhook-challenge-response.json agree on challenge field bounds and pattern (^[A-Za-z0-9_.:-]{32,255}$). Example token example-challenge-token-000000000000 is 36 chars and matches.
  • The if/then on delivery_auth.mode at static/schemas/source/core/webhook-challenge.json:56-97 correctly requires credential_fingerprint for Bearer/HMAC-SHA256 and forbids it for rfc9421not: { required: ["credential_fingerprint"] } is draft-07 valid for "property must be absent."
  • XOR shape in webhook-challenge-response.json (anyOf + not.required) does not trip scripts/audit-oneof.mjs — the walker at scripts/audit-oneof.mjs:255 only descends into oneOf. anyOf is unwatched. Pragmatic.
  • static/schemas/source/enums/notification-type.json enum array is byte-identical; only the top-level description changed.
  • Schema prose in sync-accounts-request.json, account.json, and notification-config.json is mutually consistent with docs/accounts/tasks/sync_accounts.mdx on the proof tuple (account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types), on idempotency keyed by subscriber_id, and on atomicity (no partial replacement on validation failure).
  • index.json registers both new schemas.
  • Changeset present at .changeset/4977-notification-configs-semantics.md with minor.
  • Challenge POST itself is signed with RFC 9421 even when delivery selects legacy auth (webhook-challenge.json:5) — right call for downgrade resistance: a legacy-credential attacker can't mint a challenge. credential_fingerprint as SHA-256 of the legacy credential is a one-way commit; leaks nothing on the wire absent low-entropy credentials.

Follow-ups (non-blocking — file as issues)

  • Semver call on the proof-of-control tightening is worth a maintainer sanity check. The old notification-config.json text required proof for wholesale feed subscribers; the new text requires it for any new or changed active subscriber (static/schemas/source/core/notification-config.json:18). A seller that shipped v3.x without proving creative.status_changed-only subscribers is now non-conformant. The changeset frames this as a clarification ("Standardize endpoint proof-of-control"), which is defensible — but a stricter read calls it a normative tightening that warrants major. Worth a thread on the WG list before the next release cuts.
  • Spell out the canonicalization of credential_fingerprint. webhook-challenge.json:52 says "SHA-256 hex fingerprint of the exact legacy credential string" — "exact" is doing a lot of work. Add whether bytes are taken raw, whether whitespace/case is preserved, encoding (UTF-8 assumed). Otherwise a buyer and seller can disagree on the hash and the challenge will fail-closed in a way that's hard to debug.
  • Add a line to the changeset explicitly calling out the scope expansion. "Previously scoped to wholesale feed subscribers; now applies to any new or changed active account-level webhook subscriber." Helps adopters diffing against current behavior catch it.
  • Consider sunsetting the token alias. webhook-challenge-response.json accepts both challenge and token for backward compatibility — useful at introduction, but the longer it stays the longer every seller carries two-branch verification code. A deprecation note on the token field with a target version would be the right shape.

Minor nits (non-blocking)

  1. anyOf + not.required for the XOR works but oneOf of two required clauses would be more idiomatic. static/schemas/source/core/webhook-challenge-response.json:21-39. The current shape was likely chosen to keep audit-oneof.mjs quiet — if so, leave a one-line schema comment saying so, otherwise the next contributor will "fix" it back to oneOf and trip the gate.

Approving on the strength of the schema-vs-docs coherence and the cleanly scoped surface. The semver question is the only thing I'd want a second pair of eyes on, and it's not a block — the wire shapes are unchanged and the new schemas are additive.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. One schema gap on a new security-sensitive surface — delivery_auth.mode isn't enforced as required, so the conditional if/then discriminator on webhook-challenge.json is not total and the documented receiver-side invariant ("reject unless delivery_auth matches pending registration") cannot be checked against an empty object. One-line fix.

Things I checked

  • webhook-challenge.json:37-100delivery_auth.allOf[if/then] branches both gate on required: ["mode"]. A payload with delivery_auth: {} matches neither branch and validates as-is. mode needs to be required at the delivery_auth level.
  • webhook-challenge-response.json:23-40 — "exactly one of challenge/token" via anyOf + not.required[both] is sound and doesn't trip scripts/audit-oneof.mjs (only walks oneOf). No baseline ratchet needed.
  • Cross-file notification_configs[] semantics across notification-config.json, sync-accounts-request.json, sync-accounts-response.json, core/account.json, and sync_accounts.mdx — consistent on replacement semantics, subscriber_id keying, omit-vs-[], write-only credentials, and proof-of-control activation.
  • Changeset exists (.changeset/4977-notification-configs-semantics.md), marked minor. New schemas are additive; the rest tightens descriptions. Borderline — see follow-up.
  • Example challenge token "example-challenge-token-000000000000" (36 chars) validates against ^[A-Za-z0-9_.:-]{32,255}$.
  • The "RFC 9421-signed challenge even when delivery_auth selects legacy mode" rule is consistent with existing AdCP downgrade-resistance posture.

Must fix

  1. delivery_auth.mode is not enforced as required. static/schemas/source/core/webhook-challenge.json:37-100. The two if/then branches each include required: ["mode"] inside the if, so when mode is absent neither branch fires. With no top-level required: ["mode"] on delivery_auth, an empty object validates and the discriminator is unverifiable. Add "required": ["mode"] on delivery_auth. One line.

Follow-ups (non-blocking — file as issues)

  • credential_fingerprint exposure to an unproven URL. webhook-challenge.json:50-54. The SHA-256 of the buyer's legacy credential ships in the challenge body to a URL that has not yet proved control. Squatter / typo'd receivers capture the fingerprint before registration ever completes. If notification_config.authentication.credentials is a user-chosen 32-char string (the current floor in push-notification-config.json), the fingerprint is offline-crackable. Either drop the fingerprint from the challenge body and surface it after proof completes, or raise the entropy floor on authentication.credentials in the same surface.
  • Response should echo account_id and subscriber_id. webhook-challenge-response.json only echoes the challenge value. A multi-tenant receiver with no pending-registration discipline can blindly echo and the seller can't tell. Adding tuple identifiers (with additionalProperties: false) forces receivers to bind their decision to a specific pending registration. Today additionalProperties: false actively prevents an honest receiver from including them as defense-in-depth.
  • Proof tuple should pin the resolved IP, not just the hostname. Reactivation does fresh SSRF, but active records continue honoring cached proof against a hostname that may have re-resolved into a reserved range. Bind the proof tuple to the connect-pinned IP at proof time, or require periodic re-validation for active subscribers.
  • Changeset type is borderline. Pre-PR security.mdx qualified proof-of-control as "high-volume event families, including wholesale feed webhooks"; this PR broadens to all account-level subscribers and elevates the unsigned-challenge case to an RFC 9421 MUST. If the WG reading is "the qualifier was illustrative," minor is right. If any seller shipped to the narrow reading, this is major. Worth a one-line confirmation from the WG before merge.
  • Challenge tuple documented as including URL but payload omits it. Receiver disambiguation relies on RFC 9421's @target-uri derived component. A redundant url in the body lets receivers detect host-rewriting intermediaries without that dependency.
  • Challenge regex permits chars outside base64url. webhook-challenge.json:18 and webhook-challenge-response.json:13,20 use ^[A-Za-z0-9_.:-]{32,255}$ while the prose recommends base64url. Tighten to ^[A-Za-z0-9_-]{32,255}$ or drop the base64url claim. Entropy is fine either way (~190 bits floor).
  • Retry-with-same-challenge needs a receiver-side rule. Seller-side single-use is preserved, but receivers seeing a duplicate POST can't tell whether it's a legitimate retry, an in-flight duplicate, or a replay. Spec the receiver as idempotent within a short window (e.g., 60s) and require the seller's one-retry budget to sit inside that window.
  • Cross-protocol consistency note on push_notification_config. Add a sentence to push-notification-config.json stating the proof-of-control flow does NOT apply to per-task transport; authenticity there comes from RFC 9421 and the echoed token. Avoids implementers extrapolating challenge-on-every-callback and creating a fresh SSRF amplification vector on every async task.
  • dry_run honesty. dry_run: true correctly forbids network challenges, but the response should distinguish "would succeed because tuple already has cached proof" from "would require a proof step on the real call" so callers don't read dry_run success as endpoint validity.

Minor nits

  1. Response-side prose is request-shaped. static/schemas/source/account/sync-accounts-response.json:150 says "re-sending an existing subscriber_id replaces that subscriber's config" — readers don't re-send. Rephrase as "Entries are uniquely keyed by account-scoped subscriber_id."
  2. Duplicate length bounds. webhook-challenge.json:13-19 and webhook-challenge-response.json:11-13,18-20 set minLength/maxLength redundantly with the {32,255} quantifier in the pattern. Harmless.

Ship after delivery_auth.required: ["mode"] lands and the changeset categorization is confirmed with the WG.

@bokelley bokelley force-pushed the bokelley/notification-configs-semantics branch from f5193c3 to b6ee210 Compare May 24, 2026 10:52

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. Clean clarification PR — declarative-replace semantics, proof-of-control tuple, and the no-account.*-lifecycle disclaimer all land coherently across six schema files and three docs files.

Things I checked

  • Proof tuple (account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types) is byte-identical across static/schemas/source/account/sync-accounts-request.json:63, docs/accounts/tasks/sync_accounts.mdx:207, and the required-field set on static/schemas/source/core/webhook-challenge.json:413-421. No drift.
  • delivery_auth.allOf if/then on webhook-challenge.json:358-400 correctly forbids credential_fingerprint for rfc9421 and requires it for Bearer/HMAC-SHA256. additionalProperties: false plus the not.required branch is the right shape.
  • The event_types rejection field-path is consistent: notification-config.json, sync-accounts-request.json, and sync_accounts.mdx:86,125 all point error.field at accounts[i].notification_configs[j].event_types for bad types and notification_configs[j].url for proof failures.
  • enums/notification-type.json change is description-only — no enum values added or removed, no enumDescriptions touched. Backward-compatible.
  • Changeset present at .changeset/4977-notification-configs-semantics.md, type minor. New schemas are additive; no field renames, no required flips on existing fields; no breaking wire change.
  • index.json:413-424 wires the two new schemas into the core section.
  • webhook-challenge-response.json anyOf[required challenge | required token] + not[required challenge AND token] is semantically exactly-one-of under draft-07. Correct, even if it's a different idiom from the rest of the repo.

Follow-ups (non-blocking — file as issues)

  • Exactly-one-of idiom drift. static/schemas/source/core/webhook-challenge-response.json:260-277 uses top-level anyOf+not for exactly-one-of, while the rest of the repo (e.g., account/sync-accounts-response.json:177-220) uses oneOf with reciprocal not.required inside each branch and is baselined in scripts/oneof-discriminators.baseline.json. The chosen form sidesteps audit-oneof.mjs silently. Either restructure as a two-branch oneOf and ratchet the baseline, or add a one-line comment in the file justifying the deviation so a future reader doesn't think the walker missed it.
  • Anti-replay surface on webhook-challenge.json. No explicit nonce / issued_at / expires_at. Anti-replay leans entirely on RFC 9421's created/expires signature parameters plus single-use scoping in prose (sync_accounts.mdx line near cryptographically random, single-use). Worth confirming that's the intentional pivot vs the Standard-Webhooks webhook-id nonce convention, and saying so in the schema description so implementers don't reinvent a nonce field.
  • Normative broadening worth a migration note. Prior text on notification-config.json url description scoped the MUST to wholesale-feed subscribers; the new text broadens to "any new or changed active subscriber." The or equivalent proof-of-control escape hatch survives so this is not a wire break, but a previously-conformant wholesale-feed-only implementer now has more surface to cover. A one-line migration note in the changeset would help.
  • Bound the url element of the proof tuple explicitly. webhook-challenge.json:5 says the POST is sent "after URL normalization and SSRF validation," and the tuple includes normalized url, but the body carries no url field — the URL is bound only via the POST destination. An interpretive reader could reasonably ask whether they should add a url field. Recommend adding a sentence: "Receivers MUST treat the request URL itself as the bound url element of the tuple."

Minor nits (non-blocking)

  1. Prose alternation between "account-anchored events" and "account-anchored resource events" across notification-config.json:5, security.mdx, and sync_accounts.mdx. Harmless, but worth picking one phrase in a follow-up sweep.

Approved on the strength of the proof-tuple coherence and the schema/docs alignment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant