UnENVerse 0.42.6
Local-first desktop secrets manager — TypeScript frontend
Loading...
Searching...
No Matches
modals.ts File Reference
import type;

Functions

function export applySchemaTooltips ()
 
function export buildCatChips (selected:string[]=[])
 
function export dynamicSecretFields ()
 
function refreshRequiredMarker(labelEl:HTMLElement, keyInput:HTMLInputElement|null, label:string,) applyUnknownSecretType (rawType:string|null)
 A5 (2026-09-14): the * on the value label, and aria-required on the field itself, used to be static — written once by dynamicSecretFields from cfg.keyLabel and never touched again, so an env_var entry with a named variable (or any entry carrying a seed) still showed "required" although primaryIsOptional — the one predicate that decides whether the form insists on the primary slot — already said it was not.
 
function export populateProjectSelect ()
 
function export updateNamePreview ()
 Paint the "Generated variable" line under the value field.
 
function wireGeneratorPopover() wireNamePreview ()
 Which required cookies (from the currently selected preset) the form's own jar — the primary value plus every extra_vars row — does not have yet.
 
function _saveDraft ()
 
function _makeExtraVarRow (key='', value='', secret=false, isPublic=false)
 Suggested extra_vars names for Phase 24.5's sixteen new types — the per-type field tables from the design, without building sixteen bespoke widgets.
 
function export suggestExtraVarsForType (type:SecretType)
 Populates extra_vars with blank rows named for the type just picked — only when the list is still empty, so this never touches an entry that already has values (editing an existing one, or a type change the user changed their mind about and changed back).
 
function clearDraft ()
 
function export openEdit (e:Event, idx:number)
 
function export closeModal ()
 
function async saveModal ()
 

Variables

let pendingRateLimitNote = ''
 Rate-limit text the structured count/period pair cannot express, carried from the entry being edited through to the next save.
 
export type TypeConfig
 
export const TYPE_CONFIG
 
export const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,23}$/
 What a label may be.
 
let _localPresetBound = false
 
function wireCaptureImport() wireCookieSplit() let _sessionPresetBound = false
 gpg_key: paste the public key, fill the expiry and the public identifiers.
 
const DRAFT_KEY = 'unenverse-form-draft'
 
let _draftBound = false
 

Function Documentation

◆ applySchemaTooltips()

function export applySchemaTooltips ( )

◆ buildCatChips()

function export buildCatChips (   selected:string[] = [])

◆ dynamicSecretFields()

function export dynamicSecretFields ( )

◆ applyUnknownSecretType()

function refreshRequiredMarker(labelEl:HTMLElement, keyInput:HTMLInputElement|null, label:string,) applyUnknownSecretType ( rawType:string|  null)

A5 (2026-09-14): the * on the value label, and aria-required on the field itself, used to be static — written once by dynamicSecretFields from cfg.keyLabel and never touched again, so an env_var entry with a named variable (or any entry carrying a seed) still showed "required" although primaryIsOptional — the one predicate that decides whether the form insists on the primary slot — already said it was not.

Reads the form's current state into the shape primaryIsOptional expects, so there is exactly one place that answers "is the value optional right now" and this is a consumer of it, not a second copy of the logic. Read the add/edit form into an entry.

**base is spread first and the form's values overwrite it** (Phase 23, E4). Before this the function returned a freshly constructed object literal with no spread at all, so every field the form has no input for survived only if the save path remembered to re-attach it by name — and it re-attached five. Editing an entry's description therefore erased its pool, and every field a later phase adds would be erased the same way, silently, on the first edit.

A field the form does have an input for is present in the literal even when the input is empty, so clearing a box still clears the field: object spread copies a key whose value is undefined. The four TOTP fields of the form, as the entry writes them.

An unreadable seed is kept as typed rather than dropped: saveModal refuses the save and says why, and a form that silently discarded the field would leave the user staring at an empty box with no idea it had rejected anything. validateTotpField is what decides; this only reads.

Parameters equal to the defaults are written as undefined, i.e. absent. A ‘totp_algorithm: 'SHA1’` on every entry cannot be told apart from a defaulted one, and then nothing can say whether the issuer chose it or we did. Repaint the seed field's status line, and reveal the parameter row when the seed is not on the default settings.

The status line is the whole of the feedback: an otpauth:// URI is 120 characters of which four matter, and a form that accepts one silently gives the user no way to know whether it read the right issuer — or read it at all. It never prints the seed. Wire the seed field's live feedback and its reveal button.

Assigned, never added (invariant 9): openAdd runs on every modal open, and addEventListener here would stack one handler per open — the failure that shows up as a reveal toggle flipping an even number of times and appearing to do nothing. Re-mask the seed field and clear its status.

