From 007875dbce37d76bdc1a14b175b5f12d91b1c02c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:31:47 +0900 Subject: [PATCH 1/2] test(autosave): preflight required save before key scan --- src/autosave/packageOptionPreflight.test.ts | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/autosave/packageOptionPreflight.test.ts diff --git a/src/autosave/packageOptionPreflight.test.ts b/src/autosave/packageOptionPreflight.test.ts new file mode 100644 index 00000000..02b66eb7 --- /dev/null +++ b/src/autosave/packageOptionPreflight.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { createDocumentAutosaveQueue } from './package.js'; + +describe('framework-free autosave option preflight', () => { + it('rejects an invalid required save descriptor before key enumeration', () => { + let ownKeysCalls = 0; + const options = new Proxy( + { save: 1 }, + { + ownKeys() { + ownKeysCalls += 1; + throw new Error('private option keys'); + }, + }, + ); + + expect(() => createDocumentAutosaveQueue(options as never)).toThrowError( + expect.objectContaining({ + code: 'invalid_options', + message: 'Document autosave queue options are invalid.', + }), + ); + expect(ownKeysCalls).toBe(0); + }); +}); From 735602a30af207955b9550b638ffa0d717a1be8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:35:44 +0900 Subject: [PATCH 2/2] fix(autosave): preflight required save capability --- src/autosave/package.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/autosave/package.ts b/src/autosave/package.ts index 64fb4436..3e971ae4 100644 --- a/src/autosave/package.ts +++ b/src/autosave/package.ts @@ -240,9 +240,11 @@ function createInvalidQueueOptionsError(): InternalDocumentAutosaveQueueError { /** * Read exact queue option values without invoking accessors or retaining extras. * - * The optional observer must be an enumerable data property when present. - * Unknown keys, symbols, accessors, non-enumerable fields, and reflection - * failures are rejected before any host callback can execute. + * The required save callback is validated before global key enumeration so an + * already-invalid capability cannot trigger unrelated caller-controlled Proxy + * key reflection. The optional observer must be an enumerable data property + * when present. Unknown keys, symbols, accessors, non-enumerable fields, and + * reflection failures are rejected before any host callback can execute. */ function readDocumentAutosaveQueueOptions( options: DocumentAutosaveQueueOptions, @@ -254,6 +256,15 @@ function readDocumentAutosaveQueueOptions( if (typeof options !== 'object' || options === null) { throw createInvalidQueueOptionsError(); } + const saveDescriptor = Object.getOwnPropertyDescriptor(options, 'save'); + if ( + saveDescriptor === undefined || + !saveDescriptor.enumerable || + !Object.prototype.hasOwnProperty.call(saveDescriptor, 'value') || + typeof saveDescriptor.value !== 'function' + ) { + throw createInvalidQueueOptionsError(); + } const optionKeys = Reflect.ownKeys(options); if ( optionKeys.length < 1 || @@ -268,20 +279,15 @@ function readDocumentAutosaveQueueOptions( ) { throw createInvalidQueueOptionsError(); } - const saveDescriptor = Object.getOwnPropertyDescriptor(options, 'save'); const observerDescriptor = Object.getOwnPropertyDescriptor( options, 'onSnapshotChange', ); if ( - saveDescriptor === undefined || - !saveDescriptor.enumerable || - !Object.prototype.hasOwnProperty.call(saveDescriptor, 'value') || - typeof saveDescriptor.value !== 'function' || - (observerDescriptor !== undefined && - (!observerDescriptor.enumerable || - !Object.prototype.hasOwnProperty.call(observerDescriptor, 'value') || - typeof observerDescriptor.value !== 'function')) + observerDescriptor !== undefined && + (!observerDescriptor.enumerable || + !Object.prototype.hasOwnProperty.call(observerDescriptor, 'value') || + typeof observerDescriptor.value !== 'function') ) { throw createInvalidQueueOptionsError(); }