memorio 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +98 -435
  2. package/SECURITY.md +152 -42
  3. package/SUMMARY.md +1 -1
  4. package/adr/001-state-proxy-model.md +95 -96
  5. package/adr/002-observer-semantics.md +179 -180
  6. package/adr/003-deep-mutation-semantics.md +7 -8
  7. package/adr/004-array-mutation-semantics.md +127 -128
  8. package/adr/005-scheduler-contract.md +148 -149
  9. package/adr/006-context-isolation.md +91 -92
  10. package/adr/007-mutation-records.md +5 -6
  11. package/adr/008-transactions.md +6 -7
  12. package/adr/009-history-model.md +6 -7
  13. package/adr/README.md +46 -46
  14. package/adr/template.md +48 -49
  15. package/bin/cli.js +68 -0
  16. package/global.cjs +1462 -323
  17. package/global.js +1459 -324
  18. package/index.cjs +1462 -323
  19. package/index.d.ts +1 -0
  20. package/index.js +1459 -324
  21. package/llms.txt +42 -5
  22. package/markdown/AUDIT-REPORT.md +7 -8
  23. package/markdown/CACHE.md +190 -99
  24. package/markdown/DEVTOOLS.md +0 -1
  25. package/markdown/DISPATCH.md +0 -1
  26. package/markdown/HISTORY.md +0 -1
  27. package/markdown/IDB.md +0 -1
  28. package/markdown/IMPORT.md +0 -1
  29. package/markdown/INSPECT.md +0 -1
  30. package/markdown/LOGGER.md +0 -1
  31. package/markdown/MEMORY-ATTACHMENT.md +0 -1
  32. package/markdown/MEMORY.md +0 -1
  33. package/markdown/OBSERVER.md +0 -1
  34. package/markdown/PLATFORM.md +277 -271
  35. package/markdown/REDUX.md +54 -0
  36. package/markdown/SCHEMA.md +0 -1
  37. package/markdown/SESSION.md +0 -1
  38. package/markdown/SQLITE.md +0 -1
  39. package/markdown/STATE.md +0 -1
  40. package/markdown/STORE.md +0 -1
  41. package/markdown/SYNC.md +0 -1
  42. package/markdown/TYPED.md +0 -1
  43. package/markdown/USEOBSERVER.md +0 -1
  44. package/modules/redux.cjs +381 -10
  45. package/modules/redux.cjs.map +1 -1
  46. package/modules/redux.js +381 -10
  47. package/modules/redux.js.map +1 -1
  48. package/package.json +14 -2
  49. package/types/broadcast.d.ts +61 -0
  50. package/types/computed.d.ts +96 -0
  51. package/types/encryption.d.ts +129 -0
  52. package/types/exports.d.ts +9 -0
  53. package/types/memorio.d.ts +19 -12
  54. package/types/security.d.ts +67 -0
  55. package/types/session.d.ts +23 -5
  56. package/types/store.d.ts +19 -3
  57. package/vsix/memorio.vsix +0 -0
  58. package/markdown/CHANGELOG.md +0 -243
  59. package/markdown/PROJECT.md +0 -311
  60. package/markdown/SECURITY.md +0 -330
package/SECURITY.md CHANGED
@@ -1,64 +1,174 @@
1
- # Security
1
+ # Security & Encryption
2
2
 
3
- Memorio follows NIST and NSA security standards at the enterprise level.
3
+ This document covers Memorio's optional encryption layer, client-side multitenancy, and general security posture. For the short version, see the **Security** section in the main [README](./README.md).
4
4
 
