@ouro.bot/cli 0.1.0-alpha.227 → 0.1.0-alpha.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.credentialToolDefinitions = void 0;
4
+ const credential_access_1 = require("./credential-access");
5
+ const runtime_1 = require("../nerves/runtime");
6
+ exports.credentialToolDefinitions = [
7
+ {
8
+ tool: {
9
+ type: "function",
10
+ function: {
11
+ name: "credential_get",
12
+ description: "Get credential metadata for a domain. Returns username, notes, and creation date. Never returns passwords — the credential gateway handles secret injection internally.",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ domain: {
17
+ type: "string",
18
+ description: "Domain to look up (e.g. 'airbnb.com', 'api.openweathermap.org')",
19
+ },
20
+ },
21
+ required: ["domain"],
22
+ },
23
+ },
24
+ },
25
+ handler: async (args) => {
26
+ (0, runtime_1.emitNervesEvent)({
27
+ component: "repertoire",
28
+ event: "repertoire.credential_tool_call",
29
+ message: "credential_get invoked",
30
+ meta: { tool: "credential_get", domain: args.domain },
31
+ });
32
+ try {
33
+ const store = (0, credential_access_1.getCredentialStore)();
34
+ const meta = await store.get(args.domain);
35
+ if (!meta) {
36
+ return `No credential found for "${args.domain}".`;
37
+ }
38
+ return JSON.stringify(meta, null, 2);
39
+ }
40
+ catch (err) {
41
+ /* v8 ignore next -- defensive: store.get wraps errors @preserve */
42
+ return `Credential error: ${err instanceof Error ? err.message : String(err)}`;
43
+ }
44
+ },
45
+ summaryKeys: ["domain"],
46
+ },
47
+ {
48
+ tool: {
49
+ type: "function",
50
+ function: {
51
+ name: "credential_store",
52
+ description: "Store credentials the agent acquired (e.g. during sign-up). The password is accepted in args because the model generated it. Stored passwords are never returned later — only metadata is visible.",
53
+ parameters: {
54
+ type: "object",
55
+ properties: {
56
+ domain: {
57
+ type: "string",
58
+ description: "Domain these credentials are for (e.g. 'airbnb.com')",
59
+ },
60
+ username: {
61
+ type: "string",
62
+ description: "Username or email for the account",
63
+ },
64
+ password: {
65
+ type: "string",
66
+ description: "Password for the account",
67
+ },
68
+ notes: {
69
+ type: "string",
70
+ description: "Optional notes about this credential",
71
+ },
72
+ },
73
+ required: ["domain", "username", "password"],
74
+ },
75
+ },
76
+ },
77
+ handler: async (args) => {
78
+ (0, runtime_1.emitNervesEvent)({
79
+ component: "repertoire",
80
+ event: "repertoire.credential_tool_call",
81
+ message: "credential_store invoked",
82
+ meta: { tool: "credential_store", domain: args.domain },
83
+ });
84
+ try {
85
+ const store = (0, credential_access_1.getCredentialStore)();
86
+ await store.store(args.domain, {
87
+ username: args.username,
88
+ password: args.password,
89
+ notes: args.notes,
90
+ });
91
+ return `Credentials stored for "${args.domain}".`;
92
+ }
93
+ catch (err) {
94
+ /* v8 ignore next -- defensive: store.store wraps errors @preserve */
95
+ return `Credential store error: ${err instanceof Error ? err.message : String(err)}`;
96
+ }
97
+ },
98
+ confirmationRequired: true,
99
+ summaryKeys: ["domain"],
100
+ },
101
+ {
102
+ tool: {
103
+ type: "function",
104
+ function: {
105
+ name: "credential_list",
106
+ description: "List stored credential domains. Returns metadata only (domain, username, notes, creation date). Never returns passwords.",
107
+ parameters: {
108
+ type: "object",
109
+ properties: {
110
+ search: {
111
+ type: "string",
112
+ description: "Optional search filter to match against domain names",
113
+ },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ handler: async (args) => {
119
+ (0, runtime_1.emitNervesEvent)({
120
+ component: "repertoire",
121
+ event: "repertoire.credential_tool_call",
122
+ message: "credential_list invoked",
123
+ meta: { tool: "credential_list" },
124
+ });
125
+ try {
126
+ const store = (0, credential_access_1.getCredentialStore)();
127
+ let items = await store.list();
128
+ // Client-side search filter
129
+ if (args.search) {
130
+ const term = args.search.toLowerCase();
131
+ items = items.filter((item) => item.domain.toLowerCase().includes(term) ||
132
+ (item.username && item.username.toLowerCase().includes(term)));
133
+ }
134
+ return JSON.stringify(items, null, 2);
135
+ }
136
+ catch (err) {
137
+ /* v8 ignore next -- defensive: store.list wraps errors @preserve */
138
+ return `Credential list error: ${err instanceof Error ? err.message : String(err)}`;
139
+ }
140
+ },
141
+ summaryKeys: ["search"],
142
+ },
143
+ {
144
+ tool: {
145
+ type: "function",
146
+ function: {
147
+ name: "credential_delete",
148
+ description: "Delete stored credentials for a domain.",
149
+ parameters: {
150
+ type: "object",
151
+ properties: {
152
+ domain: {
153
+ type: "string",
154
+ description: "Domain whose credentials should be deleted",
155
+ },
156
+ },
157
+ required: ["domain"],
158
+ },
159
+ },
160
+ },
161
+ handler: async (args) => {
162
+ (0, runtime_1.emitNervesEvent)({
163
+ component: "repertoire",
164
+ event: "repertoire.credential_tool_call",
165
+ message: "credential_delete invoked",
166
+ meta: { tool: "credential_delete", domain: args.domain },
167
+ });
168
+ try {
169
+ const store = (0, credential_access_1.getCredentialStore)();
170
+ const deleted = await store.delete(args.domain);
171
+ if (deleted) {
172
+ return `Credentials for "${args.domain}" deleted.`;
173
+ }
174
+ return `No credential found for "${args.domain}".`;
175
+ }
176
+ catch (err) {
177
+ /* v8 ignore next -- defensive: store.delete wraps errors @preserve */
178
+ return `Credential delete error: ${err instanceof Error ? err.message : String(err)}`;
179
+ }
180
+ },
181
+ confirmationRequired: true,
182
+ summaryKeys: ["domain"],
183
+ },
184
+ ];
@@ -9,7 +9,6 @@
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.TRAVEL_ADVISORY_RSS_URL = void 0;
12
- exports.setWeatherVaultConfig = setWeatherVaultConfig;
13
12
  exports.getWeather = getWeather;
14
13
  exports.getWeatherByCity = getWeatherByCity;
15
14
  exports.parseAdvisoryLevel = parseAdvisoryLevel;
@@ -20,22 +19,13 @@ exports.geocode = geocode;
20
19
  exports.reverseGeocode = reverseGeocode;
21
20
  exports.searchPOI = searchPOI;
22
21
  const runtime_1 = require("../nerves/runtime");
23
- const bitwarden_client_1 = require("./bitwarden-client");
22
+ const credential_access_1 = require("./credential-access");
24
23
  // --- OpenWeatherMap ---
25
- /**
26
- * Vault item ID and field name for OpenWeatherMap API key.
27
- * Override via setWeatherVaultConfig() for custom vault layouts.
28
- */
29
- let weatherVaultItemId = "openweathermap-api";
30
- let weatherVaultField = "apiKey";
31
- function setWeatherVaultConfig(itemId, field) {
32
- weatherVaultItemId = itemId;
33
- if (field)
34
- weatherVaultField = field;
35
- }
24
+ /** Domain used for weather API key credential retrieval. */
25
+ const WEATHER_CREDENTIAL_DOMAIN = "api.openweathermap.org";
36
26
  async function getWeatherApiKey() {
37
- const client = (0, bitwarden_client_1.getBitwardenClient)();
38
- return client.getRawSecret(weatherVaultItemId, weatherVaultField);
27
+ const store = (0, credential_access_1.getCredentialStore)();
28
+ return store.getRawSecret(WEATHER_CREDENTIAL_DOMAIN, "password");
39
29
  }
40
30
  async function getWeather(lat, lon) {
41
31
  return withNervesEvents("getWeather", { lat, lon }, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.227",
3
+ "version": "0.1.0-alpha.228",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -44,7 +44,7 @@ Follow this pattern for every page interaction:
44
44
  3. Use `browser_type` for username/password fields
45
45
  4. Use `browser_click` to submit
46
46
  5. Wait for redirect, then verify login succeeded via snapshot
47
- 6. NEVER hardcode credentials -- use `vault_get` to retrieve login info
47
+ 6. NEVER hardcode credentials -- use `credential_get` to retrieve login info
48
48
 
49
49
  ### Search Forms (Hotels, Flights, Rentals)
50
50
  1. Navigate to the search page
@@ -58,7 +58,7 @@ Follow this pattern for every page interaction:
58
58
  ### Booking Forms
59
59
  1. **ALWAYS confirm with the user before proceeding to payment**
60
60
  2. Fill traveler information
61
- 3. Use vault credentials for payment (via `vault_get` -- NEVER type raw card numbers)
61
+ 3. Use stored credentials for payment (via `credential_get` -- NEVER type raw card numbers)
62
62
  4. Screenshot the final review page for user confirmation
63
63
  5. Only click "Book" / "Confirm" after explicit user approval
64
64
 
@@ -98,7 +98,7 @@ The stealth configuration handles most fingerprinting automatically. Additionall
98
98
  - **CAPTCHAs**: Stop and ask the user to solve manually.
99
99
  - **Stale elements**: Re-snapshot the page and retry the interaction.
100
100
  - **Blocked/403**: The site may have detected automation. Wait 30 seconds and try with a different approach (e.g., direct URL instead of navigation).
101
- - **Session expired**: Re-login using stored vault credentials.
101
+ - **Session expired**: Re-login using stored stored credentials.
102
102
 
103
103
  ## Human Confirmation Gates
104
104
 
@@ -71,10 +71,25 @@ Present top 3-5 options comparing:
71
71
  - **Never proceed with a financial commitment without explicit approval**
72
72
 
73
73
  ### Credential Handling
74
- - Use `vault_get` to retrieve payment credentials (never expose raw values)
75
- - Use `vault_get` for loyalty program numbers
76
- - Use browser-navigation skill form patterns for entering credentials
77
- - The credential gateway ensures secrets never enter model context
74
+
75
+ Credentials are managed through the credential access layer, which stores
76
+ agent-owned secrets encrypted in the bundle vault. Raw passwords never
77
+ enter model context.
78
+
79
+ - Use `credential_get` to check what credentials exist for a domain (metadata only, never passwords)
80
+ - Use `credential_store` to save credentials the agent acquired (e.g., during sign-up for a service)
81
+ - The credential gateway automatically injects secrets into API requests via `getRawSecret()`
82
+ - Use browser-navigation skill form patterns for entering credentials during interactive sessions
83
+
84
+ **How credentials work:**
85
+ - Agent-owned credentials are AES-256-GCM encrypted in `~/AgentBundles/<agent>.ouro/vault/`
86
+ - The master key lives at `~/.agentsecrets/<agent>/vault.key` (auto-generated on first use)
87
+ - The agent can sign up for services and store its own credentials
88
+ - Stored passwords are never returned to the model — only metadata (domain, username, notes)
89
+
90
+ **Upgrade path:** Install `aac` (Bitwarden Agent Access CLI) for vault-backed credentials.
91
+ When `aac` is available, the harness can read credentials from Bitwarden directly,
92
+ enabling access to human-managed vault items without storing them locally.
78
93
 
79
94
  ### Post-Booking
80
95
  - Save confirmation details (confirmation number, dates, hotel name, airline, booking reference)
@@ -105,35 +120,15 @@ Track and reference these travel preferences:
105
120
  - `weather_lookup` - Current weather by city or coordinates
106
121
  - `travel_advisory` - US State Dept advisory by country code
107
122
  - `geocode_search` - Location/POI search with coordinates
108
- - `vault_get` - Retrieve credentials (payment, loyalty)
109
- - `vault_store` - Store new credentials (family trust required)
123
+ - `credential_get` - Check credential metadata for a domain (never returns passwords)
124
+ - `credential_store` - Store credentials the agent acquired (family trust, confirmation required)
125
+ - `credential_list` - List stored credential domains
126
+ - `credential_delete` - Delete stored credentials (family trust, confirmation required)
110
127
 
111
128
  ### MCP Tools (when configured)
112
129
  - Browser tools via `@playwright/mcp` - see `browser-navigation` skill
113
130
  - Duffel flight search (when MCP server available)
114
131
  - Expedia hotel search (when MCP server available)
115
- - Bitwarden vault management via `@bitwarden/mcp-server` (agent-facing interactive vault access)
116
-
117
- ### Bitwarden MCP Server
118
-
119
- For agent-facing vault interaction (browsing items, searching, reading fields), configure
120
- `@bitwarden/mcp-server` as an MCP server in agent.json:
121
-
122
- ```json
123
- {
124
- "mcpServers": {
125
- "bitwarden": {
126
- "command": "npx",
127
- "args": ["@bitwarden/mcp-server"]
128
- }
129
- }
130
- }
131
- ```
132
-
133
- This is distinct from the built-in `bw` CLI-based credential gateway, which handles
134
- harness-internal secret injection (e.g., API keys injected into tool requests via
135
- `getRawSecret()`). The MCP server is for the agent to interactively browse and manage
136
- vault items on behalf of the user.
137
132
 
138
133
  ### Human Confirmation Required For
139
134
  - Any booking or payment
@@ -1,290 +0,0 @@
1
- "use strict";
2
- /**
3
- * Bitwarden vault client for credential management.
4
- *
5
- * SDK-first approach: attempts dynamic import of @bitwarden/sdk-napi,
6
- * falls back to `bw` CLI with --session flag.
7
- *
8
- * Raw secrets are NEVER returned to callers except via getRawSecret(),
9
- * which is reserved for internal use by the credential gateway (apiRequest vaultKey).
10
- */
11
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
- if (k2 === undefined) k2 = k;
13
- var desc = Object.getOwnPropertyDescriptor(m, k);
14
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
- desc = { enumerable: true, get: function() { return m[k]; } };
16
- }
17
- Object.defineProperty(o, k2, desc);
18
- }) : (function(o, m, k, k2) {
19
- if (k2 === undefined) k2 = k;
20
- o[k2] = m[k];
21
- }));
22
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
- Object.defineProperty(o, "default", { enumerable: true, value: v });
24
- }) : function(o, v) {
25
- o["default"] = v;
26
- });
27
- var __importStar = (this && this.__importStar) || (function () {
28
- var ownKeys = function(o) {
29
- ownKeys = Object.getOwnPropertyNames || function (o) {
30
- var ar = [];
31
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
- return ar;
33
- };
34
- return ownKeys(o);
35
- };
36
- return function (mod) {
37
- if (mod && mod.__esModule) return mod;
38
- var result = {};
39
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
- __setModuleDefault(result, mod);
41
- return result;
42
- };
43
- })();
44
- Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.BitwardenClient = void 0;
46
- exports.getBitwardenClient = getBitwardenClient;
47
- exports.resetBitwardenClient = resetBitwardenClient;
48
- const child_process_1 = require("child_process");
49
- const runtime_1 = require("../nerves/runtime");
50
- class BitwardenClient {
51
- sessionKey = null;
52
- connected = false;
53
- clientMode = null;
54
- async connect(config) {
55
- (0, runtime_1.emitNervesEvent)({
56
- event: "client.vault_connect_start",
57
- component: "clients",
58
- message: "connecting to Bitwarden vault",
59
- meta: { mode: config.mode ?? "auto" },
60
- });
61
- try {
62
- if (config.mode === "cli") {
63
- await this.connectCli(config);
64
- }
65
- else {
66
- // Try SDK first, fall back to CLI
67
- try {
68
- await this.connectSdk(config);
69
- }
70
- catch {
71
- (0, runtime_1.emitNervesEvent)({
72
- event: "client.vault_sdk_fallback",
73
- component: "clients",
74
- message: "SDK not available, falling back to bw CLI",
75
- meta: {},
76
- });
77
- await this.connectCli(config);
78
- }
79
- }
80
- (0, runtime_1.emitNervesEvent)({
81
- event: "client.vault_connect_end",
82
- component: "clients",
83
- message: "connected to Bitwarden vault",
84
- meta: { mode: this.clientMode },
85
- });
86
- }
87
- catch (err) {
88
- (0, runtime_1.emitNervesEvent)({
89
- level: "error",
90
- event: "client.vault_connect_error",
91
- component: "clients",
92
- message: "failed to connect to Bitwarden vault",
93
- /* v8 ignore next -- defensive: execCli always wraps in Error @preserve */
94
- meta: { reason: err instanceof Error ? err.message : String(err) },
95
- });
96
- throw err;
97
- }
98
- }
99
- async disconnect() {
100
- this.sessionKey = null;
101
- this.connected = false;
102
- this.clientMode = null;
103
- (0, runtime_1.emitNervesEvent)({
104
- event: "client.vault_disconnect",
105
- component: "clients",
106
- message: "disconnected from Bitwarden vault",
107
- meta: {},
108
- });
109
- }
110
- isConnected() {
111
- return this.connected;
112
- }
113
- async getItem(id) {
114
- this.requireConnected();
115
- return this.withNervesEvents("getItem", { id }, async () => {
116
- const raw = await this.fetchRawItem(id);
117
- return this.stripSecrets(raw);
118
- });
119
- }
120
- async createItem(name, fields) {
121
- this.requireConnected();
122
- return this.withNervesEvents("createItem", { name }, async () => {
123
- const itemFields = Object.entries(fields).map(([k, v]) => ({
124
- name: k,
125
- value: v,
126
- type: 0, // text type
127
- }));
128
- const itemTemplate = {
129
- type: 1, // login type
130
- name,
131
- fields: itemFields,
132
- login: {},
133
- };
134
- const encoded = Buffer.from(JSON.stringify(itemTemplate)).toString("base64");
135
- const stdout = await this.execCli(["create", "item", encoded]);
136
- const parsed = this.parseCliResponse(stdout);
137
- return { id: parsed.id, name: parsed.name, type: parsed.type };
138
- });
139
- }
140
- async listItems(search) {
141
- this.requireConnected();
142
- return this.withNervesEvents("listItems", { search }, async () => {
143
- const args = ["list", "items"];
144
- if (search) {
145
- args.push("--search", search);
146
- }
147
- const stdout = await this.execCli(args);
148
- const parsed = this.parseCliResponse(stdout);
149
- const items = Array.isArray(parsed.data) ? parsed.data : (Array.isArray(parsed) ? parsed : []);
150
- return items.map((item) => ({ id: item.id, name: item.name, type: item.type }));
151
- });
152
- }
153
- async deleteItem(id) {
154
- this.requireConnected();
155
- return this.withNervesEvents("deleteItem", { id }, async () => {
156
- await this.execCli(["delete", "item", id]);
157
- });
158
- }
159
- /**
160
- * Internal: used by credential gateway (PR 5) to get raw secret for HTTP injection.
161
- * NOT exposed via tools -- only called by apiRequest() internally.
162
- */
163
- async getRawSecret(itemId, fieldName) {
164
- this.requireConnected();
165
- return this.withNervesEvents("getRawSecret", { itemId, fieldName }, async () => {
166
- const raw = await this.fetchRawItem(itemId);
167
- // Check custom fields first
168
- if (raw.fields) {
169
- const field = raw.fields.find((f) => f.name === fieldName);
170
- if (field)
171
- return field.value;
172
- }
173
- // Check login.password
174
- if (fieldName === "password" && raw.login?.password) {
175
- return raw.login.password;
176
- }
177
- throw new Error(`field "${fieldName}" not found on vault item "${itemId}"`);
178
- });
179
- }
180
- // --- Private methods ---
181
- requireConnected() {
182
- if (!this.connected) {
183
- throw new Error("vault not connected -- call connect() first");
184
- }
185
- }
186
- async withNervesEvents(operation, meta, fn) {
187
- (0, runtime_1.emitNervesEvent)({
188
- event: "client.request_start",
189
- component: "clients",
190
- message: `vault ${operation} starting`,
191
- meta: { operation, ...meta },
192
- });
193
- try {
194
- const result = await fn();
195
- (0, runtime_1.emitNervesEvent)({
196
- event: "client.request_end",
197
- component: "clients",
198
- message: `vault ${operation} complete`,
199
- meta: { operation, ...meta },
200
- });
201
- return result;
202
- }
203
- catch (err) {
204
- (0, runtime_1.emitNervesEvent)({
205
- level: "error",
206
- event: "client.error",
207
- component: "clients",
208
- message: `vault ${operation} failed`,
209
- /* v8 ignore next -- defensive: callers always throw Error instances @preserve */
210
- meta: { operation, reason: err instanceof Error ? err.message : String(err), ...meta },
211
- });
212
- throw err;
213
- }
214
- }
215
- async connectSdk(_config) {
216
- // Attempt dynamic import of SDK
217
- // This will throw if the package is not installed
218
- const sdk = await Promise.resolve(`${"@bitwarden/sdk-napi"}`).then(s => __importStar(require(s)));
219
- /* v8 ignore start -- SDK not yet published; path tested when package becomes available @preserve */
220
- const client = new sdk.BitwardenClient({ apiUrl: _config.serverUrl ?? "https://api.bitwarden.com" }, 0);
221
- await client.auth().loginAccessToken(_config.accessToken, "");
222
- this.connected = true;
223
- this.clientMode = "sdk";
224
- /* v8 ignore stop */
225
- }
226
- async connectCli(config) {
227
- this.sessionKey = config.accessToken;
228
- // Verify CLI is available and session key works
229
- await this.execCli(["status"]);
230
- this.connected = true;
231
- this.clientMode = "cli";
232
- }
233
- async fetchRawItem(id) {
234
- const stdout = await this.execCli(["get", "item", id]);
235
- const parsed = this.parseCliResponse(stdout);
236
- return parsed;
237
- }
238
- stripSecrets(raw) {
239
- const item = {
240
- id: raw.id,
241
- name: raw.name,
242
- type: raw.type,
243
- };
244
- if (raw.notes)
245
- item.notes = raw.notes;
246
- if (raw.fields)
247
- item.fields = raw.fields;
248
- if (raw.login) {
249
- item.login = {};
250
- if (raw.login.username)
251
- item.login.username = raw.login.username;
252
- if (raw.login.uris)
253
- item.login.uris = raw.login.uris;
254
- // password intentionally NOT copied
255
- }
256
- return item;
257
- }
258
- parseCliResponse(stdout) {
259
- const parsed = JSON.parse(stdout);
260
- // bw CLI with --response wraps in { success, data }
261
- if (parsed.success !== undefined && parsed.data !== undefined) {
262
- return parsed.data;
263
- }
264
- return parsed;
265
- }
266
- execCli(args) {
267
- return new Promise((resolve, reject) => {
268
- const fullArgs = [...args, "--session", this.sessionKey, "--response"];
269
- (0, child_process_1.execFile)("bw", fullArgs, { timeout: 30_000 }, (err, stdout, _stderr) => {
270
- if (err) {
271
- reject(new Error(`bw CLI error: ${err.message}`));
272
- return;
273
- }
274
- resolve(stdout);
275
- });
276
- });
277
- }
278
- }
279
- exports.BitwardenClient = BitwardenClient;
280
- // Singleton accessor
281
- let _client = null;
282
- function getBitwardenClient() {
283
- if (!_client) {
284
- _client = new BitwardenClient();
285
- }
286
- return _client;
287
- }
288
- function resetBitwardenClient() {
289
- _client = null;
290
- }