{"id":"7b5839f4adea6f5a37744ee8a14a746181aa6ec215cbf8ae0b60bee4c6f7eecc","pubkey":"daa41bedb68591363bf4407f687cb9789cc543ed024bb77c22d2c84d88f54153","created_at":1759543861,"kind":30817,"tags":[["d","blossom-folder-lists"],["title","Blossom Folder Lists"],["k","30000"],["client","nostrhub.io"]],"content":"# Summary\nThis NIP standardizes how clients publish and consume parameterized, replaceable Nostr events that model file-system-style folders for blob storage backends (e.g., Blossom, NIP-96, Satellite). Each event represents a folder and lists the blob hashes it contains, allowing multiple clients to share identical folder structures.\n\n# Motivation\nBlossom and related services expose flat blob namespaces. Bloom and similar applications need a portable way to represent folders without changing storage protocols. A shared event format lets clients:\n\n- Synchronize folder structures across devices.\n- Coordinate folder names and memberships.\n- Extend storage tooling without proprietary metadata.\n\n# Terminology\n- **Folder List Event** – kind 30000 parameterized replaceable event that declares one folder and enumerates blob hashes (`src/lib/folderList.ts:6`).\n- **Folder Identifier** – the `d` tag value that uniquely identifies a folder event (`src/lib/folderList.ts:25`).\n- **Normalized Folder Path** – slash-delimited path produced by `normalizeFolderPathInput` (`src/utils/blobMetadataStore.ts:539`).\n- **Blob Hash** – lowercase SHA-256 hex digest referencing a stored object.\n- **Private Folder List** – encrypted kind 30000 event under the `d` tag `private` holding user-only metadata (`src/lib/privateList.ts:5`).\n\n# Public Folder Lists\n\n## Event Kind\nClients **must** publish folder definitions as kind 30000 parameterized replaceable events (per NIP-33 / NIP-51 semantics).\n\n## Identifier (`d` tag)\nEvery folder event **must** include:\n\n```json\n[\"d\", \"<identifier>\"]\n```\n\n`<identifier>` must be `bloom-folder:<path>`, where `<path>` is:\n\n- `__root__` for the root folder.\n- Otherwise `encodeURIComponent(<normalized-path>)`.\n\nRelays and clients **must** treat identifiers as case-sensitive.\n\n## Folder Path Tag\n```json\n[\"folder\", \"<normalized-path>\"]\n```\n\n- `<normalized-path>` must follow the normalization rules below.  \n- Root folders use an empty string.\n\n## Display Name\nOptional tag:\n\n```json\n[\"name\", \"<display-name>\"]\n```\n\nIf absent, consumers **should** fall back to `event.content` when non-empty (`src/lib/folderList.ts:80`).\n\n## Blob Membership\nFor each blob, add:\n\n```json\n[\"x\", \"<sha256>\"]\n```\n\n- `<sha256>` must be 64 lowercase hex characters.  \n- Duplicate hashes in one event **should** be ignored.\n\n## Event Content\n- `content` *may* mirror the human-readable folder name.  \n- If both `[\"name\", ...]` and `content` are present, the tag takes precedence.\n\n## Additional Tags\n- Implementations *may* add extra tags.  \n- Consumers **must** ignore unknown tags unless otherwise agreed.\n\n# Private Folder Lists (Encrypted)\n\nBloom keeps a user-only catalog of blob metadata and folder assignments.\n\n## Event Kind & Identifier\n- Still kind 30000.  \n- Use the parameterized identifier:\n\n```json\n[\"d\", \"private\"]\n```\n\n## Encryption\n- `content` **must** be encrypted to the author with NIP-44, falling back to NIP-04 when NIP-44 is unavailable (`src/lib/privateList.ts:144`, `src/lib/privateList.ts:191`).  \n- Consumers **must** decrypt with the signer's keypair.\n\n## Payload Structure\nThe decrypted JSON **must** follow this schema:\n\n```json\n{\n  \"version\": 1,\n  \"entries\": [\n    {\n      \"sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n      \"encryption\": {\n        \"algorithm\": \"aes-256-gcm\",\n        \"key\": \"base64-key\",\n        \"iv\": \"base64-iv\"\n      },\n      \"metadata\": {\n        \"name\": \"Vacation Photo.jpg\",\n        \"type\": \"image/jpeg\",\n        \"size\": 2048000,\n        \"audio\": null,\n        \"folderPath\": \"photos/trips/2025\"\n      },\n      \"servers\": [\"https://blossom.example\", \"https://files.other\"],\n      \"updatedAt\": 1719945600\n    }\n  ]\n}\n```\n\n### Structure Rules\n- `version` **must** be 1.  \n- Every entry **must** include `sha256`. Clients ignore entries with missing `sha256`.  \n- `encryption` is optional; when present, include `algorithm`, `key`, and `iv`.  \n- `metadata` is optional; `folderPath` should follow the same normalization rules. Use `null` to clear a folder assignment.  \n- `servers` is optional; Clients should trim each URL of trailing slashes.  \n- `updatedAt` is optional but recommended for conflict resolution.  \n- Clients publishing private lists overwrite the entire `entries` array; partial updates are undefined.  \n\n# Path Normalization\nWhen converting user input to folder paths (`src/utils/blobMetadataStore.ts:520–550`):\n\n1. Trim leading/trailing whitespace.  \n2. Split on `/`, trim each segment, discard empty segments.  \n3. Reject if any segment, after lowercasing and removing non-alphanumerics, contains the reserved keyword `private`.  \n4. Join remaining segments with `/`. An empty result denotes the root.  \n5. Inputs that normalize to `null` or `undefined` should be rejected.  \n\n# Root Folder\nRepresent the root folder with:\n\n```json\n[\"d\", \"bloom-folder:__root__\"]\n[\"folder\", \"\"]\n```\n\n- Clients **should not** store blob hashes in the root event; absence of hashes implies \"unfiled\" blobs.  \n\n# Client Behavior\n- Keep only the most recent event per identifier (the one with the highest `created_at` value wins).  \n- When applying folder membership, replace prior state with the latest event (`src/lib/folderList.ts:97`).  \n- Remove a blob by publishing a new event without that blob's `[\"x\", ...]` tag.  \n- Delete a folder by publishing the identifier with no `[\"x\", ...]` tags and an empty display name.  \n- For private lists, refresh and overwrite the complete entry set (`src/lib/privateList.ts:134`).  \n\n# Relay Recommendations\n- Support parameterized replaceable semantics keyed by `(kind, pubkey, d)`.  \n- Optionally enforce size limits to prevent oversized folder events.  \n\n# Security Considerations\n- Public folder lists expose mappings from pubkeys to blob hashes; treat as public metadata.  \n- Avoid placing sensitive information in folder names—blobs may reside on third-party servers.  \n- For private folder lists, implementations **must** safeguard decrypted payloads (e.g., never log plaintext).  \n\n# Examples\n\n## Example Public Folder Event (JSON Template)\n```json\n{\n  \"kind\": 30000,\n  \"created_at\": 1719945600,\n  \"pubkey\": \"b6d1…\",\n  \"tags\": [\n    [\"d\", \"bloom-folder:photos%2Ftrips%2F2025\"],\n    [\"folder\", \"photos/trips/2025\"],\n    [\"name\", \"Trips 2025\"],\n    [\"x\", \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\n    [\"x\", \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"]\n  ],\n  \"content\": \"Trips 2025\"\n}\n```\n\n## Example Root Folder Event\n```json\n{\n  \"kind\": 30000,\n  \"created_at\": 1719945601,\n  \"pubkey\": \"b6d1…\",\n  \"tags\": [\n    [\"d\", \"bloom-folder:__root__\"],\n    [\"folder\", \"\"]\n  ],\n  \"content\": \"  \"\n}\n```\n\n## Example Private Folder List Event (Encrypted Template)\n```json\n{\n  \"kind\": 30000,\n  \"created_at\": 1719945602,\n  \"pubkey\": \"b6d1…\",\n  \"tags\": [\n    [\"d\", \"private\"]\n  ],\n  \"content\": \"<encrypted nip44 ciphertext>\"\n}\n```\n\nThe decrypted payload utilizes the schema shown above.\n\n# Reference Implementation\n- Public folder events: `src/lib/folderList.ts`  \n- Path normalization helpers: `src/utils/blobMetadataStore.ts:520–550`  \n- Private folder list serialization: `src/lib/privateList.ts`","sig":"a38105d491d5e175351a89cadb93a30afc7ace202df5af892e54ac4696ec838b0fed38afe8fa32cda1d00a426a1d8a8c42286a9d27f397d2f49447d9bb5907d8"}