Called on every open, not only on close: a reveal toggle left on shows the seed on every later visit to the form, and the form is opened from a card the user may be showing somebody. The real secretType of the entry currently open, when this build has no <option> for it — null in the ordinary case. Read by formToEntry (to preserve it rather than defaulting to api_key) and by saveModal (to refuse outright). Reset on every fillForm call, including openAdd's, where it is always null — a new entry can only ever be a type this form offers. A11: shows or hides the "made by a newer UnENVerse" banner and (dis)ables Save.

◆ populateProjectSelect()

function export populateProjectSelect ( )

◆ updateNamePreview()

function export updateNamePreview ( )

Paint the "Generated variable" line under the value field.

The template has five inputs feeding it — provider, key id, version, label, role — and before this the only way to find out what they produced was to run a copy and read the file. The value is shown as dots: the point is the name, and a form that prints the secret next to it is a form that puts it in a screenshot. Live preview for a composite entry's template (Phase 24.1).

Reads the same #f-extra-vars-list rows formToEntry does — parts are extra_vars, so there is nowhere else this could read from without building a second copy of "what are the current rows" (invariant already established by E4's formToEntry). The cookie paste helper.

Dropping a raw document.cookie string, a DevTools "Copy all as JSON" or a cookies.txt into the value field and pressing this splits it into one extra_vars row per cookie, attributes and all.

Splitting is offered, never automatic. A jar in one field is a perfectly good way to store a session — it is what the Cookie: header wants — and rewriting the user's value the moment they paste is the kind of help that loses a character they had fixed by hand. The status line says what was found and, when the attributes are missing, what that costs. Fills the form from a parsed capture. Replaces the cookie rows (pressing it twice must not duplicate them) and never touches a field the capture lacks. How the common self-hosted apps want their API key (Phase 24.5, local_service). Only conventions that are stable and widely documented are listed. Jellyfin's Authorization: MediaBrowser Token="{key}" needs a value template, which auth_template carries. Its primary documentation is a script-rendered page that could not be read for this change; the form is confirmed by several independent client libraries and the server's own mirror of its API documentation, and the older X-Emby-Token header is marked deprecated there, which is why it is not offered.

◆ wireNamePreview()

function wireGeneratorPopover() wireNamePreview ( )

Which required cookies (from the currently selected preset) the form's own jar — the primary value plus every extra_vars row — does not have yet.

Reads the form directly rather than formToEntry(), so this can run on every keystroke without constructing a whole entry each time. Shows/hides the preset picker and, when one is selected, which of its required cookies the jar is missing. Never edits the jar itself — see the module doc on session-presets.ts for why this is informational only. Assignment-guarded (invariant 9): openModal runs on every open. Wires the "+ Add variable" button and the delegated key-input listener that refreshes the required-marker (A5).

Bug fix (2026-09-15): this used to be bound only inside openAdd()'s _draftBound guard, which conflated "bind the draft auto-save listeners" with "bind the add-variable button" — two unrelated concerns sharing one flag. Since _draftBound is set only by openAdd, a session whose first modal was an edit (double-clicking an existing card, the ordinary way to attach extra variables to something already saved) got a button with no click handler at all: clicking it did nothing, silently, for the rest of that edit and every edit after it until openAdd() happened to run once. Reported as "make a template, then can't add variables — have to restart".

Moved here and called from openModal(), the one function both openAdd and openEdit already funnel through, with its own guard so it still binds exactly once per page load (invariant 9). Wires the generator popover beside the primary-value field's Generate button (A8, 2026-09-15 — "Inject unreachable while the form is open").

The Tools panel's four generators sit behind the modal overlay: reaching them while the form is open hits the backdrop and closes the form first, so "generate in Tools, Inject into the open form" was two mutually exclusive states. This popover is the same generators — generators.ts, moved out of tools.ts so there is one implementation of each rather than a form-shaped copy — reachable without leaving the form.

Use writes the primary value field, the only field a Generate button has ever targeted here (quickGenerate, on the plain button beside this caret). Extending generation to the secret field or a named variable is future scope: neither has a Generate button today, so there is nothing this popover would be replacing there.

Assigned, not added (invariant 9). openModal runs on every open, and this is called every time — like wireTotpField, not like wireExtraVarsAdd: every listener here is a property assignment (.onclick =, not addEventListener), which overwrites rather than stacks, so re-running the whole function on each open is safe and is in fact what keeps it correctly bound to #f-key-generate-caret and friends — the same static nodes on every open in the real app, but genuinely different nodes from one test to the next (loadRealIndexHtml() replaces the document per test). A one-shot guard here would bind once to whichever DOM happened to exist at the first call and silently do nothing on every open after — which is exactly the bug this function shipped with the first time it was written, caught by its own tests rather than by hand.