5
- ## Security Standards
5
+ ---
6
+
7
+ ## Nothing is encrypted by default
8
+
9
+ This applies to every layer: `state`, `store`, `session`, `idb`, `memory`, `sqlite`. Treat browser storage as client-controlled unless you explicitly opt in.
10
+
11
+ Core has zero production dependencies. No `eval`, no dynamic code execution, no bundled telemetry, sanitized keys, validated inputs, bounded journal entries, UUID-based session identifiers. Independently checkable via Socket.dev / Snyk.
12
+
13
+ ---
14
+
15
+ ## The encryption layer
16
+
17
+ Memorio ships an optional encryption layer built on the **Web Crypto API** (`crypto.subtle`): AES-GCM for authenticated encryption, PBKDF2 for password-based key derivation. It is opt-in - no layer encrypts by default, so the common case stays transparent and fast.
18
+
19
+ All `memorio.encryption` operations are **asynchronous by design** - the Web Crypto API runs cryptographic work on a separate internal thread managed by the browser/runtime, so `deriveKey()`, `encrypt()`, and `decrypt()` return Promises that never block the main thread. PBKDF2 derivation at the NIST SP 800-132-recommended 600,000 iterations typically completes in under 300ms on mid-range hardware. No explicit Web Worker offload is needed.
20
+
21
+ ### Encrypt a value
22
+
23
+ ```ts
24
+ import { memorio } from 'memorio'
25
+
26
+ // Derive a key from a password (PBKDF2, 600k iterations - NIST SP 800-132)
27
+ const key = await memorio.encryption.deriveKey('my-password', 'per-tenant-salt')
28
+ // or generate one directly, without a password:
29
+ const freshKey = await memorio.encryption.generateKey()
30
+ const freshSalt = memorio.encryption.generateSalt()
31
+
32
+ // Encrypt → envelope object { __memorio_encrypted, alg, iv, data }
33
+ const envelope = await memorio.encryption.encrypt({ token: 'abc123' }, key)
34
+
35
+ // Decrypt - wrong key rejects rather than returning garbage
36
+ const original = await memorio.encryption.decrypt(envelope, key)
37
+ // → { token: 'abc123' }
38
+
39
+ // Check a value before trying to decrypt it
40
+ memorio.encryption.isEncrypted(envelope) // true - checks the __memorio_encrypted flag
41
+ memorio.encryption.isAvailable() // false in environments without Web Crypto (e.g. old runtimes)
42
+ ```
43
+
44
+ For the common "encrypt with a password, get a string back" case, skip manual key derivation:
45
+
46
+ ```ts
47
+ const cipherText = await memorio.encryption.encryptWithPassword({ token: 'abc123' }, 'my-password')
48
+ const plain = await memorio.encryption.decryptWithPassword(cipherText, 'my-password')
49
+ ```
50
+
51
+ Both accept/return an envelope object (not a JSON string when you already have a key):
52
+
53
+ ```ts
54
+ const envelope = await memorio.encryption.encrypt({ token: 'abc123' }, key)
55
+ const plain = await memorio.encryption.decrypt(envelope, key) // returns the original value
56
+ ```
57
+
58
+ **Key import/export** for storing CryptoKey material securely (e.g. in an httpOnly cookie):
59
+
60
+ ```ts
61
+ const key = await memorio.encryption.generateKey()
62
+ const { key: b64, iv } = await memorio.encryption.exportKey(key)
63
+ // Store b64 securely, then restore later:
64
+ const restoredKey = await memorio.encryption.importKey(b64, iv)
65
+ ```
66
+
67
+ ### Store with at-rest encryption
68
+
69
+ ```ts
70
+ // Per-call encryption (opt-in)
71
+ await store.set('api_token', secretValue, { encrypt: key })
72
+ const token = await store.get('api_token', { decrypt: key })
73
+
74
+ // Or configure a default key for automatic encryption:
75
+ store.config({ encryptionKey: key })
76
+ await store.set('api_token', secretValue) // encrypted transparently
77
+ const token = await store.get('api_token') // decrypted transparently
78
+ ```
6
79
 
7
- - **NIST Guidelines**: Follows NIST SP 800-53 security controls and NIST Cybersecurity Framework
8
- - **NSA Standards**: Defense-grade security practices; considers nation-state level threats in risk assessment
80
+ `session` supports the same `config` and per-call opts.
9
81
 
