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.
- package/README.md +98 -435
- package/SECURITY.md +152 -42
- package/SUMMARY.md +1 -1
- package/adr/001-state-proxy-model.md +95 -96
- package/adr/002-observer-semantics.md +179 -180
- package/adr/003-deep-mutation-semantics.md +7 -8
- package/adr/004-array-mutation-semantics.md +127 -128
- package/adr/005-scheduler-contract.md +148 -149
- package/adr/006-context-isolation.md +91 -92
- package/adr/007-mutation-records.md +5 -6
- package/adr/008-transactions.md +6 -7
- package/adr/009-history-model.md +6 -7
- package/adr/README.md +46 -46
- package/adr/template.md +48 -49
- package/bin/cli.js +68 -0
- package/global.cjs +1462 -323
- package/global.js +1459 -324
- package/index.cjs +1462 -323
- package/index.d.ts +1 -0
- package/index.js +1459 -324
- package/llms.txt +42 -5
- package/markdown/AUDIT-REPORT.md +7 -8
- package/markdown/CACHE.md +190 -99
- package/markdown/DEVTOOLS.md +0 -1
- package/markdown/DISPATCH.md +0 -1
- package/markdown/HISTORY.md +0 -1
- package/markdown/IDB.md +0 -1
- package/markdown/IMPORT.md +0 -1
- package/markdown/INSPECT.md +0 -1
- package/markdown/LOGGER.md +0 -1
- package/markdown/MEMORY-ATTACHMENT.md +0 -1
- package/markdown/MEMORY.md +0 -1
- package/markdown/OBSERVER.md +0 -1
- package/markdown/PLATFORM.md +277 -271
- package/markdown/REDUX.md +54 -0
- package/markdown/SCHEMA.md +0 -1
- package/markdown/SESSION.md +0 -1
- package/markdown/SQLITE.md +0 -1
- package/markdown/STATE.md +0 -1
- package/markdown/STORE.md +0 -1
- package/markdown/SYNC.md +0 -1
- package/markdown/TYPED.md +0 -1
- package/markdown/USEOBSERVER.md +0 -1
- package/modules/redux.cjs +381 -10
- package/modules/redux.cjs.map +1 -1
- package/modules/redux.js +381 -10
- package/modules/redux.js.map +1 -1
- package/package.json +14 -2
- package/types/broadcast.d.ts +61 -0
- package/types/computed.d.ts +96 -0
- package/types/encryption.d.ts +129 -0
- package/types/exports.d.ts +9 -0
- package/types/memorio.d.ts +19 -12
- package/types/security.d.ts +67 -0
- package/types/session.d.ts +23 -5
- package/types/store.d.ts +19 -3
- package/vsix/memorio.vsix +0 -0
- package/markdown/CHANGELOG.md +0 -243
- package/markdown/PROJECT.md +0 -311
- package/markdown/SECURITY.md +0 -330
package/SECURITY.md
CHANGED
|
@@ -1,64 +1,174 @@
|
|
|
1
|
-
# Security
|
|
1
|
+
# Security & Encryption
|
|
2
2
|
|
|
3
|
-
Memorio
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
98
|
+
Pass an `encryptionKey` to `createContext()` and the tenant's isolated `store`/`session` automatically encrypt on write and decrypt on read:
|
|
17
99
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
103
|
+
const ctx = memorio.createContext('tenant-123', { encryptionKey: tenantKey })
|
|
25
104
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
```
|
|
130
|
+
```ts
|
|
48
131
|
import { state } from 'memorio'
|
|
49
132
|
```
|
|
50
133
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
with
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`internal.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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`
|