feat(team): expose auto_provision_brand_hierarchy_children toggle to org owners#3430
Conversation
…org owners PR #3378 added the flag and all the safety scaffolding (opt-in default off, cohort gate, audit log, coherence in resolveEffectiveMembership) but no UI to flip it. Without exposure, adoption-gating triggers on follow-up issues #3410 and #3411 were circular — orgs couldn't adopt what they couldn't toggle. - GET /api/organizations/:orgId/domains returns the new flag, the enabled_at timestamp set by migration 450's trigger, and inferred_subsidiaries — the high-confidence brand-registry rows currently classified as children of this org's verified domains. - PATCH /api/organizations/:orgId/settings accepts auto_provision_brand_hierarchy_children. Owner-only (same restriction as the existing auto_provision_verified_domain), audit-logged. - team.html renders a second toggle row below the verified-domain one with the resolved-subsidiary list so the owner can sanity-check the registry before flipping on. Hidden when there's nothing to match AND flag is off. - Tests cover both flags surfacing, GET shape with inferred subsidiaries, trigger-driven enabled_at, owner-only PATCH, and validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…provision-toggles
Code-quality bot fixup on PR #3430.
|
Security review found two real issues — not safe to merge as-is. Verdict: needs-fixes-before-merge ✅ Owner-only enforcement (verified): route-level check at ✅ Off-toggle behavior: trigger clears 🚨 Audit log claimed but NOT PRESENT in the PATCH handler. The changeset states "audit-logged via Fix: add
🔹 Forensic nit: clearing 🔹 Adversarial subsidiary claim (out of scope, worth tracking): if someone can submit a brand manifest claiming Routine: please add the |
|
Thanks for the thorough review — wanted to clarify the audit-log finding before anyone acts on it. 🚨 Blocker — audit log IS present. The What the current log does not capture: the
Summary: no blocking changes needed here. The two 🔹 nits (disabled_at forensics, adversarial subsidiary claim) are worth follow-up issues but aren't blockers on what's merged. Generated by Claude Code |
|
Thanks for the follow-up clarification, @bokelley. Noted: the The two forensic nits — missing No further action on this PR. Triaged by Claude Code. Session: https://claude.ai/code/${CLAUDE_CODE_REMOTE_SESSION_ID} Generated by Claude Code |
…ng link (#3450) * refactor(auth): universal resolveUserOrgMembership helper for org auth-check pattern Replaces 14 copy-pasted call sites across organizations.ts and member-profiles.ts that did the WorkOS-membership-then-resolveUserRole auth check inline. Two problems with the old pattern: - Every admin-only org endpoint 403'd in dev mode because WorkOS doesn't know about DEV_USERS. The dev-mode bypass that solved this lived in exactly one place (GET /api/me/member-profile), and most other routes didn't have it. - 14 subtly different versions of the same auth check, each with its own error message and role logic. The helper does it once. In dev mode it reads from local organization_memberships (seeded by dev-setup at boot). In prod it defers to WorkOS as source of truth. Verified end-to-end: every admin endpoint the team page hits now responds 200 to a dev-mode admin user where most previously returned 403. The single remaining 404 in dev (api/billing) is unrelated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(team): show brand-registry hierarchy classification + report-wrong link PR #3430's subsidiary toggle only showed children of the org. Two things owners actually need: - Parent (if classified as a subsidiary of another brand). Without this, an analyticsiq.com-style org can't tell they're classified under Alliant. - Classification confidence + last-validated date — context owners need to judge whether the data is trustworthy enough to flip the auto-provision toggle. GET /api/organizations/:orgId/domains now returns hierarchy_classification with self + parent. team.html renders a "Brand registry classification" section above the toggles showing parent → you → children, each with a "Report wrong" mailto link that routes corrections into the existing support flow without new infra. Verified end-to-end via Playwright in dev mode (depends on the universal dev-mode bypass from #3448): seeded a brand row + subsidiary, the hierarchy diagram renders correctly, "Report wrong" links work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): prod-boot guard against dev mode + 4 missed auth-check sites Security review found a HIGH ship-blocker on PR #3448: - Add a hard prod-boot guard that crashes startup if DEV_USER_EMAIL + DEV_USER_ID are set in a production-shaped environment (NODE_ENV=production OR FLY_APP_NAME present). Override via ALLOW_DEV_MODE_IN_PROD=true for the rare one-off. Pre-PR the dev-mode bypass affected one read endpoint; post-PR it widens to 23 mutating routes (delete org, ban member, transfer ownership, etc), so a stray secret-flip would now grant owner-level takeover instead of read access. Code review found 4 more sites that match the auth-check pattern but weren't refactored in the original commit: - POST /api/organizations/:orgId/members (line 2177) - POST /api/organizations/:orgId/invitations (line 2360) - DELETE /api/organizations/:orgId/invitations/:invitationId (line 2469) - POST /api/organizations/:orgId/invitations/:invitationId/resend (line 2536) - POST /api/organizations/:orgId/members/by-email (line 3092 — the hybrid AAO-admin / static-API-key / org-role path; refactored carefully to preserve the conditional-skip for static admin keys) - PATCH/DELETE /api/organizations/:orgId/members/:membershipId (line 3291) All 6 now route through resolveUserOrgMembership. billing-public.ts and certification.ts have hand-rolled dev-mode bypasses (isDevUserInvoice, isOrgMember). Filed as separate followup — different patterns that need their own analysis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(team): tenant-scoped ORDER BY, self-loop guard, source field, brand_json visual distinction Code-review FBM and product feedback on PR #3450: - SQL bug (code review): the ORDER BY scalar subquery for is_primary did not filter by workos_organization_id. If the same domain is verified across multiple tenants, the wrong org's primary flag could win the LIMIT 1. Added the tenant filter. - Self-loop guard (code review): the LEFT JOIN brands parent ON parent.domain = db.house_domain didn't guard against db.house_domain == db.domain. A malformed registry row pointing at itself would render "you are a child of yourself". Added parent.domain != db.domain to the join. - Dropped data.org_name from the mailto body — the /domains endpoint doesn't return it, so the line always rendered as "Org: " blank. - New `source` field on hierarchy_classification.self / .parent / inferred_subsidiaries (product). brand_json self-asserted gets a green "Asserted" badge; LLM-inferred gets a neutral "Inferred" badge. - For brand_json self-asserted parent relationships, render a small guidance hint pointing the admin at their own brand.json instead of the mailto support flow — fix is one PR away from where they look, not three weeks away after a triage review. - Confidence badge now suppressed for brand_json (the "confidence" frame is wrong for a self-assertion). Tests: 3 new cases (parent-row-doesn't-exist via LEFT JOIN miss, self-loop guard, source field surfacing for brand_json). 12/12 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Security finding update — actually fine. My earlier finding (#3430 (comment)) flagged a missing await orgDb.recordAuditLog({
workos_organization_id: orgId,
workos_user_id: user.id,
action: 'organization_settings_updated',
resource_type: 'organization',
resource_id: orgId,
details: updates,
});Verified on main. Captures actor + org + action + the update payload (which carries the new flag value). Also verified the sign-in path freshness concern — So #3430 as merged is solid on the load-bearing concerns. Two minor improvements are tracked separately:
Closing this thread — PR is good as-is. |
|
Correction to issue references in my last comment — actual numbers:
(I originally cited #3475 / #3476 — wrong numbers, those were placeholders.) |
…argeting (#3443) * docs(faq): strengthen IAB Tech Lab and AAMP entries Real Addie threads recurringly ask "why a separate org from IAB Tech Lab?" and "how does AdCP differ from AAMP?". The existing IAB Tech Lab FAQ entry didn't address the org/cadence question, and the AAMP entry buried the cleanest framing. Pulls the canonical "campaign layer (agentic buying) vs impression layer (agentic bidding)" framing from the addie knowledge rules into the FAQ, names the specific IAB standards AdCP coexists with, and notes that Apache 2.0 leaves IAB Tech Lab free to adopt or align. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: snapshot docs for v3.0.1 (#3414) Co-authored-by: bokelley <134922+bokelley@users.noreply.github.com> * fix(membership): centralize primary_organization_id reads, add invariant (#3375) * fix(membership): centralize primary_organization_id reads, add invariant Eleven read sites silently treated users.primary_organization_id IS NULL as "no organization" — that's how a paid Founding member at ResponsiveAds saw "no directory listing" / "not a member" despite an active subscription. The column is a denormalized pointer that drifts when user.created races ahead of organization_membership.created or the fire-and-forget backfill in enrichUserWithMembership swallows a transient error. Centralize the read through resolvePrimaryOrganization() (column → organization_memberships → opportunistic backfill). upsertUser now backfills the column too, closing the race. Existing NULL rows are backfilled by migration 448. Ongoing drift is caught by the new users-have-primary-organization integrity invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(membership): clear stale primary_organization_id pointers + harden backfill Review feedback on PR #3375: - deleteOrganizationMembership now clears users.primary_organization_id when it pointed at the deleted org. Without this, a user removed from their primary org keeps a stale pointer until something else writes the column, and resolvePrimaryOrganization keeps resolving them into the removed org. - users-have-primary-organization invariant now flags stale pointers too (column set to an org with no current membership row), not just missing pointers. Same authorization-scope risk in reverse. - upsertUser backfill is wrapped in try/catch — a transient DB failure during the opportunistic backfill no longer fails the user.created webhook. The integrity invariant catches anything that doesn't get set. - resolvePreferredOrganization tie-breaker documented (loose 'active' match, most-recent-first across paying orgs). - Migration 448 documents scale assumption. - Test polling replaces fixed-sleep race. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(membership): atomicize delete-membership pointer clear; harden upsert backfill Code-review fixup on PR #3375: - deleteOrganizationMembership wraps the DELETE + pointer-clear UPDATE in a single transaction. Two separate pool.query calls could leave a stale pointer if the UPDATE failed after the DELETE succeeded — exactly the drift the new invariant exists to catch. - upsertMembership webhook backfill now try/catches resolvePreferredOrganization + backfillPrimaryOrganization, matching upsertUser's pattern. A transient backfill failure no longer fails the membership webhook (integrity invariant catches drift; next request opportunistically backfills). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auto-link): traverse brand hierarchy when matching email domain to paying org (#3378) * fix(auto-link): traverse brand hierarchy when matching email domain to paying org resolveEffectiveMembership walked brands.house_domain when computing post-link membership, but autoLinkByVerifiedDomain did a direct verified-domain match only. Two code paths answered the same question ("does this domain belong to a paying org?") with different rules — so a child-brand employee (someone@analyticsiq.com) was denied auto-link to a paying parent org (Alliant) even though, post-link, membership would have been inherited cleanly through the brand registry. Extract a findPayingOrgForDomain helper that runs the recursive brand walk (max 5 hops, high-confidence classifications only) and refactor autoLinkByVerifiedDomain to use it. Direct matches still win over inherited (shallowest first), and auto_provision_verified_domain applies at the resolved paying-org level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auto-link): tighten trust gates per security + product review Review feedback on PR #3378: - Hierarchical inheritance is now opt-in via new auto_provision_brand_hierarchy_children column (default false, migration 449). Direct verified-domain auto-provisioning is unchanged. Matches the SaaS norm: silent join only on DNS-verified domains; fuzzy/hierarchical match requires explicit consent. Default-off because brand registry M&A data lags reality, holdcos like Publicis/IPG run subsidiaries as separate P&Ls, and child-brand employees often don't consider themselves part of the parent. - Drop OR source_type = 'brand_json' from the inheritance gate. brand.json schema has no parent/house_domain field today, so brand_json rows never actually carry inheritance data — but the OR clause was a misleading "trust signal" that would silently broaden the attack surface if anyone wired house_domain through the brand_json crawl path later. Until brand.json supports parent assertions (separate spec issue), only the high-confidence classifier path inherits. - 180-day TTL on brands.last_validated (fallback to discovered_at / created_at). Stale LLM classifications expire instead of inheriting forever — a divestiture case (Alliant divests AnalyticsIQ) ages out within 6 months even if no one notices. - Off-by-one fix in chain-reconstruction CTE: dc.depth < $2 (matching the first CTE) so hierarchy_chain doesn't include a non-paying ancestor past the matched depth. Audit-log accuracy. - Audit-log admin PATCH /api/admin/brand-enrichment/brand/:domain when house_domain changes. Each change is what can graft a new domain onto a paying org's auto-link reach, so it should be traceable per admin email + prior/new values. - Tests: cycle protection, max-depth (5+ hops), TTL expiry, opt-in default-off behavior, independent direct/hierarchy flags. 59 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(brand-classifier): note auth-relevance of house_domain output Per security review on PR #3378: brands.house_domain is now load-bearing for the autoLinkByVerifiedDomain inheritance path (gated on confidence='high'), so the classifier's output is authorization-relevant. Document which input fields are most attacker-controllable so the next person adding an enrichment source has a written warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auto-link): close coherence gap — resolveEffectiveMembership + cohort gate + audit hardening Round-2 review fixups on PR #3378. **Security (HIGH) — resolveEffectiveMembership coherence.** The first redesign tightened findPayingOrgForDomain's trust gates but did NOT update resolveEffectiveMembership, leaving a regression: child orgs would see is_member=true on the site / in Addie via inheritance with weaker gates than the auto-link path. Fixed: both CTEs now apply the same rules — high-confidence only (no brand_json), 180-day TTL, and opt-in via auto_provision_brand_hierarchy_children. An ancestor that didn't consent to auto-provisioning children doesn't grant is_member to those children either. **Code review (FBM) — cohort gate / grandfather semantics.** Flipping the opt-in flag previously captured the entire backlog of child-domain users on their next request. Now migration 450 adds auto_provision_hierarchy_enabled_at + a trigger that snapshots the flip moment, and autoLinkByVerifiedDomain checks user.created_at against that timestamp. Only new joiners flow up. **Code review (FBM) — admin PATCH transaction.** Snapshot + UPDATE + audit INSERT in /api/admin/brand-enrichment PATCH now run under one BEGIN/COMMIT with FOR UPDATE on the brand row. Concurrent admin PATCHes can no longer produce a torn audit trail. **Code review (FBM) — audit-log test coverage.** New integration suite admin-brand-enrichment-audit.test.ts (5 cases): null→value, value→value, unchanged, sentinel-org-id fallback, value→null. **Security (medium) — classifier confidence whitelist.** Sonnet output is now validated against {high, medium, low}; unexpected values collapse to low. A prompt-injected confidence claim can't graduate an edge to the auth-relevant 'high' bucket. **Tests added:** resolveEffectiveMembership coherence (opt-in gate, TTL), cohort grandfather (predates flag flip → blocked), cohort new joiner, no-users-row race (webhook ordering), trigger behavior on flag transitions, off-by-one regression guard for hierarchy_chain bounds. 72 integration tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(csrf): exempt /mcp-strict variants so the conformance grader can probe (#3421) * fix(csrf): exempt /mcp-strict variants so the conformance grader can probe The training agent's RFC 9421 conformance routes — /mcp-strict, /mcp-strict-required, /mcp-strict-forbidden — were caught by the global CSRF middleware. Server-to-server callers (the @adcp/client `grade request-signing` runner) don't carry CSRF cookies, so every signed POST got rejected with HTTP 403 + `CSRF validation failed` before the signing verifier could run. The grader read those 403s as "every vector failed, no error codes," which is exactly the symptom #2368 was tracking. Adds the three strict paths to EXEMPT_EXACT alongside the existing /mcp entry. The training agent's own auth chain (bearer or RFC 9421 signature, per buildStrictModeAuthenticator) is the proper protection for these endpoints — CSRF is the wrong layer. Test surface: csrf-middleware.test.ts already iterates EXEMPT_EXACT; the new entries get coverage automatically. The `rejects POST to path that resembles but does not match exempt prefix` test still guards against over-matching. Closes the demo-blocking half of #2368. The verifier middleware, JWKS, PostgresReplayStore, and signed-requests-runner test-kit contract were all already wired; only the CSRF exemption was missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(csrf): trim comment + add near-miss negative test Expert review on PR #3421 flagged two 🟡: - Comment block above the new exemptions was longer than the existing one-liners on /stripe-webhook, /token, etc. Trim to a 2-line rationale; the symptom story is in the commit message and changeset. - Add a parameterized negative test asserting near-miss paths (/mcp-stricter, /mcp-strict-extra, /mcp-strict/extra, /mcps, /mcp-strictly) are NOT exempt. Today's `Array.includes` check is safe by construction, but the test guards against a future refactor switching to `startsWith`. 44 tests pass (was 39). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(brand-viewer): operated / authorized agent sections with empty states (#3422) Brand viewer (/brand/view/<domain>) renders two new always-visible sections: "Agents this brand operates" (from /api/registry/operator) and "Agents authorized to sell this brand's inventory" (from /api/registry/publisher). Empty states explain what would populate each (member claim, adagents.json) so a viewer can distinguish "nothing set up" from "registry isn't surfacing it." Verified locally against 366.fr (both empty), mamamia.com.au (authorized populated via adagents.json), scope3.com (operated populated, member link to /members/scope3). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): release-train setup — 3.0.x line + 3.1 pre mode (#3420) * chore(release): release-train setup — 3.0.x line + 3.1 pre mode Stands up the infrastructure to ship 3.0.x patches independently of 3.1.x development on main, and enters pre mode so 3.1.0 stable can't ship by accident. Workflow changes: - release.yml — add 3.0.x to push triggers (alongside main + 2.6.x). changesets/action will auto-open Version Packages PRs on changeset-release/3.0.x for patch cuts. - forward-merge-3.0.yml — new. Opens a PR back to main whenever 3.0.x is updated. Direction is one-way; merge conflicts fail fast and require human resolution. - build-check.yml, training-agent-storyboards.yml — extend trigger branch lists to fire CI on 3.0.x. Pre mode: - .changeset/pre.json — enters pre mode for the 3.1 cycle. Every Version Packages cut on main now produces 3.1.0-beta.N instead of 3.1.0. Safety net against minor changeset accidents shipping stable. Documentation: - .agents/playbook.md § Release lines — branch naming, cherry-pick convention, patch eligibility rules, pre/exit semantics, and the GITHUB_TOKEN recursion friction tracked in #3417. - .agents/shortcuts/cut-patch.md — new runbook for 3.0.X cuts. - .agents/shortcuts/cut-beta.md — new runbook for 3.1.0-beta.N cuts and exiting pre mode for 3.1.0 stable. Branch creation: 3.0.x was pushed from v3.0.1 tag prior to this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): expert review fixes for release-train setup Three blockers + several issues from parallel expert reviews on PR #3420. Blockers fixed: 1. forward-merge-3.0.yml — peter-evans/create-pull-request operates on its branch ref, not the local working tree. Original logic merged into local `main` and expected peter-evans to push the divergence, but peter-evans diffs its branch ref against base and saw nothing — would have silently dropped 3.0.x patches. Restructured to: `git checkout -B forward-merge/3.0.x origin/main` BEFORE the merge, so the merge commit lives on the branch peter-evans pushes. 2. cut-patch.md audit script targeted .changeset/*.md on changeset-release/3.0.x, but changesets/action deletes those files when it builds the Version Packages PR. Restructured: audit patch-eligibility on the source commit BEFORE cherry-picking, then verify the bump level on the Version Packages PR body after. 3. cut-beta.md exit-pre-mode race window unaddressed. Between exit PR merge and v3.1.0 tag, any minor changeset slips into stable without a beta drop. Added explicit freeze guidance with two paths: freeze main for minor PRs through tag, OR accept and document in exit PR. Other fixes: - Patch eligibility rules tightened: per-surface (not "all of"), IETF errata-vs-bis test for clarifications, transport/auth/signing explicitly never patch-eligible, runner-output schema additions covered, normative docs separated from non-normative. - cut-patch.md cherry-pick conflict guidance: --abort path documented; conflicts usually mean the fix isn't patch-eligible. - Both runbooks: shell vars for VERSION placeholders so agent paste doesn't ship literal "3.0.X" / "3.1.0-beta.N". - Both runbooks: single _§3417_ callout block at top instead of scattered references — one edit when #3417 closes. - Release workflow failure recovery path documented in cut-patch.md (cross-references cut-major.md fallback). - Verification: stranded-patch check via git log v3.0.X..origin/3.0.x. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(addie): grade_agent_signing defaults transport=mcp + exposes override (#3424) The CLI's `grade request-signing` defaults to `--transport raw` (posts to per-operation AdCP endpoints). Every Addie-grade-able agent today is MCP- style (JSON-RPC tools/call), so `raw` against an MCP mount gets 404 on every probe. Surfaced live: my own grade_agent_signing wrapper from #3397 fed every vector to `raw` against `/mcp-strict` after #3421 unblocked CSRF, and the report came back as 404-on-everything. Wrapper now passes `--transport mcp` by default and exposes a `transport` parameter (`mcp` | `raw`) for operators with a raw HTTP AdCP surface. Validated locally: 31 vectors pass against http://localhost:55020/api/training-agent/mcp-strict with --transport mcp, all return 404 with the CLI default. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(team): expose auto_provision_brand_hierarchy_children toggle to org owners (#3430) * feat(team): expose auto_provision_brand_hierarchy_children toggle to org owners PR #3378 added the flag and all the safety scaffolding (opt-in default off, cohort gate, audit log, coherence in resolveEffectiveMembership) but no UI to flip it. Without exposure, adoption-gating triggers on follow-up issues #3410 and #3411 were circular — orgs couldn't adopt what they couldn't toggle. - GET /api/organizations/:orgId/domains returns the new flag, the enabled_at timestamp set by migration 450's trigger, and inferred_subsidiaries — the high-confidence brand-registry rows currently classified as children of this org's verified domains. - PATCH /api/organizations/:orgId/settings accepts auto_provision_brand_hierarchy_children. Owner-only (same restriction as the existing auto_provision_verified_domain), audit-logged. - team.html renders a second toggle row below the verified-domain one with the resolved-subsidiary list so the owner can sanity-check the registry before flipping on. Hidden when there's nothing to match AND flag is off. - Tests cover both flags surfacing, GET shape with inferred subsidiaries, trigger-driven enabled_at, owner-only PATCH, and validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop unused getPool import in org-auto-provision-toggles test Code-quality bot fixup on PR #3430. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(compliance): use per-run uuid idempotency key in inventory_list_targeting Replaces the hardcoded idempotency_key "inventory-list-targeting-create-v1" on the create_buy_with_lists step with a $generate:uuid_v4# key, matching the pattern already used by update_buy_swap_lists in the same storyboard. The static key caused verify_create_persisted to fail structurally on spec-correct frozen-response sellers: on run N+1 the create_media_buy returns the cached response (correct) while get_media_buys reads mutated live state from run N's update_swap_lists. There is no seller-side fix that is simultaneously spec-compliant. Closes #3435 https://claude.ai/code/session_011xMkh2WHvhCyzi48Di1M2d --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bokelley <134922+bokelley@users.noreply.github.com>
Summary
PR #3378 added
auto_provision_brand_hierarchy_childrenand all the safety scaffolding (opt-in default off, cohort grandfather gate, audit log, coherence inresolveEffectiveMembership) but no UI to flip it. Without exposure, the adoption-gating triggers on follow-up issues #3410 (per-subsidiary allowlist) and #3411 (UX hardening) are circular — orgs can't adopt what they can't toggle.This PR exposes the flag to org owners on the existing team page (
/team), right next to the verified-domain auto-add toggle.Changes
Backend
GET /api/organizations/:orgId/domainsnow returnsauto_provision_brand_hierarchy_children,auto_provision_hierarchy_enabled_at, andinferred_subsidiaries(high-confidence brand-registry rows currently classified as children of this org's verified domains, 180-day fresh).PATCH /api/organizations/:orgId/settingsacceptsauto_provision_brand_hierarchy_children. Owner-only (same restriction as the existingauto_provision_verified_domain), audit-logged viarecordAuditLog.Organizationinterface andupdateOrganizationcolumn map carry the new fields.Frontend
team.htmlrenders a second toggle row below the verified-domain one when (a) the brand registry has classified at least one subsidiary, OR (b) the flag is already on (so opted-in orgs can turn it back off). Owner-only.What this enables
Test plan
npm run typecheckcleanserver/tests/integration/org-auto-provision-toggles.test.ts(6 cases): GET returns new fields + inferred_subsidiaries; inferred excludes low-confidence/stale rows; owner PATCH flips the flag and trigger sets enabled_at; admin (non-owner) is rejected; flipping back to false clears enabled_at; non-boolean rejected.organizations.tslacks the dev-mode WorkOS bypass thatmember-profiles.tshas — pre-existing dev-mode gap, separate from this PR; same 403 hits the existingloadJoinRequestsandloadDomainscalls today.)/team(or/dashboard/team), see the new section if the registry has classified any subsidiaries, and toggle the flag. Should also work end-to-end against escalation Fix navbar auth display and cross-domain login #290 (Mike Hattub at AnalyticsIQ) once Alliant's owner enables it on their org.🤖 Generated with Claude Code