client.go

v1.0.0
Doc Versions Source
1
package bitwarden
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"fmt"
7
	"net/http"
8
	"net/url"
9
	"time"
10
)
11
12
const defaultBaseURL = "http://localhost:8087"
13
14
// ClientOption configures the Client using the functional options pattern.
15
type ClientOption func(*Client)
16
17
// WithBaseURL sets the base URL for the bw serve API.
18
// Default is "http://localhost:8087".
19
func WithBaseURL(u string) ClientOption {
20
	return func(c *Client) {
21
		c.transport.baseURL = u
22
	}
23
}
24
25
// WithHTTPClient sets a custom http.Client for API requests.
26
// Use this to configure timeouts, proxies, or custom transport.
27
func WithHTTPClient(hc *http.Client) ClientOption {
28
	return func(c *Client) {
29
		c.transport.httpClient = hc
30
	}
31
}
32
33
// Client provides access to a Bitwarden vault via the bw serve API.
34
// It requires the Bitwarden CLI to be running in serve mode (bw serve).
35
// Client implements the VaultClient interface.
36
//
37
// For direct server communication without the CLI, use DirectClient instead.
38
type Client struct {
39
	transport *transport
40
41
	items          *ItemsService
42
	folders        *FoldersService
43
	collections    *CollectionsService
44
	orgCollections *OrgCollectionsService
45
	organizations  *OrganizationsService
46
	orgMembers     *OrgMembersService
47
	send           *SendService
48
	attachments    *AttachmentsService
49
}
50
51
// NewClient creates a new Bitwarden vault management client that communicates
52
// via the bw serve API. The client is created in a locked state and must be
53
// unlocked using Unlock() before accessing vault data.
54
//
55
// Options can be provided to customize the client behavior:
56
//
57
//	client := bitwarden.NewClient(
58
//	    bitwarden.WithBaseURL("http://localhost:8087"),
59
//	    bitwarden.WithHTTPClient(customHTTPClient),
60
//	)
61
func NewClient(opts ...ClientOption) *Client {
62
	t := newTransport(defaultBaseURL, &http.Client{
63
		Timeout: 30 * time.Second,
64
	})
65
66
	c := &Client{transport: t}
67
68
	for _, opt := range opts {
69
		opt(c)
70
	}
71
72
	c.items = &ItemsService{t: t}
73
	c.folders = &FoldersService{t: t}
74
	c.collections = &CollectionsService{t: t}
75
	c.orgCollections = &OrgCollectionsService{t: t}
76
	c.organizations = &OrganizationsService{t: t}
77
	c.orgMembers = &OrgMembersService{t: t}
78
	c.send = &SendService{t: t}
79
	c.attachments = &AttachmentsService{t: t}
80
81
	return c
82
}
83
84
// Items returns the vault items (ciphers) service for CRUD operations on vault items.
85
func (c *Client) Items() ItemsAPI { return c.items }
86
87
// Folders returns the folders service for organizing vault items.
88
func (c *Client) Folders() FoldersAPI { return c.folders }
89
90
// Collections returns the collections service for accessing user collections.
91
func (c *Client) Collections() CollectionsAPI { return c.collections }
92
93
// OrgCollections returns the organization collections service for managing org collections.
94
func (c *Client) OrgCollections() OrgCollectionsAPI { return c.orgCollections }
95
96
// Organizations returns the organizations service for listing user organizations.
97
func (c *Client) Organizations() OrganizationsAPI { return c.organizations }
98
99
// OrgMembers returns the organization members service for member management.
100
func (c *Client) OrgMembers() OrgMembersAPI { return c.orgMembers }
101
102
// Send returns the Bitwarden Send service for creating secure shares.
103
func (c *Client) Send() SendAPI { return c.send }
104
105
// Attachments returns the attachments service for file attachment operations.
106
func (c *Client) Attachments() AttachmentsAPI { return c.attachments }
107
108
// Verify Client satisfies VaultClient at compile time.
109
var _ VaultClient = (*Client)(nil)
110
111
// Status returns the current status of the Bitwarden CLI including
112
// lock state, user information, and server URL.
113
func (c *Client) Status(ctx context.Context) (*Status, error) {
114
	data, err := c.transport.get(ctx, "/status", nil)
115
	if err != nil {
116
		return nil, err
117
	}
118
	resp, err := decodeJSON[StatusResponse](data)
119
	if err != nil {
120
		return nil, err
121
	}
122
	return parseStatusData(resp.Data)
123
}
124
125
// parseStatusData handles both formats:
126
// - bw serve: {"object":"template","template":{...}}
127
// - flat: {"serverUrl":...,"status":"unlocked",...}
128
func parseStatusData(raw json.RawMessage) (*Status, error) {
129
	var wrapped struct {
130
		Object   string  `json:"object"`
131
		Template *Status `json:"template"`
132
	}
133
	if err := json.Unmarshal(raw, &wrapped); err == nil && wrapped.Template != nil {
134
		return wrapped.Template, nil
135
	}
136
	var s Status
137
	if err := json.Unmarshal(raw, &s); err != nil {
138
		return nil, fmt.Errorf("parse status: %w", err)
139
	}
140
	return &s, nil
141
}
142
143
// Sync triggers a vault synchronization with the server.
144
// This downloads the latest vault data and updates the local cache.
145
func (c *Client) Sync(ctx context.Context) error {
146
	_, err := c.transport.post(ctx, "/sync", nil)
147
	return err
148
}
149
150
// Lock locks the vault, clearing encryption keys from memory.
151
// After locking, Unlock must be called before accessing vault data.
152
func (c *Client) Lock(ctx context.Context) error {
153
	_, err := c.transport.post(ctx, "/lock", nil)
154
	return err
155
}
156
157
// Unlock unlocks the vault with the given master password.
158
// The vault must be unlocked before accessing any vault data.
159
func (c *Client) Unlock(ctx context.Context, password string) error {
160
	_, err := c.transport.post(ctx, "/unlock", &UnlockRequest{Password: password})
161
	return err
162
}
163
164
// Generate generates a password or passphrase based on the provided options.
165
// Use GenerateOptions to configure password length, character sets, or passphrase settings.
166
func (c *Client) Generate(ctx context.Context, opts GenerateOptions) (string, error) {
167
	q := url.Values{}
168
	if opts.Lowercase != nil && *opts.Lowercase {
169
		q.Set("lowercase", "true")
170
	}
171
	if opts.Uppercase != nil && *opts.Uppercase {
172
		q.Set("uppercase", "true")
173
	}
174
	if opts.Number != nil && *opts.Number {
175
		q.Set("number", "true")
176
	}
177
	if opts.Special != nil && *opts.Special {
178
		q.Set("special", "true")
179
	}
180
	if opts.Length != nil {
181
		q.Set("length", fmt.Sprintf("%d", *opts.Length))
182
	}
183
	if opts.Passphrase != nil && *opts.Passphrase {
184
		q.Set("passphrase", "true")
185
	}
186
	if opts.Words != nil {
187
		q.Set("words", fmt.Sprintf("%d", *opts.Words))
188
	}
189
	if opts.Separator != nil {
190
		q.Set("separator", *opts.Separator)
191
	}
192
	if opts.Capitalize != nil && *opts.Capitalize {
193
		q.Set("capitalize", "true")
194
	}
195
	if opts.IncludeNumber != nil && *opts.IncludeNumber {
196
		q.Set("includeNumber", "true")
197
	}
198
	data, err := c.transport.get(ctx, "/generate", q)
199
	if err != nil {
200
		return "", err
201
	}
202
	resp, err := decodeJSON[GenerateResponse](data)
203
	if err != nil {
204
		return "", err
205
	}
206
	return resp.Data.Data, nil
207
}
208

Source Files