/**
* Hardened IPFS access controller: closes an authorship-forgery gap in
* @orbitdb/core's IPFSAccessController.
*
* Upstream, entry verification is split in two: `Entry.verify` checks the
* signature against `entry.key` (the device signing key), and `canAppend`
* checks that the identity referenced by `entry.identity` is internally
* consistent and on the write list. NOTHING binds the two — a room member
* could craft an entry that references a VICTIM's identity record while
* being signed with the attacker's own key, and every replica would accept
* it and attribute it to the victim (fingerprint badge included).
*
* This wrapper delegates to the vanilla controller and adds the missing
* check: the entry's signing key must BE the referenced identity's signing
* key. Address determinism is preserved — the AC address hashed into the DB
* manifest is computed by the inner IPFSAccessController from
* {type:"ipfs", write} only, so wrapped and vanilla clients open the SAME
* database and interop freely (never change `write`, that would fork it).
*
* Caveat: we always open by db NAME, which routes through this factory. An
* open by raw /orbitdb/... address would rebuild the vanilla controller and
* silently skip the check — don't add such a path.
*/
import { IPFSAccessController } from "@orbitdb/core";
interface CanAppendEntry {
key?: string;
identity?: string;
}
interface AccessControllerInstance {
type: string;
address: string;
write: string[];
canAppend(entry: CanAppendEntry): Promise<boolean>;
}
interface FactoryParams {
orbitdb: unknown;
identities: {
getIdentity(hash: string): Promise<{ publicKey?: string } | undefined>;
};
name?: string;
address?: string;
}
type InnerFactory = (params: FactoryParams) => Promise<AccessControllerInstance>;
/** Same call shape as IPFSAccessController({ write }); see module docs. */
export function HardenedIPFSAccessController(options?: { write?: string[] }): InnerFactory {
const base = IPFSAccessController(options) as InnerFactory;
const factory: InnerFactory = async (params) => {
const ac = await base(params);
return {
...ac,
canAppend: async (entry) =>
(await ac.canAppend(entry)) && (await signerMatchesIdentity(params.identities, entry)),
};
};
return factory;
}
/** The one check upstream forgot: entry.key must be the identity's own key. */
export async function signerMatchesIdentity(
identities: FactoryParams["identities"],
entry: CanAppendEntry,
): Promise<boolean> {
if (!entry.key || !entry.identity) return false;
try {
const ident = await identities.getIdentity(entry.identity);
return !!ident && ident.publicKey === entry.key;
} catch {
return false;
}
}