auth-introspection
| Subpath | @noy-db/hub (root export) |
| Type | Core feature — describeAuthConfig() and diagramAuthConfig() are always available. |
What it is
auth-introspection answers the question "how is authentication configured in this
vault, and what has each user actually enrolled?" — as ready-to-display text, so an
admin screen, CLI, or audit report never has to re-derive it from raw keyring files.
Two pieces of context, if this is your first noy-db auth page:
- noy-db authenticates users against a three-tier model: tier 1 is the master secret, tier 2 is routine login through enrolled authenticators (passkey, OIDC, password), tier 3 is a PIN for quick resume after idle. See session-tiers for the full model.
- Sensitive operations are guarded by policy gates — per-action rules stored with the vault that declare which tier (and which extra factor proofs) an action requires. See policy.
The service exposes three read-only surfaces over that model:
- A vault-wide English summary — the configured tiers, phrase-format rules, enrolled recovery profiles, and every configured gate, as plain text.
- A vault-wide Mermaid diagram — the same auth graph as Mermaid
flowchart TBsource, ready to render as an SVG. - A per-user enrollment view — which authenticator slots a given user has
enrolled. This surface is doubly protected: it is gated by the
view-user-authpolicy gate (disabled by default — calls fail until you enable it), and its output passes through a strict field allowlist so credential material can never leak into a rendered string.
It is always on within its layer — there is no with*() factory to opt into and no
bundle-size decision to make.
Setup
Nothing to wire for the two vault-wide surfaces. The only opt-in is the
view-user-auth gate for the per-user surface: both built-in policy presets
(PERSONAL_POLICY, STRICT_POLICY) ship it as { minTier: 1, enabled: false }, so
per-user introspection fails closed until you enable it deliberately:
import { createNoydb } from '@noy-db/hub'
import { toFile } from '@noy-db/to-file'
const db = await createNoydb({
store: toFile({ dir: './data' }),
user: 'alice',
secret: 'orange river candle mountain silver fox',
policy: {
gates: {
// Enable per-user introspection for tier-1 (secret-opened) sessions.
'view-user-auth': { minTier: 1 },
},
},
})
The policy option is merged gate-by-gate over PERSONAL_POLICY and persisted at
_meta/policy on the vault's first open; from then on the on-disk document wins. On
an existing vault, change it with db.updatePolicy(vault, { gates: { 'view-user-auth': { minTier: 1 } } })
— the override merges into the current policy, leaving the other gates intact.
Usage
Vault-wide English summary
import { createNoydb } from '@noy-db/hub'
import { toFile } from '@noy-db/to-file'
const db = await createNoydb({
store: toFile({ dir: './data' }),
user: 'alice',
secret: 'orange river candle mountain silver fox',
})
await db.openVault('acme')
const summary = await db.team.describeAuthConfig('acme')
console.log(summary)
Vault "acme" — three-tier authentication
Tier 1 — Secret (master)
Phrase format: 6+ words, lowercase letters, ≥3 chars/word
Strength validator: enforced (override available for tests only)
Tier 2 — Authenticate (routine login)
Allowed methods: WebAuthn (passkey), OIDC, Password
Slots per user: unlimited
Tier 3 — Unlock (quick resume)
Method: PIN (per-app configurable)
Recovery profiles enrolled: none
Managed-secret mode: off (post-1.0)
Sensitive-action gates:
rotate-secret — tier 1 + 1× totp|email-otp|recovery|webauthn-roaming|webauthn-platform|password|pin
recover-secret — tier 1
rotate-unlock — tier 2
export-plaintext — tier 1 + 1× totp|email-otp
view-user-auth — disabled
…one line per configured gate
The summary reflects the policy persisted at _meta/policy. If no policy document
exists on disk (a store written outside createNoydb), the summary falls back to the
defaults (6+ words, ≥3 chars/word, no gates listed).
Vault-wide Mermaid diagram
diagramAuthConfig() returns Mermaid source, not an image — pipe it through
Mermaid (CLI, browser bundle, or a docs renderer) to get the SVG. Each enabled gate
becomes a node attached to the tier it requires; disabled gates are omitted.
const mmd = await db.team.diagramAuthConfig('acme')
console.log(mmd)
flowchart TB
vault["Vault: acme"]
tier1["Tier 1<br/>Secret"]
tier2["Tier 2<br/>Multi-slot Authenticate"]
tier3["Tier 3<br/>PIN / Quick-resume"]
vault --> tier1
tier1 --> tier2
tier2 --> tier3
rotate_secret["rotate-secret<br/>tier ≥ 1"]
tier1 --> rotate_secret
rotate_unlock["rotate-unlock<br/>tier ≥ 2"]
tier2 --> rotate_unlock
…one node + one edge per enabled gate
Per-user enrollment view
Gated by view-user-auth (see Setup). Output is allowlist-sanitized:
only the slot id, method, enrollment date, and enrolling tier are rendered — wrapped
key material, WebAuthn credential ids, OIDC subject ids, password hashes, and TOTP
secrets are dropped at the allowlist boundary, never redacted into placeholders.
import { PolicyDeniedError } from '@noy-db/hub'
try {
const view = await db.team.describeUserAuth('acme', 'alice')
console.log(view)
} catch (err) {
if (err instanceof PolicyDeniedError) {
// err.gate === 'view-user-auth', err.reason e.g. 'disabled' | 'insufficient-tier'
}
throw err
}
User: alice (joined 2026-04-15, role: owner)
Tier 2 enrollments:
- webauthn (id=webauthn-yubikey-blue, enrolled 2026-04-15, via tier 1)
A user with a keyring but no tier-2 slots renders (none enrolled); an unknown user
id returns the empty string rather than throwing, so the call alone never confirms
or denies that a user exists.
For owner dashboards there is a bulk variant:
const all = await db.team.describeAllUsersAuth('acme')
// → [{ userId: 'alice', description: 'User: alice (joined 2026-04-15, role: owner)\n…' }, …]
API reference
Instance methods (on the Noydb handle)
| Method | Returns | Purpose |
|---|---|---|
db.team.describeAuthConfig(vault) | Promise<string> | Vault-wide English summary of the configured auth model. Ungated. |
db.team.diagramAuthConfig(vault) | Promise<string> | Vault-wide auth graph as Mermaid flowchart TB source. Ungated. |
db.team.describeUserAuth(vault, userId, factors?) | Promise<string> | Per-user enrollment summary. Checks the view-user-auth gate first; throws PolicyDeniedError on denial. Empty string for unknown users. |
db.team.describeAllUsersAuth(vault, factors?) | Promise<Array<{ userId: string; description: string }>> | Bulk per-user summaries (users with no keyring file are skipped). Same gate check. |
factors is an optional FactorProofBundle
({ factors?: ReadonlyArray<FactorProof>; sharedDevice?: boolean }) — only needed
when your view-user-auth gate policy demands extra factor proofs beyond the active
session tier.
Standalone functions (exported from @noy-db/hub)
The same four surfaces are exported as free functions that take a raw store — the low-level building blocks the instance methods delegate to:
import {
describeAuthConfig, // (store: NoydbStore, vault: string) => Promise<string>
diagramAuthConfig, // (store: NoydbStore, vault: string) => Promise<string>
describeUserAuth, // (store: NoydbStore, vault: string, userId: string) => Promise<string>
describeAllUsersAuth, // (store: NoydbStore, vault: string) => Promise<Array<{ userId, description }>>
} from '@noy-db/hub'
They read the vault's policy document (_meta/policy) and keyring headers directly
from the store. Note the security scoping below: the gate check lives on the instance
methods, not here.
view-user-auth
- Gate:
view-user-authis a built-in gate name. Built-in gates with no configured policy fail closed, and both presets configure it as{ minTier: 1, enabled: false }— so per-user introspection is off until enabled. Denials throwPolicyDeniedErrorcarryinggate,reason('disabled','insufficient-tier','missing-factor','stale-proof','shared-device-blocked'), and therequiredgate policy. - Allowlist: per authenticator slot, exactly four fields can ever reach the
rendered string:
id,method,enrolled_at,enrolled_via_tier. Everything else in the keyring file —wrapped_kek, the method-specificmetablob (credential ids, subject ids, hashes, secrets), the KDFsalt, recovery codes — is projected out before rendering. Dropped, not redacted: an off-allowlist field leaves no trace in the output.
Limits & gotchas
- Per-user introspection is off by default. Calling
db.team.describeUserAuth()/db.team.describeAllUsersAuth()without enablingview-user-auththrowsPolicyDeniedErrorwithreason: 'disabled'. - The gate protects the app surface, not the store. The standalone functions take
a raw
NoydbStoreand perform no gate check — anyone holding the store can already read the plaintext keyring headers they summarize. Route application code through thedb.*methods so the policy applies. - Strings, not structures. All four surfaces return display text (or Mermaid source), formatted for humans. There is no stable machine-readable schema — don't parse the summary; read the policy document via the policy APIs instead.
- The diagram is Mermaid source. You must render it yourself; nothing in noy-db produces the SVG.
- Unknown user ⇒ empty string.
describeUserAuthdeliberately does not throw for a missing keyring, so probing it does not reveal whether a user id exists. Check for''before displaying. - The summary mirrors
_meta/policy. On a vault created bycreateNoydb()the merged policy is persisted there at creation; edits made withdb.updatePolicy()show up on the next call. A store with no policy document renders the built-in defaults.
See also
- session-tiers — the three-tier authentication model this service describes.
- policy — the
VaultPolicy/GatePolicygate DSL, includingview-user-auth. - team — multi-user grant/revoke, whose keyring files feed the per-user view.
- directory — the vault-wide "who's on this team" listing, filtered by
its own visibility flags rather than the
view-user-authgate. - Services overview · Orientation map