@arloraccoon/openclaw-vaultwarden-plugin 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.mjs +66 -19
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -59,6 +59,7 @@ cd "$REPO_DIR"
59
59
  git pull --ff-only
60
60
  pnpm install --frozen-lockfile
61
61
  pnpm build
62
+ pnpm test:package
62
63
  openclaw plugins install "$REPO_DIR" --link --force --accept-capabilities
63
64
  openclaw config validate
64
65
  openclaw plugins reload vaultwarden --accept-capabilities
@@ -324,3 +325,12 @@ SecretRef provider); OpenClaw's `plugins validate` authoring check is for
324
325
  `defineToolPlugin`-style tool-only packages. Use the installed runtime smoke
325
326
  test for this package instead of treating that tool-only check as a generic
326
327
  plugin validity gate.
328
+
329
+ ## Release synchronization
330
+
331
+ Forgejo is the canonical development repository; the public GitHub mirror
332
+ excludes `.beads/`, `.crew/`, and generated `dist/` files. npm and ClawHub must
333
+ carry the same semver version. Release tags run `publish-npm.yml` through npm
334
+ Trusted Publishing (OIDC); do not add registry tokens to GitHub secrets. Run
335
+ `RELEASE-CHECKLIST.md` before creating a tag and verify both registries after
336
+ publication.
package/dist/index.mjs CHANGED
@@ -186,25 +186,51 @@ function sessionRecoverySource(session, sessionEnv, inherited) {
186
186
  return inherited[sessionEnv] ? "env" : "default";
187
187
  }
188
188
  function recoveryInstruction(source) {
189
+ const statusCheck = "First verify the host with `bw status --raw`;";
189
190
  switch (source) {
190
191
  case "file":
191
- return "Refresh the file configured for the Vaultwarden SecretRef, then retry. For a single-value file provider, run `openclaw vaultwarden --session-file <configured-path>` in a trusted terminal; do not use that command on a JSON provider file.";
192
+ return `${statusCheck} Refresh the file configured for the Vaultwarden SecretRef, then retry. For a single-value file provider, run \`openclaw vaultwarden --session-file <configured-path>\` in a trusted terminal; do not use that command on a JSON provider file.`;
192
193
  case "env":
193
- return "Refresh the configured environment SecretRef in the Gateway's environment, then retry. `openclaw vaultwarden` writes a local file and does not replace an environment SecretRef.";
194
+ return `${statusCheck} Refresh the configured environment SecretRef in the Gateway's environment, then retry. \`openclaw vaultwarden\` writes a local file and does not replace an environment SecretRef.`;
194
195
  case "exec":
195
196
  case "store":
196
- return "Refresh the configured SecretRef at its provider, then retry. `openclaw vaultwarden` writes a local file and does not update that provider.";
197
+ return `${statusCheck} Refresh the configured SecretRef at its provider, then retry. \`openclaw vaultwarden\` writes a local file and does not update that provider.`;
197
198
  case "inline":
198
- return "Replace the configured session value with a freshly unlocked session using a secure local configuration path; do not paste it into chat. `openclaw vaultwarden` writes a local file but does not change inline configuration.";
199
+ return `${statusCheck} Replace the configured session value with a freshly unlocked session using a secure local configuration path; do not paste it into chat. \`openclaw vaultwarden\` writes a local file but does not change inline configuration.`;
199
200
  default:
200
- return "On the OpenClaw host, run `openclaw vaultwarden` in a trusted terminal to create the default local session file, then configure the plugin session as a file SecretRef to that file and retry. The CLI does not read the file automatically.";
201
+ return `${statusCheck} On the OpenClaw host, run \`openclaw vaultwarden\` in a trusted terminal to create the default local session file, then configure the plugin session as a file SecretRef to that file and retry. The CLI does not read the file automatically.`;
201
202
  }
202
203
  }
