Security
Memorie treats memory as potentially sensitive application data.
Implemented today
-
Tenant isolation.
tenantId/namespace/subjectIdscoping is enforced in every adapter'slist()/count()and covered by the adapter contract test suite (runMemoryStoreContractTests, "enforces tenant isolation"). There is currently no cross-tenant query path in the core API — callers must always pass a scope. -
No content in error messages. Typed errors (
packages/types/src/errors.ts) carry structureddetails(ids, versions) but the engine does not interpolate memorycontentinto error messages. -
Typed, explicit lifecycle.
deletedis a terminal state; nothing in the core allows reviving a deleted memory (docs/MEMORY-LIFECYCLE). -
Authorization hooks (spec section 43, Phase 7):
authorizeRead,authorizeWrite,authorizeDeleteonMemoryEngineConfig.security. Each is(context: AuthorizationContext) => boolean | Promise<boolean>, wherecontextcarriesoperation,tenantId/namespace/subjectId, and (for single-memory operations)memoryId. Returning/resolvingfalsethrowsAuthorizationErrorbefore any store is touched — denial is checked first, so a synchronousfalseshort-circuits ahead of canonical or secondary store access.Hook Checked by authorizeReadget(),recall(),list(),count(),search()authorizeWriteadd()/remember(),update()/evolve()authorizeDeletedelete()const memory = createMemoryEngine({ memoryStore, security: { authorizeRead: (ctx) => ctx.tenantId === currentUser.tenantId, authorizeWrite: (ctx) => currentUser.canWrite(ctx.namespace), authorizeDelete: async (ctx) => currentUser.isAdmin, }, });All three are optional independently — configure only the ones you need; unconfigured operations proceed unchecked, same as before this was added.
-
No memory content in logs by default. The engine's own structured logging (
observability.logger, see below) never passescontentin thecontextobject it logs — only ids, namespace/subjectId/type, and counts. ALoggerimplementation that does something naive withcontext(e.g.console.log(message, context)) still won't leak memory content, because the engine never hands it any. -
Redaction hook.
security.redactonMemoryEngineConfig(RedactFnin@memorie/types):(memory, context) => Memory, applied to every memory leaving a read-path operation —get()/recall()/list()/search()/export()— after authorization passes. UnlikeauthorizeRead, which is all-or-nothing (deny the whole read),redactlets the read through while masking parts of the result — e.g. blankingcontentor stripping ametadatakey for callers who are allowed to know a memory exists but shouldn't see its raw value:const memory = createMemoryEngine({ memoryStore, security: { redact: (memory, ctx) => ctx.namespace === "medical" && !currentUser.isClinician ? { ...memory, content: "[redacted]" } : memory, }, });It must return a new object rather than mutating
memoryin place — the engine may reuse the same object for a cache write or hand it to a second caller, so an in-place mutation would leak the redaction (or the original) across callers.export()also runs throughredactwhen configured; note that a redacted export is lossy by design, soimport()-ing it back will not restore the original content — export unredacted data if you need a faithful backup. -
Encryption at rest.
@memorie/storage-securityprovidesEncryptedMemoryStore, aMemoryStoredecorator that transparently encryptscontentbefore it reaches any wrapped adapter (SQLite, Postgres, in-memory, or a future one) and decrypts it on the way back out — the engine,SearchStore/VectorStoreindexing, and everything else built against the plainMemoryStorecontract keeps working unmodified, because only what physically reaches disk is ciphertext:import { AesGcmCipher, EncryptedMemoryStore } from "@memorie/storage-security"; const memoryStore = new EncryptedMemoryStore(new PostgresStore(pool), { cipher: new AesGcmCipher(process.env.MEMORIE_ENCRYPTION_KEY!), }); const memory = createMemoryEngine({ memoryStore });AesGcmCipherimplements AES-256-GCM using onlynode:crypto— no external dependency, a fresh random IV perencrypt()call, anddecrypt()both decrypts and authenticates in one step (it throws on tampered or wrong-key ciphertext rather than returning garbage).Cipheris a small interface, so a KMS- or HSM-backed implementation can be swapped in without changingEncryptedMemoryStore.Scope: only
contentis encrypted.metadatais left as-is, because several adapters use it for structured filtering (list()/count()/keyword search) and encrypting it would break that — put anything needing the same protection intocontentinstead. Wrapping aTransactionalMemoryStorelosestransaction(); the decorator implements the plainMemoryStoresurface only. It's verified against the sharedrunMemoryStoreContractTestssuite (packages/storage-security/test/encrypted-memory-store.contract.test.ts), so it's a fully conformantMemoryStore, not a partial one.
Designed, not yet implemented
None outstanding from the original design brief's core security items
(spec section 43) — tenant isolation, authorization hooks, redaction,
and encryption at rest are all implemented above. Field-level
searchable encryption (querying encrypted content without decrypting
it first) is out of scope; EncryptedMemoryStore decrypts before
handing memories to SearchStore/VectorStore indexing, so keyword
and semantic search still work, but the underlying index/vector store
itself does not encrypt what it indexes.