circuitjson-toolkit 1.2.1 → 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.
@@ -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',
@@ -41,9 +36,10 @@ export class CircuitJsonReadOnlyDocument {
41
36
  * Seals an envelope whose exact model was already deeply frozen by validation.
42
37
  * @param {Record<string, any>} document Canonical document envelope.
43
38
  * @param {object[]} model Exact deeply frozen model owned by the proof.
39
+ * @param {{ standardBuiltins?: boolean }} [options] Proven metadata provenance.
44
40
  * @returns {Record<string, any>} The same read-only envelope.
45
41
  */
46
- static freezeValidated(document, model) {
42
+ static freezeValidated(document, model, options = {}) {
47
43
  if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
48
44
  throw new TypeError(
49
45
  'Validated document sealing requires an unforgeable validation proof.'
@@ -76,7 +72,55 @@ export class CircuitJsonReadOnlyDocument {
76
72
  }
77
73
  return CircuitJsonReadOnlyDocument.#freezeDocument(
78
74
  document,
79
- frozenRoots
75
+ frozenRoots,
76
+ options?.standardBuiltins === true
77
+ )
78
+ }
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
80
124
  )
81
125
  }
82
126
 
@@ -84,14 +128,55 @@ export class CircuitJsonReadOnlyDocument {
84
128
  * Captures owned boundaries and freezes an envelope with known frozen roots.
85
129
  * @param {Record<string, any>} document Canonical document envelope.
86
130
  * @param {Set<object>} frozenRoots Deeply frozen roots that need no revisit.
131
+ * @param {boolean} [standardBuiltins] Whether extension built-ins have proven standard prototypes.
87
132
  * @returns {Record<string, any>} The same read-only envelope.
88
133
  */
89
- static #freezeDocument(document, frozenRoots) {
134
+ static #freezeDocument(document, frozenRoots, standardBuiltins = false) {
90
135
  const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
91
136
  const metadataState = StructuredDataSnapshot.createState()
92
137
  CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
93
138
  const extensions =
94
- CircuitJsonReadOnlyDocument.#captureDocumentExtensions(document)
139
+ CircuitJsonReadOnlyDocument.#captureDocumentExtensions(
140
+ document,
141
+ standardBuiltins
142
+ )
143
+ if (extensions && typeof extensions === 'object') {
144
+ frozenRoots.add(extensions)
145
+ }
146
+ CircuitJsonMetadataBoundary.normalize(document, assets, (value) =>
147
+ CircuitJsonReadOnlyDocument.#captureMetadataRoot(
148
+ value,
149
+ metadataState
150
+ )
151
+ )
152
+ CircuitJsonReadOnlyDocument.#freezeValue(document, frozenRoots)
153
+ return document
154
+ }
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
+ )
95
180
  if (extensions && typeof extensions === 'object') {
96
181
  frozenRoots.add(extensions)
97
182
  }
@@ -178,7 +263,8 @@ export class CircuitJsonReadOnlyDocument {
178
263
  if (
179
264
  value &&
180
265
  typeof value === 'object' &&
181
- OWNED_METADATA_ROOTS.has(value) &&
266
+ (OWNED_METADATA_ROOTS.has(value) ||
267
+ CircuitJsonExtensionBoundary.owns(value)) &&
182
268
  Object.isFrozen(value)
183
269
  ) {
184
270
  return value
@@ -199,35 +285,15 @@ export class CircuitJsonReadOnlyDocument {
199
285
  * transfer-sized item and byte ceilings.
200
286
  * @param {unknown} value Extension candidate.
201
287
  * @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalization over the owned mutable snapshot.
288
+ * @param {{ standardBuiltins?: boolean }} [options] Proven source-graph provenance.
202
289
  * @returns {unknown} Deeply immutable owned extension metadata.
203
290
  */
204
- static copyReadonlyExtensionValue(value, normalize = null) {
205
- if (
206
- value &&
207
- typeof value === 'object' &&
208
- OWNED_METADATA_ROOTS.has(value) &&
209
- Object.isFrozen(value) &&
210
- normalize === null
211
- ) {
212
- return value
213
- }
214
- if (normalize !== null && typeof normalize !== 'function') {
215
- throw new TypeError('Extension normalizer must be a function.')
216
- }
217
- const snapshot = StructuredDataSnapshot.capture(
291
+ static copyReadonlyExtensionValue(value, normalize = null, options = {}) {
292
+ return CircuitJsonExtensionBoundary.copyReadonly(
218
293
  value,
219
- StructuredDataSnapshot.createState(EXTENSION_METADATA_LIMITS)
294
+ normalize,
295
+ options
220
296
  )
221
- ProtectedExtensionBinaryBoundary.protect(snapshot)
222
- const normalized = normalize ? normalize(snapshot) : snapshot
223
- if (normalized !== snapshot) {
224
- ProtectedExtensionBinaryBoundary.protect(normalized)
225
- }
226
- CircuitJsonReadOnlyDocument.#freezeValue(normalized, new Set())
227
- if (normalized && typeof normalized === 'object') {
228
- OWNED_METADATA_ROOTS.add(normalized)
229
- }
230
- return normalized
231
297
  }
232
298
 
233
299
  /**
@@ -327,32 +393,34 @@ export class CircuitJsonReadOnlyDocument {
327
393
  * Captures an unowned worker or caller extension root with extension-sized
328
394
  * bounds before the generic source metadata pass begins.
329
395
  * @param {Record<string, any>} document Canonical document envelope.
396
+ * @param {boolean} standardBuiltins Whether extension built-ins have proven standard prototypes.
330
397
  * @returns {unknown} Owned extension root or undefined.
331
398
  */
332
- static #captureDocumentExtensions(document) {
333
- let descriptor
334
- try {
335
- descriptor = Object.getOwnPropertyDescriptor(document, 'extensions')
336
- } catch {
337
- throw new TypeError(
338
- 'Canonical document extensions could not be inspected safely.'
339
- )
340
- }
341
- if (!descriptor) return undefined
342
- if (!Object.hasOwn(descriptor, 'value')) {
343
- throw new TypeError(
344
- 'Canonical document extensions must be an own data property.'
345
- )
346
- }
347
- const captured = CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
348
- descriptor.value
399
+ static #captureDocumentExtensions(document, standardBuiltins) {
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
349
423
  )
350
- if (captured === descriptor.value) return captured
351
- Object.defineProperty(document, 'extensions', {
352
- ...descriptor,
353
- value: captured
354
- })
355
- return captured
356
424
  }
357
425
 
358
426
  /**
@@ -671,7 +739,8 @@ export class CircuitJsonReadOnlyDocument {
671
739
  if (
672
740
  value &&
673
741
  typeof value === 'object' &&
674
- OWNED_METADATA_ROOTS.has(value)
742
+ (OWNED_METADATA_ROOTS.has(value) ||
743
+ CircuitJsonExtensionBoundary.owns(value))
675
744
  ) {
676
745
  return value
677
746
  }