diff --git a/.changeset/validate-untrusted-remote-values.md b/.changeset/validate-untrusted-remote-values.md new file mode 100644 index 00000000..6548dedb --- /dev/null +++ b/.changeset/validate-untrusted-remote-values.md @@ -0,0 +1,9 @@ +--- +'@remote-ui/core': patch +'@remote-ui/react': patch +'@remote-ui/vue': patch +--- + +Validate values received from the remote environment before storing or rendering them. + +`createRemoteReceiver()` now normalizes text node content to strings, rejects props containing objects that a UI framework would mistake for one of its own virtual nodes (Preact’s `constructor === undefined` shape, Vue’s `__v_isVNode` flag), rejects `__proto__` as a prop name, and requires component types to be strings. The `@remote-ui/react` host renderer never passes remote-controlled `children`, `ref`, `key`, `dangerouslySetInnerHTML`, `innerHTML`, or `outerHTML` props to host components. The `@remote-ui/vue` host renderer filters remote-controlled DOM content props, `children`, `key`, `ref`, `__proto__`, and all `onVnode*` lifecycle hooks. Vue hosts now require Vue `>=3.5.9 <4.0.0`, which checks the VNode flag before treating object children as virtual nodes. The validation helpers are exported from `@remote-ui/core` for custom receivers. diff --git a/packages/async-subscription/src/tests/create.test.ts b/packages/async-subscription/src/tests/create.test.ts index b84e371b..2f6d7423 100644 --- a/packages/async-subscription/src/tests/create.test.ts +++ b/packages/async-subscription/src/tests/create.test.ts @@ -63,12 +63,11 @@ describe('createRemoteSubscribable()', () => { it('returns the current value when the subscription is made', async () => { const subscription = createSyncSubscribable('abc'); + const remoteSubscription = createRemoteSubscribable(subscription); const newValue = 'xyz'; subscription.update(newValue); - const [, value] = await createRemoteSubscribable(subscription).subscribe( - jest.fn(), - ); + const [, value] = await remoteSubscription.subscribe(jest.fn()); expect(value).toBe(newValue); }); diff --git a/packages/async-subscription/src/tests/stateful.test.ts b/packages/async-subscription/src/tests/stateful.test.ts index 860b655b..cd97216e 100644 --- a/packages/async-subscription/src/tests/stateful.test.ts +++ b/packages/async-subscription/src/tests/stateful.test.ts @@ -95,6 +95,7 @@ describe('makeStatefulSubscribable()', () => { it('unsubscribes from the subscription on stop, and does not apply any updates', async () => { const subscription = createRemoteSubscribable('abc'); + const subscribe = jest.spyOn(subscription, 'subscribe'); const statefulSubscription = makeStatefulSubscribable(subscription); const subscriber = jest.fn(); statefulSubscription.subscribe(subscriber); @@ -109,6 +110,8 @@ describe('makeStatefulSubscribable()', () => { expect(release).toHaveBeenCalledWith(subscription); expect(subscriber).not.toHaveBeenCalled(); expect(statefulSubscription.current).not.toBe(newValue); + const [unsubscribe] = await subscribe.mock.results[0].value; + expect(unsubscribe).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 100557c6..b191042a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -50,3 +50,10 @@ export type { RemoteReceiverAttachableFragment, } from './receiver'; export {isRemoteComponent, isRemoteText, isRemoteFragment} from './utilities'; +export { + normalizeRemoteText, + assertRemotePropName, + assertSafeRemoteProp, + assertSafeRemoteProps, + assertRemoteComponentType, +} from './validation'; diff --git a/packages/core/src/receiver.ts b/packages/core/src/receiver.ts index c2e58d39..70066275 100644 --- a/packages/core/src/receiver.ts +++ b/packages/core/src/receiver.ts @@ -9,6 +9,7 @@ import { KIND_COMPONENT, KIND_FRAGMENT, KIND_ROOT, + KIND_TEXT, } from './types'; import type { ActionArgumentMap, @@ -18,6 +19,12 @@ import type { RemoteFragmentSerialization, } from './types'; import {isRemoteFragment} from './utilities'; +import { + assertRemoteComponentType, + assertRemotePropName, + assertSafeRemoteProp, + normalizeRemoteText, +} from './validation'; export const ROOT_ID = Symbol('RootId'); @@ -222,6 +229,17 @@ export function createRemoteReceiver(): RemoteReceiver { const oldProps = {...(component.props as any)}; + // The remote is untrusted: validate every prop before retaining or + // storing any of it. Fragment props are validated like any other node. + for (const key of Object.keys(newProps)) { + assertRemotePropName(key); + const newProp = (newProps as any)[key]; + if (isRemoteFragmentSerialization(newProp)) { + normalizeNode(newProp, addVersion); + } + assertSafeRemoteProp(newProp); + } + retain(newProps); Object.keys(newProps).forEach((key) => { @@ -230,9 +248,8 @@ export function createRemoteReceiver(): RemoteReceiver { if (isRemoteReceiverAttachableFragment(oldProp)) { detach(oldProp); } - if (isRemoteFragmentSerialization(newProp)) { - const attachableNewProp = normalizeNode(newProp, addVersion); - attach(attachableNewProp); + if (isRemoteReceiverAttachableFragment(newProp)) { + attach(newProp); } }); @@ -249,7 +266,7 @@ export function createRemoteReceiver(): RemoteReceiver { updateText: (id, newText) => { const text = attachedNodes.get(id) as RemoteReceiverAttachableText; - text.text = newText; + text.text = normalizeRemoteText(newText); text.version += 1; enqueueUpdate(text); }, @@ -419,17 +436,32 @@ function normalizeNode< | RemoteFragmentSerialization, R, >(node: T, normalizer: (node: T) => R) { + // The remote is untrusted. Text must be a string, component types must be + // strings, and props must not contain objects that a UI framework would + // mistake for its own virtual nodes. Validate before the normalizer stores + // anything. + if (node.kind === KIND_TEXT) { + (node as any).text = normalizeRemoteText((node as any).text); + } if (node.kind === KIND_FRAGMENT || node.kind === KIND_COMPONENT) { (node as any).children.forEach((child: T) => normalizeNode(child, normalizer), ); } + if (node.kind === KIND_COMPONENT) { + assertRemoteComponentType((node as any).type); + } if (node.kind === KIND_COMPONENT && 'props' in node) { const {props} = node as any; for (const key of Object.keys(props)) { + assertRemotePropName(key); const prop = props[key]; - if (!isRemoteFragmentSerialization(prop)) continue; - props[key] = normalizeNode(prop as any, normalizer); + if (isRemoteFragmentSerialization(prop)) { + props[key] = normalizeNode(prop as any, normalizer); + } + // Fragment props are still objects passed to host components. Check + // their full shape after normalizing text, including extra wire fields. + assertSafeRemoteProp(props[key]); } } return normalizer(node); diff --git a/packages/core/src/tests/receiver.test.ts b/packages/core/src/tests/receiver.test.ts index 491389e4..66701d80 100644 --- a/packages/core/src/tests/receiver.test.ts +++ b/packages/core/src/tests/receiver.test.ts @@ -1,10 +1,17 @@ +import {RETAIN_METHOD, RELEASE_METHOD} from '@remote-ui/rpc'; + import {createRemoteReceiver} from '../receiver'; import { ACTION_MOUNT, ACTION_INSERT_CHILD, ACTION_REMOVE_CHILD, + ACTION_UPDATE_PROPS, + ACTION_UPDATE_TEXT, KIND_COMPONENT, + KIND_FRAGMENT, + KIND_TEXT, } from '../types'; +import type {RemoteReceiverAttachableText} from '../receiver'; describe('createRemoteReceiver()', () => { describe('state', () => { @@ -68,4 +75,274 @@ describe('createRemoteReceiver()', () => { expect(receiver.attached.root.children).toHaveLength(0); }); }); + + describe('untrusted values', () => { + /** + * The remote is untrusted, and host renderers feed what this receiver + * stores straight into a UI framework. Preact treats any object whose + * `constructor` resolves to `undefined` as a virtual node, so such + * objects must never be stored where a renderer expects a string, and + * must be rejected as prop values. + */ + + function vnodeShaped() { + return {constructor: undefined, type: 'div', props: {}, __b: 1}; + } + + function component(id: string, extra: Record = {}) { + return { + id, + kind: KIND_COMPONENT, + type: 'Banner', + props: {}, + children: [], + ...extra, + } as any; + } + + function text(id: string, value: unknown) { + return {id, kind: KIND_TEXT, text: value} as any; + } + + it('stores strings for text content on mount', () => { + const receiver = createRemoteReceiver(); + + receiver.receive(ACTION_MOUNT, [ + component('1', { + children: [text('2', vnodeShaped()), text('3', 42), text('4', null)], + }), + ]); + + expect( + receiver.attached.get({id: '2'})!.text, + ).toBe('[object Object]'); + expect( + receiver.attached.get({id: '3'})!.text, + ).toBe('42'); + expect( + receiver.attached.get({id: '4'})!.text, + ).toBe(''); + }); + + it('stores strings for inserted and updated text', () => { + const receiver = createRemoteReceiver(); + + receiver.receive(ACTION_MOUNT, []); + receiver.receive( + ACTION_INSERT_CHILD, + undefined, + 0, + text('1', vnodeShaped()), + false, + ); + + const attached = receiver.attached.get({ + id: '1', + })!; + expect(attached.text).toBe('[object Object]'); + + receiver.receive(ACTION_UPDATE_TEXT, '1', Object.create(null)); + expect(attached.text).toBe('[object Object]'); + + receiver.receive(ACTION_UPDATE_TEXT, '1', undefined as any); + expect(attached.text).toBe(''); + }); + + it('stores strings for text inside fragment props', () => { + const receiver = createRemoteReceiver(); + + receiver.receive(ACTION_MOUNT, [ + component('1', { + props: { + heading: { + id: 'f', + kind: KIND_FRAGMENT, + children: [text('t', vnodeShaped())], + }, + }, + }), + ]); + + expect( + receiver.attached.get({id: 't'})!.text, + ).toBe('[object Object]'); + }); + + it('rejects vnode-shaped props on mount without attaching anything', () => { + const receiver = createRemoteReceiver(); + + expect(() => + receiver.receive(ACTION_MOUNT, [ + component('1', {props: {heading: vnodeShaped()}}), + ]), + ).toThrow(/masquerades/); + + expect(receiver.attached.get({id: '1'})).toBeNull(); + expect(receiver.attached.root.children).toHaveLength(0); + }); + + it('rejects vnode-shaped props on insert and update, keeping previous values', () => { + const receiver = createRemoteReceiver(); + + receiver.receive(ACTION_MOUNT, [ + component('1', {props: {heading: 'Hi'}}), + ]); + + expect(() => + receiver.receive( + ACTION_INSERT_CHILD, + undefined, + 1, + component('2', {props: {items: ['ok', vnodeShaped()]}}), + false, + ), + ).toThrow(/masquerades/); + expect(receiver.attached.get({id: '2'})).toBeNull(); + + expect(() => + receiver.receive(ACTION_UPDATE_PROPS, '1', { + heading: {nested: vnodeShaped()}, + }), + ).toThrow(/masquerades/); + + expect(receiver.attached.get({id: '1'})!.props).toStrictEqual({ + heading: 'Hi', + }); + }); + + it('rejects __proto__ as a prop name', () => { + const receiver = createRemoteReceiver(); + + receiver.receive(ACTION_MOUNT, [ + component('1', {props: {heading: 'Hi'}}), + ]); + + expect(() => + receiver.receive( + ACTION_UPDATE_PROPS, + '1', + JSON.parse('{"__proto__": {"polluted": true}}'), + ), + ).toThrow(/name is not allowed/); + + const {props} = receiver.attached.get({id: '1'})!; + expect(props.polluted).toBeUndefined(); + expect(Object.getPrototypeOf(props)).toBe(Object.prototype); + }); + + it('rejects a non-string component type', () => { + const receiver = createRemoteReceiver(); + + expect(() => + receiver.receive(ACTION_MOUNT, [component('1', {type: {}})]), + ).toThrow(/component type/); + }); + + it.each(['mount', 'insert', 'update'])( + 'rejects framework nodes disguised as fragment props on %s', + (action) => { + const receiver = createRemoteReceiver(); + receiver.receive(ACTION_MOUNT, [component('1')]); + + const props = { + heading: { + id: 'f', + kind: KIND_FRAGMENT, + children: [], + __v_isVNode: true, + type: 'div', + props: {innerHTML: 'injected'}, + }, + }; + + expect(() => { + if (action === 'mount') { + receiver.receive(ACTION_MOUNT, [component('2', {props})]); + } else if (action === 'insert') { + receiver.receive( + ACTION_INSERT_CHILD, + undefined, + 1, + component('2', {props}), + false, + ); + } else { + receiver.receive(ACTION_UPDATE_PROPS, '1', props); + } + }).toThrow(/masquerades/); + + expect(receiver.attached.get({id: 'f'})).toBeNull(); + }, + ); + + it('rejects invalid fragment updates before detaching or retaining anything', () => { + const receiver = createRemoteReceiver(); + receiver.receive(ACTION_MOUNT, [ + component('1', { + props: { + heading: { + id: 'old', + kind: KIND_FRAGMENT, + children: [text('old-text', 'Hi')], + }, + }, + }), + ]); + const attached = receiver.attached.get({id: '1'})!; + const oldFragment = attached.props.heading; + const oldText = receiver.attached.get({id: 'old-text'}); + const onPress = Object.assign(jest.fn(), { + [RETAIN_METHOD]: jest.fn(), + [RELEASE_METHOD]: jest.fn(), + }); + + expect(() => + receiver.receive(ACTION_UPDATE_PROPS, '1', { + onPress, + heading: { + id: 'new', + kind: KIND_FRAGMENT, + children: [text('new-text', 'New')], + }, + footer: { + id: 'invalid', + kind: KIND_FRAGMENT, + children: [component('bad', {props: {value: vnodeShaped()}})], + }, + }), + ).toThrow(/masquerades/); + + expect(onPress[RETAIN_METHOD]).not.toHaveBeenCalled(); + expect(attached.props).toStrictEqual({heading: oldFragment}); + expect(attached.version).toBe(0); + expect(receiver.attached.get({id: 'old'})).toBe(oldFragment); + expect(receiver.attached.get({id: 'old-text'})).toBe(oldText); + expect(receiver.attached.get({id: 'new'})).toBeNull(); + expect(receiver.attached.get({id: 'new-text'})).toBeNull(); + }); + + it('still accepts ordinary object, function, and fragment props', () => { + const receiver = createRemoteReceiver(); + const onPress = () => {}; + + receiver.receive(ACTION_MOUNT, [ + component('1', { + props: { + options: {list: [1, {label: 'c'}]}, + onPress, + heading: { + id: 'f', + kind: KIND_FRAGMENT, + children: [text('t', 'Hi')], + }, + }, + }), + ]); + + const {props} = receiver.attached.get({id: '1'})!; + expect(props.options).toStrictEqual({list: [1, {label: 'c'}]}); + expect(props.onPress).toBe(onPress); + expect(receiver.attached.get({id: 'f'})).not.toBeNull(); + }); + }); }); diff --git a/packages/core/src/tests/root.test.ts b/packages/core/src/tests/root.test.ts index a40a343f..a1dbe149 100644 --- a/packages/core/src/tests/root.test.ts +++ b/packages/core/src/tests/root.test.ts @@ -8,7 +8,7 @@ describe('root', () => { const root = createRemoteRoot(() => {}); expect(() => { - root.createComponent('Card'); + expect(root.createComponent('Card')).toHaveProperty('type', 'Card'); }).not.toThrow(); }); @@ -17,7 +17,7 @@ describe('root', () => { const root = createRemoteRoot(() => {}, {components}); expect(() => { - root.createComponent('Card'); + expect(root.createComponent('Card')).toHaveProperty('type', 'Card'); }).not.toThrow(); }); @@ -46,7 +46,7 @@ describe('root', () => { const root = createRemoteRoot(() => {}, {components}); expect(() => { - root.createText(); + expect(root.createText()).toHaveProperty('text', ''); }).not.toThrow(); }); }); @@ -58,6 +58,7 @@ describe('root', () => { expect(() => { const card = root.createComponent('Card'); root.append(card); + expect(root.children).toStrictEqual([card]); }).not.toThrow(); }); @@ -82,6 +83,7 @@ describe('root', () => { const image = root.createComponent('Image'); root.append(image); root.insertBefore(card, image); + expect(root.children).toStrictEqual([card, image]); }).not.toThrow(); }); @@ -108,6 +110,7 @@ describe('root', () => { const image = root.createComponent('Image'); root.append(image); root.replaceChildren(card); + expect(root.children).toStrictEqual([card]); }).not.toThrow(); }); @@ -395,7 +398,7 @@ describe('root', () => { const {arrays} = (receiver.children[0] as any).props; - expect(arrays[0].array2).toStrictEqual(expect.arrayContaining([1, 3, 4])); + expect(arrays[0].array2).toStrictEqual([1, 3, 4]); }); }); }); diff --git a/packages/core/src/tests/validation.test.ts b/packages/core/src/tests/validation.test.ts new file mode 100644 index 00000000..f2c6d415 --- /dev/null +++ b/packages/core/src/tests/validation.test.ts @@ -0,0 +1,158 @@ +import { + normalizeRemoteText, + assertSafeRemoteProp, + assertSafeRemoteProps, + assertRemotePropName, + assertRemoteComponentType, +} from '../validation'; + +/** + * Objects shaped like a Preact virtual node. `constructor: undefined` as an + * own property survives structured cloning and the RPC encoder. + */ +function preactVNode() { + return { + constructor: undefined, + type: 'div', + props: {dangerouslySetInnerHTML: {__html: 'x'}}, + __b: 1, + }; +} + +function nullPrototypeObject() { + const object = Object.create(null); + Object.assign(object, {type: 'div', props: {}, __b: 1}); + return object; +} + +function vueVNode() { + return {__v_isVNode: true, type: 'div', props: {innerHTML: 'x'}}; +} + +describe('normalizeRemoteText()', () => { + it('keeps strings', () => { + expect(normalizeRemoteText('hi')).toBe('hi'); + }); + + it('coerces primitives', () => { + expect(normalizeRemoteText(1)).toBe('1'); + expect(normalizeRemoteText(true)).toBe('true'); + }); + + it('turns nullish values into an empty string', () => { + expect(normalizeRemoteText(null)).toBe(''); + expect(normalizeRemoteText(undefined)).toBe(''); + }); + + it('describes objects without invoking their methods', () => { + const toString = jest.fn(() => 'from remote'); + const valueOf = jest.fn(() => 'from remote'); + + expect(normalizeRemoteText({toString, valueOf})).toBe('[object Object]'); + expect(normalizeRemoteText(preactVNode())).toBe('[object Object]'); + expect(normalizeRemoteText(nullPrototypeObject())).toBe('[object Object]'); + expect(normalizeRemoteText([1, 2])).toBe('[object Array]'); + expect(normalizeRemoteText(() => {})).toBe('[object Function]'); + + expect(toString).not.toHaveBeenCalled(); + expect(valueOf).not.toHaveBeenCalled(); + }); +}); + +describe('assertSafeRemoteProp()', () => { + it('accepts ordinary data', () => { + expect(() => + assertSafeRemoteProp({ + number: 1, + string: 'two', + list: [1, {empty: null}], + map: new Map([['key', {flag: true}]]), + set: new Set([1]), + callback: () => {}, + date: new Date(), + }), + ).not.toThrow(); + }); + + it('accepts cyclic data', () => { + const cyclic: any = {items: []}; + cyclic.items.push(cyclic); + cyclic.self = cyclic; + expect(() => assertSafeRemoteProp(cyclic)).not.toThrow(); + }); + + it('rejects an object with an own undefined constructor', () => { + expect(() => assertSafeRemoteProp(preactVNode())).toThrow(/masquerades/); + }); + + it('rejects an object with a null prototype', () => { + expect(() => assertSafeRemoteProp(nullPrototypeObject())).toThrow( + /masquerades/, + ); + }); + + it('rejects an object flagged as a Vue virtual node', () => { + expect(() => assertSafeRemoteProp(vueVNode())).toThrow(/masquerades/); + }); + + it('rejects a framework node shape anywhere in the value', () => { + expect(() => assertSafeRemoteProp({items: ['ok', preactVNode()]})).toThrow( + /not allowed/, + ); + expect(() => + assertSafeRemoteProp({deep: {deeper: nullPrototypeObject()}}), + ).toThrow(/not allowed/); + expect(() => assertSafeRemoteProp(new Map([['k', vueVNode()]]))).toThrow( + /not allowed/, + ); + expect(() => assertSafeRemoteProp(new Set([preactVNode()]))).toThrow( + /not allowed/, + ); + }); + + it('rejects a framework node shape hidden behind a redirected prototype', () => { + const container = Object.create({}); + container.items = [preactVNode()]; + expect(() => assertSafeRemoteProp(container)).toThrow(/not allowed/); + }); + + it('rejects an object whose prototype chain hides the constructor', () => { + const value = Object.create({constructor: undefined}); + value.type = 'div'; + expect(() => assertSafeRemoteProp(value)).toThrow(/not allowed/); + }); +}); + +describe('assertSafeRemoteProps()', () => { + it.each([undefined, null, {count: 1, onPress() {}}])( + 'accepts the props record %p', + (props) => { + expect(() => assertSafeRemoteProps(props)).not.toThrow(); + }, + ); + + it('rejects non-object records and bad names or values', () => { + expect(() => assertSafeRemoteProps('nope')).toThrow(/must be an object/); + expect(() => assertSafeRemoteProps({heading: preactVNode()})).toThrow( + /not allowed/, + ); + expect(() => + assertSafeRemoteProps(JSON.parse('{"__proto__": {"polluted": 1}}')), + ).toThrow(/name is not allowed/); + }); +}); + +describe('assertRemotePropName()', () => { + it('rejects __proto__ and non-strings', () => { + expect(() => assertRemotePropName('__proto__')).toThrow(/not allowed/); + expect(() => assertRemotePropName(1)).toThrow(/not allowed/); + expect(() => assertRemotePropName('fine')).not.toThrow(); + }); +}); + +describe('assertRemoteComponentType()', () => { + it('requires a string', () => { + expect(() => assertRemoteComponentType('Banner')).not.toThrow(); + expect(() => assertRemoteComponentType({})).toThrow(/component type/); + }); +}); diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts new file mode 100644 index 00000000..a31dc0c7 --- /dev/null +++ b/packages/core/src/validation.ts @@ -0,0 +1,107 @@ +// The remote environment is untrusted. Everything the receiver stores must be +// checked against the shape the protocol promises, because host renderers hand +// these values straight to a UI framework. Text is a string in the protocol; +// props are arbitrary data, but must never be mistaken by a framework for one +// of its own internal objects. + +/** + * Converts a value received where the protocol expects text into a string. + * Nullish values become an empty string. Objects are described without + * invoking any of their methods, so a remote can’t influence the result + * beyond the primitive it sent. + */ +export function normalizeRemoteText(value: unknown): string { + if (typeof value === 'string') return value; + if (value == null) return ''; + if (typeof value === 'object' || typeof value === 'function') { + return Object.prototype.toString.call(value); + } + return String(value); +} + +/** + * Checks a prop name received from the remote environment. Names must be + * strings and must not be able to alter the prototype of the host’s records. + */ +export function assertRemotePropName(name: unknown): asserts name is string { + if (typeof name !== 'string' || name === '__proto__') { + throw new Error(`Remote prop name is not allowed: ${String(name)}`); + } +} + +/** + * Checks a prop value received from the remote environment. Props are + * arbitrary data, but host renderers pass them directly to UI frameworks, + * which identify their own virtual nodes by shape. Preact treats any object + * whose `constructor` resolves to `undefined` as a virtual node; Vue treats + * any object flagged `__v_isVNode` as one. Such objects survive `postMessage` + * and the RPC layer, and would let a remote render arbitrary markup + * (including `dangerouslySetInnerHTML` or `innerHTML`) on the host. No + * legitimate data has these shapes, so they are rejected anywhere in the value. + */ +export function assertSafeRemoteProp(value: unknown): void { + const seen = new Set(); + const pending: unknown[] = [value]; + + while (pending.length > 0) { + const current = pending.pop(); + + if (typeof current !== 'object' || current === null) continue; + if (seen.has(current)) continue; + seen.add(current); + + // Preact: a virtual node is any object whose `constructor` is undefined. + // Vue: a virtual node is any object flagged with `__v_isVNode`. + if ( + (current as any).constructor === undefined || + (current as any).__v_isVNode === true + ) { + throw new Error( + 'Remote prop value is not allowed: object masquerades as a framework node', + ); + } + + if (current instanceof Map) { + for (const [key, item] of current) { + pending.push(key, item); + } + } else if (current instanceof Set) { + for (const item of current) pending.push(item); + } + + // Walk own enumerable keys of every object (including arrays), regardless + // of prototype: a remote can redirect an object’s prototype on the wire, + // so prototype-based “plain object” checks can’t be used to skip values. + for (const key of Object.keys(current)) { + pending.push((current as any)[key]); + } + } +} + +/** + * Checks a record of props received from the remote environment. + */ +export function assertSafeRemoteProps(props: unknown): void { + if (props == null) return; + + if (typeof props !== 'object') { + throw new Error('Remote props must be an object'); + } + + for (const name of Object.keys(props)) { + assertRemotePropName(name); + assertSafeRemoteProp((props as any)[name]); + } +} + +/** + * Checks the component type of a serialized remote component. Host renderers + * look the type up in a map of components, so it must be a string. + */ +export function assertRemoteComponentType( + type: unknown, +): asserts type is string { + if (typeof type !== 'string') { + throw new Error(`Remote component type is not allowed: ${String(type)}`); + } +} diff --git a/packages/react/package.json b/packages/react/package.json index 361b9a4c..9cf129e7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -34,6 +34,7 @@ "sideEffects": false, "devDependencies": { "@types/react-dom": "^18.0.11", + "preact": "^10.29.8", "react": "^18.2.0", "react-dom": "^18.2.0", "react-reconciler": "^0.29.0" diff --git a/packages/react/src/host/RemoteComponent.tsx b/packages/react/src/host/RemoteComponent.tsx index fe103dd3..6a2ee692 100644 --- a/packages/react/src/host/RemoteComponent.tsx +++ b/packages/react/src/host/RemoteComponent.tsx @@ -19,6 +19,23 @@ import type { const emptyObject = {}; +/** + * Prop names that are controlled by the host renderer or by the UI framework + * itself. The remote environment can name props freely, so these names are + * never taken from remote data: `children`, `ref`, and `key` would override the + * renderer’s own wiring, and the HTML props would let a remote inject markup + * into any host component that spreads its props onto a DOM element. + */ +const RESERVED_PROPS = new Set([ + 'children', + 'ref', + 'key', + 'dangerouslySetInnerHTML', + 'innerHTML', + 'outerHTML', + '__proto__', +]); + export const RemoteComponent = memo(function RemoteComponent({ receiver, component, @@ -34,6 +51,7 @@ export const RemoteComponent = memo(function RemoteComponent({ const newProps: typeof props = {}; for (const key of Object.keys(props)) { + if (RESERVED_PROPS.has(key)) continue; const prop = props[key]; newProps[key] = isRemoteReceiverAttachableFragment(prop) ? ( { jest.runAllTimers(); }); + expect(appElement.querySelector('img')?.getAttribute('src')).toBe( + 'image.png', + ); + domAct(() => { Simulate.click(appElement.querySelector('button')!); }); @@ -900,6 +904,6 @@ describe('@remote-ui/react', () => { jest.runAllTimers(); }); - expect(appElement.innerHTML).not.toContain('data-test-id="image"'); + expect(appElement.querySelector('img')).toBeNull(); }); }); diff --git a/packages/react/src/tests/preact-untrusted-values.test.ts b/packages/react/src/tests/preact-untrusted-values.test.ts new file mode 100644 index 00000000..2e094b00 --- /dev/null +++ b/packages/react/src/tests/preact-untrusted-values.test.ts @@ -0,0 +1,73 @@ +import {h, render} from 'preact'; +import {act} from 'preact/test-utils'; +import { + createRemoteReceiver, + ACTION_MOUNT, + ACTION_UPDATE_PROPS, + KIND_COMPONENT, + KIND_TEXT, +} from '@remote-ui/core'; + +import {RemoteRenderer, createController} from '../host'; + +jest.mock('react', () => jest.requireActual('preact/compat')); +jest.mock('react/jsx-runtime', () => jest.requireActual('preact/jsx-runtime')); +jest.mock('react/jsx-dev-runtime', () => + jest.requireActual('preact/jsx-runtime'), +); + +const receivedProps = jest.fn(); + +function HostSpread(props: Record) { + receivedProps(props); + return h('div', props); +} + +describe('untrusted props on preact/compat', () => { + beforeEach(() => receivedProps.mockClear()); + + it.each([ + ['innerHTML', 'mount'], + ['innerHTML', 'update'], + ['outerHTML', 'mount'], + ['outerHTML', 'update'], + ])('never spreads %s from remote props on %s', async (name, action) => { + const receiver = createRemoteReceiver(); + const controller = createController({Spread: HostSpread as any}); + const container = document.createElement('div'); + const props = { + [name]: 'injected', + title: 'kept', + }; + try { + await act(async () => { + render(h(RemoteRenderer as any, {receiver, controller}), container); + receiver.receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Spread', + props: action === 'mount' ? props : {title: 'initial'}, + children: [{id: '2', kind: KIND_TEXT, text: 'Safe child'}], + }, + ]); + await receiver.flush(); + }); + if (action === 'update') { + await act(async () => { + receiver.receive(ACTION_UPDATE_PROPS, '1', props); + await receiver.flush(); + }); + } + + const forwarded = receivedProps.mock.calls.slice(-1)[0][0]; + expect(Object.prototype.hasOwnProperty.call(forwarded, name)).toBe(false); + expect(container.querySelector('#injected-by-remote')).toBeNull(); + expect(container.querySelector('div[title="kept"]')?.textContent).toBe( + 'Safe child', + ); + } finally { + act(() => render(null, container)); + } + }); +}); diff --git a/packages/react/src/tests/untrusted-values.test.tsx b/packages/react/src/tests/untrusted-values.test.tsx new file mode 100644 index 00000000..5e0bafbf --- /dev/null +++ b/packages/react/src/tests/untrusted-values.test.tsx @@ -0,0 +1,244 @@ +/* eslint @shopify/jsx-no-hardcoded-content: off */ + +import {forwardRef, useEffect} from 'react'; +import type {ReactNode} from 'react'; +import {createRoot as createDOMRoot} from 'react-dom/client'; +import type {Root} from 'react-dom/client'; +import {act as domAct} from 'react-dom/test-utils'; +import { + createRemoteReceiver, + ACTION_MOUNT, + ACTION_UPDATE_TEXT, + ACTION_UPDATE_PROPS, + KIND_COMPONENT, + KIND_TEXT, +} from '@remote-ui/core'; + +import {RemoteRenderer, createController} from '../host'; + +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +/** + * The remote environment is untrusted: it can send any structured-cloneable + * value where the protocol expects a string (text) or plain data (props). + * React refuses to render objects that aren’t tagged with its `$$typeof` + * symbol, but it throws while doing so, tearing down the host tree; and + * hosts that run this package on `preact/compat` would render a smuggled + * virtual node as real markup. The receiver normalizes text to strings and + * rejects virtual-node shapes in props; the host renderer never spreads + * framework-reserved prop names onto host components. + */ + +const INJECTED_ID = 'injected-by-remote'; +const INJECTED_HTML = `injected`; + +function smuggledNode() { + return { + constructor: undefined, + type: 'div', + props: {dangerouslySetInnerHTML: {__html: INJECTED_HTML}}, + key: null, + ref: null, + __b: 1, + }; +} + +// Mirrors how `@remote-ui/rpc`’s basic encoder rebuilds objects: own keys, +// including a literal `constructor`, survive the trip. +function rpcDecode(value: any): any { + if (typeof value !== 'object' || value === null) return value; + if (Array.isArray(value)) return value.map(rpcDecode); + return Object.keys(value).reduce( + (object, key) => ({...object, [key]: rpcDecode(value[key])}), + {} as any, + ); +} + +function HostBanner({ + heading, + children, +}: { + heading?: string; + children?: ReactNode; +}) { + return ( +
+

{heading}

+
{children}
+
+ ); +} + +const HostSpread = forwardRef>( + function HostSpread(props, ref) { + return
; + }, +); + +function HostStateful({onMount}: {onMount(): void}) { + useEffect(onMount, [onMount]); + return
; +} + +const roots: Root[] = []; + +function mount() { + const receiver = createRemoteReceiver(); + const controller = createController({ + Banner: HostBanner, + Spread: HostSpread, + Stateful: HostStateful, + }); + const container = document.createElement('div'); + document.body.append(container); + + let root!: Root; + domAct(() => { + root = createDOMRoot(container, {onRecoverableError() {}}); + root.render(); + }); + roots.push(root); + + return { + container, + async receive(...args: any[]) { + await domAct(async () => { + (receiver.receive as any)(...rpcDecode(args)); + await receiver.flush(); + }); + }, + }; +} + +describe('untrusted values from the remote environment', () => { + afterEach(() => { + for (const root of roots.splice(0)) domAct(() => root.unmount()); + document.body.innerHTML = ''; + }); + + it('renders smuggled text content as plain text and keeps rendering', async () => { + const {container, receive} = mount(); + + await receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Banner', + props: {heading: 'Hi'}, + children: [{id: '2', kind: KIND_TEXT, text: smuggledNode()}], + }, + ]); + + expect(document.getElementById(INJECTED_ID)).toBeNull(); + expect(container.querySelector('section > div')!.textContent).toBe( + '[object Object]', + ); + + await receive(ACTION_UPDATE_TEXT, '2', smuggledNode()); + expect(document.getElementById(INJECTED_ID)).toBeNull(); + + await receive(ACTION_UPDATE_TEXT, '2', 7); + expect(container.querySelector('section > div')!.textContent).toBe('7'); + }); + + it('rejects smuggled props and keeps the previous tree', async () => { + const {container, receive} = mount(); + + await expect( + receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Banner', + props: {heading: smuggledNode()}, + children: [], + }, + ]), + ).rejects.toThrow(/masquerades/); + + await receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Banner', + props: {heading: 'Hi'}, + children: [], + }, + ]); + + await expect( + receive(ACTION_UPDATE_PROPS, '1', {heading: {items: [smuggledNode()]}}), + ).rejects.toThrow(/masquerades/); + + expect(document.getElementById(INJECTED_ID)).toBeNull(); + expect(container.querySelector('h2')!.textContent).toBe('Hi'); + }); + + it('never spreads dangerouslySetInnerHTML or children from remote props', async () => { + const {container, receive} = mount(); + + await receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Spread', + props: { + dangerouslySetInnerHTML: {__html: INJECTED_HTML}, + children: 'override', + title: 'kept', + }, + children: [], + }, + ]); + + expect(document.getElementById(INJECTED_ID)).toBeNull(); + expect(container.querySelector('div[title="kept"]')).not.toBeNull(); + expect(container.textContent).not.toContain('override'); + }); + + it.each(['mount', 'update'])( + 'never passes a remote ref to the host on %s', + async (action) => { + const {container, receive} = mount(); + const ref = jest.fn(); + const props = {ref, title: 'kept'}; + await receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Spread', + props: action === 'mount' ? props : {}, + children: [], + }, + ]); + if (action === 'update') { + await receive(ACTION_UPDATE_PROPS, '1', props); + } + + expect(container.querySelector('div[title="kept"]')).not.toBeNull(); + expect(ref).not.toHaveBeenCalled(); + }, + ); + + it('does not remount a host component when a remote key changes', async () => { + const {container, receive} = mount(); + const onMount = jest.fn(); + await receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Stateful', + props: {key: 'first', onMount}, + children: [], + }, + ]); + const element = container.firstElementChild; + expect(element).not.toBeNull(); + expect(onMount).toHaveBeenCalledTimes(1); + + await receive(ACTION_UPDATE_PROPS, '1', {key: 'second'}); + + expect(container.firstElementChild).toBe(element); + expect(onMount).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/rpc/src/tests/endpoint.test.ts b/packages/rpc/src/tests/endpoint.test.ts index 2c6bc930..ae65366c 100644 --- a/packages/rpc/src/tests/endpoint.test.ts +++ b/packages/rpc/src/tests/endpoint.test.ts @@ -1,5 +1,10 @@ import {MessageEndpoint} from '../types'; -import {createEndpoint, TERMINATE, MissingResolverError} from '../endpoint'; +import { + createEndpoint, + CALL, + TERMINATE, + MissingResolverError, +} from '../endpoint'; import {fromMessagePort} from '../adaptors'; import {release, retain} from '../memory'; @@ -34,10 +39,14 @@ describe('createEndpoint()', () => { const {port1: newPort1, port2: newPort2} = new MessageChannel(); newPort1.start(); newPort2.start(); + const oldMessageSpy = jest.spyOn(port1, 'postMessage'); + const newMessageSpy = jest.spyOn(newPort1, 'postMessage'); endpoint1.replace(fromMessagePort(newPort1)); endpoint2.replace(fromMessagePort(newPort2)); expect(await endpoint1.call.hello()).toBe('world'); + expect(newMessageSpy).toHaveBeenCalled(); + expect(oldMessageSpy).not.toHaveBeenCalled(); }); }); @@ -62,7 +71,6 @@ describe('createEndpoint()', () => { }); it('re-throws errors thrown in exposed methods', async () => { - expect.assertions(2); const {port1, port2} = new MessageChannel(); port1.start(); port2.start(); @@ -72,13 +80,14 @@ describe('createEndpoint()', () => { ); const messageEndpoint2 = fromMessagePort(port2); + let listenerResult!: Promise; const endpoint2 = createEndpoint({ ...messageEndpoint2, addEventListener(event, listener) { - messageEndpoint2.addEventListener(event, async (...args) => { - await expect(listener(...args)).rejects.toMatchObject({ - message: expect.stringContaining('this is broken'), - }); + messageEndpoint2.addEventListener(event, (...args) => { + listenerResult = Promise.resolve(listener(...args)); + // Observe the rejection until the test checks it below. + listenerResult.catch(() => undefined); }); }, }); @@ -92,6 +101,9 @@ describe('createEndpoint()', () => { await expect(endpoint1.call.hello()).rejects.toMatchObject({ message: expect.stringContaining('this is broken'), }); + await expect(listenerResult).rejects.toMatchObject({ + message: expect.stringContaining('this is broken'), + }); }); it('deletes an exposed value by passing undefined', async () => { @@ -222,7 +234,15 @@ describe('createEndpoint()', () => { const spy = jest.fn(() => 'world'); endpoint2.expose({hello: spy}); + // Capture the registered listener to simulate a message queued before + // termination, even though termination removes the event listener. + // @ts-expect-error Accessing private property for testing + const listener = (port2.listeners as Set).values().next() + .value!; endpoint2.terminate(); + await listener({ + data: [CALL, ['late-call', 'hello', []]], + } as MessageEvent); // Try to call a method after termination await expect(endpoint1.call.hello()).rejects.toMatchObject({ diff --git a/packages/vue/package.json b/packages/vue/package.json index 28e3d580..7e54ad82 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -35,15 +35,15 @@ "dependencies": { "@remote-ui/core": "^2.2.7", "@remote-ui/rpc": "^1.4.7", - "@vue/runtime-core": ">=3.0.0 <4.0.0" + "@vue/runtime-core": ">=3.5.9 <4.0.0" }, "devDependencies": { - "@vue/compiler-sfc": "^3.0.4", + "@vue/compiler-sfc": "^3.5.9", "@vue/vue3-jest": "^27.0.0-alpha.1", "ts-jest": "^27.0.0", - "vue": "^3.0.4" + "vue": "^3.5.9" }, "peerDependencies": { - "vue": ">=3.0.0 <4.0.0" + "vue": ">=3.5.9 <4.0.0" } } diff --git a/packages/vue/src/host/RemoteComponent.ts b/packages/vue/src/host/RemoteComponent.ts index 51643dea..205bb81e 100644 --- a/packages/vue/src/host/RemoteComponent.ts +++ b/packages/vue/src/host/RemoteComponent.ts @@ -1,5 +1,5 @@ import {defineComponent, h, watch, computed} from 'vue'; -import type {Ref, DefineComponent, PropType} from 'vue'; +import type {Ref, PropType, VNode} from 'vue'; import {retain, release} from '@remote-ui/core'; import type { RemoteReceiver, @@ -10,24 +10,33 @@ import type {Controller} from './controller'; import {RemoteText} from './RemoteText'; import {useAttached} from './shared'; +// The remote must not control DOM content, component identity, refs, or +// framework lifecycle hooks through props passed to a host implementation. +const RESERVED_PROPS = new Set([ + 'children', + 'dangerouslySetInnerHTML', + 'innerHTML', + 'outerHTML', + 'textContent', + 'key', + 'ref', + '__proto__', +]); + interface Props { receiver: RemoteReceiver; component: RemoteReceiverAttachableComponent; controller: Controller; } -export const RemoteComponent: DefineComponent<{ - component: {type: PropType; required: true}; - receiver: {type: PropType; required: true}; - controller: {type: PropType; required: true}; -}> = defineComponent({ +export const RemoteComponent = defineComponent({ name: 'RemoteComponent', props: { component: {type: Object as PropType, required: true}, receiver: {type: Object as PropType, required: true}, controller: {type: Object as PropType, required: true}, }, - setup({receiver, component, controller}) { + setup({receiver, component, controller}): () => VNode | null { const attached = useAttached(receiver, component); const propsRef: Ref | undefined> = computed( () => attached.value?.props as any, @@ -45,7 +54,13 @@ export const RemoteComponent: DefineComponent<{ const {children, props} = attached.value; - return h(Implementation, props as any, () => + const hostProps: Record = {}; + for (const key of Object.keys(props ?? {})) { + if (RESERVED_PROPS.has(key) || key.startsWith('onVnode')) continue; + hostProps[key] = (props as any)[key]; + } + + return h(Implementation, hostProps, () => children.map((child) => { if ('children' in child) { return h(RemoteComponent, { diff --git a/packages/vue/src/tests/package.test.ts b/packages/vue/src/tests/package.test.ts new file mode 100644 index 00000000..1c6e7789 --- /dev/null +++ b/packages/vue/src/tests/package.test.ts @@ -0,0 +1,7 @@ +describe('Vue compatibility', () => { + it('requires a Vue version that checks object children for the VNode flag', () => { + const {peerDependencies} = jest.requireActual('../../package.json'); + + expect(peerDependencies.vue).toBe('>=3.5.9 <4.0.0'); + }); +}); diff --git a/packages/vue/src/tests/untrusted-values.test.ts b/packages/vue/src/tests/untrusted-values.test.ts new file mode 100644 index 00000000..e52aacf3 --- /dev/null +++ b/packages/vue/src/tests/untrusted-values.test.ts @@ -0,0 +1,237 @@ +import {createApp, defineComponent, getCurrentInstance, h, nextTick} from 'vue'; +import { + ACTION_MOUNT, + ACTION_UPDATE_PROPS, + KIND_COMPONENT, + KIND_FRAGMENT, + KIND_TEXT, +} from '@remote-ui/core'; + +import {createRemoteReceiver, RemoteRenderer, createController} from '../host'; +import {RemoteComponent} from '../host/RemoteComponent'; + +const INJECTED_ID = 'injected-by-remote'; + +const HostBanner = defineComponent({ + props: ['heading'], + setup(props) { + return () => h('h2', [props.heading]); + }, +}); + +describe('untrusted fragment props', () => { + it.each(['mount', 'update'])( + 'rejects Vue nodes disguised as fragments on %s', + async (action) => { + const receiver = createRemoteReceiver(); + const controller = createController({Banner: HostBanner}); + const container = document.createElement('div'); + const app = createApp({ + render: () => h(RemoteRenderer, {receiver, controller}), + }); + app.mount(container); + + try { + const component = { + id: '1', + kind: KIND_COMPONENT, + type: 'Banner', + props: {heading: 'Hi'}, + children: [], + } as any; + if (action === 'update') { + receiver.receive(ACTION_MOUNT, [component]); + await receiver.flush(); + } + + // These fields survive the RPC decoder and Vue accepts the object + // as a VNode when a host renders its heading prop. + const props = { + heading: { + id: 'fragment', + kind: KIND_FRAGMENT, + children: [], + __v_isVNode: true, + type: 'div', + props: {innerHTML: `injected`}, + shapeFlag: 1, + patchFlag: 0, + }, + }; + let error: unknown; + try { + if (action === 'mount') { + receiver.receive(ACTION_MOUNT, [{...component, props}]); + } else { + receiver.receive(ACTION_UPDATE_PROPS, '1', props); + } + } catch (caught) { + error = caught; + } + await receiver.flush(); + await nextTick(); + + expect(container.querySelector(`#${INJECTED_ID}`)).toBeNull(); + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toMatch(/masquerades/); + expect(container.textContent).toBe(action === 'update' ? 'Hi' : ''); + } finally { + app.unmount(); + } + }, + ); + + it('renders an unflagged object as text instead of a Vue node', async () => { + const receiver = createRemoteReceiver(); + const controller = createController({Banner: HostBanner}); + const container = document.createElement('div'); + const app = createApp({ + render: () => h(RemoteRenderer, {receiver, controller}), + }); + app.mount(container); + try { + receiver.receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Banner', + props: { + heading: { + type: 'div', + props: {innerHTML: `injected`}, + children: null, + shapeFlag: 1, + patchFlag: 0, + }, + }, + children: [], + }, + ]); + await receiver.flush(); + await nextTick(); + + expect(container.querySelector(`#${INJECTED_ID}`)).toBeNull(); + expect(container.textContent).toBe('[object Object]'); + } finally { + app.unmount(); + } + }); +}); + +const reservedProps = [ + 'innerHTML', + 'outerHTML', + 'textContent', + 'children', + 'dangerouslySetInnerHTML', + 'key', + 'ref', + 'onVnodeBeforeMount', + 'onVnodeMounted', + 'onVnodeBeforeUpdate', + 'onVnodeUpdated', + 'onVnodeBeforeUnmount', + 'onVnodeUnmounted', + 'onVnodeCustom', +]; + +describe('untrusted Vue host props', () => { + it.each( + reservedProps.flatMap((name) => + ['mount', 'update'].map((action) => [name, action]), + ), + )('never forwards %s to the host on %s', async (name, action) => { + const receivedProps = jest.fn(); + const HostSpread = defineComponent({ + inheritAttrs: false, + setup(_, {attrs, slots}) { + const instance = getCurrentInstance()!; + return () => { + receivedProps(instance.vnode.props); + return h('div', attrs, slots.default?.()); + }; + }, + }); + const receiver = createRemoteReceiver(); + const controller = createController({Spread: HostSpread}); + const container = document.createElement('div'); + const app = createApp({ + render: () => h(RemoteRenderer, {receiver, controller}), + }); + const callback = jest.fn(); + const onClick = jest.fn(); + const props = { + [name]: + name === 'ref' || name.startsWith('onVnode') + ? callback + : `injected`, + title: 'kept', + onClick, + }; + app.mount(container); + try { + receiver.receive(ACTION_MOUNT, [ + { + id: '1', + kind: KIND_COMPONENT, + type: 'Spread', + props: action === 'mount' ? props : {title: 'initial'}, + children: [{id: '2', kind: KIND_TEXT, text: 'Safe child'}], + }, + ]); + await receiver.flush(); + if (action === 'update') { + receiver.receive(ACTION_UPDATE_PROPS, '1', props); + await receiver.flush(); + } + await nextTick(); + + const forwarded = receivedProps.mock.calls.slice(-1)[0][0]; + expect(Object.prototype.hasOwnProperty.call(forwarded, name)).toBe(false); + expect(container.querySelector(`#${INJECTED_ID}`)).toBeNull(); + const element = container.querySelector('div[title="kept"]'); + expect(element?.textContent).toBe('Safe child'); + element!.dispatchEvent(new Event('click')); + expect(onClick).toHaveBeenCalledTimes(1); + expect(callback).not.toHaveBeenCalled(); + } finally { + app.unmount(); + } + }); + + it('filters __proto__ even when a custom receiver bypasses core validation', () => { + const receivedProps = jest.fn(); + const Implementation = defineComponent({ + setup() { + receivedProps(getCurrentInstance()!.vnode.props); + return () => h('div'); + }, + }); + const component = { + id: '1', + kind: KIND_COMPONENT, + type: 'Spread', + props: JSON.parse('{"__proto__":{"polluted":true},"title":"kept"}'), + children: [], + version: 0, + } as any; + const receiver = createRemoteReceiver(); + jest.spyOn(receiver.attached, 'get').mockReturnValue(component); + const controller = createController({Spread: Implementation}); + const container = document.createElement('div'); + const app = createApp({ + render: () => h(RemoteComponent, {receiver, component, controller}), + }); + try { + app.mount(container); + const props = receivedProps.mock.calls[0][0]; + expect(props.title).toBe('kept'); + expect(Object.prototype.hasOwnProperty.call(props, '__proto__')).toBe( + false, + ); + expect(Object.getPrototypeOf(props)).toBe(Object.prototype); + } finally { + app.unmount(); + } + }); +}); diff --git a/yarn.lock b/yarn.lock index 6c900e27..7496a197 100644 --- a/yarn.lock +++ b/yarn.lock @@ -363,6 +363,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.29.11": + version "7.29.11" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz#59d76b6193f61e0303b40991cabbedfadcbd5b63" + integrity sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g== + "@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" @@ -373,6 +378,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + "@babel/helper-validator-option@^7.14.5", "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" @@ -429,7 +439,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.16.8": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.8.tgz#61c243a3875f7d0b0962b0543a33ece6ff2f1f17" integrity sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw== @@ -439,6 +449,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== +"@babel/parser@^7.29.9": + version "7.29.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.11.tgz#afa959e9d16c40d4d95012553cab363fc86cbcb7" + integrity sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag== + dependencies: + "@babel/types" "^7.29.11" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz#dbdeabb1e80f622d9f0b583efb2999605e0a567e" @@ -1242,7 +1259,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.0", "@babel/types@^7.14.9", "@babel/types@^7.15.4", "@babel/types@^7.15.6", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.14.9", "@babel/types@^7.15.4", "@babel/types@^7.15.6", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== @@ -1259,6 +1276,14 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.29.11": + version "7.29.11" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.11.tgz#774cffe8cac2ca494f1334e4052002c96bf628fc" + integrity sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ== + dependencies: + "@babel/helper-string-parser" "^7.29.11" + "@babel/helper-validator-identifier" "^7.29.7" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1852,6 +1877,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" + integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -2480,83 +2510,86 @@ magic-string "^0.27.0" react-refresh "^0.14.0" -"@vue/compiler-core@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.4.tgz#0122aca6eada4cb28b39ed930af917444755e330" - integrity sha512-snpMICsbWTZqBFnPB03qr4DtiSxVYfDF3DvbDSkN9Z9NTM8Chl8E/lYhKBSsvauq91DAWAh8PU3lr9vrLyQsug== - dependencies: - "@babel/parser" "^7.12.0" - "@babel/types" "^7.12.0" - "@vue/shared" "3.0.4" - estree-walker "^2.0.1" - source-map "^0.6.1" - -"@vue/compiler-dom@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.4.tgz#834fd4b15c5698cf9f4505c2bfbccca058a843eb" - integrity sha512-FOxbHBIkkGjYQeTz1DlXQjS1Ms8EPXQWsdTdTPeohoS0KzCz6RiOjiAG+jLtMi6Nr5GX2h0TlCvcnI8mcsicFQ== - dependencies: - "@vue/compiler-core" "3.0.4" - "@vue/shared" "3.0.4" - -"@vue/compiler-sfc@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.4.tgz#2119fe1e68d2c268aafa20461c82c139a9adf8e0" - integrity sha512-brDn6HTuK6R3oBCjtMPPsIpyJEZFinlnxjtBXww/goFJOJBAU9CrsdegwyZItNnixCFUIg4CLv4Nj1Eg/eKlfg== - dependencies: - "@babel/parser" "^7.12.0" - "@babel/types" "^7.12.0" - "@vue/compiler-core" "3.0.4" - "@vue/compiler-dom" "3.0.4" - "@vue/compiler-ssr" "3.0.4" - "@vue/shared" "3.0.4" - consolidate "^0.16.0" - estree-walker "^2.0.1" - hash-sum "^2.0.0" - lru-cache "^5.1.1" - magic-string "^0.25.7" - merge-source-map "^1.1.0" - postcss "^7.0.32" - postcss-modules "^3.2.2" - postcss-selector-parser "^6.0.4" - source-map "^0.6.1" - -"@vue/compiler-ssr@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.4.tgz#ccbd1f55734d51d1402fad825ac102002a7a07c7" - integrity sha512-4aYWQEL4+LS4+D44K9Z7xMOWMEjBsz4Li9nMcj2rxRQ35ewK6uFPodvs6ORP60iBDSkwUFZoldFlNemQlu1BFw== - dependencies: - "@vue/compiler-dom" "3.0.4" - "@vue/shared" "3.0.4" - -"@vue/reactivity@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.4.tgz#b6599dd8271a745960a03f05744ccf7991ba5d8d" - integrity sha512-AFTABrLhUYZY2on3ea9FxeXal7w3f6qIp9gT+/oG93H7dFTL5LvVnxygCopv7tvkIl/GSGQb/yK1D1gmXx1Pww== - dependencies: - "@vue/shared" "3.0.4" - -"@vue/runtime-core@3.0.4", "@vue/runtime-core@>=3.0.0 <4.0.0": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.0.4.tgz#a5b9a001560b1fd8c01a43f68b764c555de7836c" - integrity sha512-qH9e4kqU7b3u1JewvLmGmoAGY+mnuBqz7aEKb2mhpEgwa1yFv496BRuUfMXXMCix3+TndUVMJ8jt41FSdNppwg== - dependencies: - "@vue/reactivity" "3.0.4" - "@vue/shared" "3.0.4" - -"@vue/runtime-dom@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.0.4.tgz#6f81aec545f24511d2c28a315aa3391420b69c68" - integrity sha512-BGIoiTSESzWUhN0Ofi2X/q+HN8f6IUFmUEyyBGKbmx7DTAJNZhFfjqsepfXQrM5IGeTfJLB1ZEVyroDQJNXq3g== - dependencies: - "@vue/runtime-core" "3.0.4" - "@vue/shared" "3.0.4" - csstype "^2.6.8" - -"@vue/shared@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.4.tgz#6dc50f593bdfdeaa6183d1dbc15e2d45e7c6b8b3" - integrity sha512-Swfbz31AaMX48CpFl+YmIrqOH9MgJMTrltG9e26A4ZxYx9LjGuMV+41WnxFzS3Bc9nbrc6sDPM37G6nIT8NJSg== +"@vue/compiler-core@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.44.tgz#6e5b3b56ec7747390dd8d20ee31717fc58106bb4" + integrity sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ== + dependencies: + "@babel/parser" "^7.29.9" + "@vue/shared" "3.5.44" + entities "^7.0.1" + estree-walker "^2.0.2" + source-map-js "^1.2.2" + +"@vue/compiler-dom@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz#43794b48fa3acee2600709e56c75a18e6bb2fe3c" + integrity sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg== + dependencies: + "@vue/compiler-core" "3.5.44" + "@vue/shared" "3.5.44" + +"@vue/compiler-sfc@3.5.44", "@vue/compiler-sfc@^3.5.9": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz#73b85b01825d429adda0ea2d5821c2e55eb381eb" + integrity sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw== + dependencies: + "@babel/parser" "^7.29.9" + "@vue/compiler-core" "3.5.44" + "@vue/compiler-dom" "3.5.44" + "@vue/compiler-ssr" "3.5.44" + "@vue/shared" "3.5.44" + estree-walker "^2.0.2" + magic-string "^0.30.21" + postcss "^8.5.29" + source-map-js "^1.2.2" + +"@vue/compiler-ssr@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz#8036f76c6f45eebbbc6d0d1cf2fd4d76e44a3138" + integrity sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA== + dependencies: + "@vue/compiler-dom" "3.5.44" + "@vue/shared" "3.5.44" + +"@vue/reactivity@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.44.tgz#6721374ab041003d16f2a90304f720c3b35d568c" + integrity sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw== + dependencies: + "@vue/shared" "3.5.44" + +"@vue/runtime-core@3.5.44", "@vue/runtime-core@>=3.5.9 <4.0.0": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.44.tgz#e3e4a2cd13aee28c2a447df583614138b86dbabd" + integrity sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ== + dependencies: + "@vue/reactivity" "3.5.44" + "@vue/shared" "3.5.44" + +"@vue/runtime-dom@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz#7406fd4a63580b7a532b41f7cf4fc1dfedbdeddb" + integrity sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg== + dependencies: + "@vue/reactivity" "3.5.44" + "@vue/runtime-core" "3.5.44" + "@vue/shared" "3.5.44" + csstype "^3.2.3" + +"@vue/server-renderer@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.44.tgz#42617f6efc6b58fc94f181a79ef23e71db4b8d51" + integrity sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ== + dependencies: + "@vue/compiler-ssr" "3.5.44" + "@vue/runtime-dom" "3.5.44" + "@vue/shared" "3.5.44" + +"@vue/shared@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.44.tgz#eefe91aa2e17c617260c4ae4c21246be5d85642e" + integrity sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg== "@vue/vue3-jest@^27.0.0-alpha.1": version "27.0.0-alpha.1" @@ -2922,16 +2955,6 @@ better-path-resolve@1.0.0: dependencies: is-windows "^1.0.0" -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3248,13 +3271,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -consolidate@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" - integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== - dependencies: - bluebird "^3.7.2" - constant-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" @@ -3322,11 +3338,6 @@ css@^2.1.0: source-map-resolve "^0.5.2" urix "^0.1.0" -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -3344,16 +3355,16 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -csstype@^2.6.8: - version "2.6.14" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de" - integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A== - csstype@^3.0.2: version "3.0.8" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== +csstype@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + csv-generate@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-3.4.3.tgz#bc42d943b45aea52afa896874291da4b9108ffff" @@ -3592,11 +3603,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -3611,6 +3617,11 @@ enquirer@^2.3.0: dependencies: ansi-colors "^4.1.1" +entities@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -4044,6 +4055,11 @@ estree-walker@^2.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0" integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -4327,13 +4343,6 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -generic-names@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" - integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== - dependencies: - loader-utils "^1.1.0" - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -4522,11 +4531,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hash-sum@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" - integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== - he@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -4626,18 +4630,6 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -icss-utils@^4.0.0, icss-utils@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - dependencies: - postcss "^7.0.14" - ignore@^5.0.5, ignore@^5.1.1, ignore@^5.1.8: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" @@ -4674,11 +4666,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -5639,15 +5626,6 @@ load-yaml-file@^0.2.0: pify "^4.0.1" strip-bom "^3.0.0" -loader-utils@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5678,11 +5656,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -5753,6 +5726,13 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5807,13 +5787,6 @@ meow@^6.0.0: type-fest "^0.13.1" yargs-parser "^18.1.3" -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5917,6 +5890,11 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +nanoid@^3.3.20: + version "3.3.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.20.tgz#dfd4444d85aa10111ef66ba85ed72e08ba0855d8" + integrity sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ== + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -6273,16 +6251,16 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.2, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -6338,77 +6316,6 @@ portfinder@^1.0.25: debug "^3.1.1" mkdirp "^0.5.5" -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" - integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== - dependencies: - icss-utils "^4.1.1" - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" - integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== - dependencies: - icss-utils "^4.0.0" - postcss "^7.0.6" - -postcss-modules@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-3.2.2.tgz#ee390de0f9f18e761e1778dfb9be26685c02c51f" - integrity sha512-JQ8IAqHELxC0N6tyCg2UF40pACY5oiL6UpiqqcIFRWqgDYO8B0jnxzoQ0EOpPrWXvcpu6BSbQU/3vSiq7w8Nhw== - dependencies: - generic-names "^2.0.1" - icss-replace-symbols "^1.1.0" - lodash.camelcase "^4.3.0" - postcss "^7.0.32" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.2.0" - postcss-modules-values "^3.0.0" - string-hash "^1.1.1" - -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - postcss@^8.4.21: version "8.4.21" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" @@ -6418,6 +6325,20 @@ postcss@^8.4.21: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.5.29: + version "8.5.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.32.tgz#fbccef8388a0c77508c713bfd37728d646595803" + integrity sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g== + dependencies: + nanoid "^3.3.20" + picocolors "^1.1.1" + source-map-js "^1.2.2" + +preact@^10.29.8: + version "10.29.8" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.29.8.tgz#fc85b82dc2474e245b1430b51781d417361f5fc3" + integrity sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q== + preferred-pm@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/preferred-pm/-/preferred-pm-3.0.3.tgz#1b6338000371e3edbce52ef2e4f65eb2e73586d6" @@ -6948,6 +6869,11 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab" + integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw== + source-map-resolve@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -7045,11 +6971,6 @@ stream-transform@^2.1.3: dependencies: mixme "^0.5.1" -string-hash@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= - string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" @@ -7465,11 +7386,6 @@ union@~0.5.0: dependencies: qs "^6.4.0" -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -7519,11 +7435,6 @@ url-join@^2.0.5: resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= -util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - uuid@^8.2.0: version "8.3.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" @@ -7558,14 +7469,16 @@ vite@^4.1.4: optionalDependencies: fsevents "~2.3.2" -vue@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.4.tgz#872c65c143f5717bd5387c61613d9f55f4cc0f43" - integrity sha512-2o+AiQF8sAupyhbyl3oxVCl3WCwC/n5NI7VMM+gVQ231qvSB8eI7sCBloloqDJK6yA367EEtmRSeSCf4sxCC+A== +vue@^3.5.9: + version "3.5.44" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.44.tgz#de7828ab2cf53757ecb920564b7b6c36bf187aca" + integrity sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ== dependencies: - "@vue/compiler-dom" "3.0.4" - "@vue/runtime-dom" "3.0.4" - "@vue/shared" "3.0.4" + "@vue/compiler-dom" "3.5.44" + "@vue/compiler-sfc" "3.5.44" + "@vue/runtime-dom" "3.5.44" + "@vue/server-renderer" "3.5.44" + "@vue/shared" "3.5.44" w3c-hr-time@^1.0.2: version "1.0.2"