circuitjson-toolkit 1.3.0 → 1.4.0
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 +48 -0
- package/docs/api.md +84 -0
- package/docs/release-notes-v1.4.0.md +84 -0
- package/package.json +2 -1
- package/src/core/Parser.mjs +1 -1
- package/src/core/context/BinaryDataSnapshot.mjs +59 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +55 -0
- package/src/core/context/CircuitJsonExtensionBoundary.mjs +312 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +115 -59
- package/src/core/context/CircuitJsonValidationProof.mjs +122 -10
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +233 -0
- package/src/core/context/StructuredCloneAdoption.mjs +975 -0
- package/src/core/context/StructuredCloneCollectionNormalizer.mjs +86 -0
- package/src/core/context/StructuredCloneTextAccounting.mjs +55 -0
- package/src/core/context/StructuredDataSnapshot.mjs +53 -0
- package/src/core/contracts/DocumentResult.mjs +38 -6
|
@@ -1,11 +1,226 @@
|
|
|
1
1
|
import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
|
|
2
2
|
|
|
3
3
|
const PROTECTED_BINARY_GETTERS = new WeakSet()
|
|
4
|
+
const CAPTURED_BINARY_PROPERTIES = new WeakSet()
|
|
5
|
+
const PENDING_BINARY_CAPTURES = new WeakMap()
|
|
6
|
+
const BINARY_COPY_CHUNK_BYTES = 64 * 1024
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Protects byte-backed values embedded in canonical extension containers.
|
|
7
10
|
*/
|
|
8
11
|
export class ProtectedExtensionBinaryBoundary {
|
|
12
|
+
/**
|
|
13
|
+
* Protects the exact binary properties collected by a proven clone
|
|
14
|
+
* adoption traversal without rescanning every ordinary container.
|
|
15
|
+
* @param {{ owner: object, key: PropertyKey }[]} properties Binary properties.
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
static protectProperties(properties) {
|
|
19
|
+
if (!Array.isArray(properties)) {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
'Canonical extension binary properties must be an array.'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
for (const property of properties) {
|
|
25
|
+
const owner = property?.owner
|
|
26
|
+
const key = property?.key
|
|
27
|
+
let descriptor
|
|
28
|
+
try {
|
|
29
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, key)
|
|
30
|
+
} catch {
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
'Canonical extension data could not be inspected safely.'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (ProtectedExtensionBinaryBoundary.isProtected(descriptor)) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const captured = ProtectedExtensionBinaryBoundary.captureProperty(
|
|
39
|
+
owner,
|
|
40
|
+
key,
|
|
41
|
+
descriptor
|
|
42
|
+
)
|
|
43
|
+
ProtectedExtensionBinaryBoundary.protectCapturedProperty(captured)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Captures one binary property without changing its owner.
|
|
49
|
+
* @param {object} owner Binary property owner.
|
|
50
|
+
* @param {PropertyKey} key Binary property key.
|
|
51
|
+
* @param {PropertyDescriptor | undefined} [knownDescriptor] Previously validated descriptor.
|
|
52
|
+
* @returns {object} Opaque captured binary property.
|
|
53
|
+
*/
|
|
54
|
+
static captureProperty(owner, key, knownDescriptor = undefined) {
|
|
55
|
+
const capture = ProtectedExtensionBinaryBoundary.beginPropertyCapture(
|
|
56
|
+
owner,
|
|
57
|
+
key,
|
|
58
|
+
knownDescriptor
|
|
59
|
+
)
|
|
60
|
+
while (
|
|
61
|
+
!ProtectedExtensionBinaryBoundary.copyPropertyCaptureChunk(
|
|
62
|
+
capture,
|
|
63
|
+
Number.MAX_SAFE_INTEGER
|
|
64
|
+
)
|
|
65
|
+
) {
|
|
66
|
+
// The synchronous API deliberately owns the full payload atomically.
|
|
67
|
+
}
|
|
68
|
+
return ProtectedExtensionBinaryBoundary.finishPropertyCapture(capture)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Starts one branded binary property capture without copying its payload.
|
|
73
|
+
* @param {object} owner Binary property owner.
|
|
74
|
+
* @param {PropertyKey} key Binary property key.
|
|
75
|
+
* @param {PropertyDescriptor | undefined} [knownDescriptor] Previously validated descriptor.
|
|
76
|
+
* @returns {object} Opaque pending capture token.
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
static beginPropertyCapture(owner, key, knownDescriptor = undefined) {
|
|
80
|
+
let descriptor = knownDescriptor
|
|
81
|
+
if (descriptor === undefined) {
|
|
82
|
+
try {
|
|
83
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, key)
|
|
84
|
+
} catch {
|
|
85
|
+
throw new TypeError(
|
|
86
|
+
'Canonical extension data could not be inspected safely.'
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
'Canonical extension data may contain only data properties.'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
const binary = BinaryDataSnapshot.describeStandard(descriptor.value)
|
|
96
|
+
if (!binary) {
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
'Canonical extension binary property changed during adoption.'
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
const capture = Object.freeze({})
|
|
102
|
+
PENDING_BINARY_CAPTURES.set(capture, {
|
|
103
|
+
bytes: new Uint8Array(binary.byteLength),
|
|
104
|
+
descriptor: Object.freeze({ ...descriptor }),
|
|
105
|
+
key,
|
|
106
|
+
offset: 0,
|
|
107
|
+
owner,
|
|
108
|
+
source: descriptor.value,
|
|
109
|
+
sourceRange: Object.freeze({ ...binary })
|
|
110
|
+
})
|
|
111
|
+
return capture
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Copies the next bounded portion of one branded binary capture.
|
|
116
|
+
* @param {object} capture Opaque pending capture token.
|
|
117
|
+
* @param {number} [maxBytes] Maximum bytes copied in this call.
|
|
118
|
+
* @returns {boolean} Whether the entire payload has been copied.
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
static copyPropertyCaptureChunk(
|
|
122
|
+
capture,
|
|
123
|
+
maxBytes = BINARY_COPY_CHUNK_BYTES
|
|
124
|
+
) {
|
|
125
|
+
const state = PENDING_BINARY_CAPTURES.get(capture)
|
|
126
|
+
if (!state || !Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
'Canonical extension binary capture is not pending.'
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
const remaining = state.sourceRange.byteLength - state.offset
|
|
132
|
+
const count = Math.min(remaining, maxBytes)
|
|
133
|
+
BinaryDataSnapshot.copyBytesInto(
|
|
134
|
+
state.sourceRange,
|
|
135
|
+
state.bytes,
|
|
136
|
+
state.offset,
|
|
137
|
+
count
|
|
138
|
+
)
|
|
139
|
+
state.offset += count
|
|
140
|
+
return state.offset === state.sourceRange.byteLength
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Finalizes one completely copied binary property into a trusted record.
|
|
145
|
+
* @param {object} capture Opaque pending capture token.
|
|
146
|
+
* @returns {object} Opaque captured binary property.
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
static finishPropertyCapture(capture) {
|
|
150
|
+
const state = PENDING_BINARY_CAPTURES.get(capture)
|
|
151
|
+
if (!state || state.offset !== state.sourceRange.byteLength) {
|
|
152
|
+
throw new TypeError(
|
|
153
|
+
'Canonical extension binary capture is incomplete.'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
PENDING_BINARY_CAPTURES.delete(capture)
|
|
157
|
+
const ownedBinary = BinaryDataSnapshot.cloneFromBytes(
|
|
158
|
+
state.source,
|
|
159
|
+
state.sourceRange,
|
|
160
|
+
state.bytes
|
|
161
|
+
)
|
|
162
|
+
const ownedRange = BinaryDataSnapshot.describeStandard(ownedBinary)
|
|
163
|
+
if (!ownedRange) {
|
|
164
|
+
throw new TypeError(
|
|
165
|
+
'Canonical extension binary could not be isolated.'
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
const captured = Object.freeze({
|
|
169
|
+
descriptor: state.descriptor,
|
|
170
|
+
key: state.key,
|
|
171
|
+
ownedBinary,
|
|
172
|
+
ownedRange: Object.freeze({ ...ownedRange }),
|
|
173
|
+
owner: state.owner
|
|
174
|
+
})
|
|
175
|
+
CAPTURED_BINARY_PROPERTIES.add(captured)
|
|
176
|
+
return captured
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Installs one previously captured binary as a defensive-copy getter after
|
|
181
|
+
* proving its property descriptor still matches the transferred source.
|
|
182
|
+
* @param {object} captured Opaque captured binary property.
|
|
183
|
+
* @returns {void}
|
|
184
|
+
*/
|
|
185
|
+
static protectCapturedProperty(captured) {
|
|
186
|
+
if (!CAPTURED_BINARY_PROPERTIES.has(captured)) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
'Canonical extension binary capture is not trusted.'
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
CAPTURED_BINARY_PROPERTIES.delete(captured)
|
|
192
|
+
let descriptor
|
|
193
|
+
try {
|
|
194
|
+
descriptor = Object.getOwnPropertyDescriptor(
|
|
195
|
+
captured.owner,
|
|
196
|
+
captured.key
|
|
197
|
+
)
|
|
198
|
+
} catch {
|
|
199
|
+
throw new TypeError(
|
|
200
|
+
'Canonical extension data could not be inspected safely.'
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
!ProtectedExtensionBinaryBoundary.#sameDataDescriptor(
|
|
205
|
+
descriptor,
|
|
206
|
+
captured.descriptor
|
|
207
|
+
)
|
|
208
|
+
) {
|
|
209
|
+
throw new TypeError(
|
|
210
|
+
'Canonical extension binary property changed during adoption.'
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
/** @returns {ArrayBuffer | Uint8Array | DataView} A defensive binary copy. */
|
|
214
|
+
const readBinary = () =>
|
|
215
|
+
BinaryDataSnapshot.clone(captured.ownedBinary, captured.ownedRange)
|
|
216
|
+
PROTECTED_BINARY_GETTERS.add(readBinary)
|
|
217
|
+
Object.defineProperty(captured.owner, captured.key, {
|
|
218
|
+
configurable: false,
|
|
219
|
+
enumerable: descriptor.enumerable,
|
|
220
|
+
get: readBinary
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
9
224
|
/**
|
|
10
225
|
* Replaces owned binary data properties with defensive-copy getters.
|
|
11
226
|
* @param {unknown} root Owned extension graph.
|
|
@@ -98,6 +313,24 @@ export class ProtectedExtensionBinaryBoundary {
|
|
|
98
313
|
)
|
|
99
314
|
}
|
|
100
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Compares exact ordinary data descriptor semantics without coercion.
|
|
318
|
+
* @param {PropertyDescriptor | undefined} current Current descriptor.
|
|
319
|
+
* @param {PropertyDescriptor} expected Previously captured descriptor.
|
|
320
|
+
* @returns {boolean} Whether both descriptors still match.
|
|
321
|
+
*/
|
|
322
|
+
static #sameDataDescriptor(current, expected) {
|
|
323
|
+
return Boolean(
|
|
324
|
+
current &&
|
|
325
|
+
Object.hasOwn(current, 'value') &&
|
|
326
|
+
Object.hasOwn(expected, 'value') &&
|
|
327
|
+
Object.is(current.value, expected.value) &&
|
|
328
|
+
current.configurable === expected.configurable &&
|
|
329
|
+
current.enumerable === expected.enumerable &&
|
|
330
|
+
current.writable === expected.writable
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
101
334
|
/**
|
|
102
335
|
* Returns true for plain extension containers.
|
|
103
336
|
* @param {unknown} value Candidate value.
|