Files
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

199 lines
11 KiB
JavaScript

#!/usr/bin/env node
// Tests for Bitwarden Send payloads, parsing and command construction.
// Field names come from a real `bw send --fullObject` response.
//
// node tests/sends.test.js
const fs = require("fs")
const path = require("path")
const Model = {}
new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")
.replace(/^\.pragma library\s*$/m, "") + `
exports.buildSendPayload = buildSendPayload
exports.parseSends = parseSends
exports.sendExpiryLabel = sendExpiryLabel
exports.sendAccessLabel = sendAccessLabel
exports.createSendCommand = createSendCommand
exports.listSendsCommand = listSendsCommand
exports.deleteSendCommand = deleteSendCommand
exports.createItemCommand = createItemCommand
exports.editItemCommand = editItemCommand
exports.sessionEnvVar = sessionEnvVar
exports.statusCommand = statusCommand
exports.listCommand = listCommand
exports.syncCommand = syncCommand
exports.getItemCommand = getItemCommand
exports.getTotpCommand = getTotpCommand
exports.lockCommand = lockCommand
exports.deleteItemCommand = deleteItemCommand
exports.createFolderCommand = createFolderCommand
exports.listFoldersCommand = listFoldersCommand
exports.listOrganizationsCommand = listOrganizationsCommand
exports.terminalLoginCommand = terminalLoginCommand
exports.sessionHandoffReadCommand = sessionHandoffReadCommand
exports.extractSessionToken = extractSessionToken
`)(Model)
let pass = 0
const failures = []
const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`)
// --- the security property that motivated the env-based commands ------------
// argv is world-readable via /proc on a default Linux box, so no secret may
// ever appear in a command line.
const sendCmd = Model.createSendCommand("SESSIONTOKEN")[2]
check("send create reads its payload from the environment",
sendCmd.includes('"$QSBW_SEND"') && /\|\s*base64 -w0\s*\|/.test(sendCmd), sendCmd)
check("send create carries no payload in argv",
!sendCmd.includes("password") && !sendCmd.includes("text"), sendCmd)
const itemCmd = Model.createItemCommand({ organizationId: null }, "SESSIONTOKEN")[2]
check("item create reads its payload from the environment",
itemCmd.includes('"$QSBW_ITEM"'), itemCmd)
const editCmd = Model.editItemCommand("id-1", "SESSIONTOKEN")[2]
check("item edit reads its payload from the environment",
editCmd.includes('"$QSBW_ITEM"') && editCmd.includes("'id-1'"), editCmd)
// --- payload ---------------------------------------------------------------
const p = Model.buildSendPayload("Name", "secret", true, 3, 5, "pw", "note")
check("payload sets the text and its hidden flag",
p.text.text === "secret" && p.text.hidden === true, JSON.stringify(p.text))
check("deletionDate is days in the future, as an ISO string",
Math.abs(Date.parse(p.deletionDate) - (Date.now() + 3 * 86400000)) < 60000, p.deletionDate)
check("maxAccessCount of 0 means unlimited, sent as null",
Model.buildSendPayload("n", "t", false, 7, 0, "", "").maxAccessCount === null, "expected null")
check("an empty password is sent as null, not an empty string",
Model.buildSendPayload("n", "t", false, 7, 0, "", "").password === null, "expected null")
check("a blank name falls back rather than creating an unnamed Send",
Model.buildSendPayload(" ", "t", false, 7, 0, "", "").name === "Untitled Send",
Model.buildSendPayload(" ", "t", false, 7, 0, "", "").name)
for (const [days, lo, hi] of [[0, 1, 31], [999, 1, 31], [-5, 1, 31]]) {
const got = (Date.parse(Model.buildSendPayload("n", "t", false, days, 0, "", "").deletionDate) - Date.now()) / 86400000
check(`deleteInDays=${days} clamps into [${lo}, ${hi}]`, got > lo - 1 && got < hi + 1, `got ~${got.toFixed(1)} days`)
}
// --- parsing (shape taken from a real response) -----------------------------
const listed = Model.parseSends(JSON.stringify([
{ id: "b", name: "Later", type: 0, accessUrl: "u2", accessCount: 1, maxAccessCount: 3,
deletionDate: "2026-09-01T00:00:00Z", passwordSet: false, text: { text: "x", hidden: false } },
{ id: "a", name: "Sooner", type: 1, accessUrl: "u1", accessCount: 0, maxAccessCount: null,
deletionDate: "2026-08-22T00:00:00Z", passwordSet: true, file: { fileName: "f.pdf" } },
]))
check("sends are ordered by how soon they vanish",
listed.map(s => s.id).join(",") === "a,b", listed.map(s => s.id).join(","))
check("type 1 is recognised as a file Send",
listed[0].isFile === true && listed[0].fileName === "f.pdf", JSON.stringify(listed[0]))
check("passwordSet is carried through as a boolean, and no password is exposed",
listed[0].passwordSet === true && !("password" in listed[0]), JSON.stringify(listed[0]))
check("malformed JSON yields an empty list",
Model.parseSends("{{").length === 0 && Model.parseSends("").length === 0, "expected []")
// --- labels -----------------------------------------------------------------
const at = t => Date.parse(t)
const mk = d => ({ deletionDate: d })
check("a past deletion date reads as expired",
Model.sendExpiryLabel(mk("2026-08-20T00:00:00Z"), at("2026-08-21T00:00:00Z")) === "expired", "expected expired")
check("hours are used under a day",
Model.sendExpiryLabel(mk("2026-08-21T05:00:00Z"), at("2026-08-21T00:00:00Z")) === "in 5 hours", "expected 'in 5 hours'")
check("singular day is not pluralised",
Model.sendExpiryLabel(mk("2026-08-22T00:00:00Z"), at("2026-08-21T00:00:00Z")) === "in 1 day", "expected 'in 1 day'")
check("a missing deletion date yields no label",
Model.sendExpiryLabel({}, Date.now()) === "", "expected empty")
check("unlimited access omits the maximum",
Model.sendAccessLabel({ accessCount: 2, maxAccessCount: null }) === "2 views", "expected '2 views'")
check("a capped Send shows the maximum",
Model.sendAccessLabel({ accessCount: 2, maxAccessCount: 5 }) === "2 of 5 views", "expected '2 of 5 views'")
// --- no bw command may carry the session token in argv ----------------------
// /proc/<pid>/cmdline is world-readable on a default Linux install, and the
// token grants full access to the unlocked vault. It goes in BW_SESSION.
check("the session env var is BW_SESSION, which bw reads natively",
Model.sessionEnvVar() === "BW_SESSION", Model.sessionEnvVar())
const builders = [
["statusCommand", () => Model.statusCommand()],
["listCommand", () => Model.listCommand()],
["listFoldersCommand", () => Model.listFoldersCommand()],
["listOrganizationsCommand", () => Model.listOrganizationsCommand()],
["listSendsCommand", () => Model.listSendsCommand()],
["syncCommand", () => Model.syncCommand()],
["lockCommand", () => Model.lockCommand()],
["getItemCommand", () => Model.getItemCommand("id")],
["getTotpCommand", () => Model.getTotpCommand("id")],
["deleteItemCommand", () => Model.deleteItemCommand("id")],
["deleteSendCommand", () => Model.deleteSendCommand("id")],
["createFolderCommand", () => Model.createFolderCommand("f")],
["createItemCommand", () => Model.createItemCommand({ organizationId: null })],
["editItemCommand", () => Model.editItemCommand("id")],
["createSendCommand", () => Model.createSendCommand()],
]
for (const [name, build] of builders) {
const argv = build().join(" ")
check(`${name} passes no --session flag`, !argv.includes("--session"), argv)
}
// --- terminal login handoff -------------------------------------------------
const login = Model.terminalLoginCommand("login")[2]
const unlock = Model.terminalLoginCommand("unlock")[2]
const euLogin = Model.terminalLoginCommand("login", "https://vault.bitwarden.eu")[2]
check("terminal login runs in a terminal", login.includes("omarchy launch terminal"), login.slice(0, 80))
check("it falls back to a second terminal if the first is unavailable",
login.includes("alacritty"), login.slice(0, 80))
// --raw prints only the session key on stdout while prompts stay on stderr,
// which is what lets the key be captured without breaking the interactive login.
check("it captures the key with --raw", login.includes("--raw"), login.slice(0, 120))
// The panel already knows which state it is in, so the terminal does not spend
// a `bw status` round trip (~3.3s here) working it out before prompting.
check("login mode runs bw login", login.includes("bw login --raw") && !login.includes("bw unlock"), login.slice(0, 200))
check("unlock mode runs bw unlock", unlock.includes("bw unlock --raw") && !unlock.includes("bw login"), unlock.slice(0, 200))
check("neither mode probes with bw status",
!login.includes("bw status") && !unlock.includes("bw status"), "expected no status probe")
check("an unknown mode falls back to login",
Model.terminalLoginCommand("")[2].includes("bw login --raw"), "expected login")
check("terminal login configures an explicitly selected region before authenticating",
euLogin.includes("bw config server")
&& euLogin.includes("https://vault.bitwarden.eu")
&& euLogin.indexOf("bw config server") < euLogin.indexOf("bw login --raw"),
euLogin.slice(0, 300))
// Only the method name crosses the IPC boundary; the key never does.
check("a successful login reopens the panel",
login.includes("omarchy-shell io.github.elevate08.qs-bitwarden-cli open"), login.slice(0, 300))
check("the session key is not passed over IPC",
!login.includes("open $f") && !login.includes("open \"$f\""), login.slice(0, 300))
// The inner script appears twice -- once for the terminal, once for the
// alacritty fallback -- so count pauses against failure branches rather than
// assuming a single occurrence.
check("the user is only made to press a key when the login failed",
login.split("read -p").length === login.split("Not completed").length,
`${login.split("read -p").length - 1} pauses vs ${login.split("Not completed").length - 1} failure branches`)
check("a successful login closes the terminal on its own",
login.includes("Returning to the Bitwarden panel") && login.includes("sleep 1"),
"expected the success branch to close itself")
// The key is a secret at rest: tmpfs, user-only, gone when the session ends.
check("the handoff lives in XDG_RUNTIME_DIR, not on disk",
login.includes("XDG_RUNTIME_DIR"), login.slice(0, 120))
check("the handoff is created with a restrictive umask",
login.includes("umask 077") && login.includes("chmod 700"), login.slice(0, 200))
check("an incomplete login leaves nothing behind",
login.includes('rm -f'), login.slice(0, 300))
const read = Model.sessionHandoffReadCommand(true)[2]
check("the handoff is read once and removed with a byte limit",
read.includes("head -c") && read.includes("rm -f"), read)
check("an absent or empty handoff yields nothing",
read.includes("-s "), read)
// The panel parses whatever the file holds through the same extractor it uses
// for unlock output, so a stray newline or an `export BW_SESSION=` line is fine.
check("a bare key is extracted intact",
Model.extractSessionToken("abcdefghijklmnopqrstuvwxyz0123456789==") === "abcdefghijklmnopqrstuvwxyz0123456789==",
Model.extractSessionToken("abcdefghijklmnopqrstuvwxyz0123456789=="))
check("an export line is unwrapped",
Model.extractSessionToken('export BW_SESSION="tok3n-value-that-is-long-enough=="') === "tok3n-value-that-is-long-enough==",
Model.extractSessionToken('export BW_SESSION="tok3n-value-that-is-long-enough=="'))
console.log(`${pass} passed, ${failures.length} failed`)
if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) }