mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 20:33:50 +00:00
5 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
6f26e23e2e
|
Import from any password manager, and let a person say which (#207)
Two gaps. Format was decided inline in the import command by whether the text started with a brace, and --source only ever reached the CSV reader -- so a JSON or archive export could not be forced at all. If the sniff was wrong there was no way to say "this came from 1Password". And 1Password's own export button produces a .1pux, which nothing here could open. A source now names a product rather than a file format. Bitwarden exports JSON and CSV; 1Password exports .1pux and CSV. Saying --source onepassword says where the file came from, and the container is still decided by looking at the bytes. One router owns that decision instead of the command, and when nothing can read a file it prints every source that can be named rather than the bare "pass --source" it used to. 1Password's .1pux is a ZIP holding a single JSON document. Pulling in a zip library for that would have been this package's only dependency beyond commander, so the central directory is read here: node's zlib already does the decompression, and the container is a few dozen lines of offsets. Deliberately not a general ZIP implementation -- no encryption, no ZIP64, only the two compression methods an export uses. The .1pux reader keeps vaults as folders, TOTP secrets, custom sections, password history and the whole of a card. 1Password item ids are 26-character base32 rather than UUIDs, so they are hashed into a v5-shaped UUID: the same export imported twice produces the same ids, which is what makes a re-import report its items as already present instead of duplicating the vault. Trashed items are left behind and reported, since restoring deleted entries into a fresh vault would be a surprise. A category we do not model -- a passport, a server, a licence -- becomes a note carrying its fields, so an import never quietly loses one. Six more CSV products join the existing five: NordPass, Dashlane, Proton Pass, RoboForm, Apple Passwords and Firefox. Adding Apple broke 1Password: their columns are nearly identical and only 1Password's `type` separates them, so Apple now requires its absence and 1Password is asked first. There is a test for that pair, because the failure mode is silent -- every 1Password CSV had started importing as Apple. LastPass and KeePass needed nothing: LastPass only ever exports CSV, which was already read, and the same is true of KeePass's CSV. Verified end to end: the real 4,395-item Bitwarden export still reads, a .1pux round-trips through the CLI, an unidentifiable CSV prints the source list and then imports once told, and an unknown source name is refused by name. The zip reader is tested against archives the system zip produced rather than ones we wrote. 195 tests pass. The 1Password mapping is built from the documented 1PUX schema and driven by fixtures, not from a real 1Password export. Run it with --dry-run first. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
b873c2bf41
|
Keep what the Bitwarden export actually says (#206)
Four things the first pass threw away, all of them recorded in the mapping notes from the 2026-09-02 hand conversion and all of them silent. Item ids were regenerated. Bitwarden ids are already UUIDs, so keeping them is what makes a re-import idempotent: run the same export twice and the second run reports its items as already present under "skip", rather than duplicating the entire vault. createItem deliberately refuses a caller-supplied id and always mints a fresh one, so the id is adopted after construction, and only when it really is a UUID. Timestamps were restamped to "now". creationDate and revisionDate are the only record of when a password was last rotated, and overwriting them destroys that permanently. The oldest item in a real export dates to 2018; every one of them would have been dated today. Password history was dropped entirely. It is carried now, newest first, mapping lastUsedDate to changedAt and capped at MAX_HISTORY_ENTRIES. 189 items in a real export have history, so this was not a rare case. URI match rules were hardcoded to "domain". Bitwarden stores them as a number -- 0 domain, 1 host, 2 startsWith, 3 exact, 4 regex, 5 never, with null meaning domain -- and flattening them quietly widens a login pinned to an exact URL, which is a security change rather than a cosmetic one. Verified on the same 4,395-item export: all 4,395 ids carried across, stable across two runs, 189 items with history recovered, and the oldest createdAt still 2018-02-22. That export happens to use domain matching throughout, so the match mapping is covered by tests rather than by it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
1f9c25fcd2
|
Read a Bitwarden JSON export natively (#205)
Importing a Bitwarden export failed with "Not an OpenCreds database" and had to
be converted by hand first. The import command decided what a file was by
`text.trimStart().startsWith("{")`, and treated every JSON as an OpenCreds
database. A Bitwarden export starts with a brace too, so it went to parseDatabase
and was rejected there.
JSON is the format Bitwarden's own UI hands you by default, and the only one of
its formats that keeps folders, custom fields, multiple URIs per login and full
card/identity detail. Its CSV drops all of that, so "export as CSV instead" is a
lossy workaround rather than an answer.
The shape is now sniffed before deciding which reader owns the file: an `items`
array plus either a `folders` array or the `encrypted` flag. An OpenCreds
database has neither at its top level, so the two never collide, and a cheap
substring test means a large database is not parsed twice to find that out.
Everything the format carries is mapped: all four item types, Bitwarden's own
folder ids (so two folders sharing a name stay distinct), custom fields with
their hidden flag, every URI rather than only the first, TOTP secrets, and
favourites. An untitled login is still named after its host. A folderId naming no
folder is dropped rather than inventing a folder, and only folders something
actually landed in come back, so importing one item out of a big export does not
create sixteen empty folders beside it.
An encrypted export is refused outright instead of half-read. Its items are
opaque strings, so a best-effort parse would store ciphertext as if it were a
password and leave a vault full of junk that never decrypts.
Verified against a real 4,395-item export: 4,387 logins, 3 cards, 4 identities,
1 note, 16 folders, zero skipped, parsed in 56ms. Every count matches the source
file, and the command that used to fail now reports "(Bitwarden JSON)".
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
3d155af970
|
vault + teams: filter secrets by category, export to CSV, simpler help (#195)
Every secret now has a category derived from its name (db, social, server,
api, cloud, finance, crypto, ai, email, messaging, storage, dns, analytics,
devtools, auth, config, other). One rule table in @logicsrc/opencreds serves
both vaults; services win over generic words, so STRIPE_WEBHOOK_SECRET is
finance, not auth. Checked against the 1,208 distinct key names in the
profullstack team: 74 fall to "other".
Team vaults (where the shared .env secrets live):
- teams categories [team] the filter words, with per-category counts
- teams secrets <team> [project] [env] --category/-c --search/-s
names + categories, never decrypts; --format csv
- teams export <team> [project] [env] --category -o file.csv [--yes]
decrypts into team,project,env,category,key,
value,updated_at (0600); skips vaults without a
grant and names them
Personal vault (OpenCreds):
- vault list --category, and the category column in list output
- vault export --format csv: one flat row per item, keeps key/account
secrets that a Bitwarden CSV drops; --category on every export format
DX:
- examples in `logicsrc vault help` / -h / --help that start by saying which
of the two vaults you want, plus examples on teams and each subcommand
- password prompts go to stderr, so eval "$(logicsrc vault unlock)" works
- hints name the command you actually ran (logicsrc vault init, not
opencreds init)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
|||
|
80a36269bb
|
Add the LogicSRC OpenCreds specification (#140)
* Add the LogicSRC OpenCreds specification Leaving a password manager means writing every secret you own to disk in the clear, and losing whatever the spreadsheet had no column for. A CSV is plaintext by construction, lossy by omission, and carries no integrity: nothing in it says which rows were meant to be there, so a truncated import looks exactly like a complete one. The same gap showed up inside LogicSRC. `logicsrc credentials` moves .env secrets and SSH keys through end-to-end-encrypted team vaults, but it can only model a key/value pair. A card, a passport, a login with a TOTP seed, or an OAuth account with a refresh token are all things people already keep in a vault, and none of them are a key/value pair. OpenCreds defines three things: the item, the vault, and the database. - Six item types (login, card, identity, note, key, account) as one record with a type and a named field group, so everything the user typed lives in a single encrypted blob. Codes 1-4 match MarkSyncr's deployed vault and are not renumbered; compatibility is cheaper than elegance. - AES-256-GCM over that record with the item id bound as AAD. Without it, anyone with storage write access could move a low-value login's ciphertext into a high-value row and watch what the user does next. - A key hierarchy where the user key is random, not derived, so a password change re-wraps 32 bytes rather than re-encrypting a vault. The auth hash comes out of a different HKDF label than the wrap key, which is what lets it reach a server at all. - A portable .opencreds file, encrypted by default, whose header is the AAD over the payload -- so the manifest is authenticated by the same tag as the data and a truncated import fails rather than reporting success. The plaintext form exists because people move to products that read nothing else; it is opt-in, confirmed, 0600, and labelled "protected": false in its own header. Namespaces are carried as data, not fixed by the spec: labels are compiled into every ciphertext a vault has written, so editing one does not migrate a vault, it makes it undecryptable. MarkSyncr's deployed vault is conformant by declaring `marksyncr`. Ships: prd/0004, nine spec pages under docs/opencreds/, six JSON Schemas, the @logicsrc/opencreds reference implementation with CSV importers for five products, `logicsrc vault` and the standalone `opencreds` binary, and the spec page at logicsrc.com/opencreds. `vault` rather than `creds` because `creds` is already an alias of `logicsrc credentials`, and the two are different: one moves a pair between providers, the other stores a record. @logicsrc/validators now registers every schema by $id before compiling, so the database schema can $ref the item and manifest schemas rather than restating them. 120 tests, including CLI end-to-end coverage of the masking rules, exit codes, and the manifest-mismatch path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRQrfuwuYKKV5UB9kLHuX5 * Make the OpenCreds conformance claim executable The conformance page described a fixture suite and an `opencreds conformance` command that did not exist. A specification that documents a conformance surface it cannot run is a specification nobody can hold to, including us. `opencreds conformance` now runs the requirement list as code -- one check per C-number, carrying its own id and level -- and emits the report shape the spec publishes. It exits 2 when a MUST does not pass, so it can gate CI directly. The reference implementation reports 29 passed, 0 failed, 1 skipped; the skip is C19, because key management for the team profile lives in @logicsrc/plugin-credential-sharing rather than in this package, and a skipped MAY does not affect conformance. Fixtures are generated (`--emit-fixtures <dir>`) rather than hand-written. A vector produced by an implementation and then verified by it is worth more than a JSON file someone typed: the typed file drifts silently when the format moves, and the generated one cannot. Fourteen files, including an invalid/ set every conforming reader must reject -- a wrong field group, a weak KDF, an unregistered namespace, a short payload and a tampered manifest. The CLI requirements stay with the end-to-end tests that drive the real binary through a child process; a command cannot meaningfully check its own exit codes, and a masked value that is only masked in the library is not masked. conformance.md and cli.md now describe what ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRQrfuwuYKKV5UB9kLHuX5 * Add @logicsrc/opencreds to the lockfile `npm ci` refuses a lockfile that does not match package.json, and the new workspace package plus the CLI's dependency on it were never recorded: the worktree was bootstrapped by hardlinking node_modules rather than installing, so npm was never asked to update the lock. Adds the workspace link and the package entry. No dependency versions move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRQrfuwuYKKV5UB9kLHuX5 * Register PRD 0004, and stop the fixtures looking like real secrets Two CI failures, both mine. `prd/README.md` is generated by `logicsrc prd index --write` and the scaffold test asserts it is current, so adding a PRD without regenerating it leaves the repo's own conformance check failing. Regenerated. The MCP test asserts the next free PRD id against the live prd/ directory — its comment says it advances with every PRD added — so it moves to 0005. ThreatCrush flagged three of the example strings: a PEM header in the item-model docs and in the conformance fixture, and an `sk_live_` prefixed token. All placeholders, none real, but the finding is the scanner working. A fixture only has to exercise the field, and a real-looking private key header or live-key prefix sitting in the tree trains both the scanner and the people reading its output to shrug at exactly the shape that matters. Replaced with obvious placeholders rather than suppressing the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRQrfuwuYKKV5UB9kLHuX5 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |