[1;33m[WARNING][0m No existing semantic tags found. Starting from v0.0.0
v0.0.1 - Reorganize ISO build tree, add docs/plans/scripts, and stage Phase 2 Slice B+C service integration artifacts
This commit is contained in:
367
plans/nostr_config_projection.md
Normal file
367
plans/nostr_config_projection.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Nostr Config Projection — "Nostr as your home directory"
|
||||
|
||||
> **Status:** Design. This document defines how n-OS-tr projects a user's configuration from Nostr events into the live filesystem at boot, and how it publishes changes back to Nostr at shutdown.
|
||||
>
|
||||
> Depends on: [`plans/identity_subsystem.md`](identity_subsystem.md) (must run first to load the main key), [`plans/threat_model.md`](threat_model.md) (defines what this subsystem is and is not allowed to do).
|
||||
|
||||
## 1. Concept
|
||||
|
||||
Your n-OS-tr configuration — dotfiles, preferences, bookmarks, per-app state — is **not** stored on the machine. It's stored as **replaceable Nostr events, signed by your main key (NIP-06 index 0), encrypted to yourself with [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md)**.
|
||||
|
||||
At boot, after identity is loaded, the **config-loader** queries a set of user relays for these events, decrypts them, and writes the content into the correct locations under `/home/$USER/...` on the ephemeral rootfs. At shutdown (or on explicit `nostr-config save`), the **config-writer** re-emits changed files as new signed replaceable events.
|
||||
|
||||
The consequence:
|
||||
|
||||
- **Same 12 words on any n-OS-tr machine = same environment.** Your `.bashrc`, your SSH `authorized_keys`, your Firefox bookmarks, your relay list, your app preferences all reappear.
|
||||
- **No machine knows anything about you after shutdown.** The tmpfs is wiped; the Nostr events live on relays (encrypted) and on paper in your head (the mnemonic).
|
||||
- **Only you can read your config.** NIP-44 encrypts to your own npub; relay operators see ciphertext only.
|
||||
|
||||
## 1.1 Crypto foundation: `nostr_core_lib`
|
||||
|
||||
Like [`identity_subsystem.md`](identity_subsystem.md#11-crypto-foundation-nostr_core_lib), the config-loader and config-writer are built on top of [`includes/nostr_core_lib`](../includes/nostr_core_lib/). Concretely:
|
||||
|
||||
| Capability | `nostr_core_lib` symbol | NIP |
|
||||
|---------------------------------------------|------------------------------------------------------------------------------------------------|------------|
|
||||
| Create + sign replaceable slot events | [`nostr_create_and_sign_event`](../includes/nostr_core_lib/nostr_core/nostr_core.h) | NIP-01 |
|
||||
| NIP-44 self-encrypt slot plaintext | [`nostr_nip44_encrypt(priv, pub_self, plaintext, out, ...)`](../includes/nostr_core_lib/nostr_core/nip044.h:28) | NIP-44 |
|
||||
| NIP-44 self-decrypt fetched ciphertext | [`nostr_nip44_decrypt(priv, pub_self, ciphertext, out, ...)`](../includes/nostr_core_lib/nostr_core/nip044.h:62) | NIP-44 |
|
||||
| Query relays for slot events (sync) | [`synchronous_query_relays_with_progress`](../includes/nostr_core_lib/nostr_core/nostr_core.h) | NIP-01 |
|
||||
| Publish slot events | [`synchronous_publish_event_with_progress`](../includes/nostr_core_lib/nostr_core/nostr_core.h)| NIP-01 |
|
||||
| Relay pool with reconnection (streaming) | [`nostr_relay_pool_create`](../includes/nostr_core_lib/nostr_core/nostr_core.h) + [`nostr_relay_pool_subscribe`](../includes/nostr_core_lib/nostr_core/nostr_core.h) | NIP-01 |
|
||||
| NIP-42 auth (personal private relay) | [`nostr_relay_pool_set_auth`](../includes/nostr_core_lib/nostr_core/nostr_core.h) | NIP-42 |
|
||||
| NIP-11 relay info (feature check) | [`nostr_nip11_fetch_relay_info`](../includes/nostr_core_lib/nostr_core/nostr_core.h) | NIP-11 |
|
||||
| Relay-level event signing proxied via agent | NIP-46 path through [`nostr_nip46_client_sign_event`](../includes/nostr_core_lib/nostr_core/nip046.h:158) — config-writer never holds the user's raw private key | NIP-46 |
|
||||
|
||||
Two important consequences of the `nostr_core_lib` choice:
|
||||
|
||||
1. **Config-writer does not need the user's private key.** It talks to identity-agent over NIP-46 (local Unix socket), and each slot event is signed by the agent on the writer's behalf. The raw main nsec stays inside the agent's `mlock()`-ed memory per [F3 / F4 of the threat model](threat_model.md#41-the-mnemonic-and-derived-keys).
|
||||
|
||||
2. **The encryption target is the user's own pubkey.** NIP-44 "self-encryption" uses `priv_main` + `pub_main` as both ends of the ECDH; the library supports this directly (both arguments in [`nostr_nip44_encrypt`](../includes/nostr_core_lib/nostr_core/nip044.h:28)). Decryption uses the same `(priv_main, pub_main)` pair.
|
||||
|
||||
## 2. The model
|
||||
|
||||
### 2.1 Slots
|
||||
|
||||
A **slot** is a single unit of user configuration. Each slot is:
|
||||
- One replaceable Nostr event.
|
||||
- Keyed by a stable `d` tag identifying the slot (the "address" in NIP-33 terms).
|
||||
- Content is either plaintext (for public/non-secret things like a relay list that you *want* to be reuseable by other clients) or NIP-44 ciphertext encrypted to the user's own npub (the default for anything remotely personal).
|
||||
- Signed by the user's main key (NIP-06 index 0).
|
||||
|
||||
### 2.2 Manifest event
|
||||
|
||||
The **manifest** is a single special slot that lists every other slot the user wants the OS to materialize. The manifest is itself a replaceable event with a well-known `d` tag:
|
||||
|
||||
```
|
||||
d = "n-os-tr:manifest:v1"
|
||||
```
|
||||
|
||||
Its content is a JSON document whose schema is:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"generated_at": 1714000000,
|
||||
"slots": [
|
||||
{
|
||||
"name": "dotfiles/bashrc",
|
||||
"path": "/home/user/.bashrc",
|
||||
"mode": "0644",
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"name": "ssh/authorized_keys",
|
||||
"path": "/home/user/.ssh/authorized_keys",
|
||||
"mode": "0600",
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"name": "nostr/relays",
|
||||
"path": "/home/user/.config/n-os-tr/relays.json",
|
||||
"mode": "0644",
|
||||
"encrypted": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The manifest event is also NIP-44-encrypted to self by default. A user who explicitly wants their manifest public (so other clients can share data through n-OS-tr's slot scheme) can opt into unencrypted manifest publishing.
|
||||
|
||||
### 2.3 Event kinds
|
||||
|
||||
| Purpose | Kind | NIP | Notes |
|
||||
|-----------------------|-------|------------|---------------------------------------------------------|
|
||||
| Slot event (config) | 30078 | [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md) ("application-specific data") | Parameterized replaceable, user-chosen `d` tag. Matches existing convention for app state. |
|
||||
| Manifest | 30078 | NIP-78 | `d = "n-os-tr:manifest:v1"`. |
|
||||
| Role table | 30078 | NIP-78 | `d = "n-os-tr:roles:v1"`. Maps role name → NIP-06 index. See [`plans/identity_subsystem.md`](identity_subsystem.md#23-role-registration-protocol). |
|
||||
| Relay list | 10002 | [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md) | Optional; if present, config-loader prefers NIP-65 for discovering user relays. |
|
||||
|
||||
Choosing kind **30078** (NIP-78 application-specific data) aligns with existing clients and avoids claiming a new kind. All slot events use the same kind; they're distinguished by their `d` tag.
|
||||
|
||||
### 2.4 Tag conventions
|
||||
|
||||
Every slot event carries these tags:
|
||||
|
||||
```
|
||||
["d", "<slot name>"] required, unique per slot
|
||||
["client", "n-os-tr"] identifies which app wrote it
|
||||
["ver", "<semver>"] optional, version of the slot schema
|
||||
["sha256", "<hex>"] optional, hash of plaintext content
|
||||
["path", "<target path>"] optional, overrides manifest path if present
|
||||
["encrypted","nip44"] present if content is NIP-44 ciphertext
|
||||
```
|
||||
|
||||
## 3. Boot flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant IA as identity-agent
|
||||
participant CL as config-loader
|
||||
participant Sig as signer socket
|
||||
participant R as user relays
|
||||
participant FS as tmpfs /home
|
||||
|
||||
Note over IA: mnemonic loaded<br/>(see identity_subsystem.md)
|
||||
|
||||
CL->>Sig: connect, get npub for "main"
|
||||
Sig-->>CL: npub1...
|
||||
|
||||
CL->>R: REQ kind=10002 author=npub (NIP-65 relay list)
|
||||
R-->>CL: user relays (cacheable)
|
||||
|
||||
CL->>R: REQ kind=30078 author=npub d="n-os-tr:manifest:v1"
|
||||
R-->>CL: manifest ciphertext event
|
||||
|
||||
CL->>Sig: NIP-44 decrypt(manifest.content)
|
||||
Sig-->>CL: manifest JSON
|
||||
|
||||
loop for each slot in manifest
|
||||
CL->>R: REQ kind=30078 author=npub d=slot.name
|
||||
R-->>CL: slot event (ciphertext or plaintext)
|
||||
alt ciphertext
|
||||
CL->>Sig: NIP-44 decrypt
|
||||
Sig-->>CL: plaintext
|
||||
end
|
||||
CL->>FS: write plaintext to slot.path with slot.mode
|
||||
end
|
||||
|
||||
Note over CL,FS: home dir materialized
|
||||
CL->>IA: signal "ready"
|
||||
IA->>+Session: start user session
|
||||
```
|
||||
|
||||
### 3.1 Relay discovery
|
||||
|
||||
Config-loader needs to know where to query. Options, in priority order:
|
||||
|
||||
1. **Built-in bootstrap relays.** A tiny hardcoded list baked into the image (overridable per user). Used to fetch NIP-65 and manifest on a first-ever session from this mnemonic.
|
||||
2. **NIP-65 relay list (kind 10002).** Once fetched, becomes the authoritative set for this user.
|
||||
3. **User-provided relay list via kernel cmdline.** For edge cases where bootstrap relays are not reachable (offline/air-gapped setups), the user may pass `nostr.relay=wss://my-relay.example` on the boot cmdline.
|
||||
4. **Local c-relay running on this machine.** If the user is running their own personal c-relay and it already has their events cached, config-loader will query it first and fall back to the network only for misses.
|
||||
|
||||
### 3.2 Timing and ordering
|
||||
|
||||
- Config-loader is a systemd unit with `Requires=nostr-id.service After=nostr-id.service network-online.target`.
|
||||
- User session targets (display manager, shell, GUI) `After=config-loader.service`.
|
||||
- config-loader blocks session start until it has either materialized the manifest or timed out.
|
||||
- Timeout defaults to 15 seconds. On timeout, user is prompted: "No relays reachable. Continue without config? (y/N)". The fallback is an empty home (every boot pretends to be first boot).
|
||||
|
||||
### 3.3 First-ever boot with this mnemonic
|
||||
|
||||
If no manifest event exists yet (the npub has never published one), config-loader treats that as the "new user" case:
|
||||
- Creates an empty manifest.
|
||||
- Proceeds to materialize nothing.
|
||||
- User session starts with a blank home; any changes can be captured by the first save.
|
||||
|
||||
## 4. Save flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User
|
||||
participant CW as config-writer
|
||||
participant Sig as signer socket
|
||||
participant FS as /home
|
||||
participant R as user relays
|
||||
|
||||
User->>CW: trigger (explicit save, shutdown, periodic)
|
||||
|
||||
CW->>FS: scan tracked paths for changes
|
||||
FS-->>CW: list of modified slots
|
||||
|
||||
loop for each modified slot
|
||||
CW->>FS: read plaintext
|
||||
alt slot.encrypted
|
||||
CW->>Sig: NIP-44 encrypt(plaintext, to=self)
|
||||
Sig-->>CW: ciphertext
|
||||
CW->>Sig: sign event(kind=30078, d=slot, content=ciphertext, tags=...)
|
||||
else slot.encrypted == false
|
||||
CW->>Sig: sign event(kind=30078, d=slot, content=plaintext, tags=...)
|
||||
end
|
||||
Sig-->>CW: signed event
|
||||
CW->>R: publish
|
||||
end
|
||||
|
||||
CW->>Sig: sign updated manifest (if slot list changed)
|
||||
CW->>R: publish manifest
|
||||
```
|
||||
|
||||
### 4.1 When does save happen?
|
||||
|
||||
Three triggers, in increasing scope:
|
||||
|
||||
1. **Explicit.** `nostr-config save [slot-name]` — user-initiated save of one slot or all.
|
||||
2. **Shutdown.** On SIGTERM from systemd (clean shutdown), config-writer does a full save pass before poweroff continues. Hard-capped by a timeout so a bad relay doesn't block shutdown.
|
||||
3. **Periodic (opt-in).** For users who want continuous sync, an opt-in `periodic=true` flag in the manifest causes config-writer to run on a 5-minute timer. Off by default, because continuous publishing is visible metadata on the relay.
|
||||
|
||||
### 4.2 What gets saved
|
||||
|
||||
Config-writer is **not** a generic home-directory sync. It only touches files listed in the manifest. Anything outside the manifest is ephemeral by definition — which is exactly what we want.
|
||||
|
||||
Users who want to track a new file:
|
||||
|
||||
```bash
|
||||
nostr-config add-slot --name "dotfiles/vimrc" --path "/home/user/.vimrc" --mode 0644 --encrypted
|
||||
```
|
||||
|
||||
This updates the manifest and does an immediate save.
|
||||
|
||||
### 4.3 Detecting changes
|
||||
|
||||
Each slot event carries a `sha256` tag with the hash of the plaintext at publish time. Config-writer compares the current file hash to the last-saved hash; if different, the slot is saved. This avoids republishing unchanged files.
|
||||
|
||||
## 5. Conflict handling
|
||||
|
||||
Two n-OS-tr sessions, different machines, same mnemonic, both editing the same slot. Whose write wins?
|
||||
|
||||
Nostr's replaceable-event semantics say: most recent `created_at` wins. So:
|
||||
- Each slot event is tagged with a monotonic `created_at`.
|
||||
- On save, config-writer stamps the event with `now`.
|
||||
- Relays dedupe on `(author, kind, d)` keeping the latest `created_at`.
|
||||
|
||||
### 5.1 Detecting conflicts
|
||||
|
||||
When materializing at boot, config-loader also fetches the *previous* event ID (if any) and records it in `/run/nostr-config/state.json`. At save time, if a slot's remote event has advanced beyond what the loader saw at boot, we have a conflict:
|
||||
|
||||
| Scenario | Config-writer behavior |
|
||||
|-------------------------------------------|-----------------------------------------------------------------------------------|
|
||||
| Local modified, remote unchanged | Normal save. Publish new event. |
|
||||
| Local unchanged, remote advanced | No action. The old data is already overwritten on relay; we don't downgrade. |
|
||||
| Local modified **and** remote advanced | Conflict. User is prompted: show local diff vs remote diff, choose one or merge. |
|
||||
| Local modified, remote has multiple updates | Conflict with multi-option chooser. |
|
||||
|
||||
For phase-1 we implement:
|
||||
- Detect
|
||||
- Back up local plaintext to `/run/nostr-config/conflicts/<slot>.local`
|
||||
- Prefer remote by default in non-interactive modes (shutdown timeout)
|
||||
- Prompt on `nostr-config save` interactive runs.
|
||||
|
||||
### 5.2 Manifest version tag
|
||||
|
||||
The manifest itself carries a `version` integer. Session start reads the version; at save, if the remote manifest version is higher than what we started with, refuse to overwrite without an explicit merge step.
|
||||
|
||||
## 6. Example slot table
|
||||
|
||||
Concrete projection from slots to paths that the default n-OS-tr user will likely want:
|
||||
|
||||
| Slot name | Target path | Encrypted | Notes |
|
||||
|-----------------------------------|-------------------------------------------|-----------|------------------------------------|
|
||||
| `dotfiles/bashrc` | `/home/user/.bashrc` | yes | |
|
||||
| `dotfiles/bash_aliases` | `/home/user/.bash_aliases` | yes | |
|
||||
| `dotfiles/vimrc` | `/home/user/.vimrc` | yes | |
|
||||
| `dotfiles/gitconfig` | `/home/user/.gitconfig` | yes | Contains name/email |
|
||||
| `ssh/authorized_keys` | `/home/user/.ssh/authorized_keys` | yes | |
|
||||
| `ssh/config` | `/home/user/.ssh/config` | yes | |
|
||||
| `nostr/relays` | `/home/user/.config/n-os-tr/relays.json` | no | Can be NIP-65 if user prefers |
|
||||
| `nostr/blossom-servers` | `/home/user/.config/n-os-tr/blossom.json` | no | User's preferred Blossom endpoints |
|
||||
| `nostr/follows` | derived from kind 3 (NIP-02) directly | n/a | Not stored via this mechanism |
|
||||
| `firefox/bookmarks.json` | `/home/user/.mozilla/firefox/profile/bookmarks.json` | yes | Optional, app-specific |
|
||||
| `n-os-tr/roles` | `/run/nostr-id/roles.json` | yes | Managed by identity-agent, not user-edited |
|
||||
| `motd` | `/etc/motd` | no | "hello, $YOUR_NPUB" |
|
||||
|
||||
Not every slot needs to be present. The manifest enumerates only what the user chose to save.
|
||||
|
||||
## 7. Privacy properties
|
||||
|
||||
- **Confidentiality.** All personal slots are NIP-44 ciphertext. The ciphertext is IND-CCA2 under NIP-44's construction (xchacha20-poly1305 with derived keys). Relay operators observe ciphertext.
|
||||
- **Metadata exposure.** Relay operators still see: your npub, the kind (30078), the `d` tag (which is the human-readable slot name, e.g., "ssh/config"!), the `created_at`, and the event size.
|
||||
- Mitigation 1: run your own personal c-relay (NIP-06 index 2). Sync only there by default. Use remote relays as backup only.
|
||||
- Mitigation 2: optionally hash the `d` tag (`d = blake3(user_secret || slot_name)`). Debatable whether the loss of debuggability is worth the metadata win. Not v1.
|
||||
- **Replay / rollback attacks.** A malicious relay could serve an old `created_at` version of a slot. Mitigation: on boot, ask multiple relays and take the newest. If relays disagree, log a warning. For high-stakes slots (like `ssh/authorized_keys`), require agreement across N>=2 relays.
|
||||
- **Timing correlation.** Every n-OS-tr boot publishes a burst of events. An adversary observing relay traffic could correlate "these events came from the same boot." Mitigation: randomized delays between slot publishes; stagger manifest vs slot writes. Weak mitigation; users who need full anonymity should torify relay connections.
|
||||
|
||||
## 8. What this subsystem is NOT
|
||||
|
||||
- **Not a general filesystem-sync tool.** Only named slots are tracked. `git` repos, dev databases, photo libraries, anything large — all stay ephemeral.
|
||||
- **Not a binary blob store.** Slots are meant to be small JSON/text config. For photos, app binaries, datasets, etc., use Blossom via [`ginxsom`](../includes/ginxsom/).
|
||||
- **Not real-time sync.** Boot and shutdown by default. Periodic save is opt-in. Continuous multi-device sync is not a goal in v1.
|
||||
- **Not a backup system.** If all your relays forget your events simultaneously, your config is gone. Run your own c-relay. Pin events you care about.
|
||||
- **Not a place for secrets you wouldn't want a future-broken NIP-44 to reveal.** NIP-44 uses well-studied modern crypto, but any long-term encrypted-at-rest design ages. Do not use config slots to store, e.g., your Bitcoin seed. Slots are for *OS configuration*; secrets of last resort belong elsewhere (or nowhere).
|
||||
|
||||
## 9. Interaction with Phase 2 services
|
||||
|
||||
Today's services generate their own fresh keys via [`n-os-tr-firstboot`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-firstboot). Phase 3 reworks this:
|
||||
|
||||
- Service keys come from identity-agent (see [`plans/identity_subsystem.md`](identity_subsystem.md#57-integration-with-current-services)).
|
||||
- Service configuration comes from config-loader via slots:
|
||||
- `nostr/relays` → used by `c-relay` to decide which outbound peers to talk to.
|
||||
- `nostr/blossom-servers` → used by `ginxsom` for upstream mirroring.
|
||||
- `fips/peers` → used by `fips` to bootstrap.
|
||||
- None of this fundamentally changes the services themselves; it just replaces the "where does config live" question.
|
||||
|
||||
## 10. Implementation plan (Phase 3b)
|
||||
|
||||
Follow Phase 3a (identity-agent). All steps build on the `nostr_core_lib` primitives in [§1.1](#11-crypto-foundation-nostr_core_lib):
|
||||
|
||||
1. **Spec the event schema.** Nail down kind, tags, manifest JSON schema. Write it as `stack/config-projection/schema.md` plus a JSON Schema file that config-loader and config-writer both validate against on read/write.
|
||||
|
||||
2. **Prototype `nostr-config-loader` as a one-shot CLI.** Single fully static C99 binary linked against [`libnostr_core.a`](../includes/nostr_core_lib/) and built under the shared Alpine/musl toolchain (see [`plans/tui_login.md §0.1`](tui_login.md#01-toolchain-alpine--musl-dev-built-in-docker)). Output: `nostr-config-loader_static_<arch>`, `ldd` reports "not a dynamic executable."
|
||||
- Discovers user relays (bootstrap list → NIP-65 kind 10002 via [`synchronous_query_relays_with_progress`](../includes/nostr_core_lib/nostr_core/nostr_core.h)).
|
||||
- Fetches manifest (kind 30078, `d="n-os-tr:manifest:v1"`).
|
||||
- Decrypts via [`nostr_nip44_decrypt(priv_main, pub_main, ...)`](../includes/nostr_core_lib/nostr_core/nip044.h:62) — main privkey fetched from identity-agent, kept in locked memory only for the duration of the call.
|
||||
- For each slot in the manifest: single batch REQ with multiple `#d` values; decrypt each with `nostr_nip44_decrypt`; write to `slot.path` with `slot.mode`.
|
||||
- Exits when done; leaves nothing on disk except the materialized slot files.
|
||||
|
||||
3. **Prototype `nostr-config-writer` as a one-shot CLI.**
|
||||
- Reads manifest from `/run/nostr-config/manifest.json` (cached by the loader at boot).
|
||||
- Detects changed files via sha256 diff against cached hashes.
|
||||
- For each changed slot: encrypts with [`nostr_nip44_encrypt`](../includes/nostr_core_lib/nostr_core/nip044.h:28), signs via identity-agent NIP-46 ([`nostr_nip46_client_sign_event`](../includes/nostr_core_lib/nostr_core/nip046.h:158)), publishes via [`synchronous_publish_event_with_progress`](../includes/nostr_core_lib/nostr_core/nostr_core.h).
|
||||
- Republishes the manifest if the slot set changed.
|
||||
- Respects a hard timeout so shutdown doesn't block on a bad relay.
|
||||
|
||||
4. **Wire into systemd.**
|
||||
- `nostr-config-loader.service` — `Type=oneshot`, `After=nostr-id.service network-online.target`, `Before=multi-user.target`. Materializes home before user login.
|
||||
- `nostr-config-writer.service` — `Type=oneshot`, runs on `systemctl stop multi-user.target` path (drop-in in `shutdown.target.wants/`). Saves before poweroff.
|
||||
- `nostr-config-save.timer` (opt-in) — fires every 5 minutes when enabled via manifest `periodic=true`.
|
||||
|
||||
5. **CLI: `nostr-config`.** Wraps the two binaries with convenience commands:
|
||||
- `nostr-config status` — report on manifest version, last save time, unsaved changes.
|
||||
- `nostr-config save [slot|--all]` — force save.
|
||||
- `nostr-config load [slot|--all]` — pull remote and overwrite local (with confirm).
|
||||
- `nostr-config add-slot --name --path --mode [--encrypted|--plain]` — edit manifest.
|
||||
- `nostr-config remove-slot <name>` — edit manifest.
|
||||
- `nostr-config diff <slot>` — show local vs remote.
|
||||
- `nostr-config list` — print the slot table.
|
||||
|
||||
6. **Smoke test.** Extend [`iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest):
|
||||
- Given a known test mnemonic and a pre-populated local c-relay seeded with canned slot events, after `nostr-config-loader` runs, verify specific files exist at specific paths with specific sha256 sums.
|
||||
- After modifying a file and running `nostr-config save`, verify the local c-relay has a new kind-30078 event whose NIP-44-decrypted content matches the file.
|
||||
- Verify the manifest event's `created_at` advances when the slot set changes.
|
||||
- Verify that `journalctl -u nostr-config-loader` and `journalctl -u nostr-config-writer` contain no `nsec1` or 64-char lowercase hex that looks like a privkey.
|
||||
|
||||
7. **Conflict UI.** Simple interactive chooser for `nostr-config save` when a conflict is detected (per §5.1). Non-interactive mode preserves remote and writes the local version to `/run/nostr-config/conflicts/<slot>.local`.
|
||||
|
||||
8. **Periodic save (opt-in).** Timer unit, rate-limited. Explicit opt-in in the manifest.
|
||||
|
||||
9. **Self-test on boot.** Before the user's first session, loader does a manifest round-trip against a local c-relay (if present) or drops into "offline mode" cleanly. If NIP-44 decrypt fails on a slot (e.g., wrong mnemonic), user is warned *before* anything is materialized.
|
||||
|
||||
## 11. Open questions
|
||||
|
||||
- **How many slots is "too many" for a 15-second boot?** Rough math: each slot ~1-2 KB, one REQ round-trip per slot on first boot. With 50 slots and a fast relay, ~2 seconds total. We should still cap slot count (e.g., 200) and advise users with huge configs to self-host a relay.
|
||||
- **Batch fetches.** Can we issue a single REQ for all slots in the manifest and demux by `d`? Yes, NIP-01 filters allow multiple `#d` values. That's the intended optimization.
|
||||
- **Storing arbitrarily-large files as slots.** Not supported (see §8). But it's tempting. The line to hold is: slots are config, Blossom is blobs.
|
||||
- **Cross-device concurrent edits.** Phase-1 strategy is "last writer wins with conflict detection." More sophisticated CRDT-style merging is out of scope.
|
||||
- **Encrypted manifest but unencrypted individual slots.** Is that a useful mode? Probably not — if the manifest is encrypted, we can list everyone's slots publicly just by guessing `d` tags. Recommendation: manifest and slots encrypt or don't together.
|
||||
- **`d`-tag namespace collisions.** We use prefixes like `dotfiles/`, `ssh/`, `firefox/`. Should we formalize an IANA-ish registry inside the repo? Probably yes: `stack/config-projection/slot-registry.md` lists known slot names and their paths, so different n-OS-tr versions don't create divergent conventions for the same thing.
|
||||
- **Schema migration.** What happens when we bump the manifest schema from v1 to v2? The `ver` tag identifies schema version; a future loader refuses to materialize a manifest with an unsupported `version` and prompts for an explicit migration.
|
||||
Reference in New Issue
Block a user