From dbb688164ba2c16e8514bf3bed9979d42f14ba55 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sun, 19 Jul 2026 18:43:04 -0400 Subject: [PATCH] fix: harden define() branding and tests --- src/define.ts | 9 +++++++++ test/define.test.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/define.ts b/src/define.ts index 6458491..9a189a6 100644 --- a/src/define.ts +++ b/src/define.ts @@ -34,6 +34,14 @@ export function define(importMeta: ImportMeta, value: T): T { throw new TypeError('define() requires an object or function value'); } + if (Object.hasOwn(value, DEFINE_BRAND)) { + throw new TypeError('define() was already called on this value'); + } + + if (!Object.isExtensible(value)) { + throw new TypeError('define() cannot brand a frozen or sealed value'); + } + const index = counters.get(url) ?? 0; counters.set(url, index + 1); const id = `${url}#${index}`; @@ -46,5 +54,6 @@ export function define(importMeta: ImportMeta, value: T): T { /** Returns the define id of a branded value, or undefined. */ export function getDefineId(value: unknown): string | undefined { if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return undefined; + if (!Object.hasOwn(value, DEFINE_BRAND)) return undefined; return (value as Record)[DEFINE_BRAND]; } diff --git a/test/define.test.ts b/test/define.test.ts index bc11f87..925b72a 100644 --- a/test/define.test.ts +++ b/test/define.test.ts @@ -1,12 +1,16 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { define } from 'moroutine'; +import { getDefineId } from '../src/define.ts'; import { limits } from './fixtures/define.ts'; describe('define()', () => { it('returns the same object reference', () => { assert.equal(limits.maxBatch, 64); assert.equal(limits.retry, 3); + + const obj = { a: 1 }; + assert.equal(define(import.meta, obj), obj); }); it('throws on primitive values', () => { @@ -14,4 +18,46 @@ describe('define()', () => { assert.throws(() => define(import.meta, 'nope' as any), TypeError); assert.throws(() => define(import.meta, null as any), TypeError); }); + + it('throws when called twice on the same value', () => { + const obj = define(import.meta, { x: 1 }); + assert.throws(() => define(import.meta, obj), TypeError); + }); + + it('throws when branding a frozen or sealed value', () => { + const frozen = Object.freeze({ x: 1 }); + assert.throws(() => define(import.meta, frozen), TypeError); + + const sealed = Object.seal({ x: 1 }); + assert.throws(() => define(import.meta, sealed), TypeError); + }); +}); + +describe('getDefineId()', () => { + it('returns a `${url}#N`-shaped id for a defined value', () => { + const id = getDefineId(limits); + assert.match(id!, /^.+#\d+$/); + }); + + it('gives successive define() calls in one module distinct ids', () => { + const a = define(import.meta, { n: 1 }); + const b = define(import.meta, { n: 2 }); + assert.notEqual(getDefineId(a), getDefineId(b)); + }); + + it('returns undefined for unbranded objects', () => { + assert.equal(getDefineId({}), undefined); + }); + + it('returns undefined for primitives', () => { + assert.equal(getDefineId(42), undefined); + assert.equal(getDefineId('nope'), undefined); + assert.equal(getDefineId(null), undefined); + assert.equal(getDefineId(undefined), undefined); + }); + + it('returns undefined for a value that only inherits the brand via its prototype chain', () => { + const derived = Object.create(limits); + assert.equal(getDefineId(derived), undefined); + }); }); -- 2.51.2