diff --git a/backend/secretapi/app.go b/backend/secretapi/app.go index d8bb1fc..34dd1f9 100644 --- a/backend/secretapi/app.go +++ b/backend/secretapi/app.go @@ -1111,13 +1111,15 @@ func (a *app) handleMemberChannelEvents(w http.ResponseWriter, r *http.Request) }) } writeJSON(w, http.StatusOK, memberChannelEventsResponse{ - Wallet: wallet, - DeviceID: binding.DeviceID, - OrgRootID: binding.OrgRootID, - PrincipalID: binding.PrincipalID, - Events: out, - NextCursor: nextCursor, - ServerTime: time.Now().UTC().Format(time.RFC3339Nano), + Wallet: wallet, + DeviceID: binding.DeviceID, + OrgRootID: binding.OrgRootID, + PrincipalID: binding.PrincipalID, + MembershipStatus: strings.ToLower(strings.TrimSpace(rec.MembershipStatus)), + IdentityAssurance: normalizeAssuranceLevel(rec.IdentityAssurance), + Events: out, + NextCursor: nextCursor, + ServerTime: time.Now().UTC().Format(time.RFC3339Nano), }) } diff --git a/backend/secretapi/app_test.go b/backend/secretapi/app_test.go index 37a6da8..3b6ff51 100644 --- a/backend/secretapi/app_test.go +++ b/backend/secretapi/app_test.go @@ -709,6 +709,12 @@ func TestMemberChannelRegisterPollAckAndUnregister(t *testing.T) { if len(events.Events) == 0 { t.Fatalf("expected seeded events, got none") } + if events.MembershipStatus != "active" { + t.Fatalf("expected active membership status in events payload, got %+v", events) + } + if events.IdentityAssurance != assuranceOnrampAttested { + t.Fatalf("expected onramp assurance in events payload, got %+v", events) + } first := events.Events[0] ackTime := time.Now().UTC() ack := postJSONExpect[memberChannelEventAckResponse](t, a, "/member/channel/events/"+first.EventID+"/ack", memberChannelEventAckRequest{ @@ -764,6 +770,9 @@ func TestMemberChannelEventsAllowUnattestedMembership(t *testing.T) { if len(events.Events) == 0 { t.Fatalf("expected seeded events for unattested member, got none") } + if events.IdentityAssurance != assuranceCryptoDirect { + t.Fatalf("expected crypto_direct_unattested assurance for unattested member events, got %+v", events) + } } func TestMemberChannelSupportTicketOwnerOnly(t *testing.T) { diff --git a/backend/secretapi/models.go b/backend/secretapi/models.go index 5ee2442..9583486 100644 --- a/backend/secretapi/models.go +++ b/backend/secretapi/models.go @@ -258,13 +258,15 @@ type memberChannelDeviceUnregisterResponse struct { } type memberChannelEventsResponse struct { - Wallet string `json:"wallet"` - DeviceID string `json:"device_id"` - OrgRootID string `json:"org_root_id"` - PrincipalID string `json:"principal_id"` - Events []memberChannelEvent `json:"events"` - NextCursor string `json:"next_cursor"` - ServerTime string `json:"server_time"` + Wallet string `json:"wallet"` + DeviceID string `json:"device_id"` + OrgRootID string `json:"org_root_id"` + PrincipalID string `json:"principal_id"` + MembershipStatus string `json:"membership_status"` + IdentityAssurance string `json:"identity_assurance_level"` + Events []memberChannelEvent `json:"events"` + NextCursor string `json:"next_cursor"` + ServerTime string `json:"server_time"` } type memberChannelEvent struct { diff --git a/docs/api/examples/member-channel.examples.md b/docs/api/examples/member-channel.examples.md index 5ed7308..c1f4bd7 100644 --- a/docs/api/examples/member-channel.examples.md +++ b/docs/api/examples/member-channel.examples.md @@ -50,6 +50,8 @@ Authorization: Bearer "device_id": "desktop-7f6f3a9b", "org_root_id": "org.acme.root", "principal_id": "human.joshua", + "membership_status": "active", + "identity_assurance_level": "onramp_attested", "events": [ { "event_id": "evt_1031", @@ -145,3 +147,13 @@ Content-Type: application/json "correlation_id": "req_01J9A6ZC0SYF5A0M8AP8BNX7B2" } ``` + +## Owner Action Assurance Error + +```json +{ + "error": "owner support actions require onramp_attested identity assurance", + "code": "identity_assurance_insufficient", + "correlation_id": "req_01J9A72Q2Q0VQ8MCR7D2N95D3R" +} +``` diff --git a/docs/api/member-channel.openapi.yaml b/docs/api/member-channel.openapi.yaml index eb578d4..27f46f3 100644 --- a/docs/api/member-channel.openapi.yaml +++ b/docs/api/member-channel.openapi.yaml @@ -249,6 +249,8 @@ components: - device_id - org_root_id - principal_id + - membership_status + - identity_assurance_level - events - next_cursor - server_time @@ -261,6 +263,12 @@ components: type: string principal_id: type: string + membership_status: + type: string + enum: [active, none, suspended, revoked, unknown] + identity_assurance_level: + type: string + enum: [none, crypto_direct_unattested, sponsored_unattested, onramp_attested] events: type: array items: