Skip to content

Cross-Identity Public Directory Access

Enhanced s5.js supports reading files from another user’s public directory tree and subscribing to live updates — without needing their identity. This powers multi-user workflows such as creator/operator platforms, follower fan-out, public content indexes, and reactive UIs that receive push updates without polling.

FS5 child directories are stored unencrypted — only the root directory is encrypted. The barrier to cross-identity access is that outsiders cannot compute the directory’s registry public key (derived from the owner’s identity). Three methods solve this:

  1. Owner extracts a directory’s 32-byte Ed25519 public key via getPublicDirectoryKey() and shares it
  2. Reader uses that key to read file content via readFromPublicDirectory() (no identity required)
  3. Reader resolves sub-directory pubkeys via getPublicDirectoryKeyFrom() and subscribes to push updates via api.registryListen(pk)

Together, these enable genuine multi-identity app architectures on S5.

Extract the 32-byte Ed25519 public key for one of your own directories. Requires an initialized identity.

async getPublicDirectoryKey(path: string): Promise<Uint8Array>

Parameters:

  • path — Path to a directory in your own filesystem (e.g., "home/storefront")

Returns: 32-byte Uint8Array (Ed25519 public key without the 0xed multikey prefix)

Throws: If the directory doesn’t exist or doesn’t use an Ed25519 registry key.

// Owner: extract and share
const pubKey = await s5.fs.getPublicDirectoryKey("home/storefront");
const keyString = base64url(pubKey); // Share with readers via your app

Read a file from another user’s unencrypted directory tree. Does not require identity — only needs network access through s5.api.

async readFromPublicDirectory(
remotePubKey: Uint8Array,
subpath: string
): Promise<Uint8Array | undefined>

Parameters:

  • remotePubKey — 32-byte Ed25519 public key (from the owner’s getPublicDirectoryKey())
  • subpath — Path within the remote directory (e.g., "config/brand.json")

Returns: Raw file bytes as Uint8Array, or undefined if the file is not found, the path is invalid, or the file is encrypted.

Throws: If remotePubKey is not exactly 32 bytes.

// Reader: no identity required
const viewerFs = new FS5(api);
const data = await viewerFs.readFromPublicDirectory(
remotePubKey,
"config/brand.json"
);
if (data) {
const config = JSON.parse(new TextDecoder().decode(data));
}

Resolve the 32-byte Ed25519 registry public key for any sub-directory under another user’s tree. Does not require identity. The returned pubkey is ready to pass directly to api.registryListen(pk) for live push subscriptions.

async getPublicDirectoryKeyFrom(
remotePubKey: Uint8Array,
subpath: string
): Promise<Uint8Array | undefined>

Parameters:

  • remotePubKey — 32-byte Ed25519 public key (from getPublicDirectoryKey() or a shared root pubkey)
  • subpath — Path within the remote directory (e.g., "followers/alice/follow"). Empty string ("") or "/" returns the input remotePubKey unchanged (pass-through).

Returns: 32-byte Uint8Array (Ed25519 public key without the 0xed multikey prefix), or undefined if any segment is missing, the final segment is a file, the link is immutable (fixed_hash_blake3), or an intermediate directory is encrypted.

Throws: If remotePubKey is not exactly 32 bytes.

// Reader: subscribe to live updates under another user's sub-directory
const newsPk = await viewerFs.getPublicDirectoryKeyFrom(
remotePubKey,
"home/platform/acme/news"
);
if (newsPk) {
// Push updates — no polling
for await (const entry of api.registryListen(newsPk)) {
refreshNewsFeed(entry);
}
}
ScenarioResult
File foundUint8Array (raw bytes)
File not foundundefined
Directory segment missingundefined
No registry entry for keyundefined
Encrypted fileundefined (reader cannot decrypt)
Encrypted intermediate directoryundefined (defensive; child dirs are unencrypted in practice)
Empty subpathundefined
Invalid key lengthThrows Error
ScenarioResult
Directory found on a mutable-registry (Ed25519) link32-byte Uint8Array pubkey
Empty subpath ("" or "/")input remotePubKey unchanged (pass-through)
Any segment missingundefined
Final segment is a file, not a directoryundefined
Any segment is a fixed_hash_blake3 (immutable) linkundefined (no registry entry to subscribe to)
No registry entry for remote pubkeyundefined
Encrypted intermediate directoryundefined (defensive)
Invalid key lengthThrows Error

