ores.iam.account_credential

Table of Contents

The secret an account proves when it authenticates, held apart from the account's own profile. One row per account, keyed by account_id, and no row for an account that holds no credential.

The split exists because the two are written by different access paths. ores.iam.account is the profile: a screen writes it, and every column travels on the wire. This entity is the credential: the IAM service alone writes it, and no column travels anywhere. Held in one table, a profile write replaced the whole row, and a column the domain type could not carry came back empty.

Every secret column is :no_wire:, so the domain struct carries the value the service needs while a serialisation of that struct omits it.

The table is bi-temporal and audited, like every other iam entity: it carries version, the four audit columns and the valid_from=/=valid_to pair with the GIST exclusion, so the model takes the ordinary audited shape and needs no shape flag. A credential write therefore keeps its own history, and the account's history never mentions it.

The entity is projected across the whole stack, exactly as the account is: SQL schema and notify trigger, domain type, repository, generator, protocol, NATS handler and sub-registrar, generated CRUD service, history provider, presentation mapper, shell command, HTTP route, eventing, and the TypeScript twins. The secrets stay off the wire because the columns are :no_wire:, not because the entity has no wire surface. See NATS entity protocol specification.

The generated half is reads-only, and that is the specification's answer rather than ours. A write carries "the fields the user owns, and no others", and a server-owned field never appears in one; a credential's only meaningful field is server-derived, so the resource has no write record to carry, and "a read-only resource simply declares no write verb". The credential's writes are domain operations, iam.v1.ops.*.

1. Flags

2. Columns

2.1. id

Unique identifier for the credential row. A surrogate, like the account's own: nothing outside this table names it.

2.2. account_id

The account this credential belongs to. One row per account, so the generated table adds the unique index on (tenant_id, account_id).

uuid_gen()

2.3. password_hash

The hash a user account proves when it signs in. Unset for an account that cannot sign in with a password: a service, algorithm or llm account.

The stored hash embeds its own salt, so no salt column sits beside it.

ctx.alphanumeric(64)

2.4. service_password_hash

The SHA-256 hash, as hex, of a service account's machine password. Unset for an account that does not authenticate as a service.

ctx.alphanumeric(64)

2.5. totp_secret

Time-based One-Time Password secret for two-factor authentication. No verification path reads it yet, and no caller may ever see it.

ctx.alphanumeric(32)

3. SQL

3.1. Flags

4. Foreign keys

4.1. account_id

The account this credential belongs to, validated against the accounts table. The key is not flagged :bump_parent_version:: a credential write is not a change to the account, and the account's history must not record one.

5. C++

5.1. Flags

5.2. Repository

5.3. Domain includes

#include <boost/uuid/uuid.hpp>
#include <string>

5.4. Entity includes

#include <string>

5.5. Conventions

5.6. Table display

The columns an operator reads when a credential row is printed. No secret is among them: the table exists to say which account holds a credential and who last wrote it.

column header
account_id Account
id ID (UUID)
modified_by Modified By
version Version

6. Physical space

Nothing is withdrawn. The entity projects like any other; what keeps the secrets in is the :no_wire: flag on the columns, not the absence of a surface.

7. See also

Emacs 29.3 (Org mode 9.6.15)