/** * Owner/key canonicalisation for `PersistentMemoryStore` (layers 1 + 3 of the * three-layer fix — see the block stores' own files for layer 2). * * Layer 1 — validate and reject. `owner` and block `key` values are checked * against a shared allow-list; anything outside it throws rather than being * silently coerced into some backend's storage key. Rejecting has a property * encoding lacks: an owner manufactured by a bug (e.g. an empty-string * fallback) fails loudly here instead of getting a tidy, valid row. * * The allow-list is `^[A-Za-z0-9._@+:~|-]+$`. `:` and `|` stay legal because * real user ids use them (`google-oauth2|123`, `tenant:user`); `/`, `\`, * whitespace, control characters and glob characters are rejected. Because * `:` stays legal, string-keyed backends (Redis, File) still have to encode * — this allow-list alone does not make them collision-safe. * * Layer 3 — encode. `encodeSegment` (canonically defined on * `FileExtractedValueStore`, re-exported here) percent-encodes anything * outside a conservative filesystem-safe set. Backends that must flatten * (scope, owner, key) into one string use it; backends with a real * multi-column key (Postgres, SQLite) or a genuine tuple key (a nested Map) * never need it. * * `withOwnerValidation` applies layer 1 to any `PersistentMemoryStore` once, * rather than duplicating the check inside every backend — a raw backend * class stays exactly as permissive as it always was (constructing one * directly and handing it a stray `/` still works, sanitised by its own * layer 2/3 fix), and the reject-and-throw guarantee is opt-in at whichever * boundary chooses to apply it. */ import { encodeSegment as encodeSegmentImpl } from '../extract/FileExtractedValueStore.js'; import type { PersistentMemoryStore } from './types.js'; export declare class InvalidOwnerError extends Error { constructor(owner: string); } export declare class InvalidBlockKeyError extends Error { constructor(key: string); } /** * Non-throwing form, for a caller that must decide what to do about an invalid * owner rather than propagate. `wireWorkingMemory` uses this: an unusable owner * drops the memory surface for that session, exactly as an *unresolvable* one * already does, instead of throwing through the middle of a turn. Fail closed * without taking the conversation down with it. */ export declare function isValidOwner(owner: string): boolean; /** Non-throwing form of `assertValidBlockKey`. */ export declare function isValidBlockKey(key: string): boolean; /** Throws InvalidOwnerError unless owner matches the allow-list. */ export declare function assertValidOwner(owner: string): void; /** Throws InvalidBlockKeyError unless key matches the allow-list. */ export declare function assertValidBlockKey(key: string): void; /** Injective, reversible; the extracted-value namespace's encoder, re-exported. */ export declare const encodeSegment: typeof encodeSegmentImpl; /** Encode one segment of a Redis key. Escapes ':' and anything not allow-listed. */ export declare function encodeRedisSegment(part: string): string; /** * Encode one path segment. * * Three names get special handling because a filesystem reads them as something * other than a name: `.` and `..` are directory entries, and the Windows device * names above are hardware. In each case the first character is escaped, which * is enough to make the segment inert while leaving it readable and reversible. * * **Known limitation — case-insensitive filesystems.** macOS (APFS default) and * Windows (NTFS) fold case, so owners differing only by case — `Alice` and * `alice` — resolve to one directory here. Encoding the case away would make * every ordinary name unreadable (`USER.md` becomes `%55%53%45%52.md`), which * costs the human-editable-markdown property this store deliberately offers. * Closing it is a layout decision rather than a bug fix; see the board task * "Decide how the file memory store survives a case-insensitive filesystem". */ export declare function encodeFileSegment(part: string): string; /** * Reverse `encodeFileSegment`, and **never throw**. * * `decodeURIComponent` raises `URIError` on a bare `%` — `50%.md` is enough — * and `listBlocks` decodes every filename in a directory. A single hand-edited * or legacy file would otherwise take down listing for that whole (scope, * owner), not just its own entry. This store's own docs invite exactly that: * blocks are human-editable markdown and admin scripts may seed them. * * Strict on write, lenient on read: an undecodable name is returned verbatim, * which is what a human who created it by hand would expect to see. */ export declare function decodeFileSegment(part: string): string; /** * Wraps a `PersistentMemoryStore` so every call validates its owner (and * block key, where the method takes one) before reaching the backend. */ export declare function withOwnerValidation(store: PersistentMemoryStore): PersistentMemoryStore;