Skip to content

feat: grant Semrush workspace members via reused Serenity User Manager transport - #2975

Draft
ravverma wants to merge 7 commits into
mainfrom
spike/serenity-add-workspace-member
Draft

feat: grant Semrush workspace members via reused Serenity User Manager transport#2975
ravverma wants to merge 7 commits into
mainfrom
spike/serenity-add-workspace-member

Conversation

@ravverma

@ravverma ravverma commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds the ability to grant a user a Semrush workspace role, reusing the existing Serenity (Semrush) User Manager integration. This is the mechanism behind the intended flow: user's Semrush data fetch fails 403 (not yet a member) → provision them onto the workspace → retry.

Endpoint (proving ground for the mechanism):

POST /v2/orgs/:spaceCatId/brands/:brandId/serenity/members
body { "members": ["user@example.com"], "role": "role/workspace/viewer" }

Resolves the brand's Semrush workspace (resolveBrandWorkspace — sub-workspace or flat parent) and calls Semrush User Manager POST /v1/workspaces/{id}/members.

Auth — dedicated Semrush IMS technical account (not the caller's token)

The outbound Semrush call is authenticated with a token minted for a dedicated Semrush IMS technical account (SEMRUSH_IMS_TECH_ID / SEMRUSH_IMS_TECH_SECRET, client_credentials) — not the calling user's token, and not the app's existing IMS service client.

This is deliberate: the provisioning flow must be able to add a user who is not yet a member of the workspace. The mint identity holds the member-management rights, so the end user doesn't need them. The Adobe caller is still authorized at the org level (AccessControlUtil) before anything is minted.

  • src/support/serenity/semrush-ims-token.jsmintSemrushImsToken(env, log): client_credentials mint; token host from IMS_HOST; scope defaults to openid,AdobeID,user_management_sdk,additional_info.projectedProductContext (override via SEMRUSH_IMS_TECH_SCOPE); 503 on missing config, 502 on IMS failure; never logs secrets/token.
  • addMembers order: authorize (org) → validate members[] → mint token → transport, so a bad/unauthorized request never mints.

Changes

  • semrush-ims-token.js (new) + addMembers controller + addWorkspaceMembers transport.
  • routes/index.js, facs-capabilities.js (llmo/can_configure), required-capabilities.js (organization:write).
  • Unit tests: mint module (7) + addMembers (10, incl. "uses minted token not caller", mint ordering, mint-config 503). .env.example documents the new vars.

Open (verify before productizing)

  1. @semrush.com target-eligibility — the User Manager spec notes external (non-@semrush.com) users can't be invited; whether an adobe.com member resolves depends on Semrush-side identity mapping. Needs a dev/live check.
  2. Trigger predicate — the real flow should fire on a 403 = authenticated-but-not-a-member (fixable), not a 401 (identity invalid; not fixable by add-member).

Verification

type-check (base+strict) ✓ · lint ✓ · serenity controller + mint suites (239) ✓. Not yet exercised against live/dev Semrush.

🤖 Generated with Claude Code

ravverma and others added 2 commits July 31, 2026 11:33
…r Manager transport

SPIKE (throwaway proof, ADR-draft-2): add POST /v2/orgs/:spaceCatId/brands/:brandId/serenity/members
to grant users a Semrush workspace role, reusing the existing Serenity User Manager transport,
IMS-bearer auth, and secrets — no new config.

- rest-transport.js: addWorkspaceMembers(workspaceId, members, role) via the wired `users` client
  (POST /v1/workspaces/{id}/members; body { members, role }). Present in user-manager-client 1.5.0.
- controllers/serenity.js: addMembers handler (resolveSemrushImsToken -> authorize ->
  resolveBrandWorkspace -> transport); validates members[], role defaults role/workspace/viewer.
- routes/index.js + facs-capabilities.js (llmo/can_configure) + route/controller unit + route tests.

Caller's IMS token is forwarded as-is (Semrush is the auth boundary). Open: correct capability level
and whether non-@semrush.com emails require IMS->Semrush identity mapping — verify in dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

The route-coverage test requires every route in routes/index.js to appear in
routeRequiredCapabilities or INTERNAL_ROUTES. Classify the new members grant as
organization:write, matching sibling serenity write ops (activate/deactivate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ravverma ravverma changed the title spike: grant Semrush workspace members via reused Serenity User Manager transport feat: grant Semrush workspace members via reused Serenity User Manager transport Jul 31, 2026
…ct type-check

The new type-check:strict tier (noImplicitAny) flagged the spike transport method's
untyped params. Annotate workspaceId/members/role per the serenity dir's JSDoc rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.90710% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/support/serenity/rest-transport.js 82.60% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

…ad of caller token

The /serenity/members grant is now authenticated by a token minted for a dedicated
Semrush IMS technical account (SEMRUSH_IMS_TECH_ID/SECRET, client_credentials) rather
than the calling user's IMS token. This lets the "user hits 401/403 → provision them"
flow add a user who is not yet a member of the workspace: the mint identity holds the
member-management rights, the end user need not.

- add src/support/serenity/semrush-ims-token.js: mintSemrushImsToken(env, log) —
  client_credentials mint; token endpoint host from IMS_HOST; scope override via
  SEMRUSH_IMS_TECH_SCOPE; 503 on missing config, 502 on IMS failure; never logs secrets.
- controllers/serenity.js addMembers: org-level authorize + validation first, THEN mint
  the dedicated token, THEN call the transport (no more caller/promise-token path here).
- rest-transport addWorkspaceMembers doc updated (bearer = minted dedicated IMS token).
- tests: mint unit tests + addMembers uses-minted-token / mint-after-gate / mint-config
  failure; .env.example documents the new vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-gated)

Productizes the member-add mechanism as the real flow: when a brand-scoped Semrush
data read fails because the caller is not yet a workspace member (401/403), provision
them (viewer) using the dedicated Semrush IMS token and retry the read once.

- add src/support/serenity/member-autoprovision.js: withMemberAutoProvision (single
  retry, best-effort — a failed grant surfaces the original read error; no-op when
  disabled / missing ids / non-401-403) + isSemrushMembershipDenied.
- controllers/serenity.js: wrap the 5 brand-scoped reads (prompts, markets, getMarket,
  tags, models) via readWithProvision; gate on SERENITY_MEMBER_AUTOPROVISION (default
  off) + the existing serenity-active/workspace gate; resolve caller email.
- mapError: surface upstream 422 as unprocessableEntity (was flattened to 502); the
  limit/emails body stays server-side only.
- utils.js: resolveCallerEmail. .env.example documents the flag.
- tests: member-autoprovision (9), addMembers 422 passthrough, read-provision wiring (3).

The standalone POST /serenity/members endpoint is kept for testing and must be removed
before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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