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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
|
|
2
|
+
import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
|
|
3
|
+
import { StructuredDataSnapshot } from './StructuredDataSnapshot.mjs'
|
|
4
|
+
|
|
5
|
+
const OWNED_EXTENSION_ROOTS = new WeakSet()
|
|
6
|
+
const EXTENSION_METADATA_LIMITS = Object.freeze({
|
|
7
|
+
label: 'Canonical extension data',
|
|
8
|
+
maxBytes: 128 * 1024 * 1024,
|
|
9
|
+
maxItems: 4_000_000,
|
|
10
|
+
preserveBinary: true
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Owns and seals canonical extension graphs independently of document fields.
|
|
15
|
+
*/
|
|
16
|
+
export class CircuitJsonExtensionBoundary {
|
|
17
|
+
/**
|
|
18
|
+
* Returns whether an object is an already owned extension root.
|
|
19
|
+
* @param {unknown} value Extension candidate.
|
|
20
|
+
* @returns {boolean} Whether this boundary owns the root.
|
|
21
|
+
*/
|
|
22
|
+
static owns(value) {
|
|
23
|
+
return Boolean(
|
|
24
|
+
value &&
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
OWNED_EXTENSION_ROOTS.has(value)
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates one deeply frozen source-extension snapshot.
|
|
32
|
+
* @param {unknown} value Extension candidate.
|
|
33
|
+
* @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalizer.
|
|
34
|
+
* @param {{ standardBuiltins?: boolean }} [options] Proven graph provenance.
|
|
35
|
+
* @returns {unknown} Deeply immutable owned extension metadata.
|
|
36
|
+
*/
|
|
37
|
+
static copyReadonly(value, normalize = null, options = {}) {
|
|
38
|
+
if (
|
|
39
|
+
CircuitJsonExtensionBoundary.owns(value) &&
|
|
40
|
+
Object.isFrozen(value) &&
|
|
41
|
+
normalize === null
|
|
42
|
+
) {
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
if (normalize !== null && typeof normalize !== 'function') {
|
|
46
|
+
throw new TypeError('Extension normalizer must be a function.')
|
|
47
|
+
}
|
|
48
|
+
const standardBuiltins = options?.standardBuiltins === true
|
|
49
|
+
const state = StructuredDataSnapshot.createState({
|
|
50
|
+
...EXTENSION_METADATA_LIMITS,
|
|
51
|
+
standardBuiltins
|
|
52
|
+
})
|
|
53
|
+
const snapshot = standardBuiltins
|
|
54
|
+
? StructuredDataSnapshot.adoptStructuredClone(value, state)
|
|
55
|
+
: StructuredDataSnapshot.capture(value, state)
|
|
56
|
+
CircuitJsonExtensionBoundary.#rejectBinaryRoot(snapshot)
|
|
57
|
+
const adoption = standardBuiltins
|
|
58
|
+
? StructuredDataSnapshot.consumeStructuredCloneAdoption(snapshot)
|
|
59
|
+
: null
|
|
60
|
+
if (adoption && normalize === null) {
|
|
61
|
+
StructuredDataSnapshot.sealStructuredCloneAdoption(adoption)
|
|
62
|
+
} else {
|
|
63
|
+
ProtectedExtensionBinaryBoundary.protect(snapshot)
|
|
64
|
+
}
|
|
65
|
+
const normalized = normalize ? normalize(snapshot) : snapshot
|
|
66
|
+
if (normalized !== snapshot) {
|
|
67
|
+
ProtectedExtensionBinaryBoundary.protect(normalized)
|
|
68
|
+
}
|
|
69
|
+
if (!adoption || normalize !== null) {
|
|
70
|
+
CircuitJsonExtensionBoundary.#freezeValue(normalized)
|
|
71
|
+
}
|
|
72
|
+
CircuitJsonExtensionBoundary.#markOwned(normalized)
|
|
73
|
+
return normalized
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cooperatively adopts one structured-clone extension graph.
|
|
78
|
+
* @param {unknown} value Extension candidate.
|
|
79
|
+
* @param {{ standardBuiltins?: boolean, yield: () => Promise<void> | void }} options Proven provenance and host scheduler.
|
|
80
|
+
* @returns {Promise<unknown>} Deeply immutable owned extension metadata.
|
|
81
|
+
*/
|
|
82
|
+
static async copyReadonlyAsync(value, options) {
|
|
83
|
+
if (
|
|
84
|
+
CircuitJsonExtensionBoundary.owns(value) &&
|
|
85
|
+
Object.isFrozen(value)
|
|
86
|
+
) {
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
options?.standardBuiltins !== true ||
|
|
91
|
+
typeof options?.yield !== 'function'
|
|
92
|
+
) {
|
|
93
|
+
return CircuitJsonExtensionBoundary.copyReadonly(
|
|
94
|
+
value,
|
|
95
|
+
null,
|
|
96
|
+
options
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
const state = StructuredDataSnapshot.createState({
|
|
100
|
+
...EXTENSION_METADATA_LIMITS,
|
|
101
|
+
standardBuiltins: true
|
|
102
|
+
})
|
|
103
|
+
const snapshot = await StructuredDataSnapshot.adoptStructuredCloneAsync(
|
|
104
|
+
value,
|
|
105
|
+
state,
|
|
106
|
+
options.yield
|
|
107
|
+
)
|
|
108
|
+
CircuitJsonExtensionBoundary.#rejectBinaryRoot(snapshot)
|
|
109
|
+
CircuitJsonExtensionBoundary.#markOwned(snapshot)
|
|
110
|
+
return snapshot
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Captures one document extension field synchronously.
|
|
115
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
116
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
117
|
+
* @returns {unknown} Owned extension root or undefined.
|
|
118
|
+
*/
|
|
119
|
+
static captureDocument(document, standardBuiltins) {
|
|
120
|
+
const descriptor =
|
|
121
|
+
CircuitJsonExtensionBoundary.#documentDescriptor(document)
|
|
122
|
+
if (!descriptor) return undefined
|
|
123
|
+
const captured = CircuitJsonExtensionBoundary.copyReadonly(
|
|
124
|
+
descriptor.value,
|
|
125
|
+
null,
|
|
126
|
+
{ standardBuiltins }
|
|
127
|
+
)
|
|
128
|
+
if (captured === descriptor.value) return captured
|
|
129
|
+
Object.defineProperty(document, 'extensions', {
|
|
130
|
+
...descriptor,
|
|
131
|
+
value: captured
|
|
132
|
+
})
|
|
133
|
+
return captured
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Cooperatively captures a document extension field and rejects replacement
|
|
138
|
+
* races across scheduling boundaries.
|
|
139
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
140
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
141
|
+
* @param {() => Promise<void> | void} yieldControl Host scheduler.
|
|
142
|
+
* @returns {Promise<unknown>} Owned extension root or undefined.
|
|
143
|
+
*/
|
|
144
|
+
static async captureDocumentAsync(
|
|
145
|
+
document,
|
|
146
|
+
standardBuiltins,
|
|
147
|
+
yieldControl
|
|
148
|
+
) {
|
|
149
|
+
const descriptor =
|
|
150
|
+
CircuitJsonExtensionBoundary.#documentDescriptor(document)
|
|
151
|
+
if (!descriptor) return undefined
|
|
152
|
+
const captured = await CircuitJsonExtensionBoundary.copyReadonlyAsync(
|
|
153
|
+
descriptor.value,
|
|
154
|
+
{
|
|
155
|
+
standardBuiltins,
|
|
156
|
+
yield: yieldControl
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
const current =
|
|
160
|
+
CircuitJsonExtensionBoundary.#documentDescriptor(document)
|
|
161
|
+
if (
|
|
162
|
+
!current ||
|
|
163
|
+
!CircuitJsonExtensionBoundary.#sameDataDescriptor(
|
|
164
|
+
current,
|
|
165
|
+
descriptor
|
|
166
|
+
)
|
|
167
|
+
) {
|
|
168
|
+
throw new TypeError(
|
|
169
|
+
'Canonical document extensions changed during adoption.'
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
if (captured === descriptor.value) return captured
|
|
173
|
+
Object.defineProperty(document, 'extensions', {
|
|
174
|
+
...descriptor,
|
|
175
|
+
value: captured
|
|
176
|
+
})
|
|
177
|
+
return captured
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reads and validates the optional document extension descriptor.
|
|
182
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
183
|
+
* @returns {PropertyDescriptor | undefined} Extension descriptor.
|
|
184
|
+
*/
|
|
185
|
+
static #documentDescriptor(document) {
|
|
186
|
+
let descriptor
|
|
187
|
+
try {
|
|
188
|
+
descriptor = Object.getOwnPropertyDescriptor(document, 'extensions')
|
|
189
|
+
} catch {
|
|
190
|
+
throw new TypeError(
|
|
191
|
+
'Canonical document extensions could not be inspected safely.'
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
if (descriptor && !Object.hasOwn(descriptor, 'value')) {
|
|
195
|
+
throw new TypeError(
|
|
196
|
+
'Canonical document extensions must be an own data property.'
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
return descriptor
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rejects unsupported binary extension roots.
|
|
204
|
+
* @param {unknown} value Extension root.
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
static #rejectBinaryRoot(value) {
|
|
208
|
+
if (BinaryDataSnapshot.describeStandard(value)) {
|
|
209
|
+
throw new TypeError(
|
|
210
|
+
'Canonical extension root must be a plain data container.'
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Marks one object root as owned by this boundary.
|
|
217
|
+
* @param {unknown} value Extension root.
|
|
218
|
+
* @returns {void}
|
|
219
|
+
*/
|
|
220
|
+
static #markOwned(value) {
|
|
221
|
+
if (value && typeof value === 'object') {
|
|
222
|
+
OWNED_EXTENSION_ROOTS.add(value)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Deeply freezes one captured plain-data graph.
|
|
228
|
+
* @param {unknown} value Captured graph.
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
static #freezeValue(value) {
|
|
232
|
+
const seen = new Set()
|
|
233
|
+
const stack = [{ exit: false, value }]
|
|
234
|
+
while (stack.length) {
|
|
235
|
+
const frame = stack.pop()
|
|
236
|
+
const current = frame.value
|
|
237
|
+
if (frame.exit) {
|
|
238
|
+
try {
|
|
239
|
+
Object.freeze(current)
|
|
240
|
+
} catch {
|
|
241
|
+
throw new TypeError(
|
|
242
|
+
'Canonical document values could not be frozen safely.'
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
if (!CircuitJsonExtensionBoundary.#container(current)) continue
|
|
248
|
+
if (seen.has(current)) continue
|
|
249
|
+
seen.add(current)
|
|
250
|
+
let descriptors
|
|
251
|
+
try {
|
|
252
|
+
descriptors = Object.getOwnPropertyDescriptors(current)
|
|
253
|
+
} catch {
|
|
254
|
+
throw new TypeError(
|
|
255
|
+
'Canonical document values could not be inspected safely.'
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
const children = []
|
|
259
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
260
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
261
|
+
if (
|
|
262
|
+
ProtectedExtensionBinaryBoundary.isProtected(descriptor)
|
|
263
|
+
) {
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
throw new TypeError(
|
|
267
|
+
'Canonical document may contain only data properties.'
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
children.push(descriptor.value)
|
|
271
|
+
}
|
|
272
|
+
stack.push({ exit: true, value: current })
|
|
273
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
274
|
+
stack.push({ exit: false, value: children[index] })
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns true for plain extension containers.
|
|
281
|
+
* @param {unknown} value Candidate value.
|
|
282
|
+
* @returns {boolean} Whether the value is a plain container.
|
|
283
|
+
*/
|
|
284
|
+
static #container(value) {
|
|
285
|
+
if (!value || typeof value !== 'object') return false
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
return Object.getPrototypeOf(value) === Array.prototype
|
|
288
|
+
}
|
|
289
|
+
const prototype = Object.getPrototypeOf(value)
|
|
290
|
+
return prototype === Object.prototype || prototype === null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compares exact ordinary data descriptor semantics.
|
|
295
|
+
* @param {PropertyDescriptor} current Current descriptor.
|
|
296
|
+
* @param {PropertyDescriptor} expected Captured descriptor.
|
|
297
|
+
* @returns {boolean} Whether both descriptors match.
|
|
298
|
+
*/
|
|
299
|
+
static #sameDataDescriptor(current, expected) {
|
|
300
|
+
return Boolean(
|
|
301
|
+
Object.hasOwn(current, 'value') &&
|
|
302
|
+
Object.hasOwn(expected, 'value') &&
|
|
303
|
+
Object.is(current.value, expected.value) &&
|
|
304
|
+
current.configurable === expected.configurable &&
|
|
305
|
+
current.enumerable === expected.enumerable &&
|
|
306
|
+
current.writable === expected.writable
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
Object.freeze(CircuitJsonExtensionBoundary.prototype)
|
|
312
|
+
Object.freeze(CircuitJsonExtensionBoundary)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
|
|
2
|
+
import { CircuitJsonExtensionBoundary } from './CircuitJsonExtensionBoundary.mjs'
|
|
2
3
|
import { CircuitJsonMetadataBoundary } from './CircuitJsonMetadataBoundary.mjs'
|
|
3
4
|
import { CircuitJsonValidationAuthority } from './CircuitJsonValidationAuthority.mjs'
|
|
4
5
|
import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
|
|
@@ -10,12 +11,6 @@ const SEALED_ASSETS = new WeakSet()
|
|
|
10
11
|
const ASSET_PAYLOAD_LENGTHS = new WeakMap()
|
|
11
12
|
const OWNED_METADATA_ROOTS = new WeakSet()
|
|
12
13
|
const METADATA_BUDGETS = new WeakMap()
|
|
13
|
-
const EXTENSION_METADATA_LIMITS = Object.freeze({
|
|
14
|
-
label: 'Canonical extension data',
|
|
15
|
-
maxBytes: 128 * 1024 * 1024,
|
|
16
|
-
maxItems: 4_000_000,
|
|
17
|
-
preserveBinary: true
|
|
18
|
-
})
|
|
19
14
|
const ASSET_SCALAR_FIELDS = new Set([
|
|
20
15
|
'id',
|
|
21
16
|
'kind',
|
|
@@ -82,6 +77,53 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
82
77
|
)
|
|
83
78
|
}
|
|
84
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Cooperatively seals an envelope whose exact model is already validated.
|
|
82
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
83
|
+
* @param {object[]} model Exact deeply frozen model owned by the proof.
|
|
84
|
+
* @param {{ standardBuiltins?: boolean, yield: () => Promise<void> | void }} options Proven provenance and host scheduler.
|
|
85
|
+
* @returns {Promise<Record<string, any>>} The same read-only envelope.
|
|
86
|
+
*/
|
|
87
|
+
static async freezeValidatedAsync(document, model, options) {
|
|
88
|
+
if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
'Validated document sealing requires an unforgeable validation proof.'
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
const descriptor = Object.getOwnPropertyDescriptor(document, 'model')
|
|
94
|
+
if (
|
|
95
|
+
!descriptor ||
|
|
96
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
97
|
+
descriptor.value !== model ||
|
|
98
|
+
!Array.isArray(model) ||
|
|
99
|
+
!Object.isFrozen(model) ||
|
|
100
|
+
typeof options?.yield !== 'function'
|
|
101
|
+
) {
|
|
102
|
+
throw new TypeError(
|
|
103
|
+
'Validated document sealing requires its exact frozen model.'
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
const frozenRoots = new Set([model])
|
|
107
|
+
const sourceReference = Object.getOwnPropertyDescriptor(
|
|
108
|
+
document,
|
|
109
|
+
'sourceReference'
|
|
110
|
+
)
|
|
111
|
+
if (
|
|
112
|
+
sourceReference &&
|
|
113
|
+
Object.hasOwn(sourceReference, 'value') &&
|
|
114
|
+
sourceReference.value &&
|
|
115
|
+
typeof sourceReference.value === 'object'
|
|
116
|
+
) {
|
|
117
|
+
frozenRoots.add(sourceReference.value)
|
|
118
|
+
}
|
|
119
|
+
return CircuitJsonReadOnlyDocument.#freezeDocumentAsync(
|
|
120
|
+
document,
|
|
121
|
+
frozenRoots,
|
|
122
|
+
options?.standardBuiltins === true,
|
|
123
|
+
options.yield
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
85
127
|
/**
|
|
86
128
|
* Captures owned boundaries and freezes an envelope with known frozen roots.
|
|
87
129
|
* @param {Record<string, any>} document Canonical document envelope.
|
|
@@ -111,6 +153,43 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
111
153
|
return document
|
|
112
154
|
}
|
|
113
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Captures ordinary document boundaries while adopting extensions in
|
|
158
|
+
* bounded work slices.
|
|
159
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
160
|
+
* @param {Set<object>} frozenRoots Deeply frozen roots.
|
|
161
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
162
|
+
* @param {() => Promise<void> | void} yieldControl Host scheduler.
|
|
163
|
+
* @returns {Promise<Record<string, any>>} The same read-only envelope.
|
|
164
|
+
*/
|
|
165
|
+
static async #freezeDocumentAsync(
|
|
166
|
+
document,
|
|
167
|
+
frozenRoots,
|
|
168
|
+
standardBuiltins,
|
|
169
|
+
yieldControl
|
|
170
|
+
) {
|
|
171
|
+
const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
|
|
172
|
+
const metadataState = StructuredDataSnapshot.createState()
|
|
173
|
+
CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
|
|
174
|
+
const extensions =
|
|
175
|
+
await CircuitJsonReadOnlyDocument.#captureDocumentExtensionsAsync(
|
|
176
|
+
document,
|
|
177
|
+
standardBuiltins,
|
|
178
|
+
yieldControl
|
|
179
|
+
)
|
|
180
|
+
if (extensions && typeof extensions === 'object') {
|
|
181
|
+
frozenRoots.add(extensions)
|
|
182
|
+
}
|
|
183
|
+
CircuitJsonMetadataBoundary.normalize(document, assets, (value) =>
|
|
184
|
+
CircuitJsonReadOnlyDocument.#captureMetadataRoot(
|
|
185
|
+
value,
|
|
186
|
+
metadataState
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
CircuitJsonReadOnlyDocument.#freezeValue(document, frozenRoots)
|
|
190
|
+
return document
|
|
191
|
+
}
|
|
192
|
+
|
|
114
193
|
/**
|
|
115
194
|
* Reads only an internally-created defensive asset data getter.
|
|
116
195
|
* @param {PropertyDescriptor | undefined} descriptor Data descriptor.
|
|
@@ -184,7 +263,8 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
184
263
|
if (
|
|
185
264
|
value &&
|
|
186
265
|
typeof value === 'object' &&
|
|
187
|
-
OWNED_METADATA_ROOTS.has(value)
|
|
266
|
+
(OWNED_METADATA_ROOTS.has(value) ||
|
|
267
|
+
CircuitJsonExtensionBoundary.owns(value)) &&
|
|
188
268
|
Object.isFrozen(value)
|
|
189
269
|
) {
|
|
190
270
|
return value
|
|
@@ -209,35 +289,11 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
209
289
|
* @returns {unknown} Deeply immutable owned extension metadata.
|
|
210
290
|
*/
|
|
211
291
|
static copyReadonlyExtensionValue(value, normalize = null, options = {}) {
|
|
212
|
-
|
|
213
|
-
value &&
|
|
214
|
-
typeof value === 'object' &&
|
|
215
|
-
OWNED_METADATA_ROOTS.has(value) &&
|
|
216
|
-
Object.isFrozen(value) &&
|
|
217
|
-
normalize === null
|
|
218
|
-
) {
|
|
219
|
-
return value
|
|
220
|
-
}
|
|
221
|
-
if (normalize !== null && typeof normalize !== 'function') {
|
|
222
|
-
throw new TypeError('Extension normalizer must be a function.')
|
|
223
|
-
}
|
|
224
|
-
const snapshot = StructuredDataSnapshot.capture(
|
|
292
|
+
return CircuitJsonExtensionBoundary.copyReadonly(
|
|
225
293
|
value,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
standardBuiltins: options?.standardBuiltins === true
|
|
229
|
-
})
|
|
294
|
+
normalize,
|
|
295
|
+
options
|
|
230
296
|
)
|
|
231
|
-
ProtectedExtensionBinaryBoundary.protect(snapshot)
|
|
232
|
-
const normalized = normalize ? normalize(snapshot) : snapshot
|
|
233
|
-
if (normalized !== snapshot) {
|
|
234
|
-
ProtectedExtensionBinaryBoundary.protect(normalized)
|
|
235
|
-
}
|
|
236
|
-
CircuitJsonReadOnlyDocument.#freezeValue(normalized, new Set())
|
|
237
|
-
if (normalized && typeof normalized === 'object') {
|
|
238
|
-
OWNED_METADATA_ROOTS.add(normalized)
|
|
239
|
-
}
|
|
240
|
-
return normalized
|
|
241
297
|
}
|
|
242
298
|
|
|
243
299
|
/**
|
|
@@ -341,31 +397,30 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
341
397
|
* @returns {unknown} Owned extension root or undefined.
|
|
342
398
|
*/
|
|
343
399
|
static #captureDocumentExtensions(document, standardBuiltins) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
400
|
+
return CircuitJsonExtensionBoundary.captureDocument(
|
|
401
|
+
document,
|
|
402
|
+
standardBuiltins
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Cooperatively captures a worker extension root and rejects document-level
|
|
408
|
+
* replacement races across scheduling boundaries.
|
|
409
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
410
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
411
|
+
* @param {() => Promise<void> | void} yieldControl Host scheduler.
|
|
412
|
+
* @returns {Promise<unknown>} Owned extension root or undefined.
|
|
413
|
+
*/
|
|
414
|
+
static async #captureDocumentExtensionsAsync(
|
|
415
|
+
document,
|
|
416
|
+
standardBuiltins,
|
|
417
|
+
yieldControl
|
|
418
|
+
) {
|
|
419
|
+
return CircuitJsonExtensionBoundary.captureDocumentAsync(
|
|
420
|
+
document,
|
|
421
|
+
standardBuiltins,
|
|
422
|
+
yieldControl
|
|
362
423
|
)
|
|
363
|
-
if (captured === descriptor.value) return captured
|
|
364
|
-
Object.defineProperty(document, 'extensions', {
|
|
365
|
-
...descriptor,
|
|
366
|
-
value: captured
|
|
367
|
-
})
|
|
368
|
-
return captured
|
|
369
424
|
}
|
|
370
425
|
|
|
371
426
|
/**
|
|
@@ -684,7 +739,8 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
684
739
|
if (
|
|
685
740
|
value &&
|
|
686
741
|
typeof value === 'object' &&
|
|
687
|
-
OWNED_METADATA_ROOTS.has(value)
|
|
742
|
+
(OWNED_METADATA_ROOTS.has(value) ||
|
|
743
|
+
CircuitJsonExtensionBoundary.owns(value))
|
|
688
744
|
) {
|
|
689
745
|
return value
|
|
690
746
|
}
|
|
@@ -57,18 +57,62 @@ export class CircuitJsonValidationProof {
|
|
|
57
57
|
* @returns {Record<string, any>} The same read-only document envelope.
|
|
58
58
|
*/
|
|
59
59
|
static validateAndAttach(document, options = {}) {
|
|
60
|
+
const model = CircuitJsonValidationProof.#validateModel(document)
|
|
61
|
+
return CircuitJsonValidationProof.#attachAndSeal(
|
|
62
|
+
document,
|
|
63
|
+
model,
|
|
64
|
+
options
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates a clone-owned model, yields to the host, and then seals its
|
|
70
|
+
* extension envelope without weakening the matching runtime proof.
|
|
71
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
72
|
+
* @param {{ standardBuiltins?: boolean, yield?: () => Promise<void> | void }} [options] Proven metadata provenance and host scheduler.
|
|
73
|
+
* @returns {Promise<Record<string, any>>} The same read-only document envelope.
|
|
74
|
+
*/
|
|
75
|
+
static async validateAndAttachAsync(document, options = {}) {
|
|
76
|
+
const model = CircuitJsonValidationProof.#validateModel(document)
|
|
77
|
+
await CircuitJsonValidationProof.#yieldToHost(options?.yield)
|
|
78
|
+
return CircuitJsonValidationProof.#attachAndSealAsync(document, model, {
|
|
79
|
+
...options,
|
|
80
|
+
yield: () => CircuitJsonValidationProof.#yieldToHost(options?.yield)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates and freezes one exact model before any cooperative yield.
|
|
86
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
87
|
+
* @returns {object[]} Stable validated model.
|
|
88
|
+
*/
|
|
89
|
+
static #validateModel(document) {
|
|
60
90
|
const model = CircuitJsonValidationProof.#requireModelData(document)
|
|
91
|
+
if (CircuitJsonValidationProof.#matches(document, model)) return model
|
|
92
|
+
const errors = CircuitJsonValidationAuthority.validateAndFreeze(model)
|
|
93
|
+
if (errors.length) throw new TypeError(errors[0])
|
|
94
|
+
if (CircuitJsonValidationProof.#requireModelData(document) !== model) {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
'CircuitJSON document model changed during validation.'
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
return model
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Attaches the private proof and seals the matching document envelope.
|
|
104
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
105
|
+
* @param {object[]} model Stable validated model.
|
|
106
|
+
* @param {{ standardBuiltins?: boolean }} options Proven metadata provenance.
|
|
107
|
+
* @returns {Record<string, any>} Read-only document envelope.
|
|
108
|
+
*/
|
|
109
|
+
static #attachAndSeal(document, model, options) {
|
|
110
|
+
if (CircuitJsonValidationProof.#requireModelData(document) !== model) {
|
|
111
|
+
throw new TypeError(
|
|
112
|
+
'CircuitJSON document model changed before sealing.'
|
|
113
|
+
)
|
|
114
|
+
}
|
|
61
115
|
if (!CircuitJsonValidationProof.#matches(document, model)) {
|
|
62
|
-
const errors =
|
|
63
|
-
CircuitJsonValidationAuthority.validateAndFreeze(model)
|
|
64
|
-
if (errors.length) throw new TypeError(errors[0])
|
|
65
|
-
if (
|
|
66
|
-
CircuitJsonValidationProof.#requireModelData(document) !== model
|
|
67
|
-
) {
|
|
68
|
-
throw new TypeError(
|
|
69
|
-
'CircuitJSON document model changed during validation.'
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
116
|
Object.defineProperty(document, VALIDATION_PROOF, {
|
|
73
117
|
configurable: false,
|
|
74
118
|
enumerable: false,
|
|
@@ -96,6 +140,74 @@ export class CircuitJsonValidationProof {
|
|
|
96
140
|
return readonlyDocument
|
|
97
141
|
}
|
|
98
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Attaches the proof and cooperatively seals a matching clone-owned
|
|
145
|
+
* document envelope.
|
|
146
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
147
|
+
* @param {object[]} model Stable validated model.
|
|
148
|
+
* @param {{ standardBuiltins?: boolean, yield: () => Promise<void> }} options Proven provenance and normalized scheduler.
|
|
149
|
+
* @returns {Promise<Record<string, any>>} Read-only document envelope.
|
|
150
|
+
*/
|
|
151
|
+
static async #attachAndSealAsync(document, model, options) {
|
|
152
|
+
if (CircuitJsonValidationProof.#requireModelData(document) !== model) {
|
|
153
|
+
throw new TypeError(
|
|
154
|
+
'CircuitJSON document model changed before sealing.'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
if (!CircuitJsonValidationProof.#matches(document, model)) {
|
|
158
|
+
Object.defineProperty(document, VALIDATION_PROOF, {
|
|
159
|
+
configurable: false,
|
|
160
|
+
enumerable: false,
|
|
161
|
+
value: new CircuitJsonValidationToken(
|
|
162
|
+
model,
|
|
163
|
+
VALIDATION_TOKEN_SECRET
|
|
164
|
+
),
|
|
165
|
+
writable: false
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
const readonlyDocument =
|
|
169
|
+
await CircuitJsonReadOnlyDocument.freezeValidatedAsync(
|
|
170
|
+
document,
|
|
171
|
+
model,
|
|
172
|
+
options
|
|
173
|
+
)
|
|
174
|
+
if (
|
|
175
|
+
CircuitJsonValidationProof.#requireModelData(readonlyDocument) !==
|
|
176
|
+
model ||
|
|
177
|
+
!CircuitJsonValidationProof.#matches(readonlyDocument, model)
|
|
178
|
+
) {
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
'CircuitJSON document model changed while sealing its validation proof.'
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
return readonlyDocument
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Yields through an injected scheduler, the browser scheduler API, or a
|
|
188
|
+
* zero-delay host task in that order.
|
|
189
|
+
* @param {(() => Promise<void> | void) | undefined} yieldControl Optional host scheduler.
|
|
190
|
+
* @returns {Promise<void>}
|
|
191
|
+
*/
|
|
192
|
+
static async #yieldToHost(yieldControl) {
|
|
193
|
+
if (yieldControl !== undefined) {
|
|
194
|
+
if (typeof yieldControl !== 'function') {
|
|
195
|
+
throw new TypeError(
|
|
196
|
+
'Structured-clone yield control must be a function.'
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
await yieldControl()
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const scheduler = globalThis.scheduler
|
|
203
|
+
const schedulerYield = scheduler?.yield
|
|
204
|
+
if (typeof schedulerYield === 'function') {
|
|
205
|
+
await Reflect.apply(schedulerYield, scheduler, [])
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 0))
|
|
209
|
+
}
|
|
210
|
+
|
|
99
211
|
/**
|
|
100
212
|
* Returns true when an envelope proof matches its current model reference.
|
|
101
213
|
* @param {unknown} document Document candidate.
|