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.
@@ -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
- if (
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
- StructuredDataSnapshot.createState({
227
- ...EXTENSION_METADATA_LIMITS,
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
- let descriptor
345
- try {
346
- descriptor = Object.getOwnPropertyDescriptor(document, 'extensions')
347
- } catch {
348
- throw new TypeError(
349
- 'Canonical document extensions could not be inspected safely.'
350
- )
351
- }
352
- if (!descriptor) return undefined
353
- if (!Object.hasOwn(descriptor, 'value')) {
354
- throw new TypeError(
355
- 'Canonical document extensions must be an own data property.'
356
- )
357
- }
358
- const captured = CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
359
- descriptor.value,
360
- null,
361
- { 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
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.