“Missing” in the tables above means genuinely absent (no registry entry). If a directory along the subpath is known to exist but its content is temporarily unavailable (a transient blob 404 during network propagation), both methods throw a retryable S5DirectoryLoadError instead of returning undefined — so a momentary outage is never mistaken for deleted content. Retry with backoff. A 404 of the final file’s content in readFromPublicDirectory() still returns undefined (best-effort read). See Error Handling.

A storefront operator publishes content; viewers read it and subscribe to a news feed for live updates.

import { S5, FS5 } from '@julesl23/s5js';
// === Operator (one-time setup) ===
const operatorS5 = await S5.create({ initialPeers: [...] });
await operatorS5.recoverIdentityFromSeedPhrase(seedPhrase);
// Publish content
await operatorS5.fs.put("home/storefront/catalogue.json", catalogueData);
await operatorS5.fs.put("home/storefront/news/latest.json", latestNews);
// Extract and share the root storefront public key
const storefrontPubKey = await operatorS5.fs.getPublicDirectoryKey(
"home/storefront"
);
// Share storefrontPubKey with viewers (platform config, URL param, etc.)
// === Viewer (reading files — no identity required) ===
const viewerS5 = await S5.create({ initialPeers: [...] });
// Note: no recoverIdentityFromSeedPhrase() call
const data = await viewerS5.fs.readFromPublicDirectory(
storefrontPubKey,
"catalogue.json"
);
const catalogue = JSON.parse(new TextDecoder().decode(data!));
// === Viewer (live subscribing to news updates) ===
const newsPk = await viewerS5.fs.getPublicDirectoryKeyFrom(
storefrontPubKey,
"news"
);
if (newsPk) {
// Iterator yields RegistryEntry on every update to the news directory
for await (const entry of viewerS5.api.registryListen(newsPk)) {
console.log("News updated, refreshing…");
await refreshNewsFeed(viewerS5.fs, storefrontPubKey);
}
}
  • Creator / operator platforms — operators publish storefronts; viewers read them without needing operator credentials.
  • Follower fan-out — a creator subscribes to each follower’s follow record for push updates, replacing polling.
  • Cross-persona data sharing — in apps with multiple personas per user, one persona publishes while another reads.
  • Public content indexes — a service maintains a public index of resources that clients read and subscribe to.
  • Reactive UIs — replace polling with registryListen() subscriptions over cross-identity pubkeys.
  • Access control: Once you share a directory’s public key, anyone with it can read every file in that directory tree. Only share pubkeys for data you genuinely want to be public.
  • Encryption: FS5 child directories are unencrypted; only the root is encrypted. Don’t put private data in child directories and then share a public key for them.
  • Identity leakage: The returned 32-byte pubkeys do not leak the owner’s identity seed or signing keys — they’re derived public values. Safe to share.
  • No write access: These methods are read-only. A reader cannot modify the owner’s data. Multi-writer is not supported.
  • Length validation: Both methods throw on remotePubKey.length !== 32 to prevent accidental misuse with prefixed (33-byte) keys.
  • getPublicDirectoryKey() — beta.46+
  • readFromPublicDirectory() — beta.46+
  • getPublicDirectoryKeyFrom() — beta.47+
  • Retryable S5DirectoryLoadError on transient directory 404s (instead of undefined) — beta.50+

All three methods are fully additive with no breaking changes to earlier APIs.