
# Settings, secrets and stored state

Three kinds of data outlive a reload: settings the reader changed, state an extension keeps for
itself, and secrets an extension was handed. Each has its own document and its own default, and
each can be pointed at a store of your own.

## Settings

An extension reads its settings through `workspace.getConfiguration`, over two layers: every
manifest's declared defaults, and whatever the reader has written over them.

```ts
vscode.workspace.getConfiguration("markdown").get("preview.theme", "auto");
await vscode.workspace.getConfiguration().update("markdown.preview.theme", "dark");
```

The written layer is a `SettingsStore` — dotted keys to values, read and written whole rather
than one key at a time:

```ts
interface SettingsStore {
  read(): Record<string, unknown> | undefined;
  write(settings: Record<string, unknown>): void;
  watch?(changed: () => void): () => void;
}
```

A workbench passed no `settings` option keeps this in `localStorage`, under the key
`codelet.settings`, so an `update()` survives a reload. `localSettings("my-key")` is that same
store under a key of your own:

```ts
import { localSettings, Workbench } from "codelet/workbench";

new Workbench({ parent, fs, settings: localSettings("my-app-settings") });
```

A host with its own place to keep preferences — a dialog, a profile on a server — hands over a
store instead:

```ts
import type { SettingsStore } from "codelet/workbench";

const settings: SettingsStore = {
  read: () => mine.all(),
  write: (all) => mine.save(all),
  // Optional: call `changed` when settings move somewhere else — another tab, your own dialog —
  // and every extension watching one is told through `onDidChangeConfiguration`.
  watch: (changed) => mine.subscribe(changed),
};

new Workbench({ parent, fs, settings });
```

The store is read once, so it's the object that has to stay stable across renders, not the
values it returns. Nothing renders a setting, so `renderWorkbench()` takes no `settings` option.

For a reader-facing editor, mount [`codelet/extensions/settings`](/extensions/settings): it adds
**Preferences: Open Settings (JSON)** to the palette, a tab listing every key any manifest
declared, and writes back to this same store as the reader types.

## Extension state

`context.globalState` and `context.workspaceState` are where an extension keeps its own data
between reloads — not what the reader typed, but what the extension remembers about itself.

```ts
context.globalState.update("lastRun", Date.now());
context.workspaceState.get<string[]>("recentSearches", []);
```

They're kept the same way settings are, over a second `SettingsStore` you can also hand over:

```ts
new Workbench({ parent, fs, state: localSettings("my-app-state") });
```

A workbench passed no `state` option keeps this in `localStorage` too, under its own key
(`codelet.state`), separate from the reader's settings — what an extension writes for itself
shouldn't collide with what the reader wrote. Stopping an extension and starting it again reads
what it wrote before; `globalState` is shared across every workspace an extension opens,
`workspaceState` is scoped to the one showing.

## Secrets

`context.secrets` is where a token the reader typed, or a key an extension was handed, goes.

::warning
**With no `secrets` option, a workbench keeps them in memory and loses them when the tab
closes.** This is the default, and it's the honest one: a page can't encrypt on its own, so
writing a secret to `localStorage` would be a token left in the clear under an API that says
otherwise. Once unlocked, a secret is readable by every script in the page — the shell, every
extension you mounted, an LSP client, a `.vsix` the reader installed from Open VSX. No browser
API hides a value from the origin holding it.
::

`webauthnSecrets()` seals the secrets document in `localStorage` under a key derived from the
reader's own passkey:

```ts
import { Workbench, webauthnSecrets } from "codelet/workbench";

new Workbench({ parent, fs, secrets: webauthnSecrets() });
```

The first secret an extension stores enrolls a passkey; a later page derives the same key on the
first read. Two commands land in the palette wherever a store can be unlocked:

- **Secrets: Remember Secrets on This Device** — runs the enrollment from a click, the one
  gesture that always works (Safari refuses a passkey ceremony without one).
- **Secrets: Forget Secrets on This Device** — drops everything sealed here and stops keeping,
  until the row above is used again. The passkey itself stays; a page has no way to delete one.

| Option       | Type                  | Default             | Description                                            |
| ------------ | --------------------- | ------------------- | ------------------------------------------------------ |
| `name`       | `string`              | the page's host     | What the reader sees the passkey called.               |
| `key`        | `string`              | `"codelet.secrets"` | The `localStorage` key the sealed document goes under. |
| `attachment` | `"platform" \| "any"` | `"platform"`        | Which authenticators may hold the key.                 |

`"platform"` binds the key to this machine — Touch ID, Windows Hello, a laptop's own sensor —
which is what makes "on this device" true. `"any"` also accepts a phone over hybrid and a
security key with `hmac-secret`, at the cost of the key travelling with the reader rather than
staying put.

A host with somewhere real to keep a secret — a server, a native shell with a keychain — hands
over its own `SecretsStore` and gets a far better guarantee than anything a page can manage:

```ts
import type { SecretsStore } from "codelet/workbench";

const secrets: SecretsStore = {
  read: () => mine.load(),
  write: (all) => mine.save(all),
  unlock: () => mine.authenticate(), // optional — puts the palette command there at all
  forget: () => mine.clear(), // optional
  remembering: () => mine.unlocked, // optional
  reports: (to) => mine.onMessage(to), // optional — a Secrets log line, a toast for `notify`
};
```

::warning
`webauthnSecrets()` is encrypted **at rest, and only at rest** — exactly what VSCode's own
`SecretStorage` promises, and no more. It stops a disk read, a backup or a shared machine from
seeing anything but ciphertext. It does not stop a script running in the page once the reader has
unlocked: that plaintext is in the page like everything else.
::

:read-more{to="/api/workspace"}
