mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-09-10 19:26:00 +00:00
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>
This commit is contained in:
parent
b1805d08e5
commit
80a36269bb
56 changed files with 8903 additions and 9 deletions
421
docs/opencreds/spec.md
Normal file
421
docs/opencreds/spec.md
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
# The OpenCreds Specification
|
||||
|
||||
Version: **0.1** (draft)
|
||||
Status: draft — the wire formats below are implemented by `@logicsrc/opencreds`
|
||||
and are expected to change only additively before 1.0.
|
||||
|
||||
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT and MAY are to be
|
||||
interpreted as described in RFC 2119.
|
||||
|
||||
## 1. Scope
|
||||
|
||||
OpenCreds specifies three things:
|
||||
|
||||
1. **The item** — the record shape for a stored credential (§3).
|
||||
2. **The vault** — how items are encrypted and how keys are derived (§4).
|
||||
3. **The database** — the portable file a vault exports to and imports from (§5).
|
||||
|
||||
It does not specify storage, synchronization, transport, autofill, or user
|
||||
interface. An implementation that produces and consumes conforming items and
|
||||
databases is conforming regardless of where it puts the bytes.
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
| Term | Meaning |
|
||||
| --- | --- |
|
||||
| **item** | One credential record: a login, a card, an identity, a note, a key or an account. |
|
||||
| **vault** | A set of items sharing one *user key*. |
|
||||
| **user key** | The 256-bit symmetric key that every item in a vault is encrypted under. |
|
||||
| **master password** | The human secret from which a `user`-profile vault's wrapping key is derived. |
|
||||
| **wrap key** | Derived from the master password; encrypts the user key. Never leaves the device. |
|
||||
| **auth hash** | Derived from the master password under a different label; the only password-derived value sent to a server. |
|
||||
| **recovery key** | A random 128-bit value that wraps a second copy of the user key. |
|
||||
| **namespace** | The domain-separation label prefix for a vault (§4.6). |
|
||||
| **profile** | How the user key is managed: `user` or `team` (§4.7). |
|
||||
| **database** | A vault serialized to a single file (§5). |
|
||||
| **manifest** | The authenticated inventory of a database (§5.3). |
|
||||
|
||||
## 3. The item
|
||||
|
||||
### 3.1 Record shape
|
||||
|
||||
An item is a JSON object. Every item MUST carry:
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `v` | integer | Item schema version. `1` for this specification. |
|
||||
| `id` | string | A UUID, generated by the client. See §4.4 — it is bound into the ciphertext. |
|
||||
| `type` | string | One of `login`, `card`, `identity`, `note`, `key`, `account`. |
|
||||
| `name` | string | Display name. MAY be empty. |
|
||||
| `createdAt` | string | RFC 3339 timestamp. |
|
||||
| `updatedAt` | string | RFC 3339 timestamp. |
|
||||
|
||||
An item MAY carry:
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `favorite` | boolean | Defaults to `false`. |
|
||||
| `folderId` | string \| null | A folder id declared in the same vault, or `null`. |
|
||||
| `notes` | string | Free text. For `note` items this is the content. |
|
||||
| `fields` | array | Custom fields (§3.3). |
|
||||
| `attachments` | array | Attachment references (§3.4). |
|
||||
| `history` | array | Password history (§3.5). `login` items only. |
|
||||
| `<type>` | object | The field group named by `type` (§3.2). |
|
||||
|
||||
An item MUST NOT carry a field group other than the one named by its `type`. An
|
||||
implementation reading an unknown top-level field MUST preserve it on round trip
|
||||
rather than dropping it; this is what makes the format forward-compatible.
|
||||
|
||||
### 3.2 Type codes and field groups
|
||||
|
||||
The type code is the integer an implementation MAY store in plaintext alongside
|
||||
the ciphertext so a server can filter and paginate without decrypting.
|
||||
|
||||
| `type` | Code | Group | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `login` | 1 | `login` | Username, password, TOTP, matching URIs |
|
||||
| `card` | 2 | `card` | Payment card |
|
||||
| `identity` | 3 | `identity` | Name, address and identity document numbers |
|
||||
| `note` | 4 | — | Free text only; content lives in `notes` |
|
||||
| `key` | 5 | `key` | SSH/PGP/API keys, certificates, `.env` secrets |
|
||||
| `account` | 6 | `account` | A provider account and its OAuth tokens |
|
||||
|
||||
Codes 1–4 are fixed by MarkSyncr's deployed vault and MUST NOT be renumbered.
|
||||
Codes 5 and 6 are introduced by this specification. Codes 7+ are reserved.
|
||||
|
||||
The groups are specified field by field in [item-model.md](./item-model.md).
|
||||
|
||||
### 3.3 Custom fields
|
||||
|
||||
```json
|
||||
{ "name": "Employee ID", "value": "A-4417", "type": "text", "hidden": false }
|
||||
```
|
||||
|
||||
`type` MUST be one of `text`, `hidden`, `boolean`, or `linked`. A `hidden` field
|
||||
is displayed masked; it is not encrypted differently — everything in the item is
|
||||
already inside one ciphertext.
|
||||
|
||||
### 3.4 Attachments
|
||||
|
||||
An item carries attachment *references*, not bytes:
|
||||
|
||||
```json
|
||||
{ "id": "…", "name": "passport.pdf", "size": 148213,
|
||||
"contentType": "application/pdf", "digest": "sha256-…", "key": "…" }
|
||||
```
|
||||
|
||||
`key` is the base64 AES-256 key the blob was encrypted under, held inside the
|
||||
item ciphertext so the blob store never sees it. An implementation that does not
|
||||
store blobs MUST still round-trip the references.
|
||||
|
||||
### 3.5 Password history
|
||||
|
||||
```json
|
||||
{ "password": "the previous value", "changedAt": "2026-08-01T12:00:00.000Z" }
|
||||
```
|
||||
|
||||
Newest first. An implementation MUST cap history at 20 entries: the item blob is
|
||||
rewritten on every save, and an uncapped array grows the ciphertext without
|
||||
bound. History is only defined for `login` items.
|
||||
|
||||
## 4. The vault
|
||||
|
||||
### 4.1 Primitives
|
||||
|
||||
| Purpose | Algorithm |
|
||||
| --- | --- |
|
||||
| Password stretching | PBKDF2-HMAC-SHA256 |
|
||||
| Key derivation | HKDF-SHA256 |
|
||||
| Symmetric encryption | AES-256-GCM, 96-bit IV, 128-bit tag |
|
||||
| Digests | SHA-256 |
|
||||
|
||||
All four are available in WebCrypto. An implementation MUST NOT substitute
|
||||
another cipher for AES-GCM in this version.
|
||||
|
||||
### 4.2 KDF parameters
|
||||
|
||||
Parameters travel *with* the vault so they can be strengthened later without
|
||||
invalidating anyone's data:
|
||||
|
||||
```json
|
||||
{ "kdf": "pbkdf2-sha256", "iterations": 600000, "salt": "<base64, ≥16 bytes>" }
|
||||
```
|
||||
|
||||
- `iterations` MUST default to 600,000 for a vault created under this version.
|
||||
- A client MUST refuse to derive below 100,000 iterations. Parameters arrive
|
||||
from a server, which makes them attacker-controlled if the server is
|
||||
compromised: serving `iterations: 1` would turn every captured auth hash into
|
||||
an offline guessing exercise with no work factor.
|
||||
- `argon2id` is a REGISTERED value and is not yet specified. A client that does
|
||||
not implement it MUST refuse the vault rather than fall back.
|
||||
|
||||
### 4.3 Key hierarchy
|
||||
|
||||
```
|
||||
master password ─PBKDF2(salt, iterations)─► master key (32 bytes)
|
||||
│
|
||||
┌────────────────────────────────┼────────────────────────────┐
|
||||
HKDF(<ns>:vault:wrap:v1) HKDF(<ns>:vault:auth:v1) HKDF(<ns>:vault:recovery:v1)
|
||||
│ │ │
|
||||
wrap key ──AES-GCM──► protected user key recovery wrap key
|
||||
│ │
|
||||
auth hash ──► server recovery blob
|
||||
```
|
||||
|
||||
- The user key is 32 random bytes generated on the client. It is what items are
|
||||
encrypted under, and it is never derived from the password — so changing the
|
||||
master password re-wraps one key rather than re-encrypting every item.
|
||||
- The master key is never used to encrypt anything directly.
|
||||
- The auth hash is the only password-derived value that may leave the device. A
|
||||
server storing it MUST hash it again before storage.
|
||||
- A recovery key is 16 random bytes, presented to the user in a grouped
|
||||
base32-style encoding, and wraps a second copy of the user key. It exists so a
|
||||
forgotten master password is survivable without the server learning anything.
|
||||
|
||||
### 4.4 The item envelope
|
||||
|
||||
To encrypt an item:
|
||||
|
||||
1. Serialize the item to UTF-8 JSON with `v` present.
|
||||
2. Generate a fresh 96-bit IV. An IV MUST NOT be reused under one key.
|
||||
3. Compute the additional authenticated data:
|
||||
`AAD = UTF8("<namespace>:vault:item:<v>:<id>")`
|
||||
4. `AES-GCM(userKey, iv, plaintext, AAD)`.
|
||||
|
||||
The stored envelope is:
|
||||
|
||||
```json
|
||||
{ "id": "<uuid>", "type": 1, "ciphertext": "<base64>", "iv": "<base64>" }
|
||||
```
|
||||
|
||||
Binding the id means a ciphertext cannot be moved between rows without
|
||||
decryption failing. On decryption an implementation MUST additionally verify
|
||||
that the decrypted `id` equals the envelope `id`; the AAD already makes a
|
||||
mismatch unreachable, and the check costs nothing.
|
||||
|
||||
A vault read MUST be tolerant per item: when one item fails to decrypt, the
|
||||
implementation MUST return the items that succeeded together with a list of the
|
||||
ids that failed, rather than failing the whole read. A single corrupt row must
|
||||
not hide someone's vault from them.
|
||||
|
||||
### 4.5 Vault metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"opencreds": "0.1",
|
||||
"namespace": "opencreds",
|
||||
"profile": "user",
|
||||
"kdf": "pbkdf2-sha256",
|
||||
"kdfIterations": 600000,
|
||||
"kdfSalt": "<base64>",
|
||||
"protectedUserKey": "<base64>",
|
||||
"protectedUserKeyIv": "<base64>",
|
||||
"recoveryKeyBlob": "<base64>",
|
||||
"recoveryKeyIv": "<base64>",
|
||||
"authHash": "<base64>",
|
||||
"createdAt": "…",
|
||||
"updatedAt": "…"
|
||||
}
|
||||
```
|
||||
|
||||
Key material is base64 text rather than binary. Where this travels as JSON over
|
||||
an HTTP API, binary round-trips as an escaped hex string and invites encoding
|
||||
mistakes on exactly the values that must not be corrupted.
|
||||
|
||||
### 4.6 Namespaces
|
||||
|
||||
Every domain-separation label in this specification is prefixed by the vault's
|
||||
`namespace`:
|
||||
|
||||
```
|
||||
<namespace>:vault:wrap:v1
|
||||
<namespace>:vault:auth:v1
|
||||
<namespace>:vault:recovery:v1
|
||||
<namespace>:vault:item:<v>:<id>
|
||||
<namespace>:database:v1
|
||||
```
|
||||
|
||||
A vault created under this specification MUST use the namespace `opencreds`. A
|
||||
vault created before it MAY declare its own — `marksyncr` is registered — and is
|
||||
conformant with that declaration.
|
||||
|
||||
This exists because labels are baked into every ciphertext already written.
|
||||
Editing one makes every existing vault in the world undecryptable, so a label is
|
||||
append-only in the strongest sense: it can be superseded, never changed. Carrying
|
||||
the prefix as data is what lets a deployed vault become conformant without
|
||||
re-encrypting a single item.
|
||||
|
||||
Registered namespaces: `opencreds`, `marksyncr`. A namespace MUST match
|
||||
`^[a-z][a-z0-9-]{1,31}$`.
|
||||
|
||||
### 4.7 Profiles
|
||||
|
||||
A profile is how the user key is managed. The item envelope (§4.4) is identical
|
||||
in both.
|
||||
|
||||
**`user`** — the user key is wrapped by a key derived from a master password, as
|
||||
in §4.3. One person, one password, one vault.
|
||||
|
||||
**`team`** — the user key (there called the vault key) is generated randomly and
|
||||
wrapped to each member with an anonymous sealed box against that member's X25519
|
||||
public key. The server stores one wrapped key per member and never sees the key
|
||||
itself. Granting access is an existing member unwrapping and re-sealing to the
|
||||
new member's public key. This is the scheme `logicsrc credentials` already
|
||||
implements; see [credential-sharing.md](../credential-sharing.md).
|
||||
|
||||
A vault MUST declare exactly one profile. An implementation MAY support one
|
||||
profile and remain conforming; it MUST refuse a vault whose profile it does not
|
||||
implement rather than attempting to open it.
|
||||
|
||||
## 5. The portable database
|
||||
|
||||
### 5.1 Shape
|
||||
|
||||
A database is a single JSON document. It exists in two forms, distinguished by
|
||||
`protected`.
|
||||
|
||||
**Encrypted (default):**
|
||||
|
||||
```json
|
||||
{
|
||||
"opencreds": "0.1",
|
||||
"type": "opencreds.database",
|
||||
"protected": true,
|
||||
"namespace": "opencreds",
|
||||
"exportedAt": "2026-08-29T18:00:00.000Z",
|
||||
"generator": { "name": "@logicsrc/opencreds", "version": "0.1.0" },
|
||||
"kdf": { "kdf": "pbkdf2-sha256", "iterations": 600000, "salt": "<base64>" },
|
||||
"manifest": { "itemCount": 42, "types": { "login": 40, "card": 2 },
|
||||
"folderCount": 3, "digest": "<base64 SHA-256>" },
|
||||
"iv": "<base64>",
|
||||
"ciphertext": "<base64>"
|
||||
}
|
||||
```
|
||||
|
||||
**Plaintext:**
|
||||
|
||||
```json
|
||||
{
|
||||
"opencreds": "0.1",
|
||||
"type": "opencreds.database",
|
||||
"protected": false,
|
||||
"namespace": "opencreds",
|
||||
"exportedAt": "…",
|
||||
"generator": { … },
|
||||
"manifest": { … },
|
||||
"folders": [ { "id": "…", "name": "Work" } ],
|
||||
"items": [ { "v": 1, "id": "…", "type": "login", … } ]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Export key
|
||||
|
||||
An export is encrypted under its own key, not the vault's user key. A database
|
||||
that reused the user key would be undecryptable anywhere except the vault it
|
||||
came from, which defeats the point of the file.
|
||||
|
||||
```
|
||||
export passphrase ─PBKDF2(export salt, iterations)─► HKDF("<ns>:database:v1") ─► export key
|
||||
```
|
||||
|
||||
The export salt is fresh per export and carried in the file. An implementation
|
||||
MAY instead accept a raw 32-byte key, in which case `kdf` is omitted.
|
||||
|
||||
The payload encrypted is the UTF-8 JSON of `{ "folders": [...], "items": [...] }`,
|
||||
under AES-256-GCM with a fresh IV and:
|
||||
|
||||
```
|
||||
AAD = UTF8(JSON of the header: opencreds, type, protected, namespace,
|
||||
exportedAt, generator, kdf, manifest — keys in that order)
|
||||
```
|
||||
|
||||
Binding the header means the manifest is authenticated by the same tag as the
|
||||
data. An attacker cannot restate the item count, swap the generator, or downgrade
|
||||
`protected` without the decryption failing.
|
||||
|
||||
### 5.3 Manifest
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `itemCount` | Number of items in the payload. |
|
||||
| `types` | Item count per type name. Types with zero items are omitted. |
|
||||
| `folderCount` | Number of folders in the payload. |
|
||||
| `digest` | Base64 SHA-256 over the item ids, sorted lexicographically and joined by `\n`. |
|
||||
|
||||
After decrypting, an implementation MUST recompute all four and MUST refuse the
|
||||
import if any disagrees. A truncated file, a dropped item and a re-ordered
|
||||
payload are all detectable; without the manifest a partial import is
|
||||
indistinguishable from a complete one.
|
||||
|
||||
For a plaintext database the manifest is still REQUIRED and MUST still be
|
||||
verified. It is not authenticated — nothing in a plaintext file is — but it
|
||||
still catches truncation and accidental editing.
|
||||
|
||||
### 5.4 The plaintext form
|
||||
|
||||
A plaintext database is every secret in a vault, in a file, in the clear. It
|
||||
exists because people move to products that read nothing else.
|
||||
|
||||
An implementation:
|
||||
|
||||
- MUST default to the encrypted form.
|
||||
- MUST require an explicit opt-in for the plaintext form, and SHOULD require a
|
||||
second confirmation.
|
||||
- MUST write `"protected": false` in the header, so the file is identifiable as
|
||||
unprotected without parsing the rest of it.
|
||||
- SHOULD write the file with owner-only permissions where the platform has them.
|
||||
- SHOULD warn that the file cannot be un-leaked.
|
||||
|
||||
### 5.5 Extension and media type
|
||||
|
||||
The conventional extension is `.opencreds`. The media type is
|
||||
`application/vnd.logicsrc.opencreds+json`.
|
||||
|
||||
## 6. Audit
|
||||
|
||||
An implementation that records vault operations SHOULD emit events of the form:
|
||||
|
||||
```json
|
||||
{ "type": "opencreds.audit_event", "id": "…", "action": "item.create",
|
||||
"itemId": "…", "itemType": "login", "principal": { … },
|
||||
"fingerprint": "<salted SHA-256, truncated>", "createdAt": "…" }
|
||||
```
|
||||
|
||||
An audit event MUST NOT contain a secret value. Where a value must be referenced,
|
||||
it is referenced by salted fingerprint — an equality marker, not secret storage.
|
||||
|
||||
Registered actions: `vault.create`, `vault.unlock`, `vault.unlock_failed`,
|
||||
`vault.rekey`, `vault.recovery_reset`, `item.create`, `item.update`,
|
||||
`item.delete`, `item.restore`, `item.purge`, `database.export`,
|
||||
`database.export_plaintext`, `database.import`.
|
||||
|
||||
## 7. Conformance
|
||||
|
||||
See [conformance.md](./conformance.md) for the requirement checklist and the
|
||||
fixture suite. In summary, a conforming implementation:
|
||||
|
||||
1. Reads and writes items per §3 without dropping unknown fields.
|
||||
2. Implements the envelope and key hierarchy of §4 for at least one profile.
|
||||
3. Reads and writes both database forms per §5, verifying the manifest.
|
||||
4. Refuses KDF parameters below the floor, unknown profiles, and unknown
|
||||
namespaces.
|
||||
5. Passes the published fixtures.
|
||||
|
||||
## 8. Schemas
|
||||
|
||||
Published in `@logicsrc/schemas`:
|
||||
|
||||
| Schema | File |
|
||||
| --- | --- |
|
||||
| Item | `logicsrc-opencreds-item.schema.json` |
|
||||
| Vault meta | `logicsrc-opencreds-vault-meta.schema.json` |
|
||||
| Item envelope | `logicsrc-opencreds-envelope.schema.json` |
|
||||
| Database | `logicsrc-opencreds-database.schema.json` |
|
||||
| Manifest | `logicsrc-opencreds-manifest.schema.json` |
|
||||
| Audit event | `logicsrc-opencreds-audit-event.schema.json` |
|
||||
|
||||
## 9. Version history
|
||||
|
||||
| Version | Date | Change |
|
||||
| --- | --- | --- |
|
||||
| 0.1 | 2026-08-29 | Initial draft. Six item types, one envelope, two profiles, one database format. |
|
||||
Loading…
Add table
Add a link
Reference in a new issue