Skip to content

feat(admin): support-users — gate widen + JIT team support-access endpoints#54

Merged
alejandro-runner merged 4 commits intosynvya-stagingfrom
feat/support-users
May 6, 2026
Merged

feat(admin): support-users — gate widen + JIT team support-access endpoints#54
alejandro-runner merged 4 commits intosynvya-stagingfrom
feat/support-users

Conversation

@alejandro-runner
Copy link
Copy Markdown
Member

Summary

Implements the Keycast-side capabilities described in docs/synvya/support-users.md: a Synvya support agent (UCAN with admin_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

  1. docs(synvya): clarify support-users §3.2 — adds the new client spec link, restricts /support-access to fully-provisioned teams (400 on missing stored key), switches the release-side filter from connected_client_pubkey (NULL until first connection) to a label LIKE 'support:{caller}%' prefix that the grant endpoint stamps at create time.
  2. feat(admin): widen create_team gatePOST /api/teams now accepts is_support_admin callers in addition to full admins and first-team users. Emits a structured audit line when a support admin creates a team.
  3. feat(admin): add support-access endpointsPOST /api/admin/teams/:id/support-access (mints labeled authorization, idempotent membership add, sends AuthorizationCommand::Upsert); DELETE /api/admin/teams/:id/support-access (bulk-revokes the caller's own support-labeled auths, removes member row, preserves admin). Adds AuthorizationRepository::find_active_support_for_caller.
  4. 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

  • Cold-start "register new restaurant" flow (§5.1 of the spec): support agent calls POST /api/teams (now permitted), becomes admin via create_with_admin, then uses the standard team-admin endpoints (add_key, add_authorization, invite_user). No /support-access call needed for teams the agent created.
  • Diagnostic "open existing restaurant" flow (§5.2): client searches via user-lookup/user-teams, calls POST /support-access, receives a bunker_url with default-24h / max-7d expiry, JIT membership is added. DELETE /support-access cleans up.
  • Audit lines: 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 the support_admins Redis mirror call from Synvya/server can land independently in that repo.

Test plan

  • CI runs cargo fmt --all -- --check, cargo clippy -- -D warnings, and cargo test --features integration-tests (the new repo tests need a Postgres test DB).
  • Local: cd api && cargo test --features integration-tests --test admin_support_access_test.
  • Manual on staging:
    • Promote a test pubkey to support_admins via the existing POST /api/admin/support-admins (full-admin auth).
    • Log in as that user, confirm UCAN carries admin_role: \"support\" via GET /api/admin/status.
    • POST /api/teams succeeds; team is created with the support agent as admin.
    • On a second pre-existing team (with stored key), POST /api/admin/teams/:id/support-access returns a working bunker_url; the agent appears as a member; signer logs an Upsert.
    • DELETE /api/admin/teams/:id/support-access revokes the auth (signer logs Remove), removes the member row, second call is a no-op.
    • Audit lines visible in Cloud Logging.

🤖 Generated with Claude Code

alejandro-runner and others added 4 commits May 5, 2026 16:29
…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>
@alejandro-runner alejandro-runner merged commit 55b8bd6 into synvya-staging May 6, 2026
@alejandro-runner alejandro-runner deleted the feat/support-users branch May 6, 2026 03:27
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