Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions src/autosave/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ||
Expand All @@ -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();
}
Expand Down
25 changes: 25 additions & 0 deletions src/autosave/packageOptionPreflight.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading