circuitjson-toolkit 1.0.17 → 1.1.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/NOTICE.md +31 -0
- package/README.md +267 -107
- package/docs/api.md +501 -96
- package/docs/capabilities.md +70 -0
- package/docs/migration/behaviors.md +45 -0
- package/docs/migration/parser.md +60 -0
- package/docs/migration/renderers.md +515 -0
- package/docs/migration/root.md +740 -0
- package/docs/migration.md +120 -0
- package/docs/model-format.md +175 -57
- package/docs/provenance.md +206 -0
- package/docs/release-notes-v1.1.0.md +154 -0
- package/docs/testing.md +117 -7
- package/package.json +31 -5
- package/spec/api-baseline-v1.0.17.json +1 -0
- package/spec/baseline-provenance-v1.0.17.json +7 -0
- package/spec/circuitjson-schema-snapshot.json +321 -0
- package/spec/circuitjson-schema-source.json +28 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +27 -20
- package/src/capabilities.mjs +1 -0
- package/src/core/ArchiveEntryPath.mjs +93 -0
- package/src/core/ArchiveLimits.mjs +31 -0
- package/src/core/ArchiveLimitsValidator.mjs +107 -0
- package/src/core/AsyncInputOwnership.mjs +56 -0
- package/src/core/AttachedValueLimits.mjs +67 -0
- package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
- package/src/core/CircuitJsonDocument.mjs +19 -61
- package/src/core/CircuitJsonElementTypes.mjs +10 -0
- package/src/core/CircuitJsonElementValidator.mjs +98 -847
- package/src/core/CircuitJsonIndexer.mjs +274 -194
- package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
- package/src/core/CircuitJsonParser.mjs +75 -13
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
- package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
- package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
- package/src/core/CircuitJsonUnitParsers.mjs +101 -0
- package/src/core/CircuitJsonUnits.mjs +13 -87
- package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
- package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
- package/src/core/CircuitJsonValidationUnits.mjs +6 -0
- package/src/core/ManufacturingService.mjs +323 -0
- package/src/core/Parser.mjs +343 -0
- package/src/core/ParserOptions.mjs +333 -0
- package/src/core/PcbBoundsSelectionModel.mjs +55 -19
- package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
- package/src/core/PcbInteractionIndex.mjs +368 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
- package/src/core/ProjectAsyncInputOwner.mjs +70 -0
- package/src/core/ProjectLoader.mjs +975 -0
- package/src/core/SimulationService.mjs +790 -0
- package/src/core/ToolkitCapabilities.mjs +130 -0
- package/src/core/ZipArchiveInspector.mjs +649 -0
- package/src/core/context/BinaryDataSnapshot.mjs +217 -0
- package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
- package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
- package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
- package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
- package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
- package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
- package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
- package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
- package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
- package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
- package/src/core/context/PcbSpatialIndex.mjs +701 -0
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
- package/src/core/context/StructuredDataSnapshot.mjs +683 -0
- package/src/core/contracts/DocumentResult.mjs +198 -0
- package/src/core/contracts/ProjectResult.mjs +96 -0
- package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
- package/src/core/contracts/ToolkitAsset.mjs +493 -0
- package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
- package/src/core/contracts/ToolkitError.mjs +176 -0
- package/src/core/contracts/ToolkitProgress.mjs +89 -0
- package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
- package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
- package/src/core/query/CircuitTraversal.mjs +343 -0
- package/src/core/query/ComponentGrouping.mjs +275 -0
- package/src/core/query/QueryNetlistBuilder.mjs +306 -0
- package/src/core/query/QueryService.mjs +435 -0
- package/src/core/query/RegexPattern.mjs +75 -0
- package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
- package/src/core/rendering/CanonicalBomRows.mjs +92 -0
- package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
- package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
- package/src/core/rendering/PcbRenderPlan.mjs +429 -0
- package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
- package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
- package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
- package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
- package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
- package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
- package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
- package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
- package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
- package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
- package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
- package/src/core/scene3d/Scene3dOptions.mjs +363 -0
- package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
- package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
- package/src/core/worker/ParserWorkerClient.mjs +997 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +623 -0
- package/src/core/worker/WorkerResponseData.mjs +905 -0
- package/src/extensions.mjs +37 -0
- package/src/index.mjs +14 -9
- package/src/interaction.mjs +2 -0
- package/src/manufacturing.mjs +1 -0
- package/src/parser.mjs +12 -2
- package/src/project.mjs +5 -0
- package/src/query.mjs +1 -0
- package/src/renderers.mjs +3 -29
- package/src/scene3d.mjs +3 -0
- package/src/simulation.mjs +1 -0
- package/src/styles/renderers.css +24 -0
- package/src/testing/ToolkitContractFixtures.mjs +124 -0
- package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
- package/src/testing/runToolkitContract.mjs +705 -0
- package/src/testing.mjs +3 -0
- package/src/ui/BomTableRenderer.mjs +304 -0
- package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
- package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
- package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
- package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
- package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
- package/src/ui/PcbSvgRenderer.mjs +41 -0
- package/src/ui/SafeSvgPaint.mjs +26 -0
- package/src/ui/SafeXmlText.mjs +60 -0
- package/src/ui/SchematicGeometryBounds.mjs +540 -0
- package/src/ui/SchematicSvgRenderer.mjs +110 -0
- package/src/ui/SchematicTableGeometry.mjs +319 -0
- package/src/ui/SchematicTextAnchor.mjs +55 -0
- package/src/ui/SchematicTextBounds.mjs +98 -0
- package/src/workers/parser.worker.mjs +59 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
|
|
2
|
+
import { CircuitJsonMetadataBoundary } from './CircuitJsonMetadataBoundary.mjs'
|
|
3
|
+
import { CircuitJsonValidationAuthority } from './CircuitJsonValidationAuthority.mjs'
|
|
4
|
+
import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
|
|
5
|
+
import { StructuredDataSnapshot } from './StructuredDataSnapshot.mjs'
|
|
6
|
+
|
|
7
|
+
const PROTECTED_ASSET_BYTES = new WeakMap()
|
|
8
|
+
const PROTECTED_ASSET_DATA_GETTERS = new WeakSet()
|
|
9
|
+
const SEALED_ASSETS = new WeakSet()
|
|
10
|
+
const ASSET_PAYLOAD_LENGTHS = new WeakMap()
|
|
11
|
+
const OWNED_METADATA_ROOTS = new WeakSet()
|
|
12
|
+
const METADATA_BUDGETS = new WeakMap()
|
|
13
|
+
const EXTENSION_METADATA_LIMITS = Object.freeze({
|
|
14
|
+
label: 'Canonical extension data',
|
|
15
|
+
maxBytes: 128 * 1024 * 1024,
|
|
16
|
+
maxItems: 2_000_000,
|
|
17
|
+
preserveBinary: true
|
|
18
|
+
})
|
|
19
|
+
const ASSET_SCALAR_FIELDS = new Set([
|
|
20
|
+
'id',
|
|
21
|
+
'kind',
|
|
22
|
+
'name',
|
|
23
|
+
'mediaType',
|
|
24
|
+
'byteLength'
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Owns canonical document records while leaving binary views usable.
|
|
29
|
+
*/
|
|
30
|
+
export class CircuitJsonReadOnlyDocument {
|
|
31
|
+
/**
|
|
32
|
+
* Captures caller-owned boundaries and deeply freezes the canonical envelope.
|
|
33
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
34
|
+
* @returns {Record<string, any>} The same read-only envelope.
|
|
35
|
+
*/
|
|
36
|
+
static freeze(document) {
|
|
37
|
+
return CircuitJsonReadOnlyDocument.#freezeDocument(document, new Set())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Seals an envelope whose exact model was already deeply frozen by validation.
|
|
42
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
43
|
+
* @param {object[]} model Exact deeply frozen model owned by the proof.
|
|
44
|
+
* @returns {Record<string, any>} The same read-only envelope.
|
|
45
|
+
*/
|
|
46
|
+
static freezeValidated(document, model) {
|
|
47
|
+
if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
'Validated document sealing requires an unforgeable validation proof.'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
const descriptor = Object.getOwnPropertyDescriptor(document, 'model')
|
|
53
|
+
if (
|
|
54
|
+
!descriptor ||
|
|
55
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
56
|
+
descriptor.value !== model ||
|
|
57
|
+
!Array.isArray(model) ||
|
|
58
|
+
!Object.isFrozen(model)
|
|
59
|
+
) {
|
|
60
|
+
throw new TypeError(
|
|
61
|
+
'Validated document sealing requires its exact frozen model.'
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
const frozenRoots = new Set([model])
|
|
65
|
+
const sourceReference = Object.getOwnPropertyDescriptor(
|
|
66
|
+
document,
|
|
67
|
+
'sourceReference'
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
sourceReference &&
|
|
71
|
+
Object.hasOwn(sourceReference, 'value') &&
|
|
72
|
+
sourceReference.value &&
|
|
73
|
+
typeof sourceReference.value === 'object'
|
|
74
|
+
) {
|
|
75
|
+
frozenRoots.add(sourceReference.value)
|
|
76
|
+
}
|
|
77
|
+
return CircuitJsonReadOnlyDocument.#freezeDocument(
|
|
78
|
+
document,
|
|
79
|
+
frozenRoots
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Captures owned boundaries and freezes an envelope with known frozen roots.
|
|
85
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
86
|
+
* @param {Set<object>} frozenRoots Deeply frozen roots that need no revisit.
|
|
87
|
+
* @returns {Record<string, any>} The same read-only envelope.
|
|
88
|
+
*/
|
|
89
|
+
static #freezeDocument(document, frozenRoots) {
|
|
90
|
+
const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
|
|
91
|
+
const metadataState = StructuredDataSnapshot.createState()
|
|
92
|
+
CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
|
|
93
|
+
const extensions =
|
|
94
|
+
CircuitJsonReadOnlyDocument.#captureDocumentExtensions(document)
|
|
95
|
+
if (extensions && typeof extensions === 'object') {
|
|
96
|
+
frozenRoots.add(extensions)
|
|
97
|
+
}
|
|
98
|
+
CircuitJsonMetadataBoundary.normalize(document, assets, (value) =>
|
|
99
|
+
CircuitJsonReadOnlyDocument.#captureMetadataRoot(
|
|
100
|
+
value,
|
|
101
|
+
metadataState
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
CircuitJsonReadOnlyDocument.#freezeValue(document, frozenRoots)
|
|
105
|
+
return document
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reads only an internally-created defensive asset data getter.
|
|
110
|
+
* @param {PropertyDescriptor | undefined} descriptor Data descriptor.
|
|
111
|
+
* @returns {{ trusted: boolean, value: unknown }} Trusted read result.
|
|
112
|
+
*/
|
|
113
|
+
static readProtectedAssetData(descriptor) {
|
|
114
|
+
if (
|
|
115
|
+
!descriptor ||
|
|
116
|
+
typeof descriptor.get !== 'function' ||
|
|
117
|
+
descriptor.set !== undefined ||
|
|
118
|
+
descriptor.enumerable !== true ||
|
|
119
|
+
descriptor.configurable !== false ||
|
|
120
|
+
!PROTECTED_ASSET_DATA_GETTERS.has(descriptor.get)
|
|
121
|
+
) {
|
|
122
|
+
return { trusted: false, value: null }
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
trusted: true,
|
|
126
|
+
value: Reflect.apply(descriptor.get, undefined, [])
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Protects one newly owned canonical asset without first recopying it.
|
|
132
|
+
* @param {Record<string, any>} asset Toolkit-owned canonical asset.
|
|
133
|
+
* @returns {Record<string, any>} The same protected asset.
|
|
134
|
+
*/
|
|
135
|
+
static protectOwnedAsset(asset) {
|
|
136
|
+
CircuitJsonReadOnlyDocument.#protectAsset(asset)
|
|
137
|
+
return asset
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Atomically captures one canonical asset for accounting and later ownership.
|
|
142
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
143
|
+
* @param {object | null} [metadataBudget] Shared metadata budget token.
|
|
144
|
+
* @param {boolean} [rejectFrozenBytes] Whether raw frozen bytes are invalid.
|
|
145
|
+
* @param {((byteLength: number, identity: { id: unknown, name: unknown }) => void) | null} [acceptPayload] Pre-copy byte-limit check.
|
|
146
|
+
* @returns {Record<string, any>} Owned asset snapshot.
|
|
147
|
+
*/
|
|
148
|
+
static captureAsset(
|
|
149
|
+
asset,
|
|
150
|
+
metadataBudget = null,
|
|
151
|
+
rejectFrozenBytes = false,
|
|
152
|
+
acceptPayload = null
|
|
153
|
+
) {
|
|
154
|
+
return CircuitJsonReadOnlyDocument.#captureAsset(
|
|
155
|
+
asset,
|
|
156
|
+
CircuitJsonReadOnlyDocument.#metadataState(metadataBudget),
|
|
157
|
+
rejectFrozenBytes,
|
|
158
|
+
acceptPayload
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates, normalizes, and isolates one clone-safe metadata value.
|
|
164
|
+
* @param {unknown} value Metadata candidate.
|
|
165
|
+
* @returns {unknown} Isolated metadata snapshot.
|
|
166
|
+
*/
|
|
167
|
+
static copyMetadataValue(value) {
|
|
168
|
+
return StructuredDataSnapshot.capture(value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a deeply frozen plain-data snapshot of clone-safe metadata.
|
|
173
|
+
* @param {unknown} value Metadata candidate.
|
|
174
|
+
* @param {object | null} [metadataBudget] Shared metadata budget token.
|
|
175
|
+
* @returns {unknown} Deeply immutable owned metadata.
|
|
176
|
+
*/
|
|
177
|
+
static copyReadonlyMetadataValue(value, metadataBudget = null) {
|
|
178
|
+
if (
|
|
179
|
+
value &&
|
|
180
|
+
typeof value === 'object' &&
|
|
181
|
+
OWNED_METADATA_ROOTS.has(value) &&
|
|
182
|
+
Object.isFrozen(value)
|
|
183
|
+
) {
|
|
184
|
+
return value
|
|
185
|
+
}
|
|
186
|
+
const snapshot = StructuredDataSnapshot.capture(
|
|
187
|
+
value,
|
|
188
|
+
CircuitJsonReadOnlyDocument.#metadataState(metadataBudget)
|
|
189
|
+
)
|
|
190
|
+
CircuitJsonReadOnlyDocument.#freezeValue(snapshot, new Set())
|
|
191
|
+
if (snapshot && typeof snapshot === 'object') {
|
|
192
|
+
OWNED_METADATA_ROOTS.add(snapshot)
|
|
193
|
+
}
|
|
194
|
+
return snapshot
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Creates one deeply frozen source-extension snapshot under its separate
|
|
199
|
+
* transfer-sized item and byte ceilings.
|
|
200
|
+
* @param {unknown} value Extension candidate.
|
|
201
|
+
* @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalization over the owned mutable snapshot.
|
|
202
|
+
* @returns {unknown} Deeply immutable owned extension metadata.
|
|
203
|
+
*/
|
|
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(
|
|
218
|
+
value,
|
|
219
|
+
StructuredDataSnapshot.createState(EXTENSION_METADATA_LIMITS)
|
|
220
|
+
)
|
|
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
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates an opaque request-global source-metadata traversal budget.
|
|
235
|
+
* @returns {object} Branded metadata budget token.
|
|
236
|
+
*/
|
|
237
|
+
static createMetadataBudget() {
|
|
238
|
+
const budget = Object.freeze({})
|
|
239
|
+
METADATA_BUDGETS.set(budget, StructuredDataSnapshot.createState())
|
|
240
|
+
return budget
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Measures UTF-8 text without allocating encoded bytes.
|
|
245
|
+
* @param {string} value Text payload.
|
|
246
|
+
* @returns {number} UTF-8 byte length.
|
|
247
|
+
*/
|
|
248
|
+
static utf8ByteLength(value) {
|
|
249
|
+
if (typeof value !== 'string') {
|
|
250
|
+
throw new TypeError('Expected a string payload.')
|
|
251
|
+
}
|
|
252
|
+
return CircuitJsonReadOnlyDocument.#utf8ByteLength(value)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Copies one canonical asset through its trusted defensive payload slot.
|
|
257
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
258
|
+
* @returns {Record<string, any>} Own data fields with copied payload bytes.
|
|
259
|
+
*/
|
|
260
|
+
static copyAssetFields(asset) {
|
|
261
|
+
return CircuitJsonReadOnlyDocument.#copyAssetFields(asset, true)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Copies canonical asset metadata without materializing payload bytes.
|
|
266
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
267
|
+
* @returns {Record<string, any>} Own metadata fields with null data.
|
|
268
|
+
*/
|
|
269
|
+
static copyAssetMetadataFields(asset) {
|
|
270
|
+
return CircuitJsonReadOnlyDocument.#copyAssetFields(asset, false)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Returns the payload length captured with the owned asset value.
|
|
275
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
276
|
+
* @param {object | null} [metadataBudget] Shared metadata budget token.
|
|
277
|
+
* @returns {number} Exact resident payload byte length.
|
|
278
|
+
*/
|
|
279
|
+
static assetPayloadByteLength(asset, metadataBudget = null) {
|
|
280
|
+
const captured = CircuitJsonReadOnlyDocument.#captureAsset(
|
|
281
|
+
asset,
|
|
282
|
+
CircuitJsonReadOnlyDocument.#metadataState(metadataBudget)
|
|
283
|
+
)
|
|
284
|
+
return ASSET_PAYLOAD_LENGTHS.get(captured)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Reads scalar/index fields while retaining already-owned source metadata.
|
|
289
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
290
|
+
* @param {object | null} [metadataBudget] Shared metadata budget token.
|
|
291
|
+
* @returns {Record<string, any>} Descriptor-safe index fields.
|
|
292
|
+
*/
|
|
293
|
+
static copyAssetIndexFields(asset, metadataBudget = null) {
|
|
294
|
+
return CircuitJsonReadOnlyDocument.#copyAssetFields(
|
|
295
|
+
asset,
|
|
296
|
+
false,
|
|
297
|
+
false,
|
|
298
|
+
CircuitJsonReadOnlyDocument.#metadataState(metadataBudget)
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Reads the optional document asset array as an own data property.
|
|
304
|
+
* @param {unknown} document Canonical document envelope.
|
|
305
|
+
* @returns {unknown} Asset array candidate.
|
|
306
|
+
*/
|
|
307
|
+
static #documentAssets(document) {
|
|
308
|
+
if (!document || typeof document !== 'object') return undefined
|
|
309
|
+
let descriptor
|
|
310
|
+
try {
|
|
311
|
+
descriptor = Object.getOwnPropertyDescriptor(document, 'assets')
|
|
312
|
+
} catch {
|
|
313
|
+
throw new TypeError(
|
|
314
|
+
'Canonical document assets could not be inspected safely.'
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
if (!descriptor) return undefined
|
|
318
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
319
|
+
throw new TypeError(
|
|
320
|
+
'Canonical document assets must be an own data property.'
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
return descriptor.value
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Captures an unowned worker or caller extension root with extension-sized
|
|
328
|
+
* bounds before the generic source metadata pass begins.
|
|
329
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
330
|
+
* @returns {unknown} Owned extension root or undefined.
|
|
331
|
+
*/
|
|
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
|
|
349
|
+
)
|
|
350
|
+
if (captured === descriptor.value) return captured
|
|
351
|
+
Object.defineProperty(document, 'extensions', {
|
|
352
|
+
...descriptor,
|
|
353
|
+
value: captured
|
|
354
|
+
})
|
|
355
|
+
return captured
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Captures one asset with a single caller-owned descriptor traversal.
|
|
360
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
361
|
+
* @param {{ seen: Map<object, unknown>, items: number }} metadataState Shared metadata state.
|
|
362
|
+
* @param {boolean} [rejectFrozenBytes] Whether raw frozen bytes are invalid.
|
|
363
|
+
* @param {((byteLength: number, identity: { id: unknown, name: unknown }) => void) | null} [acceptPayload] Pre-copy byte-limit check.
|
|
364
|
+
* @returns {Record<string, any>} Owned sealed asset.
|
|
365
|
+
*/
|
|
366
|
+
static #captureAsset(
|
|
367
|
+
asset,
|
|
368
|
+
metadataState,
|
|
369
|
+
rejectFrozenBytes = false,
|
|
370
|
+
acceptPayload = null
|
|
371
|
+
) {
|
|
372
|
+
if (SEALED_ASSETS.has(asset)) {
|
|
373
|
+
CircuitJsonReadOnlyDocument.#accountSealedAsset(
|
|
374
|
+
asset,
|
|
375
|
+
metadataState,
|
|
376
|
+
acceptPayload
|
|
377
|
+
)
|
|
378
|
+
return asset
|
|
379
|
+
}
|
|
380
|
+
if (!asset || typeof asset !== 'object' || Array.isArray(asset)) {
|
|
381
|
+
throw new TypeError('Canonical asset must be a plain object.')
|
|
382
|
+
}
|
|
383
|
+
const previouslyCaptured = metadataState.seen.get(asset)
|
|
384
|
+
if (SEALED_ASSETS.has(previouslyCaptured)) return previouslyCaptured
|
|
385
|
+
let prototype
|
|
386
|
+
let keys
|
|
387
|
+
try {
|
|
388
|
+
prototype = Object.getPrototypeOf(asset)
|
|
389
|
+
keys = Reflect.ownKeys(asset)
|
|
390
|
+
} catch {
|
|
391
|
+
throw new TypeError(
|
|
392
|
+
'Canonical asset must expose inspectable plain object data.'
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
396
|
+
throw new TypeError('Canonical asset must be a plain object.')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const fields = []
|
|
400
|
+
const identity = { id: undefined, name: undefined }
|
|
401
|
+
let payloadLength = null
|
|
402
|
+
let binaryPayload = false
|
|
403
|
+
for (const key of keys) {
|
|
404
|
+
if (typeof key !== 'string') {
|
|
405
|
+
throw new TypeError('Canonical asset keys must be strings.')
|
|
406
|
+
}
|
|
407
|
+
let descriptor
|
|
408
|
+
try {
|
|
409
|
+
descriptor = Object.getOwnPropertyDescriptor(asset, key)
|
|
410
|
+
} catch {
|
|
411
|
+
throw new TypeError(
|
|
412
|
+
'Canonical asset properties could not be inspected.'
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
if (!descriptor) {
|
|
416
|
+
throw new TypeError(
|
|
417
|
+
'Canonical asset properties changed during inspection.'
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
if (key === 'data' && PROTECTED_ASSET_BYTES.has(asset)) {
|
|
421
|
+
const bytes = PROTECTED_ASSET_BYTES.get(asset)
|
|
422
|
+
fields.push({ key, descriptor, bytes, range: null })
|
|
423
|
+
payloadLength = bytes.byteLength
|
|
424
|
+
binaryPayload = true
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
428
|
+
throw new TypeError(
|
|
429
|
+
'Canonical assets may contain only data properties.'
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
if (ASSET_SCALAR_FIELDS.has(key)) {
|
|
433
|
+
CircuitJsonReadOnlyDocument.#validateAssetScalar(
|
|
434
|
+
key,
|
|
435
|
+
descriptor.value
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
if (key === 'id' || key === 'name') {
|
|
439
|
+
identity[key] = descriptor.value
|
|
440
|
+
}
|
|
441
|
+
let range = null
|
|
442
|
+
if (key === 'data') {
|
|
443
|
+
CircuitJsonReadOnlyDocument.#validateAssetPayload(
|
|
444
|
+
descriptor.value
|
|
445
|
+
)
|
|
446
|
+
range = BinaryDataSnapshot.describe(descriptor.value)
|
|
447
|
+
if (range) {
|
|
448
|
+
payloadLength = range.byteLength
|
|
449
|
+
binaryPayload = true
|
|
450
|
+
} else {
|
|
451
|
+
payloadLength =
|
|
452
|
+
typeof descriptor.value === 'string'
|
|
453
|
+
? CircuitJsonReadOnlyDocument.#utf8ByteLength(
|
|
454
|
+
descriptor.value
|
|
455
|
+
)
|
|
456
|
+
: 0
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
fields.push({ key, descriptor, bytes: null, range })
|
|
460
|
+
}
|
|
461
|
+
if (payloadLength === null) {
|
|
462
|
+
throw new TypeError(
|
|
463
|
+
'Canonical asset data must be an own data property.'
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
if (binaryPayload && rejectFrozenBytes && Object.isFrozen(asset)) {
|
|
467
|
+
throw new TypeError(
|
|
468
|
+
'Frozen canonical asset bytes cannot be protected.'
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
if (acceptPayload !== null) {
|
|
472
|
+
if (typeof acceptPayload !== 'function') {
|
|
473
|
+
throw new TypeError('Asset payload check must be a function.')
|
|
474
|
+
}
|
|
475
|
+
acceptPayload(payloadLength, identity)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const result = Object.create(prototype)
|
|
479
|
+
metadataState.seen.set(asset, result)
|
|
480
|
+
for (const field of fields) {
|
|
481
|
+
if (field.key === 'data' && field.bytes) {
|
|
482
|
+
CircuitJsonReadOnlyDocument.#defineProtectedData(
|
|
483
|
+
result,
|
|
484
|
+
new Uint8Array(field.bytes),
|
|
485
|
+
field.descriptor.enumerable
|
|
486
|
+
)
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
489
|
+
if (field.key === 'source') {
|
|
490
|
+
const source = CircuitJsonReadOnlyDocument.#captureMetadataRoot(
|
|
491
|
+
field.descriptor.value,
|
|
492
|
+
metadataState
|
|
493
|
+
)
|
|
494
|
+
Object.defineProperty(result, field.key, {
|
|
495
|
+
...field.descriptor,
|
|
496
|
+
value: source
|
|
497
|
+
})
|
|
498
|
+
continue
|
|
499
|
+
}
|
|
500
|
+
if (field.key === 'data' && field.range) {
|
|
501
|
+
CircuitJsonReadOnlyDocument.#defineProtectedData(
|
|
502
|
+
result,
|
|
503
|
+
BinaryDataSnapshot.copyBytes(
|
|
504
|
+
field.descriptor.value,
|
|
505
|
+
field.range
|
|
506
|
+
),
|
|
507
|
+
field.descriptor.enumerable
|
|
508
|
+
)
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
Object.defineProperty(result, field.key, field.descriptor)
|
|
512
|
+
}
|
|
513
|
+
SEALED_ASSETS.add(result)
|
|
514
|
+
ASSET_PAYLOAD_LENGTHS.set(result, payloadLength)
|
|
515
|
+
metadataState.seen.set(result, result)
|
|
516
|
+
return result
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Accounts one pre-owned asset in a new request without cloning metadata.
|
|
521
|
+
* @param {Record<string, any>} asset Owned sealed asset.
|
|
522
|
+
* @param {{ seen: Map<object, unknown>, accounted: Set<object>, items: number }} metadataState Shared metadata state.
|
|
523
|
+
* @param {Function | null} acceptPayload Optional payload-limit callback.
|
|
524
|
+
* @returns {void}
|
|
525
|
+
*/
|
|
526
|
+
static #accountSealedAsset(asset, metadataState, acceptPayload) {
|
|
527
|
+
if (acceptPayload !== null && typeof acceptPayload !== 'function') {
|
|
528
|
+
throw new TypeError('Asset payload check must be a function.')
|
|
529
|
+
}
|
|
530
|
+
if (metadataState.seen.get(asset) !== asset) {
|
|
531
|
+
const source = Object.getOwnPropertyDescriptor(asset, 'source')
|
|
532
|
+
if (source && !Object.hasOwn(source, 'value')) {
|
|
533
|
+
throw new TypeError(
|
|
534
|
+
'Canonical asset source must be an own data property.'
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
StructuredDataSnapshot.account(
|
|
538
|
+
source ? source.value : null,
|
|
539
|
+
metadataState
|
|
540
|
+
)
|
|
541
|
+
metadataState.seen.set(asset, asset)
|
|
542
|
+
}
|
|
543
|
+
if (!acceptPayload) return
|
|
544
|
+
const identity = { id: undefined, name: undefined }
|
|
545
|
+
for (const field of ['id', 'name']) {
|
|
546
|
+
const descriptor = Object.getOwnPropertyDescriptor(asset, field)
|
|
547
|
+
if (descriptor && Object.hasOwn(descriptor, 'value')) {
|
|
548
|
+
identity[field] = descriptor.value
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
acceptPayload(ASSET_PAYLOAD_LENGTHS.get(asset), identity)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Copies one already-captured asset with optional payload materialization.
|
|
556
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
557
|
+
* @param {boolean} includeData Whether to copy payload bytes.
|
|
558
|
+
* @param {boolean} [copySource] Whether to isolate source metadata.
|
|
559
|
+
* @param {{ seen: Map<object, unknown>, items: number } | null} [metadataState] Shared metadata state.
|
|
560
|
+
* @returns {Record<string, any>} Copied fields.
|
|
561
|
+
*/
|
|
562
|
+
static #copyAssetFields(
|
|
563
|
+
asset,
|
|
564
|
+
includeData,
|
|
565
|
+
copySource = true,
|
|
566
|
+
metadataState = null
|
|
567
|
+
) {
|
|
568
|
+
const captured = CircuitJsonReadOnlyDocument.#captureAsset(
|
|
569
|
+
asset,
|
|
570
|
+
metadataState || StructuredDataSnapshot.createState()
|
|
571
|
+
)
|
|
572
|
+
const result = Object.create(null)
|
|
573
|
+
let keys
|
|
574
|
+
try {
|
|
575
|
+
keys = Reflect.ownKeys(captured)
|
|
576
|
+
} catch {
|
|
577
|
+
throw new TypeError('Canonical asset keys could not be inspected.')
|
|
578
|
+
}
|
|
579
|
+
for (const key of keys) {
|
|
580
|
+
let descriptor
|
|
581
|
+
try {
|
|
582
|
+
descriptor = Object.getOwnPropertyDescriptor(captured, key)
|
|
583
|
+
} catch {
|
|
584
|
+
throw new TypeError(
|
|
585
|
+
'Canonical asset properties could not be inspected.'
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
if (key === 'data' && PROTECTED_ASSET_BYTES.has(captured)) {
|
|
589
|
+
result.data = includeData
|
|
590
|
+
? new Uint8Array(PROTECTED_ASSET_BYTES.get(captured))
|
|
591
|
+
: null
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
594
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
595
|
+
throw new TypeError(
|
|
596
|
+
'Canonical assets may contain only data properties.'
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
if (key === 'source') {
|
|
600
|
+
result.source = copySource
|
|
601
|
+
? StructuredDataSnapshot.capture(descriptor.value)
|
|
602
|
+
: descriptor.value
|
|
603
|
+
continue
|
|
604
|
+
}
|
|
605
|
+
result[key] =
|
|
606
|
+
key === 'data' && !includeData ? null : descriptor.value
|
|
607
|
+
}
|
|
608
|
+
return result
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Rejects object coercion in canonical scalar asset fields.
|
|
613
|
+
* @param {string} key Asset field name.
|
|
614
|
+
* @param {unknown} value Asset field value.
|
|
615
|
+
* @returns {void}
|
|
616
|
+
*/
|
|
617
|
+
static #validateAssetScalar(key, value) {
|
|
618
|
+
const type = typeof value
|
|
619
|
+
if (
|
|
620
|
+
value === null ||
|
|
621
|
+
value === undefined ||
|
|
622
|
+
['string', 'number', 'boolean', 'bigint'].includes(type)
|
|
623
|
+
) {
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
throw new TypeError(
|
|
627
|
+
`Canonical asset ${key} must be a primitive scalar.`
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Rejects unsupported or coercion-capable asset payload values.
|
|
633
|
+
* @param {unknown} value Asset data value.
|
|
634
|
+
* @returns {void}
|
|
635
|
+
*/
|
|
636
|
+
static #validateAssetPayload(value) {
|
|
637
|
+
if (
|
|
638
|
+
value === null ||
|
|
639
|
+
value === undefined ||
|
|
640
|
+
typeof value === 'string' ||
|
|
641
|
+
BinaryDataSnapshot.byteLength(value) !== null
|
|
642
|
+
) {
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
throw new TypeError(
|
|
646
|
+
'Canonical asset data must be text, binary data, or null.'
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Resolves an opaque metadata budget token or creates local capture state.
|
|
652
|
+
* @param {object | null} budget Metadata budget token.
|
|
653
|
+
* @returns {{ seen: Map<object, unknown>, items: number }} Capture state.
|
|
654
|
+
*/
|
|
655
|
+
static #metadataState(budget) {
|
|
656
|
+
if (budget === null || budget === undefined) {
|
|
657
|
+
return StructuredDataSnapshot.createState()
|
|
658
|
+
}
|
|
659
|
+
const state = METADATA_BUDGETS.get(budget)
|
|
660
|
+
if (!state) throw new TypeError('Invalid source-metadata budget.')
|
|
661
|
+
return state
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Captures and brands one metadata root for idempotent document sealing.
|
|
666
|
+
* @param {unknown} value Metadata candidate.
|
|
667
|
+
* @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
|
|
668
|
+
* @returns {unknown} Owned normalized root.
|
|
669
|
+
*/
|
|
670
|
+
static #captureMetadataRoot(value, state) {
|
|
671
|
+
if (
|
|
672
|
+
value &&
|
|
673
|
+
typeof value === 'object' &&
|
|
674
|
+
OWNED_METADATA_ROOTS.has(value)
|
|
675
|
+
) {
|
|
676
|
+
return value
|
|
677
|
+
}
|
|
678
|
+
const snapshot = StructuredDataSnapshot.capture(value, state)
|
|
679
|
+
if (snapshot && typeof snapshot === 'object') {
|
|
680
|
+
OWNED_METADATA_ROOTS.add(snapshot)
|
|
681
|
+
}
|
|
682
|
+
return snapshot
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Replaces every dense array item with its captured asset snapshot.
|
|
687
|
+
* @param {unknown} assets Canonical asset array candidate.
|
|
688
|
+
* @param {{ seen: Map<object, unknown>, items: number }} metadataState Shared metadata state.
|
|
689
|
+
* @returns {void}
|
|
690
|
+
*/
|
|
691
|
+
static #captureAssetData(assets, metadataState) {
|
|
692
|
+
if (!Array.isArray(assets)) return
|
|
693
|
+
let prototype
|
|
694
|
+
let keys
|
|
695
|
+
let lengthDescriptor
|
|
696
|
+
try {
|
|
697
|
+
prototype = Object.getPrototypeOf(assets)
|
|
698
|
+
keys = Reflect.ownKeys(assets)
|
|
699
|
+
lengthDescriptor = Object.getOwnPropertyDescriptor(assets, 'length')
|
|
700
|
+
} catch {
|
|
701
|
+
throw new TypeError(
|
|
702
|
+
'Canonical assets must expose a dense plain array.'
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
const length = lengthDescriptor?.value
|
|
706
|
+
if (
|
|
707
|
+
prototype !== Array.prototype ||
|
|
708
|
+
!Number.isSafeInteger(length) ||
|
|
709
|
+
length < 0 ||
|
|
710
|
+
keys.length !== length + 1
|
|
711
|
+
) {
|
|
712
|
+
throw new TypeError(
|
|
713
|
+
'Canonical assets must expose a dense plain array.'
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
for (let index = 0; index < length; index += 1) {
|
|
717
|
+
let itemDescriptor
|
|
718
|
+
try {
|
|
719
|
+
itemDescriptor = Object.getOwnPropertyDescriptor(
|
|
720
|
+
assets,
|
|
721
|
+
String(index)
|
|
722
|
+
)
|
|
723
|
+
} catch {
|
|
724
|
+
throw new TypeError(
|
|
725
|
+
'Canonical assets could not be inspected safely.'
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
if (!itemDescriptor || !Object.hasOwn(itemDescriptor, 'value')) {
|
|
729
|
+
throw new TypeError(
|
|
730
|
+
'Canonical assets must contain only dense data properties.'
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
const captured = CircuitJsonReadOnlyDocument.#captureAsset(
|
|
734
|
+
itemDescriptor.value,
|
|
735
|
+
metadataState,
|
|
736
|
+
true
|
|
737
|
+
)
|
|
738
|
+
if (captured !== itemDescriptor.value) {
|
|
739
|
+
Object.defineProperty(assets, String(index), {
|
|
740
|
+
...itemDescriptor,
|
|
741
|
+
value: captured
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Protects one toolkit-owned binary asset before it crosses a public boundary.
|
|
749
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
750
|
+
* @returns {void}
|
|
751
|
+
*/
|
|
752
|
+
static #protectAsset(asset) {
|
|
753
|
+
if (!asset || typeof asset !== 'object') return
|
|
754
|
+
if (PROTECTED_ASSET_BYTES.has(asset)) return
|
|
755
|
+
let descriptor
|
|
756
|
+
try {
|
|
757
|
+
descriptor = Object.getOwnPropertyDescriptor(asset, 'data')
|
|
758
|
+
} catch {
|
|
759
|
+
throw new TypeError(
|
|
760
|
+
'Canonical asset data could not be inspected safely.'
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
764
|
+
throw new TypeError(
|
|
765
|
+
'Canonical asset data must be an own data property.'
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
CircuitJsonReadOnlyDocument.#validateAssetPayload(descriptor.value)
|
|
769
|
+
const range = BinaryDataSnapshot.describe(descriptor.value)
|
|
770
|
+
if (!range) return
|
|
771
|
+
if (Object.isFrozen(asset)) {
|
|
772
|
+
throw new TypeError(
|
|
773
|
+
'Frozen canonical asset bytes cannot be protected.'
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
CircuitJsonReadOnlyDocument.#defineProtectedData(
|
|
777
|
+
asset,
|
|
778
|
+
BinaryDataSnapshot.copyBytes(descriptor.value, range),
|
|
779
|
+
descriptor.enumerable
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Installs one private byte snapshot and a defensive-copy getter.
|
|
785
|
+
* @param {object} asset Owned asset.
|
|
786
|
+
* @param {Uint8Array} bytes Isolated bytes.
|
|
787
|
+
* @param {boolean} enumerable Original enumerability.
|
|
788
|
+
* @returns {void}
|
|
789
|
+
*/
|
|
790
|
+
static #defineProtectedData(asset, bytes, enumerable) {
|
|
791
|
+
PROTECTED_ASSET_BYTES.set(asset, bytes)
|
|
792
|
+
/** @returns {Uint8Array} A defensive payload copy. */
|
|
793
|
+
const readAssetData = () => new Uint8Array(bytes)
|
|
794
|
+
PROTECTED_ASSET_DATA_GETTERS.add(readAssetData)
|
|
795
|
+
Object.defineProperty(asset, 'data', {
|
|
796
|
+
configurable: false,
|
|
797
|
+
enumerable,
|
|
798
|
+
get: readAssetData
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Measures UTF-8 bytes without allocating an encoded copy.
|
|
804
|
+
* @param {string} value Text payload.
|
|
805
|
+
* @returns {number} UTF-8 byte length.
|
|
806
|
+
*/
|
|
807
|
+
static #utf8ByteLength(value) {
|
|
808
|
+
let length = 0
|
|
809
|
+
for (const character of value) {
|
|
810
|
+
const codePoint = character.codePointAt(0)
|
|
811
|
+
length +=
|
|
812
|
+
codePoint <= 0x7f
|
|
813
|
+
? 1
|
|
814
|
+
: codePoint <= 0x7ff
|
|
815
|
+
? 2
|
|
816
|
+
: codePoint <= 0xffff
|
|
817
|
+
? 3
|
|
818
|
+
: 4
|
|
819
|
+
}
|
|
820
|
+
return length
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Freezes one owned clone-safe container in child-first order.
|
|
825
|
+
* @param {unknown} value Candidate value.
|
|
826
|
+
* @param {Set<object>} seen Visited object identities.
|
|
827
|
+
* @returns {void}
|
|
828
|
+
*/
|
|
829
|
+
static #freezeValue(value, seen) {
|
|
830
|
+
const stack = [{ exit: false, value }]
|
|
831
|
+
while (stack.length) {
|
|
832
|
+
const frame = stack.pop()
|
|
833
|
+
const current = frame.value
|
|
834
|
+
if (frame.exit) {
|
|
835
|
+
try {
|
|
836
|
+
Object.freeze(current)
|
|
837
|
+
} catch {
|
|
838
|
+
throw new TypeError(
|
|
839
|
+
'Canonical document values could not be frozen safely.'
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
continue
|
|
843
|
+
}
|
|
844
|
+
if (!CircuitJsonReadOnlyDocument.#container(current)) continue
|
|
845
|
+
if (seen.has(current)) continue
|
|
846
|
+
seen.add(current)
|
|
847
|
+
|
|
848
|
+
let keys
|
|
849
|
+
try {
|
|
850
|
+
keys = Reflect.ownKeys(current)
|
|
851
|
+
} catch {
|
|
852
|
+
throw new TypeError(
|
|
853
|
+
'Canonical document values could not be inspected safely.'
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
const children = []
|
|
857
|
+
for (const key of keys) {
|
|
858
|
+
let descriptor
|
|
859
|
+
try {
|
|
860
|
+
descriptor = Object.getOwnPropertyDescriptor(current, key)
|
|
861
|
+
} catch {
|
|
862
|
+
throw new TypeError(
|
|
863
|
+
'Canonical document properties could not be inspected safely.'
|
|
864
|
+
)
|
|
865
|
+
}
|
|
866
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
867
|
+
const protectedData =
|
|
868
|
+
key === 'data' && PROTECTED_ASSET_BYTES.has(current)
|
|
869
|
+
const protectedExtensionBinary =
|
|
870
|
+
ProtectedExtensionBinaryBoundary.isProtected(descriptor)
|
|
871
|
+
if (protectedData || protectedExtensionBinary) continue
|
|
872
|
+
throw new TypeError(
|
|
873
|
+
'Canonical document may contain only data properties.'
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
children.push(descriptor.value)
|
|
877
|
+
}
|
|
878
|
+
stack.push({ exit: true, value: current })
|
|
879
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
880
|
+
stack.push({ exit: false, value: children[index] })
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Returns true for containers that can be frozen without breaking views.
|
|
887
|
+
* @param {unknown} value Candidate value.
|
|
888
|
+
* @returns {boolean} Whether the value is a supported container.
|
|
889
|
+
*/
|
|
890
|
+
static #container(value) {
|
|
891
|
+
if (!value || typeof value !== 'object') return false
|
|
892
|
+
try {
|
|
893
|
+
if (Array.isArray(value)) {
|
|
894
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
895
|
+
throw new TypeError(
|
|
896
|
+
'Canonical document arrays must use Array.prototype.'
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
return true
|
|
900
|
+
}
|
|
901
|
+
const prototype = Object.getPrototypeOf(value)
|
|
902
|
+
if (prototype === Object.prototype || prototype === null) {
|
|
903
|
+
return true
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Binary values and other platform objects are deliberately not
|
|
907
|
+
// frozen here. Their payload-specific protection happens before
|
|
908
|
+
// this traversal; canonical arrays and records are the only
|
|
909
|
+
// containers whose children need recursive sealing.
|
|
910
|
+
return false
|
|
911
|
+
} catch {
|
|
912
|
+
throw new TypeError(
|
|
913
|
+
'Canonical document values could not be inspected safely.'
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
Object.freeze(CircuitJsonReadOnlyDocument.prototype)
|
|
920
|
+
Object.freeze(CircuitJsonReadOnlyDocument)
|