feat(admin): support-users — gate widen + JIT team support-access endpoints#54
Merged
alejandro-runner merged 4 commits intosynvya-stagingfrom May 6, 2026
Merged
Conversation
…bel-based filter
Three clarifications now that a separate client spec exists and the
implementation has settled:
- Add the Synvya/client per-repo spec link to the header. Closes the
cross-repo gap where keycast §6 named client work but no client
spec existed.
- §3.2: state that /support-access targets fully-provisioned teams
(existing-restaurant diagnostics, §5.2). Cold-start (§5.1) uses the
standard team-admin endpoints — the agent is already admin via
create_with_admin. Return 400 on missing stored key.
- §3.2 / §3.3: switch from `connected_client_pubkey = caller` to
`label LIKE 'support:{caller}%'` for identifying a support agent's
own authorizations. The connected_client_pubkey column is NULL until
the bunker is first used; the label is set at create time and is
reliable from the grant onward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per docs/synvya/support-users.md §3.1. Support admins can now create teams; they become the team's admin via the existing create_with_admin path and proceed through the standard team-admin flow (add_key, add_authorization, invite_user) for the cold-start "register new restaurant" use case. Emit a structured audit line when a support admin (not full admin) creates a team, mirroring the §7 log shape. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements §3.2 and §3.3 of docs/synvya/support-users.md.
POST /api/admin/teams/:id/support-access
- is_support_admin gated, tenant-scoped.
- Verifies the team exists in caller's tenant.
- Rejects with 400 if the team has no stored key (provisioned-teams-only).
- Adds caller as `member` (admin rows preserved if already an admin).
- Mints a fresh authorization against the team's stored key, derived
bunker keys, default-24h / max-7d expiry, label
`support:{caller_pubkey_hex}` for release-side filtering.
- Sends AuthorizationCommand::Upsert so the bunker is live without
lazy-load latency.
- Returns bunker_url + authorization summary.
DELETE /api/admin/teams/:id/support-access
- Looks up the caller's active support authorizations on the team via
the new AuthorizationRepository.find_active_support_for_caller helper
(filters on `label LIKE 'support:{caller}%'`).
- Soft-revokes each, notifies the signer with AuthorizationCommand::Remove.
- Removes the caller's `team_users` row only if role = 'member';
`admin` rows are preserved (e.g., the support agent who created the
team via the cold-start flow).
- Idempotent — second call returns revoked: 0, removed: false.
Routes registered under the existing admin block (auth_cors layer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Repository-level integration tests for the new
AuthorizationRepository::find_active_support_for_caller helper that
backs DELETE /api/admin/teams/:id/support-access:
- Returns only authorizations whose label starts with the caller's own
`support:{caller_pubkey_hex}` prefix; another support admin's rows,
owner-minted server-bunker rows, and null-label rows are excluded.
- Excludes rows where revoked_at is non-null.
- Is team-scoped: a support auth on team B does not surface when
querying team A.
- Matches labels with a human-readable suffix (the grant endpoint
appends `(suffix)` after the caller prefix when the request body
supplies one).
Handler-level HTTP tests (gate widening on POST /api/teams,
end-to-end grant/release behavior, signer-channel notifications) are
deferred — they require a fuller test harness (state, key_manager,
secret_pool) than any current admin-endpoint test sets up. Suitable
for a follow-up once the harness pattern is established for these
endpoints; covered for now by deployment smoke tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Keycast-side capabilities described in
docs/synvya/support-users.md: a Synvya support agent (UCAN withadmin_role: \"support\") can create new restaurant teams and gain just-in-time membership on existing teams to do diagnostic work.The four-spec set is now complete: server (mirror), systemtools (toggle UI), keycast (this PR), and client (Synvya/client#464 for the consumer-side UI spec).
Commits
docs(synvya): clarify support-users §3.2— adds the new client spec link, restricts/support-accessto fully-provisioned teams (400 on missing stored key), switches the release-side filter fromconnected_client_pubkey(NULL until first connection) to alabel LIKE 'support:{caller}%'prefix that the grant endpoint stamps at create time.feat(admin): widen create_team gate—POST /api/teamsnow acceptsis_support_admincallers in addition to full admins and first-team users. Emits a structured audit line when a support admin creates a team.feat(admin): add support-access endpoints—POST /api/admin/teams/:id/support-access(mints labeled authorization, idempotent membership add, sendsAuthorizationCommand::Upsert);DELETE /api/admin/teams/:id/support-access(bulk-revokes the caller's own support-labeled auths, removesmemberrow, preservesadmin). AddsAuthorizationRepository::find_active_support_for_caller.test(admin): find_active_support_for_caller filter semantics— repository-level integration tests covering the label-prefix filter, revoked exclusion, team scoping, and label-suffix matching. Handler-level HTTP tests are deferred (require a heavier harness than any current admin test sets up).Behavior
POST /api/teams(now permitted), becomes admin viacreate_with_admin, then uses the standard team-admin endpoints (add_key,add_authorization,invite_user). No/support-accesscall needed for teams the agent created.user-lookup/user-teams, callsPOST /support-access, receives abunker_urlwith default-24h / max-7d expiry, JIT membership is added.DELETE /support-accesscleans up.Team created by support admin: ...,Support access granted: ...,Support access released: ..., plus the per-revoke line that already exists.Dependencies
The NIP-98 service-auth foundation (
api/src/api/nip98_extractor.rs) is already in place, so thesupport_adminsRedis mirror call fromSynvya/servercan land independently in that repo.Test plan
cargo fmt --all -- --check,cargo clippy -- -D warnings, andcargo test --features integration-tests(the new repo tests need a Postgres test DB).cd api && cargo test --features integration-tests --test admin_support_access_test.support_adminsvia the existingPOST /api/admin/support-admins(full-admin auth).admin_role: \"support\"viaGET /api/admin/status.POST /api/teamssucceeds; team is created with the support agent asadmin.POST /api/admin/teams/:id/support-accessreturns a workingbunker_url; the agent appears as amember; signer logs anUpsert.DELETE /api/admin/teams/:id/support-accessrevokes the auth (signer logsRemove), removes thememberrow, second call is a no-op.🤖 Generated with Claude Code