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