package shared import ( "fmt" "time" "galaxy/authsession/internal/domain/devicesession" "galaxy/authsession/internal/domain/gatewayprojection" ) // Session mirrors the frozen internal read-model DTO used by later trusted // transport handlers. type Session struct { // DeviceSessionID is the stable identifier of one device session. DeviceSessionID string // UserID is the stable identifier of the session owner. UserID string // ClientPublicKey is the base64-encoded raw 32-byte Ed25519 public key of // the device session. ClientPublicKey string // Status reports whether the session is active or revoked. Status string // CreatedAt is the RFC3339 UTC timestamp at which the session was created. CreatedAt string // RevokedAt is the RFC3339 UTC timestamp at which the session was revoked, // when the session is revoked. RevokedAt *string // RevokeReasonCode is the machine-readable revoke reason code when the // session is revoked. RevokeReasonCode *string // RevokeActorType is the machine-readable revoke actor type when the // session is revoked. RevokeActorType *string // RevokeActorID is the optional stable revoke actor identifier when the // session is revoked. RevokeActorID *string } // ToSession converts source-of-truth session into the frozen internal read DTO // shape. func ToSession(record devicesession.Session) (Session, error) { if err := record.Validate(); err != nil { return Session{}, fmt.Errorf("map session: %w", err) } result := Session{ DeviceSessionID: record.ID.String(), UserID: record.UserID.String(), ClientPublicKey: record.ClientPublicKey.String(), Status: string(record.Status), CreatedAt: formatTime(record.CreatedAt), } if record.Revocation != nil { revokedAt := formatTime(record.Revocation.At) reasonCode := record.Revocation.ReasonCode.String() actorType := record.Revocation.ActorType.String() result.RevokedAt = &revokedAt result.RevokeReasonCode = &reasonCode result.RevokeActorType = &actorType if record.Revocation.ActorID != "" { actorID := record.Revocation.ActorID result.RevokeActorID = &actorID } } return result, nil } // ToSessions converts every source-of-truth session into the frozen internal // read DTO shape. func ToSessions(records []devicesession.Session) ([]Session, error) { result := make([]Session, 0, len(records)) for index, record := range records { mapped, err := ToSession(record) if err != nil { return nil, fmt.Errorf("map session %d: %w", index, err) } result = append(result, mapped) } return result, nil } // ToGatewayProjectionSnapshot converts source-of-truth session into the // separate gateway-facing projection model. func ToGatewayProjectionSnapshot(record devicesession.Session) (gatewayprojection.Snapshot, error) { if err := record.Validate(); err != nil { return gatewayprojection.Snapshot{}, fmt.Errorf("map gateway projection snapshot: %w", err) } snapshot := gatewayprojection.Snapshot{ DeviceSessionID: record.ID, UserID: record.UserID, ClientPublicKey: record.ClientPublicKey.String(), Status: gatewayprojection.Status(record.Status), } if record.Revocation != nil { snapshot.RevokedAt = cloneTimePointer(commonTimePointer(record.Revocation.At.UTC())) snapshot.RevokeReasonCode = record.Revocation.ReasonCode snapshot.RevokeActorType = record.Revocation.ActorType snapshot.RevokeActorID = record.Revocation.ActorID } if err := snapshot.Validate(); err != nil { return gatewayprojection.Snapshot{}, fmt.Errorf("map gateway projection snapshot: %w", err) } return snapshot, nil } func formatTime(value time.Time) string { return value.UTC().Format(time.RFC3339) } func commonTimePointer(value time.Time) *time.Time { return &value } func cloneTimePointer(value *time.Time) *time.Time { if value == nil { return nil } cloned := *value return &cloned }