Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 372 additions & 0 deletions apps/logicsrc-web/src/app/opencreds/page.tsx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/logicsrc-web/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const STATIC_ROUTES: Array<{
{ path: "/", changeFrequency: "weekly", priority: 1.0 },
{ path: "/docs", changeFrequency: "weekly", priority: 0.9 },
{ path: "/openontology", changeFrequency: "weekly", priority: 0.9 },
{ path: "/opencreds", changeFrequency: "weekly", priority: 0.9 },
{ path: "/openprd", changeFrequency: "weekly", priority: 0.9 },
{ path: "/openontology/explore", changeFrequency: "daily", priority: 0.7 },
{ path: "/openspec", changeFrequency: "weekly", priority: 0.8 },
Expand Down
1 change: 1 addition & 0 deletions apps/logicsrc-web/src/components/site-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const NAV: Array<{ href: string; label: string; external?: boolean }> = [
{ href: "/agentbyte", label: "AgentByte" },
{ href: "/credential-sharing", label: "Credentials" },
{ href: "/openontology", label: "OpenOntology" },
{ href: "/opencreds", label: "OpenCreds" },
{ href: "/openprd", label: "OpenPRD" },
{ href: "/#cli", label: "CLI" },
{ href: "/docs", label: "Docs" },
Expand Down
1 change: 1 addition & 0 deletions apps/logicsrc-web/src/lib/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const DOCS_DIR = resolve(process.cwd(), "../../docs");
// Curated, public-facing reference docs. Internal notes (roadmap, positioning,
// arcade) are intentionally excluded.
export const DOC_SLUGS = [
"opencreds",
"openprd",
"openontology",
"openontology-governance",
Expand Down
129 changes: 129 additions & 0 deletions docs/opencreds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# OpenCreds

Status: 0.1 draft · reference implementation available (`@logicsrc/opencreds`)

Slug: `opencreds`

OpenCreds is a LogicSRC OpenSpec for **credential records and portable vaults**.
It defines what a credential item is, how a vault is encrypted, and what a vault
looks like as a file — so that moving a vault between two products is a
supported operation rather than a plaintext CSV export.

It exists because leaving a password manager currently means writing every
secret you own to disk in the clear, and losing whatever the spreadsheet had no
column for.

- Full specification: [`docs/opencreds/spec.md`](./opencreds/spec.md)
- Item model: [`docs/opencreds/item-model.md`](./opencreds/item-model.md)
- Cryptography: [`docs/opencreds/crypto.md`](./opencreds/crypto.md)
- Portable database: [`docs/opencreds/database.md`](./opencreds/database.md)
- Importing from other products: [`docs/opencreds/interop.md`](./opencreds/interop.md)
- CLI: [`docs/opencreds/cli.md`](./opencreds/cli.md)
- Conformance: [`docs/opencreds/conformance.md`](./opencreds/conformance.md)
- Security model: [`docs/opencreds/security.md`](./opencreds/security.md)
- FAQ: [`docs/opencreds/faq.md`](./opencreds/faq.md)

## What it defines

**One record, six types.** Logins, cards, identities, notes, keys and accounts
are not six features — they are one record with a `type` and a named field
group. Everything the user typed lives inside a single encrypted blob, which is
what makes password history free: it is an array in that blob, encrypted by
construction rather than needing its own protected table.

```json
{
"v": 1,
"id": "6f1e7b3a-1f4e-4f0f-9a1d-6a2f0b6f8d21",
"type": "login",
"name": "GitHub",
"folderId": null,
"notes": "",
"login": {
"username": "anthony",
"password": "…",
"totp": "otpauth://totp/GitHub:anthony?secret=…",
"uris": [{ "uri": "https://github.com", "match": "domain" }]
},
"history": [],
"createdAt": "2026-08-29T00:00:00.000Z",
"updatedAt": "2026-08-29T00:00:00.000Z"
}
```

**One envelope.** AES-256-GCM over that JSON, with the item id bound in as
additional authenticated data. A ciphertext moved from one row to another fails
to decrypt rather than quietly showing the wrong credential — without that,
anyone with database write access could swap a low-value login's ciphertext into
a high-value one and watch what the user does next.

**One key hierarchy.** The master password is stretched once by PBKDF2-SHA256
into a master key, and everything else is derived from it by HKDF with a
distinct label. The only password-derived value that ever reaches a server comes
out of a different label than the wrapping key, so holding it does not help
decrypt anything.

**One file.** A vault exports as a single `.opencreds` JSON document, encrypted
by default, carrying a manifest — item count, type histogram, digest over the
item ids — that is authenticated by the same tag as the data. A truncated
import fails instead of looking like a complete one.

## What it does not define

Sync. Storage. Autofill. A conforming vault is a file and a key; how two devices
reconcile, where the ciphertext lives, and how a browser fills a form are all
left to the implementation.

## Implementations

| Implementation | Profile | Namespace | Notes |
| --- | --- | --- | --- |
| `@logicsrc/opencreds` | `user`, `team` | `opencreds` | Reference implementation; local store and CLI |
| `logicsrc credentials` | `team` | `opencreds` | `.env` secrets and SSH keys as `key` items |
| `@marksyncr/vault` | `user` | `marksyncr` | Pre-dates the spec; conformant via its declared namespace |

MarkSyncr's vault shipped before OpenCreds and has domain-separation labels
baked into every ciphertext already written. Labels cannot be edited — changing
one makes every existing vault undecryptable — so the spec carries the label
prefix as a declared per-vault `namespace` rather than mandating a single
string. See [crypto.md](./opencreds/crypto.md#namespaces).

## Quick start

```bash
# Create a vault (asks for a master password; prints a recovery key once)
logicsrc vault init

# Add items
logicsrc vault add login --name GitHub --username anthony --url https://github.com
logicsrc vault add card --name "Visa ending 4242"
logicsrc vault add key --name "deploy key" --key-type ssh --file ~/.ssh/id_ed25519

# List (never prints secret values)
logicsrc vault list --type login

# Move the vault somewhere else, encrypted
logicsrc vault export --out vault.opencreds
logicsrc vault import vault.opencreds --dry-run

# Import from another product
logicsrc vault import bitwarden-export.csv --source bitwarden --dry-run
```

The same commands ship as the standalone `opencreds` binary, so
`logicsrc vault validate` and `opencreds validate` are the same contract.

## Relationship to the other LogicSRC specs

- **Credential Sharing** ([credential-sharing.md](./credential-sharing.md))
moves secrets *between providers* — `.env`, Doppler, Railway, GitHub, SSH. It
models a key/value pair and a sync plan. OpenCreds models the **record** and
the **vault file**. A `key` item is what a synced `.env` entry becomes when it
is stored rather than moved.
- **OpenContext** ([opencontext.md](./opencontext.md)) governs what an agent may
*read*. An agent that resolves a context bundle may be entitled to one
OpenCreds item and not the vault; the permission decision is OpenContext's,
the record shape is OpenCreds'.
- **OpenOntology** ([openontology.md](./openontology.md)) names the entities a
credential belongs to. An `account` item's `provider` is an ontology entity,
not a free string, where an ontology is in use.
155 changes: 155 additions & 0 deletions docs/opencreds/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# The OpenCreds CLI

The CLI is part of the conformance surface: flags, output shapes and exit codes
are specified, not incidental. The same commands ship twice — as
`logicsrc vault …` and as the standalone `opencreds` binary — from one
implementation, so the two can never drift.

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Success. |
| 1 | Usage error — unknown flag, missing argument, unreadable file. |
| 2 | Validation failure — a document did not conform. |
| 3 | Crypto failure — wrong password, failed tag, manifest mismatch. |
| 4 | Refused — the operation needs a confirmation that was not given. |

## Vault

```bash
opencreds init [--namespace opencreds] [--iterations 600000] [--password-stdin]
```

Creates a vault. Prompts for a master password twice, prints a recovery key
once, and never prints it again. Refuses if a vault already exists at the target
unless `--force`. `--password-stdin` reads one line instead and skips the
confirmation — for scripted provisioning, where there is nobody to mistype.

```bash
opencreds unlock # prints a session token to export
opencreds unlock --persist [--timeout 15]
opencreds lock # drops a persisted session
opencreds status # vault present? locked? counts by type
```

Unlocking has two shapes, and the difference is a flag rather than a default
because it is a real trade:

- **Token (default).** `unlock` prints `export OPENCREDS_SESSION="…"`. Nothing
touches disk and the session dies with the shell.
- **Persisted (`--persist`).** The same token in a 0600 file with an expiry, so
a script can unlock once and run many commands. A readable user key on disk is
the vault; the command says so when you use it, and `lock` removes it.

`status` is the one command that works locked. It reports counts and never
values, because counts are already observable to whoever holds the storage.

```bash
opencreds recover # unlock with the recovery key, set a new password
```

A password change re-wraps the user key. Not one item is re-encrypted, which is
why it is instant on a vault of any size.

## Items

```bash
opencreds add <type> --name <name> [type flags…]
opencreds list [--type <type>] [--folder <name>] [--search <text>] [--json]
opencreds get <id|name> [--field <path>] [--reveal]
opencreds edit <id|name> [flags…]
opencreds rm <id|name> [--purge]
opencreds restore <id>
```

`list` and `get` MUST NOT print secret values by default. `get` prints the item
with every secret field masked; `--reveal` prints one field named by `--field`,
so revealing is always a deliberate act naming a single value. `--json` output is
masked identically — a pipeline is not an authorization.

Type flags follow the field group names, kebab-cased:
`--username`, `--password`, `--totp`, `--url`,
`--cardholder-name`, `--number`, `--exp-month`, `--exp-year`, `--code`,
`--first-name`, `--last-name`, `--email`, `--phone`, `--address1`, …,
`--key-type`, `--algorithm`, `--public-key`, `--private-key`, `--file`, `--path`, `--mode`,
`--provider`, `--account-id`, `--handle`, `--access-token`, `--refresh-token`, `--scope`.

`--password -` and every other secret flag read from stdin when given `-`, so a
secret need not appear in the shell history or the process list.

## Database

```bash
opencreds export [--out vault.opencreds] [--passphrase-stdin]
opencreds export --plaintext --out vault.json --yes
opencreds export --format bitwarden-csv --out vault.csv --yes

opencreds import <file> [--dry-run] [--merge skip|replace|duplicate]
opencreds import <file> --source bitwarden|onepassword|chrome|lastpass|keepass
```

`export` writes the encrypted form. `--plaintext` prints what it is about to do
and exits 4 without `--yes`.

`import` with `--dry-run` reports counts by type, folders to be created,
duplicates detected and rows that could not be mapped, and writes nothing. A
manifest mismatch exits 3 and writes nothing regardless of flags.

Output of a dry run:

```
opencreds import vault.opencreds --dry-run

Source vault.opencreds (opencreds 0.1, encrypted, namespace opencreds)
Exported 2026-08-29T18:00:00.000Z by @logicsrc/opencreds 0.1.0
Manifest verified — 42 items, 3 folders

login 38 2 already present (skip)
card 2
key 1
account 1

Folders Work, Personal (new), Archive
Skipped 0

Nothing written. Re-run without --dry-run to import.
```

## Validation

```bash
opencreds validate <file> # a database, or a plaintext item document
opencreds validate --stdin
```

Exits 0 when the document conforms, 2 when it does not, and prints one diagnostic
per failure with a JSON pointer into the document:

```
/items/17/login/uris/0/match "fuzzy" is not a valid match rule
/manifest/itemCount says 42, payload has 41
```

## Conformance

```bash
opencreds conformance # a table, one row per requirement
opencreds conformance --json # the report, for CI
opencreds conformance --emit-fixtures <dir> # generate the fixture set
```

Runs the suite against this implementation and reports each requirement as pass,
fail or skip. Exits 2 when a MUST does not pass, so it can gate CI directly. An
implementation claiming conformance SHOULD run it there.

`--emit-fixtures` writes the generated fixture set, so another implementation
can be tested against exactly what this one produces and accepts. See
[conformance.md](./conformance.md).

## What the CLI never does

- It never prints a secret value except through `get --reveal --field`.
- It never writes a plaintext file without an explicit flag and a confirmation.
- It never sends anything anywhere. There is no telemetry, no account, and no
network call in any command listed on this page.
Loading
Loading