You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* Support value descriptors in apiManipulation
* Refine apiManipulation value descriptor handling
* Trim apiManipulation descriptor duplication
* Test value descriptor override without define: true
Adds two unit tests for apiManipulation:
- Confirms an existing DOM-style value function descriptor (set via
Object.defineProperty with writable/configurable/enumerable) can be
remotely overridden via a value descriptor change *without*
define: true, and that the original descriptor attributes
(writable/configurable/enumerable) are preserved through the merge
in wrapProperty().
- Confirms that setting define: true on a change for a property that
already exists still falls through to wrapProperty(), so the new
function replaces the existing one rather than being skipped.
Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
* Mask method replacements to preserve native identity
When apiManipulation replaces an existing method-valued descriptor with a
configured function, mask the replacement against the original DOM method
so that toString(), toString.toString(), .name and .length still resemble
the native method. Without this, third-party scripts can trivially detect
the override via fn.toString() ('function noop() {}') or fn.name.
Implementation: in wrapApiDescriptor, when the new descriptor's value is
a function and an existing own value descriptor with a function value
exists, wrap the configured replacement with maskMethodReplacement().
That helper creates a fresh wrapper function (so we never mutate shared
singletons such as functionMap.noop), copies the original function's
name and length onto it, and applies wrapToString() against the original
to preserve toString output. The resulting value is then defined via
ContentFeature.defineProperty in the normal way, which adds its debug
flag and a second wrapToString layer; the two layers compose correctly
because the inner mask is wrapToString rather than wrapFunction (the
latter creates a non-configurable own toString that violates the Proxy
get-trap invariant once the outer wrapToString is added on top).
Adds unit tests asserting:
- toString() returns the original method's source rather than the
replacement's body
- toString.toString() still resolves to Function.prototype.toString
- .name and .length match the original method
- When the original is a real native function (Object.prototype
.hasOwnProperty), the masked toString() contains '[native code]'
- The configured replacement is still what executes at call time
Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
* Use captured globals in apiManipulation method masking
The masking helper added in the previous commit invoked the configured
replacement via the page-visible `Reflect.apply` and set `name`/`length`
on the wrapper via the page-visible `Object.defineProperty`. Both are
re-readable by hostile pages after content scope initialisation:
- `Reflect.apply` is reassignable on `globalThis.Reflect`, letting a
page observe or alter every invocation of the protected method.
- `Object.defineProperty` is reassignable on `globalThis.Object`,
letting a page block or poison the name/length masking.
Switch the helper to:
- `ReflectApply` — new bound primitive exported from captured-globals,
matching the existing `ReflectDeleteProperty` pattern.
- `objectDefineProperty` — already-exported captured `Object.defineProperty`.
Adds a unit test that swaps in spies for `globalThis.Reflect.apply` and
`globalThis.Object.defineProperty` after feature construction and asserts:
1. `Object.defineProperty` is not called during `wrapApiDescriptor`
(the helper used captured `objectDefineProperty`).
2. Masked `name`/`length`/`toString()` remain intact under tampering.
3. End-to-end invocation still routes to the configured replacement.
Note: `ContentFeature.defineProperty`'s outer debug-wrapper still reads
`Reflect.apply` from the page; that is pre-existing and out of scope for
this helper. The captured `ReflectApply` here guarantees the helper's
own inner call (helper → configured replacement) cannot be intercepted.
Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
* apiManipulation: shadow-define inherited methods + setterValue support
Adds two capabilities to the descriptor-change path so configs can target
real-world media API surfaces that the existing wrapApiDescriptor cannot:
1. Shadow-define inherited methods. The previous define: true guard was
!(key in api), which is alse for any property reachable via the
prototype chain (e.g. MediaDevices.prototype.addEventListener, inherited
from EventTarget.prototype). Switch the guard to
!Object.prototype.hasOwnProperty.call(api, key) so a config can define
an own property on MediaDevices.prototype that shadows the inherited
one without modifying EventTarget.prototype (which would affect every
other EventTarget consumer - window, document, elements, etc.).
2. setterValue field on descriptor changes. Accessor properties such as
MediaDevices.prototype.ondevicechange (an EventHandler IDL attribute)
are a getter+setter pair. The existing getterValue path produces only
{ get }, so wrapProperty's {...origDescriptor, ...descriptor} merge
preserves the original setter; assigning mediaDevices.ondevicechange = fn
still registers a real listener. Adding setterValue to the descriptor
shape lets configs override the setter half too, invoking the configured
function via captured ReflectApply on each assignment. getterValue
and setterValue may be supplied independently or together.
Validation is updated to accept the accessor shape when only setterValue
is present, and to reject mixing accessor (getterValue/setterValue) with
value (�alue) shapes in a single change.
Adds unit tests covering:
- shadow-define an inherited method with define: true
- no-op behaviour when define: true is omitted and key is inherited
- setter override alongside getter
- setter-only override (original getter preserved)
- validation for setterValue-only changes
- validation rejecting setterValue + value mixing
Motivation: the original Claude camera-prompt-on-login report turned out
to be triggered by Claude subscribing to mediaDevices.devicechange during
its bootstrap (via both �ddEventListener('devicechange', ...) and
mediaDevices.ondevicechange = ...). With these two additions a single
apiManipulation config can suppress every JS-side device-enumeration trigger
without touching EventTarget.prototype or shipping a separate JS shim.
* Mask setterValue replacement against original native setter
When apiManipulation overrides a setter (`setterValue`) on an existing
accessor descriptor, the replacement setter is now masked against the
original `origDescriptor.set` via the existing `maskMethodReplacement`
helper. This preserves the observable identity of the accessor for
descriptor inspection: `Object.getOwnPropertyDescriptor(...).set.toString()`,
`.set.name`, and `.set.length` mirror the original setter rather than
exposing our internal `setter(v) {...}` shape. For native event-handler
IDL attributes (e.g. `Element.prototype.onclick`,
`MediaDevices.prototype.ondevicechange`) this prevents trivial detection
of the override via descriptor probing.
Implementation: extract `origDescriptor = getOwnPropertyDescriptor(api,
key)` once per call in `wrapApiDescriptor`, then branch on
`descriptorKind`:
- For value descriptors, mask the replacement against the original
function value (existing behaviour, refactored to share the lookup).
- For accessor descriptors, additionally mask `descriptor.set` against
`origDescriptor.set` whenever both are functions. The getter path is
left alone because configured getters are generated by processAttr
per-read and don't typically need toString-fidelity.
Also fixes blocking TypeScript errors from the previous commit:
- `createApiDescriptor` now uses a permissive intermediate object type
internally so `descriptor.get` / `descriptor.set` assignments inside
the accessor branch type-check, and returns a cast back to
`Partial<StrictPropertyDescriptor>`.
- The `configSetting` parameter type now allows `undefined` (matches
the setter-only call shape), with a localised cast inside the value
branch where `wrapApiDescriptor`'s early-return already guarantees a
defined value.
Adds a unit test asserting that, after applying a `setterValue` change
over an existing accessor descriptor, the wrapped `descriptor.set`'s
`toString()`, `toString.toString()`, `.name`, and `.length` match the
original setter, and the configured replacement still swallows the
assignment (original setter not invoked).
Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
* apiManipulation: define only for missing APIs; wrap inherited methods
Retain define:true for properties absent from the prototype chain only.
When an API exists (own or inherited), apply value/getterValue/setterValue
overrides—including shadow-defining inherited methods such as
MediaDevices.prototype.addEventListener without requiring define.
* Revert define behavior and JSDoc to branch baseline
Restore unchanged define semantics from c7be658. Value/getterValue/setterValue
and method masking remain; inherited APIs still require define: true to shadow-define.
* Restore define JSDoc and main semantics (key in api)
Revert expanded define documentation and hasOwnProperty shadow-define
behavior. Remove inherited shadow-define tests. define:true remains for
adding properties that are absent from the target object.
* Integrate privacy-configuration #5215 MediaDevices apiManipulation config
Bump @duckduckgo/privacy-configuration to 1779290401870 (merged PR #5215
schema: setterValue/value descriptors with original define comment).
Restore inherited-property shadow-defining without define: true so
MediaDevices.prototype.addEventListener/removeEventListener overrides
match the deployed remote-config shape. Add unit coverage for the
#5215 apiChanges payload and schema validation.
* apiManipulation: guard inherited descriptor merges against invalid shapes
Extract mergePropertyDescriptors from wrapProperty so shadow-defining
inherited APIs uses the same value/accessor compatibility rules. Skip
the override (instead of throwing) when remote config shape disagrees with
the live prototype descriptor kind.
Clarify define vs inherited shadow-define JSDoc. Add unit coverage for
descriptor-kind mismatches on inherited properties.
* apiManipulation: require define:true for inherited shadow-define
Remove the implicit else branch that shadow-defined prototype-chain
properties without define. Inherited overrides now use the same
define:true && !hasOwnProperty path as intended, with descriptor-kind
guards via mergePropertyDescriptors.
Update unit tests; privacy-configuration #5215 entries will need
define: true added in remote config.
* Fix mergePropertyDescriptors return type for StrictPropertyDescriptor
Normalize merged descriptors so tsc accepts them at defineProperty and
wrapProperty call sites. No remote config change required.
* Fix tab-suspension test config for updated tabSuspension schema
privacy-configuration #1779290401870 expects inputFieldFocusDetection
settings as { state } object, not a bare string.
* apiManipulation: restore implicit inherited shadow-define
Re-add the else path for prototype-chain properties so remote config
(e.g. MediaDevices addEventListener) works without define: true.
Properties absent from the entire chain still no-op unless define: true.
Descriptor-kind mismatches still skip via mergePropertyDescriptors.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
* @property {import('../utils.js').ConfigSetting} [getterValue] - The value returned from a getter.
78
+
* @property {import('../utils.js').ConfigSetting} [setterValue] - The function invoked when the property is assigned. Used alongside (or instead of) getterValue to override accessor-style properties such as event handlers (e.g., `MediaDevices.prototype.ondevicechange`).
79
+
* @property {import('../utils.js').ConfigSetting} [value] - The value assigned to a value descriptor, including methods.
69
80
* @property {boolean} [enumerable] - Whether the property is enumerable.
70
81
* @property {boolean} [configurable] - Whether the property is configurable.
71
-
* @property {boolean} [define] - Whether to define the property if it does not exist.
82
+
* @property {boolean} [define] - When true, define a new own property if the key is absent from `api` and its entire prototype chain. When false (default), skip changes for properties that do not exist at all; override own properties via `wrapProperty`; override inherited properties by shadow-defining an own property on `api`.
0 commit comments