10
- ## Supply Chain Security
82
+ > ⚠️ **Sync vs async changes once encryption is on.** Without encryption, `store.get`/`store.set` (and `session`'s equivalents) are synchronous - they return the value directly. As soon as either call touches `encrypt`/`decrypt` (per-call, or because a default `encryptionKey` is configured), both become asynchronous and return a `Promise`. Code written against the plain synchronous API will silently receive a `Promise` instead of a value if encryption is turned on later - always `await` `store.get`/`store.set` defensively if there's any chance encryption gets enabled down the line.
11
83
 
12
- - **Socket.dev**: Minimum target score 90%; all alarms must be resolved before release
13
- - **Dependency Management**: Zero production dependencies (fully dependency-free); dev dependencies audited regularly
14
- - **Prohibited**: No `eval()` usage, no encrypted/obfuscated code in builds, no hardcoded secrets
84
+ ---
85
+
86
+ ## Contexts are not authorization
87
+
88
+ ```ts
89
+ memorio.createContext('tenant-123')
90
+ ```
91
+
92
+ Contexts organize and isolate application concerns by namespace-prefixing keys - they are **not** a security boundary. Any script running in the same JS runtime can attempt operations against another context. Real tenant isolation belongs at the auth/backend layer.
93
+
94
+ ---
95
+
96
+ ## Encrypted multitenancy
15
97
 
16
- ## Code Security
98
+ Pass an `encryptionKey` to `createContext()` and the tenant's isolated `store`/`session` automatically encrypt on write and decrypt on read:
17
99
 
18
- - No hardcoded credentials or API keys
19
- - Secure random session ID generation (`crypto.randomUUID` → `crypto.getRandomValues` → fallback)
20
- - Input validation on all public APIs
21
- - XSS prevention on DevTools data export
22
- - Property-based access control on global objects (`Object.defineProperty` with `enumerable: false`)
100
+ ```ts
101
+ const tenantKey = await memorio.encryption.deriveKey(tenantPassword, tenantSalt)
23
102
 
24
- ## OWASP Compliance
103
+ const ctx = memorio.createContext('tenant-123', { encryptionKey: tenantKey })
25
104
 
26
- Addresses OWASP Top 10 (2021):
27
- - A01:2021 - Broken Access Control (global object protection, property locks)
28
- - A02:2021 - Cryptographic Failures (crypto.randomUUID for session IDs)
29
- - A03:2021 - Injection (CSS sanitization in devtools)
30
- - A05:2021 - Security Misconfiguration (minimal surface area, no bundled secrets)
31
- - A06:2021 - Vulnerable and Outdated Components (regular npm audit, Socket.dev)
32
- - A07:2021 - Identification and Authentication Failures (N/A - library, no auth)
33
- - A08:2021 - Software and Data Integrity Failures (strict tsconfig, lock files)
34
- - A09:2021 - Security Logging and Monitoring Failures (DevTools inspect, Logger module)
35
- - A10:2021 - Server-Side Request Forgery (N/A - no network requests)
105
+ // Values are encrypted at rest within this context's namespace
106
+ await ctx.store.set('secrets', { apiKey: '…' })
107
+ const secrets = await ctx.store.get('secrets') // decrypted transparently
36
108
 
37
- ## Global API Security
109
+ // Another tenant with a different key can't read this data
110
+ const otherCtx = memorio.createContext('tenant-456', { encryptionKey: otherKey })
111
+ const leaked = await otherCtx.store.get('memorio_store_secrets') // fails to decrypt
112
+ ```
113
+
114
+ This adds a cryptographic wall on top of the namespace prefix: even if a tenant's key prefix is guessed, the ciphertext is unreadable without the key. It still doesn't replace backend authorization - see above.
115
+
116
+ > ⚠️ **`cache` is never encrypted, even inside an encrypted context.** `createContext(name, { encryptionKey })` only encrypts that context's `store` and `session` - its `cache` stays volatile, in-memory, and in plaintext regardless. Don't put anything in `ctx.cache` you wouldn't put in unencrypted `cache`.
117
+
118
+ `memorio.isolate()` is a thin alias for `createContext()` and accepts the same `{ encryptionKey }` option.
119
+
120
+ > **Key management is yours.** Derive keys from authenticated session material or generate them with `memorio.encryption.generateKey()`. Never hardcode keys in client bundles.
121
+
122
+ ---
38
123
 
39
- Memorio does **not** expose its APIs on `globalThis` by default. The opt-in
40
- entrypoint `import 'memorio/global'` installs `state`, `store`, `session`,
41
- `cache`, `idb`, `sqlite`, `observer`, and `useObserver` on `globalThis`.
42
- This is an explicit consumer choice - not inferred from `NODE_ENV` or
43
- bundler environment variables.
124
+ ## Global API security
125
+
126
+ Memorio does **not** expose its APIs on `globalThis` by default. The opt-in entrypoint `import 'memorio/global'` installs `state`, `store`, `session`, `cache`, `idb`, `sqlite`, `observer`, and `useObserver` on `globalThis` - this is an explicit consumer choice, never inferred from `NODE_ENV` or bundler environment variables.
44
127
 
45
128
  For application code, prefer explicit named imports:
46
129
 
47
- ```js
130
+ ```ts
48
131
  import { state } from 'memorio'
49
132
  ```
50
133
 
51
- See [SECURITY-HARDENING.md](./SECURITY-HARDENING.md) for a full hardening
52
- guide covering storage encryption, context isolation, logger data capture,
53
- and platform capability assumptions.
134
+ ---
135
+
136
+ ## Code-level hardening
137
+
138
+ - No hardcoded credentials, API keys, or secrets anywhere in the codebase or build output.
139
+ - Session IDs use a secure random source with graceful fallback: `crypto.randomUUID` → `crypto.getRandomValues` → a documented fallback if neither is available.
140
+ - Public APIs validate their inputs rather than trusting caller-supplied shapes.
141
+ - Global objects installed via `memorio/global` use `Object.defineProperty` with `enumerable: false` where applicable, rather than plain assignment.
142
+ - No obfuscated or minified-beyond-readability code shipped in a way that would hide behavior from review.
143
+
144
+ ---
145
+
146
+ ## Supply chain
54
147
 
55
- If you find a security vulnerability:
148
+ - Zero production dependencies in core - nothing to audit transitively for the parts you actually ship.
149
+ - Dev dependencies are audited periodically (`npm audit` and equivalent).
150
+ - Package integrity is checkable independently via Socket.dev / Snyk before you adopt a given version.
56
151
 
57
- 1. Email [Dario Passariello](mailto:dariopassarielloa@gmail.com)
58
- 2. Or visit https://dario.passariello.ca/contact/
152
+ > If you require a specific minimum Socket.dev/Snyk score or a documented audit trail as part of a procurement process, verify the current score yourself at adoption time - this document doesn't pin a number that could go stale.
59
153
 
60
- Do not open public issues for security vulnerabilities.
154
+ ---
155
+
156
+ ## Reporting a vulnerability
157
+
158
+ Please don't open a public issue for a security vulnerability. Instead:
159
+
160
+ 1. Email [Dario Passariello](mailto:dariopassarielloa@gmail.com), or
161
+ 2. Use the contact form at https://dario.passariello.ca/contact/
61
162
 
62
163
  ---
63
- *Document version: 2.0 - Last updated: 2026-05-19*
64
- *Owner: BigLogic Security Team*
164
+
165
+ ## Enterprise compliance notes
166
+
167
+ | Concern | What you get | Impact |
168
+ | --- | --- | --- |
169
+ | **Opt-in encryption** | `memorio.encryption` provides AES-GCM + PBKDF2. `store`/`session` and encrypted contexts support automatic encrypt-on-write/decrypt-on-read. | Not encrypted by default - you must opt in via `store.config({ encryptionKey })` or `createContext(id, { encryptionKey })`. Without it, PII, tokens, and regulated data (GDPR/HIPAA) are at rest in plaintext. Enabling it also changes the calling convention - see the sync/async warning above. |
170
+ | **Client-side multitenancy** | `createContext(id, { encryptionKey })` provides namespace + cryptographic isolation between tenants. | Still **not** a security boundary - any script in the same JS runtime can attempt context operations. Encryption makes cross-tenant reads cryptographically hard, but real authorization must live on the backend. |
171
+ | **DevTools** | Expose data for development only. | Only active when you opt into `memorio/global`; never ship enabled in production. |
172
+ | **Formal compliance frameworks (NIST, OWASP, etc.)** | No formal third-party audit or certification has been published for this project. | If your procurement process requires a certified compliance framework, commission an independent audit rather than relying on self-declared conformance - see [When to use something else](./README.md#when-to-use-something-else) in the README. |
173
+
174
+ If any of these gaps block adoption, keep Memorio for memory/state lifecycle and pair it with a dedicated secrets manager or backend-enforced authorization for the pieces it explicitly doesn't cover.
package/SUMMARY.md CHANGED
@@ -29,7 +29,7 @@
29
29
 
30
30
  | File | Cosa documenta |
31
31
  |---|---|
32
- | `AUDIT-REPORT.md` | Audit di sicurezza/performance/affidabilità/qualità del codice per la v4.9.5 (datato 2026-09-06), con azioni correttive già applicate. |
32
+ | `AUDIT-REPORT.md` | Audit di sicurezza/performance/affidabilità/qualità del codice per la v5.1.0 (datato 2026-09-06), con azioni correttive già applicate. |
33
33
  | `CACHE.md` | Reference della cache in-memory: API, quando usarla, limiti. |
34
34
  | `CHANGELOG.md` | Storico versioni dalla v2.5.0 alla `Unreleased`, con bugfix, refactoring e note di sicurezza per ogni release. |
35
35
  | `DEVTOOLS.md` | Strumenti di debug da console del browser per ispezionare/gestire lo stato di Memorio. |
@@ -1,96 +1,95 @@
1
- # ADR-001: State Proxy Model
2
-
3
- > **Status:** Accepted
4
- > **Date:** 2026-09-12
5
- > **Deciders:** Memorio 5.x Core Team
6
-
7
- ## Context
8
-
9
- Memorio's reactive state is built on a JavaScript `Proxy`. Every read of
10
- a nested object re-wraps the child in a Proxy so that deep
11
- `set`/`delete` traps fire and dispatch events. The design must answer:
12
-
13
- - How are proxy wrappers cached to avoid exponential allocation on
14
- repeated reads?
15
- - What is the reference-identity contract between the proxy and the
16
- raw target?
17
- - Which property names are reserved and cannot be used as state keys?
18
- - How are non-plain objects (Date, Set, Map, functions) handled?
19
-
20
- ## Assumptions
21
-
22
- - The Proxy target is always a plain `Object` or `Array`.
23
- - A single `buildProxy` invocation creates one Proxy per raw object.
24
- - The Proxy is shared across all import paths via `globalThis`
25
- singletons (see ADR-006).
26
- - `__DEV__` is a build-time constant (replaced to `false` in
27
- production, `true` in dev/test).
28
-
29
- ## Decision
30
-
31
- ### Wrapper caching
32
-
33
- `buildProxy` caches the wrapper in a `WeakMap<rawTarget, proxyWrapper>`.
34
- On every `get` trap, after unwrapping with `TARGET`, the result is looked
35
- up in the cache; if present, the existing wrapper is returned instead of
36
- allocating a new one. This keeps repeated nested reads at O(1) in
37
- allocation and O(1) in proxy depth.
38
-
39
- ### Reference identity
40
-
41
- - `TARGET` is a `Symbol` accessor on the proxy that returns the raw
42
- underlying target. `state[TARGET]` === raw root.
43
- - `state.a[TARGET]` === raw target's `a` property.
44
- - `JSON.stringify(proxy)` works because `ownKeys` and
45
- `getOwnPropertyDescriptor` delegate to `Reflect`.
46
- - `deepRaw(value)` unwraps all Memorio proxies recursively using
47
- `TARGET` and a `WeakSet` for cycle safety, returning a plain clone
48
- with no wrapper references.
49
-
50
- ### Reserved property names
51
-
52
- `protect` array in `core/internal.ts` reserves these keys on the root
53
- `state` proxy:
54
-
55
- ```
56
- list, state, store, idb, cache, sqlite, observer, useObserver,
57
- remove, removeAll, _platform, _capabilities, _sessionId
58
- ```
59
-
60
- Setting any of these keys as state data is rejected by the `set` trap
61
- with a console error. Attempting to set any `_`-prefixed key that
62
- collides with internal bookkeeping is also guarded.
63
-
64
- ### Non-plain objects
65
-
66
- - `Date`, `Set`, `Map`, `RegExp`, functions, and other non-plain
67
- instances are returned by reference — they are **not** re-wrapped in a
68
- Proxy.
69
- - `deepRaw` only traverses `Object` and `Array` constructors; all other
70
- types are returned by reference.
71
-
72
- ### Path tracking
73
-
74
- Every `get` trap for a string property sets `internal.lastAccessedPath`
75
- to the full dotted path (`state.foo.bar`). When
76
- `internal.tracking` is true, the path is also added to
77
- `internal.trackedPaths`.
78
-
79
- ## Consequences
80
-
81
- - **Positive:** O(1) wrapper allocation on repeated nested reads; no
82
- proxy-depth accumulation on spread updates (regression tested).
83
- - **Positive:** `TARGET` symbol prevents prototype-pollution attacks
84
- via `__proto__`/`constructor`/`prototype` because those are string
85
- keys that are simply treated as data keys on the raw target.
86
- - **Positive:** `deepRaw` produces clean data for persistence and
87
- snapshots without leaking proxy wrappers.
88
- - **Negative:** The `WeakMap` cache is per-module-instance, but since
89
- the proxy target is a singleton on `globalThis`, the cache is
90
- effectively shared across import paths.
91
- - **Negative:** Non-plain objects cannot be deeply observed for inner
92
- mutations — this is an accepted limitation documented in the API.
93
-
94
- ## Compliance tests
95
-
96
- - `tests/vitest/tests/contracts/adr-001-state.test.ts`
1
+ # ADR-001: State Proxy Model
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ Memorio's reactive state is built on a JavaScript `Proxy`. Every read of
9
+ a nested object re-wraps the child in a Proxy so that deep
10
+ `set`/`delete` traps fire and dispatch events. The design must answer:
11
+
12
+ - How are proxy wrappers cached to avoid exponential allocation on
13
+ repeated reads?
14
+ - What is the reference-identity contract between the proxy and the
15
+ raw target?
16
+ - Which property names are reserved and cannot be used as state keys?
17
+ - How are non-plain objects (Date, Set, Map, functions) handled?
18
+
19
+ ## Assumptions
20
+
21
+ - The Proxy target is always a plain `Object` or `Array`.
22
+ - A single `buildProxy` invocation creates one Proxy per raw object.
23
+ - The Proxy is shared across all import paths via `globalThis`
24
+ singletons (see ADR-006).
25
+ - `__DEV__` is a build-time constant (replaced to `false` in
26
+ production, `true` in dev/test).
27
+
28
+ ## Decision
29
+
30
+ ### Wrapper caching
31
+
32
+ `buildProxy` caches the wrapper in a `WeakMap<rawTarget, proxyWrapper>`.
33
+ On every `get` trap, after unwrapping with `TARGET`, the result is looked
34
+ up in the cache; if present, the existing wrapper is returned instead of
35
+ allocating a new one. This keeps repeated nested reads at O(1) in
36
+ allocation and O(1) in proxy depth.
37
+
38
+ ### Reference identity
39
+
40
+ - `TARGET` is a `Symbol` accessor on the proxy that returns the raw
41
+ underlying target. `state[TARGET]` === raw root.
42
+ - `state.a[TARGET]` === raw target's `a` property.
43
+ - `JSON.stringify(proxy)` works because `ownKeys` and
44
+ `getOwnPropertyDescriptor` delegate to `Reflect`.
45
+ - `deepRaw(value)` unwraps all Memorio proxies recursively using
46
+ `TARGET` and a `WeakSet` for cycle safety, returning a plain clone
47
+ with no wrapper references.
48
+
49
+ ### Reserved property names
50
+
51
+ `protect` array in `core/internal.ts` reserves these keys on the root
52
+ `state` proxy:
53
+
54
+ ```
55
+ list, state, store, idb, cache, sqlite, observer, useObserver,
56
+ remove, removeAll, _platform, _capabilities, _sessionId
57
+ ```
58
+
59
+ Setting any of these keys as state data is rejected by the `set` trap
60
+ with a console error. Attempting to set any `_`-prefixed key that
61
+ collides with internal bookkeeping is also guarded.
62
+
63
+ ### Non-plain objects
64
+
65
+ - `Date`, `Set`, `Map`, `RegExp`, functions, and other non-plain
66
+ instances are returned by reference - they are **not** re-wrapped in a
67
+ Proxy.
68
+ - `deepRaw` only traverses `Object` and `Array` constructors; all other
69
+ types are returned by reference.
70
+
71
+ ### Path tracking
72
+
73
+ Every `get` trap for a string property sets `internal.lastAccessedPath`
74
+ to the full dotted path (`state.foo.bar`). When
75
+ `internal.tracking` is true, the path is also added to
76
+ `internal.trackedPaths`.
77
+
78
+ ## Consequences
79
+
80
+ - **Positive:** O(1) wrapper allocation on repeated nested reads; no
81
+ proxy-depth accumulation on spread updates (regression tested).
82
+ - **Positive:** `TARGET` symbol prevents prototype-pollution attacks
83
+ via `__proto__`/`constructor`/`prototype` because those are string
84
+ keys that are simply treated as data keys on the raw target.
85
+ - **Positive:** `deepRaw` produces clean data for persistence and
86
+ snapshots without leaking proxy wrappers.
87
+ - **Negative:** The `WeakMap` cache is per-module-instance, but since
88
+ the proxy target is a singleton on `globalThis`, the cache is
89
+ effectively shared across import paths.
90
+ - **Negative:** Non-plain objects cannot be deeply observed for inner
91
+ mutations - this is an accepted limitation documented in the API.
92
+
93
+ ## Compliance tests
94
+
95
+ - `tests/vitest/tests/contracts/adr-001-state.test.ts`