summaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authors <[email protected]>2025-11-03 21:17:12 -0500
committers <[email protected]>2025-11-03 21:17:12 -0500
commitf7fcfa623e670dc533bb378912829c73a3593e63 (patch)
tree910119ff7293b407affa9ff34706d627d77a3a04 /internal
downloaddborg-f7fcfa623e670dc533bb378912829c73a3593e63.tar.gz
dborg-f7fcfa623e670dc533bb378912829c73a3593e63.zip
hi
Diffstat (limited to 'internal')
-rw-r--r--internal/client/admin.go124
-rw-r--r--internal/client/client.go119
-rw-r--r--internal/client/client_test.go46
-rw-r--r--internal/client/npd.go91
-rw-r--r--internal/client/osint.go32
-rw-r--r--internal/client/skiptrace.go179
-rw-r--r--internal/client/sl.go53
-rw-r--r--internal/client/usrsx.go72
-rw-r--r--internal/client/x.go23
-rw-r--r--internal/config/config.go36
-rw-r--r--internal/config/errors.go7
-rw-r--r--internal/models/admin.go43
-rw-r--r--internal/models/npd.go42
-rw-r--r--internal/models/osint.go29
-rw-r--r--internal/models/skiptrace.go92
-rw-r--r--internal/models/sl.go23
-rw-r--r--internal/models/usrsx.go33
-rw-r--r--internal/models/x.go22
-rw-r--r--internal/utils/output.go39
19 files changed, 1105 insertions, 0 deletions
diff --git a/internal/client/admin.go b/internal/client/admin.go
new file mode 100644
index 0000000..a5a9519
--- /dev/null
+++ b/internal/client/admin.go
@@ -0,0 +1,124 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) ListAccounts() (*models.AdminResponse, error) {
+ data, err := c.Get("/admin/accounts", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) CreateAccount(req *models.AccountCreateRequest) (*models.AdminResponse, error) {
+ data, err := c.Post("/admin/accounts", req)
+ if err != nil {
+ return nil, err
+ }
+
+ var account models.Account
+ if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" {
+ return &models.AdminResponse{
+ Success: true,
+ Account: &account,
+ }, nil
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) DeleteAccount(apiKey string) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s", url.PathEscape(apiKey))
+ data, err := c.Delete(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) UpdateCredits(apiKey string, credits int) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey))
+ req := &models.AddCreditsRequest{
+ Credits: credits,
+ }
+
+ data, err := c.Post(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SetCredits(apiKey string, credits int) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey))
+ req := &models.SetCreditsRequest{
+ Credits: credits,
+ }
+
+ data, err := c.Put(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var account models.Account
+ if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" {
+ return &models.AdminResponse{
+ Success: true,
+ Account: &account,
+ }, nil
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) ToggleAccount(apiKey string, enable bool) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/disable", url.PathEscape(apiKey))
+ req := &models.DisableAccountRequest{
+ Disabled: !enable,
+ }
+
+ data, err := c.Patch(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/client.go b/internal/client/client.go
new file mode 100644
index 0000000..098479f
--- /dev/null
+++ b/internal/client/client.go
@@ -0,0 +1,119 @@
+package client
+
+import (
+ "bytes"
+ "dborg/internal/config"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+type Client struct {
+ config *config.Config
+ httpClient *http.Client
+}
+
+func New(cfg *config.Config) (*Client, error) {
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+
+ return &Client{
+ config: cfg,
+ httpClient: &http.Client{
+ Timeout: cfg.Timeout,
+ },
+ }, nil
+}
+
+func (c *Client) doRequest(method, path string, params url.Values, body interface{}) ([]byte, error) {
+ fullURL := c.config.BaseURL + path
+ if params != nil && len(params) > 0 {
+ fullURL += "?" + params.Encode()
+ }
+
+ var reqBody io.Reader
+ if body != nil {
+ jsonData, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal request body: %w", err)
+ }
+ reqBody = bytes.NewBuffer(jsonData)
+ }
+
+ req, err := http.NewRequest(method, fullURL, reqBody)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ var resp *http.Response
+ var lastErr error
+
+ for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
+ if attempt > 0 {
+ time.Sleep(time.Duration(attempt) * time.Second)
+ }
+
+ resp, err = c.httpClient.Do(req)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
+ return io.ReadAll(resp.Body)
+ }
+
+ bodyBytes, _ := io.ReadAll(resp.Body)
+
+ switch resp.StatusCode {
+ case http.StatusForbidden:
+ lastErr = fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
+ case http.StatusUnauthorized:
+ lastErr = fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes))
+ case http.StatusTooManyRequests:
+ lastErr = fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes))
+ case http.StatusBadRequest:
+ lastErr = fmt.Errorf("bad request (400): %s", string(bodyBytes))
+ default:
+ lastErr = fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 {
+ break
+ }
+ }
+
+ return nil, lastErr
+}
+
+func (c *Client) Get(path string, params url.Values) ([]byte, error) {
+ return c.doRequest(http.MethodGet, path, params, nil)
+}
+
+func (c *Client) Post(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPost, path, nil, body)
+}
+
+func (c *Client) Delete(path string) ([]byte, error) {
+ return c.doRequest(http.MethodDelete, path, nil, nil)
+}
+
+func (c *Client) Patch(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPatch, path, nil, body)
+}
+
+func (c *Client) Put(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPut, path, nil, body)
+}
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
new file mode 100644
index 0000000..9bf453d
--- /dev/null
+++ b/internal/client/client_test.go
@@ -0,0 +1,46 @@
+package client
+
+import (
+ "dborg/internal/config"
+ "testing"
+ "time"
+)
+
+func TestNewClient(t *testing.T) {
+ tests := []struct {
+ name string
+ config *config.Config
+ wantErr bool
+ }{
+ {
+ name: "valid config",
+ config: &config.Config{
+ APIKey: "test-key",
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "test-agent",
+ },
+ wantErr: false,
+ },
+ {
+ name: "missing API key",
+ config: &config.Config{
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "test-agent",
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := New(tt.config)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/internal/client/npd.go b/internal/client/npd.go
new file mode 100644
index 0000000..c63327b
--- /dev/null
+++ b/internal/client/npd.go
@@ -0,0 +1,91 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchNPD(params *models.NPDParams) (*models.NPDResponse, error) {
+ queryParams := url.Values{}
+
+ if params.ID != "" {
+ queryParams.Add("id", params.ID)
+ }
+ if params.FirstName != "" {
+ queryParams.Add("firstname", params.FirstName)
+ }
+ if params.LastName != "" {
+ queryParams.Add("lastname", params.LastName)
+ }
+ if params.MiddleName != "" {
+ queryParams.Add("middlename", params.MiddleName)
+ }
+ if params.DOB != "" {
+ queryParams.Add("dob", params.DOB)
+ }
+ if params.SSN != "" {
+ queryParams.Add("ssn", params.SSN)
+ }
+ if params.Phone1 != "" {
+ queryParams.Add("phone1", params.Phone1)
+ }
+ if params.Address != "" {
+ queryParams.Add("address", params.Address)
+ }
+ if params.City != "" {
+ queryParams.Add("city", params.City)
+ }
+ if params.State != "" {
+ queryParams.Add("st", params.State)
+ }
+ if params.Zip != "" {
+ queryParams.Add("zip", params.Zip)
+ }
+ if params.CountyName != "" {
+ queryParams.Add("county_name", params.CountyName)
+ }
+ if params.NameSuffix != "" {
+ queryParams.Add("name_suff", params.NameSuffix)
+ }
+ if params.AKA1FullName != "" {
+ queryParams.Add("aka1fullname", params.AKA1FullName)
+ }
+ if params.AKA2FullName != "" {
+ queryParams.Add("aka2fullname", params.AKA2FullName)
+ }
+ if params.AKA3FullName != "" {
+ queryParams.Add("aka3fullname", params.AKA3FullName)
+ }
+ if params.Alt1DOB != "" {
+ queryParams.Add("alt1dob", params.Alt1DOB)
+ }
+ if params.Alt2DOB != "" {
+ queryParams.Add("alt2dob", params.Alt2DOB)
+ }
+ if params.Alt3DOB != "" {
+ queryParams.Add("alt3dob", params.Alt3DOB)
+ }
+ if params.StartDate != "" {
+ queryParams.Add("startdat", params.StartDate)
+ }
+ if params.MaxHits > 0 && params.MaxHits != 10 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+ if params.SortBy != "" {
+ queryParams.Add("sort_by", params.SortBy)
+ }
+
+ data, err := c.Get("/npd/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.NPDResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse NPD response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/osint.go b/internal/client/osint.go
new file mode 100644
index 0000000..70dbb72
--- /dev/null
+++ b/internal/client/osint.go
@@ -0,0 +1,32 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) LookupBSSID(params *models.BSSIDParams) (*models.BSSIDLookupResponse, error) {
+ path := fmt.Sprintf("/osint/bssid/%s", url.PathEscape(params.BSSID))
+
+ queryParams := url.Values{}
+ if params.All {
+ queryParams.Add("all", "true")
+ }
+ if params.Map {
+ queryParams.Add("map", "true")
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.BSSIDLookupResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse BSSID lookup response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/skiptrace.go b/internal/client/skiptrace.go
new file mode 100644
index 0000000..b1d5008
--- /dev/null
+++ b/internal/client/skiptrace.go
@@ -0,0 +1,179 @@
+package client
+
+import (
+ "bufio"
+ "bytes"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+func parseSSEResponse(data []byte) ([]byte, error) {
+ scanner := bufio.NewScanner(bytes.NewReader(data))
+
+ const maxScanTokenSize = 10 * 1024 * 1024
+ buf := make([]byte, maxScanTokenSize)
+ scanner.Buffer(buf, maxScanTokenSize)
+
+ var resultData []byte
+ var foundResult bool
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if line == "event: result" {
+ foundResult = true
+ continue
+ }
+
+ if foundResult && strings.HasPrefix(line, "data: ") {
+ resultData = []byte(strings.TrimPrefix(line, "data: "))
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("error reading SSE response: %w", err)
+ }
+
+ if resultData == nil {
+ return nil, fmt.Errorf("no result event found in SSE response")
+ }
+
+ trimmed := strings.TrimSpace(string(resultData))
+ if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
+ return nil, fmt.Errorf("API returned: %s", trimmed)
+ }
+
+ return resultData, nil
+}
+
+func (c *Client) getSSE(path string, params url.Values) ([]byte, error) {
+ fullURL := c.config.BaseURL + path
+ if params != nil && len(params) > 0 {
+ fullURL += "?" + params.Encode()
+ }
+
+ req, err := http.NewRequest("GET", fullURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ switch resp.StatusCode {
+ case http.StatusForbidden:
+ return nil, fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
+ case http.StatusUnauthorized:
+ return nil, fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes))
+ case http.StatusTooManyRequests:
+ return nil, fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes))
+ case http.StatusBadRequest:
+ return nil, fmt.Errorf("bad request (400): %s", string(bodyBytes))
+ default:
+ return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+ }
+
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ }
+
+ contentType := resp.Header.Get("Content-Type")
+
+ if strings.HasPrefix(string(data), "event:") || strings.Contains(contentType, "text/event-stream") {
+ return parseSSEResponse(data)
+ }
+
+ return data, nil
+}
+
+func (c *Client) SearchPeople(params *models.SkiptraceParams) (*models.SkiptraceResponse, error) {
+ queryParams := url.Values{}
+ queryParams.Set("first_name", params.FirstName)
+ queryParams.Set("last_name", params.LastName)
+
+ if params.City != "" {
+ queryParams.Set("city", params.City)
+ }
+ if params.State != "" {
+ queryParams.Set("state", params.State)
+ }
+ if params.Age != "" {
+ queryParams.Set("age", params.Age)
+ }
+
+ data, err := c.getSSE("/prem/skiptrace/people/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) GetPersonReport(sxKey string, selection int) (*models.SkiptraceReportResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/people/report/%s/%d", sxKey, selection)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceReportResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchPhone(phone string) (*models.SkiptracePhoneResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/phone/%s", phone)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptracePhoneResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchEmail(email string) (*models.SkiptraceEmailResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/email/%s", email)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceEmailResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/sl.go b/internal/client/sl.go
new file mode 100644
index 0000000..fb3b270
--- /dev/null
+++ b/internal/client/sl.go
@@ -0,0 +1,53 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchStealerLogs(params *models.SLParams) (*models.SLResponse, error) {
+ queryParams := url.Values{}
+ queryParams.Add("query", params.Query)
+
+ if params.MaxHits > 0 && params.MaxHits != 10 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+ if params.SortBy != "" {
+ queryParams.Add("sort_by", params.SortBy)
+ }
+ if params.IngestStartDate != "" {
+ queryParams.Add("ingest_start_date", params.IngestStartDate)
+ }
+ if params.IngestEndDate != "" {
+ queryParams.Add("ingest_end_date", params.IngestEndDate)
+ }
+ if params.PostedStartDate != "" {
+ queryParams.Add("posted_start_date", params.PostedStartDate)
+ }
+ if params.PostedEndDate != "" {
+ queryParams.Add("posted_end_date", params.PostedEndDate)
+ }
+ if params.Format != "" && params.Format != "json" {
+ queryParams.Add("format", params.Format)
+ }
+
+ data, err := c.Get("/sl/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ if params.Format != "" && params.Format != "json" {
+ return &models.SLResponse{
+ Message: string(data),
+ }, nil
+ }
+
+ var response models.SLResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse stealer logs response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/usrsx.go b/internal/client/usrsx.go
new file mode 100644
index 0000000..456acbf
--- /dev/null
+++ b/internal/client/usrsx.go
@@ -0,0 +1,72 @@
+package client
+
+import (
+ "bufio"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+func (c *Client) CheckUsernameStream(params *models.USRSXParams, callback func(result json.RawMessage) error) error {
+ queryParams := url.Values{}
+
+ if len(params.Sites) > 0 {
+ queryParams.Add("sites", strings.Join(params.Sites, ","))
+ }
+ if params.Fuzzy {
+ queryParams.Add("fuzzy", "true")
+ }
+ if params.MaxTasks > 0 && params.MaxTasks != 50 {
+ queryParams.Add("max_tasks", fmt.Sprintf("%d", params.MaxTasks))
+ }
+
+ path := fmt.Sprintf("/osint/username/%s", url.PathEscape(params.Username))
+ fullURL := c.config.BaseURL + path
+ if len(queryParams) > 0 {
+ fullURL += "?" + queryParams.Encode()
+ }
+
+ req, err := http.NewRequest(http.MethodGet, fullURL, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+ req.Header.Set("Accept", "application/x-ndjson, application/json")
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to execute request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
+ }
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ line := scanner.Bytes()
+ if len(line) == 0 {
+ continue
+ }
+
+ if err := callback(json.RawMessage(line)); err != nil {
+ return err
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ if !strings.Contains(err.Error(), "context deadline exceeded") && !strings.Contains(err.Error(), "timeout") {
+ return fmt.Errorf("stream reading error: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/client/x.go b/internal/client/x.go
new file mode 100644
index 0000000..8bdb21c
--- /dev/null
+++ b/internal/client/x.go
@@ -0,0 +1,23 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchTwitterHistory(username string) (*models.XResponse, error) {
+ path := fmt.Sprintf("/x/search/%s", url.PathEscape(username))
+ data, err := c.Get(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.XResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse Twitter/X response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..44ca7e6
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,36 @@
+package config
+
+import (
+ "os"
+ "time"
+)
+
+type Config struct {
+ APIKey string
+ BaseURL string
+ Timeout time.Duration
+ MaxRetries int
+ UserAgent string
+}
+
+func New() *Config {
+ return &Config{
+ APIKey: os.Getenv("DBORG_API_KEY"),
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "dborg-cli/1.0",
+ }
+}
+
+func (c *Config) WithAPIKey(key string) *Config {
+ c.APIKey = key
+ return c
+}
+
+func (c *Config) Validate() error {
+ if c.APIKey == "" {
+ return ErrMissingAPIKey
+ }
+ return nil
+}
diff --git a/internal/config/errors.go b/internal/config/errors.go
new file mode 100644
index 0000000..4fd3636
--- /dev/null
+++ b/internal/config/errors.go
@@ -0,0 +1,7 @@
+package config
+
+import "errors"
+
+var (
+ ErrMissingAPIKey = errors.New("API key required: set DBORG_API_KEY environment variable or use --api-key flag")
+)
diff --git a/internal/models/admin.go b/internal/models/admin.go
new file mode 100644
index 0000000..5cf0f37
--- /dev/null
+++ b/internal/models/admin.go
@@ -0,0 +1,43 @@
+package models
+
+type Account struct {
+ APIKey string `json:"api_key"`
+ Name string `json:"name"`
+ Credits int `json:"credits"`
+ Unlimited bool `json:"unlimited"`
+ Disabled bool `json:"disabled"`
+ IsPremium bool `json:"is_premium"`
+ CreatedAt interface{} `json:"created_at,omitempty"`
+}
+
+type AccountCreateRequest struct {
+ Name string `json:"name"`
+ Credits int `json:"credits,omitempty"`
+ Unlimited bool `json:"unlimited,omitempty"`
+ IsPremium bool `json:"is_premium,omitempty"`
+}
+
+type AccountUpdateRequest struct {
+ Credits int `json:"credits,omitempty"`
+ Disabled bool `json:"disabled"`
+}
+
+type AddCreditsRequest struct {
+ Credits int `json:"credits"`
+}
+
+type SetCreditsRequest struct {
+ Credits int `json:"credits"`
+}
+
+type DisableAccountRequest struct {
+ Disabled bool `json:"disabled"`
+}
+
+type AdminResponse struct {
+ Success bool `json:"success,omitempty"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+ Account *Account `json:"account,omitempty"`
+ Accounts []Account `json:"accounts,omitempty"`
+}
diff --git a/internal/models/npd.go b/internal/models/npd.go
new file mode 100644
index 0000000..a1da05e
--- /dev/null
+++ b/internal/models/npd.go
@@ -0,0 +1,42 @@
+package models
+
+type NPDParams struct {
+ ID string `json:"id,omitempty"`
+ FirstName string `json:"firstname,omitempty"`
+ LastName string `json:"lastname,omitempty"`
+ MiddleName string `json:"middlename,omitempty"`
+ DOB string `json:"dob,omitempty"`
+ SSN string `json:"ssn,omitempty"`
+ Phone1 string `json:"phone1,omitempty"`
+ Address string `json:"address,omitempty"`
+ City string `json:"city,omitempty"`
+ State string `json:"st,omitempty"`
+ Zip string `json:"zip,omitempty"`
+ CountyName string `json:"county_name,omitempty"`
+ NameSuffix string `json:"name_suff,omitempty"`
+ AKA1FullName string `json:"aka1fullname,omitempty"`
+ AKA2FullName string `json:"aka2fullname,omitempty"`
+ AKA3FullName string `json:"aka3fullname,omitempty"`
+ Alt1DOB string `json:"alt1dob,omitempty"`
+ Alt2DOB string `json:"alt2dob,omitempty"`
+ Alt3DOB string `json:"alt3dob,omitempty"`
+ StartDate string `json:"startdat,omitempty"`
+ MaxHits int `json:"max_hits,omitempty"`
+ SortBy string `json:"sort_by,omitempty"`
+}
+
+type NPDResponse struct {
+ MaxHits int `json:"max_hits"`
+ Results struct {
+ ElapsedTimeMicros int `json:"elapsed_time_micros"`
+ Errors []string `json:"errors"`
+ Hits []map[string]any `json:"hits"`
+ NumHits int `json:"num_hits"`
+ } `json:"results"`
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
diff --git a/internal/models/osint.go b/internal/models/osint.go
new file mode 100644
index 0000000..7170c27
--- /dev/null
+++ b/internal/models/osint.go
@@ -0,0 +1,29 @@
+package models
+
+type BSSIDParams struct {
+ BSSID string
+ All bool
+ Map bool
+}
+
+type LocationInfo struct {
+ Latitude float64 `json:"latitude"`
+ Longitude float64 `json:"longitude"`
+ Accuracy int `json:"accuracy"`
+}
+
+type BSSIDResult struct {
+ BSSID string `json:"bssid"`
+ Location *LocationInfo `json:"location"`
+ MapURL string `json:"map_url,omitempty"`
+}
+
+type BSSIDLookupResponse struct {
+ BSSID string `json:"bssid"`
+ Results []BSSIDResult `json:"results"`
+ MapURL string `json:"map_url,omitempty"`
+}
+
+type ErrorResponse struct {
+ Error string `json:"error"`
+}
diff --git a/internal/models/skiptrace.go b/internal/models/skiptrace.go
new file mode 100644
index 0000000..c87fe72
--- /dev/null
+++ b/internal/models/skiptrace.go
@@ -0,0 +1,92 @@
+package models
+
+import "encoding/json"
+
+type SkiptraceParams struct {
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ City string `json:"city,omitempty"`
+ State string `json:"state,omitempty"`
+ Age string `json:"age,omitempty"`
+}
+
+type SkiptraceResponse struct {
+ Data map[string]interface{} `json:"-"`
+ SXKey string `json:"sx_key,omitempty"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptraceResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptraceResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
+
+type SkiptraceReportResponse struct {
+ Data map[string]interface{} `json:"-"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptraceReportResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptraceReportResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
+
+type SkiptracePhoneResponse struct {
+ Data map[string]interface{} `json:"-"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptracePhoneResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptracePhoneResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
+
+type SkiptraceEmailResponse struct {
+ Data map[string]interface{} `json:"-"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptraceEmailResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptraceEmailResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
diff --git a/internal/models/sl.go b/internal/models/sl.go
new file mode 100644
index 0000000..d55279f
--- /dev/null
+++ b/internal/models/sl.go
@@ -0,0 +1,23 @@
+package models
+
+type SLParams struct {
+ Query string `json:"query"`
+ MaxHits int `json:"max_hits,omitempty"`
+ SortBy string `json:"sort_by,omitempty"`
+ IngestStartDate string `json:"ingest_start_date,omitempty"`
+ IngestEndDate string `json:"ingest_end_date,omitempty"`
+ PostedStartDate string `json:"posted_start_date,omitempty"`
+ PostedEndDate string `json:"posted_end_date,omitempty"`
+ Format string `json:"format,omitempty"`
+}
+
+type SLResponse struct {
+ MaxHits int `json:"max_hits"`
+ Results interface{} `json:"results"`
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
diff --git a/internal/models/usrsx.go b/internal/models/usrsx.go
new file mode 100644
index 0000000..f9264be
--- /dev/null
+++ b/internal/models/usrsx.go
@@ -0,0 +1,33 @@
+package models
+
+type USRSXParams struct {
+ Username string `json:"username"`
+ Sites []string `json:"sites,omitempty"`
+ Fuzzy bool `json:"fuzzy,omitempty"`
+ MaxTasks int `json:"max_tasks,omitempty"`
+}
+
+type USRSXResponse struct {
+ Username string `json:"username"`
+ Results []SiteResult `json:"results"`
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type SiteResult struct {
+ SiteName string `json:"site_name"`
+ Username string `json:"username"`
+ URL string `json:"url"`
+ Status string `json:"status"`
+ ResponseCode int `json:"response_code"`
+ Category string `json:"category,omitempty"`
+ Timestamp string `json:"timestamp,omitempty"`
+ Type string `json:"type,omitempty"`
+ Elapsed float64 `json:"elapsed,omitempty"`
+ Error string `json:"error,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+}
diff --git a/internal/models/x.go b/internal/models/x.go
new file mode 100644
index 0000000..f8c7a70
--- /dev/null
+++ b/internal/models/x.go
@@ -0,0 +1,22 @@
+package models
+
+type XResponse struct {
+ Username string `json:"username,omitempty"`
+ PreviousUsernames []UserHistory `json:"previous_usernames,omitempty"`
+
+ Query string `json:"query,omitempty"`
+ Response string `json:"response,omitempty"`
+ Data interface{} `json:"data,omitempty"`
+
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type UserHistory struct {
+ Username string `json:"username"`
+ TimeAgo string `json:"time_ago"`
+}
diff --git a/internal/utils/output.go b/internal/utils/output.go
new file mode 100644
index 0000000..3f2347c
--- /dev/null
+++ b/internal/utils/output.go
@@ -0,0 +1,39 @@
+package utils
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "text/tabwriter"
+)
+
+func PrintJSON(data any) error {
+ output, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format JSON: %w", err)
+ }
+ fmt.Println(string(output))
+ return nil
+}
+
+func PrintTable(headers []string, rows [][]string) {
+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+ defer w.Flush()
+
+ for _, h := range headers {
+ fmt.Fprintf(w, "%s\t", h)
+ }
+ fmt.Fprintln(w)
+
+ for _, row := range rows {
+ for _, col := range row {
+ fmt.Fprintf(w, "%s\t", col)
+ }
+ fmt.Fprintln(w)
+ }
+}
+
+func PrintError(err error) {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+}