The two document-level listeners (outside-click, Escape) are the exception: those genuinely must bind only once — addEventListener on document would stack one pair per form open, each pair closing the popover redundantly, forever. They are guarded by _genPopoverDocListenersBound and, because that guard means they cannot be rebound on later opens, they re-query #f-gen-popover/#f-key-generate-caret by id inside the handler rather than closing over the nodes captured at first bind — so they still find the right element after any later DOM replacement. Closes the popover and clears its output. Called on every form open — unlike wireGeneratorPopover, which binds its handlers once, this must run every time or a value generated for one entry would still be sitting there, one click from being applied to the next entry this form opens on. Bind the preview to the five inputs that feed it.

Assignment on a permanent node, guarded (invariant 9): openModal runs on every open, and addEventListener here would repaint the preview N times per keystroke after N opens.

◆ _saveDraft()

function _saveDraft ( )

◆ _makeExtraVarRow()

function _makeExtraVarRow (   key = '',
  value = '',
  secret = false,
  isPublic = false 
)

Suggested extra_vars names for Phase 24.5's sixteen new types — the per-type field tables from the design, without building sixteen bespoke widgets.

[key, secret]; secret pre-ticks the mask checkbox for a name that is realistically going to hold one.

Deliberately excludes anything the form already has a dedicated field for: provider (name/SSID/site), the primary value (passphrase/key/body), account_name (username), api_secret, description (a note's body).

◆ suggestExtraVarsForType()

function export suggestExtraVarsForType (   type:SecretType)

Populates extra_vars with blank rows named for the type just picked — only when the list is still empty, so this never touches an entry that already has values (editing an existing one, or a type change the user changed their mind about and changed back).

A user-driven change on the type select is the only caller; fillForm sets .value directly, which fires no change event, so loading an existing entry never triggers this.

◆ clearDraft()

function clearDraft ( )

◆ openEdit()

function export openEdit (   e:Event,
  idx:number 
)

◆ closeModal()

function export closeModal ( )

◆ saveModal()

function async saveModal ( )

Marks an entry as rotated: stamps last_rotated_at, snapshots the current value into version_history (capped 50), resets the rotation clock, and clears the compromised flag. Links three previously separate rotation facts.

Puts a generated value into the Add/Edit form's secret field.

The guard is on the overlay being open, not on the field existing. #f-key is static markup in index.html, so it is in the document whether or not the modal is showing — which meant every "→ Inject" button in the Tools panel reported "Injected into form" with the form closed, wrote the value into a hidden input nobody could see, and then lost it: openModal does not read that field, and closeModal clears the draft. The user pressed a button, was told it worked, and nothing happened. Same class as invariant 8 — presence in the DOM is not the same as being on screen.

Copy one entry at a profile.

profile absent means "whatever the setting says" — the caret menu passes an explicit one for a single copy and does not persist it (Phase 23): a one-off "give me everything" must not silently change what the next fifty copies contain.

The caret beside the copy button: the three profiles, the raw value, and the request forms.

Plain text is escaped; pass html...`` for markup.

Variable Documentation

◆ pendingRateLimitNote

let pendingRateLimitNote = ''

Rate-limit text the structured count/period pair cannot express, carried from the entry being edited through to the next save.

Module-level state that points at the entry currently in the form, so it has the problem CLAUDE.md's invariants are about: something holds a reference and the thing it points at changes. What clears it is fillForm, which every path into the form goes through — fillForm({}) for a new entry normalises to no note and blanks it. Do not read this without having gone through fillForm first, or a note from the last entry edited lands on a different one.

◆ TypeConfig

export const TypeConfig
Initial value:
= {
providerLabel: string;
providerPlaceholder: string;
showAccount: boolean;
keyLabel: string;
keyPlaceholder: string;
}
const string
Definition auth-panel.ts:37

◆ TYPE_CONFIG

export const TYPE_CONFIG

◆ LABEL_RE

export const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,23}$/

What a label may be.

Narrow on purpose: it is a name segment, so anything that would normalise away (spaces, punctuation, an empty leading character) is refused at the form rather than silently transliterated into something the user did not type.

◆ _localPresetBound

let _localPresetBound = false

◆ _sessionPresetBound

function wireCaptureImport () wireCookieSplit () let _sessionPresetBound ( ) = false

gpg_key: paste the public key, fill the expiry and the public identifiers.

Assignment-guarded (invariant 9): openModal runs on every open.

◆ DRAFT_KEY

const DRAFT_KEY = 'unenverse-form-draft'

◆ _draftBound

let _draftBound = false