203
204
  var sessionFailurePattern = /(?:session|token|authentication|credential).{0,48}(?:expired|invalid|unauthori[sz]ed|rejected|not valid)|(?:expired|invalid|unauthori[sz]ed|rejected).{0,48}(?:session|token|credential)|not logged in|you are not logged in|vault is locked|vault must be unlocked|\b401\b/i;
205
+ function classifySessionFailure(error) {
206
+ const text = cliErrorText(error);
207
+ if (!sessionFailurePattern.test(text)) return void 0;
208
+ if (/expired|session.*(?:invalid|not valid)|(?:invalid|not valid).*session/i.test(text)) {
209
+ return "expired";
210
+ }
211
+ if (/vault (?:is )?locked|vault must be unlocked|locked/i.test(text)) return "locked";
212
+ if (/not logged in|unauthenticated|\b401\b|unauthori[sz]ed/i.test(text)) {
213
+ return "unauthenticated";
214
+ }
215
+ return "unavailable";
216
+ }
217
+ function sessionFailurePrefix(kind) {
218
+ switch (kind) {
219
+ case "locked":
220
+ return "Vaultwarden vault is locked";
221
+ case "unauthenticated":
222
+ return "Vaultwarden authentication is unavailable";
223
+ case "unavailable":
224
+ return "Vaultwarden session could not be used";
225
+ default:
226
+ return "Vaultwarden session is expired";
227
+ }
228
+ }
204
229
  function redactCliError(error) {
205
- if (sessionFailurePattern.test(cliErrorText(error))) {
230
+ const failureKind = classifySessionFailure(error);
231
+ if (failureKind) {
206
232
  const source = error && typeof error === "object" && "recoverySource" in error ? error.recoverySource : void 0;
207
- return `Vaultwarden session is unavailable or expired. ${recoveryInstruction(source ?? "default")} The plugin will not prompt for or expose the master password.`;
233
+ return `${sessionFailurePrefix(failureKind)}. ${recoveryInstruction(source ?? "default")} The plugin will not prompt for or expose the master password.`;
208
234
  }
209
235
  return "Vaultwarden CLI command failed";
210
236
  }
@@ -284,12 +310,13 @@ import { Type } from "typebox";
284
310
  import { textResult } from "openclaw/plugin-sdk/tool-results";
285
311
 
286
312
  // src/audit.ts
287
- function emitVaultwardenAudit(audit, operation, outcome) {
313
+ function emitVaultwardenAudit(audit, operation, outcome, itemId) {
288
314
  if (!audit) {
289
315
  return;
290
316
  }
291
317
  try {
292
- audit({ event: "vaultwarden.tool", operation, outcome });
318
+ const safeItemId = itemId && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(itemId) ? itemId : void 0;
319
+ audit({ event: "vaultwarden.tool", operation, outcome, ...safeItemId ? { itemId: safeItemId } : {} });
293
320
  } catch {
294
321
  }
295
322
  }
@@ -323,6 +350,22 @@ import { textResult as textResult2 } from "openclaw/plugin-sdk/tool-results";
323
350
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
324
351
  var MAX_ITEM_BYTES = 64 * 1024;
325
352
  var MAX_SECRET_BYTES = 24 * 1024;
353
+ var mutationLocks = /* @__PURE__ */ new Map();
354
+ async function withMutationLock(itemId, operation) {
355
+ const previous = mutationLocks.get(itemId) ?? Promise.resolve();
356
+ let release;
357
+ const current = new Promise((resolve2) => {
358
+ release = resolve2;
359
+ });
360
+ mutationLocks.set(itemId, current);
361
+ await previous;
362
+ try {
363
+ return await operation();
364
+ } finally {
365
+ release();
366
+ if (mutationLocks.get(itemId) === current) mutationLocks.delete(itemId);
367
+ }
368
+ }
326
369
  function checkedString(value, label, maxBytes = MAX_SECRET_BYTES) {
327
370
  if (typeof value !== "string" || !value.trim() || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value) || Buffer.byteLength(value, "utf8") > maxBytes) {
328
371
  throw new Error(`Invalid ${label}`);
@@ -444,15 +487,19 @@ async function createLoginVaultItem(params) {
444
487
  }
445
488
  async function updateLoginVaultItem(params) {
446
489
  const itemId = checkedId(params.itemId, "item ID");
447
- const current = await params.runner(["get", "item", itemId]);
448
- const item = mergeLoginVaultItem(JSON.parse(current.stdout), params.patch);
449
- const result = await params.runner(["edit", "item", itemId], encodeVaultItem(item));
450
- return parseMutationItem(result.stdout);
490
+ return withMutationLock(itemId, async () => {
491
+ const current = await params.runner(["get", "item", itemId]);
492
+ const item = mergeLoginVaultItem(JSON.parse(current.stdout), params.patch);
493
+ const result = await params.runner(["edit", "item", itemId], encodeVaultItem(item));
494
+ return parseMutationItem(result.stdout);
495
+ });
451
496
  }
452
497
  async function deleteLoginVaultItem(params) {
453
498
  const itemId = checkedId(params.itemId, "item ID");
454
- await params.runner(["delete", "item", itemId]);
455
- return { id: itemId, deleted: true };
499
+ return withMutationLock(itemId, async () => {
500
+ await params.runner(["delete", "item", itemId]);
501
+ return { id: itemId, deleted: true };
502
+ });
456
503
  }
457
504
  function stringValue(value) {
458
505
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -596,10 +643,10 @@ function createVaultwardenUpdateItemTool(runner, audit) {
596
643
  try {
597
644
  const { itemId, ...patch } = params;
598
645
  const result = await updateLoginVaultItem({ runner, itemId, patch });
599
- emitVaultwardenAudit(audit, "update_item", "success");
646
+ emitVaultwardenAudit(audit, "update_item", "success", itemId);
600
647
  return jsonToolResult(result);
601
648
  } catch (error) {
602
- emitVaultwardenAudit(audit, "update_item", "error");
649
+ emitVaultwardenAudit(audit, "update_item", "error", params.itemId);
603
650
  return errorToolResult(error);
604
651
  }
605
652
  }
@@ -621,10 +668,10 @@ function createVaultwardenDeleteItemTool(runner, audit) {
621
668
  try {
622
669
  if (params.confirmation !== params.itemId) throw new Error("Confirmation mismatch");
623
670
  const result = await deleteLoginVaultItem({ runner, itemId: params.itemId });
624
- emitVaultwardenAudit(audit, "delete_item", "success");
671
+ emitVaultwardenAudit(audit, "delete_item", "success", params.itemId);
625
672
  return jsonToolResult(result);
626
673
  } catch (error) {
627
- emitVaultwardenAudit(audit, "delete_item", "error");
674
+ emitVaultwardenAudit(audit, "delete_item", "error", params.itemId);
628
675
  return errorToolResult(error);
629
676
  }
630
677
  }
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@arloraccoon/openclaw-vaultwarden-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Metadata-safe Vaultwarden tools for OpenClaw with opt-in item mutations",
5
5
  "scripts": {
6
6
  "build": "esbuild index.ts --bundle --platform=node --format=esm --external:openclaw '--external:openclaw/*' --external:typebox --outfile=dist/index.mjs && esbuild src/secret-resolver-entry.ts --bundle --platform=node --format=esm --outfile=dist/secret-resolver.mjs",
7
7
  "test": "vitest run src/*.test.ts",
8
+ "test:package": "bash tests/package-smoke.sh",
8
9
  "typecheck": "tsc --noEmit",
9
10
  "lint": "eslint . --max-warnings 0"
10
11
  },
11
12
  "repository": {
12
13
  "type": "git",
13
- "url": "https://github.com/ArloTheRaccoon/openclaw-vaultwarden-plugin"
14
+ "url": "git+https://github.com/ArloTheRaccoon/openclaw-vaultwarden-plugin.git"
14
15
  },
16
+ "packageManager": "pnpm@10.33.2",
15
17
  "license": "MIT",
16
18
  "type": "module",
17
19
  "main": "./dist/index.mjs",