From 95db6cebccc65caa4cacc43c0ad1647a55d7ffef Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 15:11:54 -0600 Subject: [PATCH 1/7] Updated CHANGELOG and package.json --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c767a55..64c424d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ This project follows [Semantic Versioning](https://semver.org/). --- +## [1.0.3] — 2026-03-28 + +### Added + +- **Wider `NodeChild` / `NodeChildren` types** — `NodeChild` now accepts `boolean`; `NodeChildren` accepts nested arrays and full reactive functions. Conditional patterns like `condition && element` work without `as any` casts. Boolean values are filtered out in `appendChildren`, `bindChildNode`, `Fragment()`, `htm.ts`, and `resolveChild`. +- **`onCleanup()` lifecycle hook** — `onCleanup(callback, element)` registers teardown logic (closing sockets, clearing timers, removing listeners) tied to an element's disposal. Integrates with the existing `dispose()` system so cleanup runs automatically when `when()`, `match()`, or `each()` swap content. +- **`query()` `select` option** — Optional `select` function that transforms cached data before returning it to consumers. Raw response stays in cache; `select` runs on read, enabling derived views without extra signals. +- **`formatNumber()` and `formatCurrency()`** — `Intl`-based formatting utilities exported from `sibujs/browser`. `formatNumber` wraps `Intl.NumberFormat`; `formatCurrency` is a convenience shorthand that sets `style: "currency"`. + +### Fixed + +- **Boolean values no longer render as text** — `false`, `true` are filtered in all rendering paths (`tagFactory`, `bindChildNode`, `Fragment`, `htm.ts`, `resolveChild`) preventing visible `"false"` text nodes. +- **Lint fixes** — Resolved unused variable in `router.basic.test.ts` and formatting issues flagged by Biome. + +--- + ## [1.0.2] — 2026-03-27 ### Fixed diff --git a/package.json b/package.json index 4a30d20..a3cd741 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sibujs", - "version": "1.0.2", + "version": "1.0.3", "description": "A lightweight, function-based frontend framework that combines the best of React, Svelte, and Vue — with zero VDOM and maximum simplicity. Designed for developers who want fine-grained reactivity and full control without compilation or magic.", "keywords": [ "frontend", From 9487727c338809848170d361ea8775a3fa149ad9 Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 22:30:29 -0600 Subject: [PATCH 2/7] ci: use npm install instead of npm ci --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aab4d99..e156d9e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Install dependencies - run: npm ci + run: npm install - name: Run tests run: npm test From 077718418208d14423f9aeddb63876ce57f6454c Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 22:51:26 -0600 Subject: [PATCH 3/7] trusted-publisher --- .github/workflows/publish.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cdf4e5b..f25d1f3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,17 +4,21 @@ on: release: types: [published] +permissions: + id-token: write + contents: read + jobs: publish: runs-on: ubuntu-latest steps: - - name: Checkout código + - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" registry-url: "https://registry.npmjs.org" - name: Install dependencies @@ -28,5 +32,3 @@ jobs: - name: Publish to npm run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 60ca8614d9e3e817349edbee3c1734f9a9b10120 Mon Sep 17 00:00:00 2001 From: hexplus Date: Mon, 6 Apr 2026 21:02:41 -0600 Subject: [PATCH 4/7] fix: audit hardening, Accessor brand type, eslint-plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isDev() defaults to false in production (no __SIBU_DEV__ flag) - globalStore strips __proto__/constructor/prototype from action patches - workerFn JSDoc documents CSP/blob-URL incompatibility - Accessor phantom brand on all reactive getters (signal, derived, memo, memoFn, writable, array) — zero runtime cost, better DX hover - NodeChildren explicitly lists Accessor - New sibujs-eslint-plugin: no-called-accessor-in-prop warns when a signal getter is called in a prop value position instead of passed directly --- src/core/dev.ts | 2 +- src/core/rendering/types.ts | 12 ++++++++++-- src/core/signals/array.ts | 5 +++-- src/core/signals/derived.ts | 5 +++-- src/core/signals/memo.ts | 3 ++- src/core/signals/memoFn.ts | 3 ++- src/core/signals/signal.ts | 21 +++++++++++++++++++-- src/core/signals/writable.ts | 3 ++- src/patterns/globalStore.ts | 9 ++++++++- src/platform/worker.ts | 9 +++++++++ 10 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/core/dev.ts b/src/core/dev.ts index a823458..dc42ed3 100644 --- a/src/core/dev.ts +++ b/src/core/dev.ts @@ -19,7 +19,7 @@ export function isDev(): boolean { ? !!(globalThis as any).__SIBU_DEV__ : typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ - : true; // default to dev mode when no flag is set + : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"; // safe default: off in browser, on in test/dev Node } // Cache dev mode at module load — avoids 3 typeof checks per call diff --git a/src/core/rendering/types.ts b/src/core/rendering/types.ts index c0cc83f..105fbd8 100644 --- a/src/core/rendering/types.ts +++ b/src/core/rendering/types.ts @@ -1,3 +1,5 @@ +import type { Accessor } from "../signals/signal"; + export type NodeChild = | Node | Element @@ -6,7 +8,13 @@ export type NodeChild = | string | number | boolean - | (() => NodeChild) + | Accessor // reactive signal getter — pass directly, do not call + | (() => NodeChild) // explicit arrow wrapper — also reactive | null | undefined; -export type NodeChildren = NodeChild | NodeChild[] | NodeChild[][] | (() => NodeChild | NodeChild[]); +export type NodeChildren = + | NodeChild + | NodeChild[] + | NodeChild[][] + | Accessor + | (() => NodeChild | NodeChild[]); diff --git a/src/core/signals/array.ts b/src/core/signals/array.ts index eb00353..d27a768 100644 --- a/src/core/signals/array.ts +++ b/src/core/signals/array.ts @@ -1,6 +1,7 @@ import { enqueueBatchedSignal } from "../../reactivity/batch"; import type { ReactiveSignal } from "../../reactivity/signal"; import { notifySubscribers, recordDependency } from "../../reactivity/track"; +import type { Accessor } from "./signal"; import { signal } from "./signal"; /** @@ -51,7 +52,7 @@ export interface ArrayActions { clear(): void; } -export function array(initial: T[] = []): [() => T[], ArrayActions] { +export function array(initial: T[] = []): [Accessor, ArrayActions] { const [arr, setArr] = signal([...initial]); const actions: ArrayActions = { @@ -163,7 +164,7 @@ export function array(initial: T[] = []): [() => T[], ArrayActions] { * clear(); // [] * ``` */ -export function reactiveArray(initial: T[] = []): [() => readonly T[], ArrayActions] { +export function reactiveArray(initial: T[] = []): [Accessor, ArrayActions] { // Mutable internal storage — never exposed directly let data: T[] = [...initial]; diff --git a/src/core/signals/derived.ts b/src/core/signals/derived.ts index 98e44ca..7199a56 100644 --- a/src/core/signals/derived.ts +++ b/src/core/signals/derived.ts @@ -1,6 +1,7 @@ import type { ReactiveSignal } from "../../reactivity/signal"; import { recordDependency, track, trackingSuspended } from "../../reactivity/track"; import { devAssert } from "../dev"; +import type { Accessor } from "./signal"; /** * derived creates a derived reactive signal whose value updates when dependencies change. @@ -12,7 +13,7 @@ import { devAssert } from "../dev"; * - On re-evaluation, dependencies are re-tracked via track() so that * derived-of-derived chains propagate correctly. */ -export function derived(getter: () => T, options?: { name?: string }): () => T { +export function derived(getter: () => T, options?: { name?: string }): Accessor { devAssert(typeof getter === "function", "derived: argument must be a getter function."); const debugName = options?.name; const cs: any = {}; @@ -78,5 +79,5 @@ export function derived(getter: () => T, options?: { name?: string }): () => if (hook) hook.emit("computed:create", { signal: cs, name: debugName, getter: computedGetter }); - return computedGetter; + return computedGetter as Accessor; } diff --git a/src/core/signals/memo.ts b/src/core/signals/memo.ts index 09d6d78..099d373 100644 --- a/src/core/signals/memo.ts +++ b/src/core/signals/memo.ts @@ -1,4 +1,5 @@ import { derived } from "./derived"; +import type { Accessor } from "./signal"; /** * memo returns a memoized value that only recomputes when its @@ -10,6 +11,6 @@ import { derived } from "./derived"; * @param factory Function that computes the memoized value * @returns Getter function that returns the memoized value */ -export function memo(factory: () => T): () => T { +export function memo(factory: () => T): Accessor { return derived(factory); } diff --git a/src/core/signals/memoFn.ts b/src/core/signals/memoFn.ts index 1e885d6..3995a7d 100644 --- a/src/core/signals/memoFn.ts +++ b/src/core/signals/memoFn.ts @@ -1,4 +1,5 @@ import { derived } from "./derived"; +import type { Accessor } from "./signal"; /** * memoFn returns a memoized callback function that only updates @@ -8,6 +9,6 @@ import { derived } from "./derived"; * @param callback The callback function to memoize * @returns Getter that returns the current memoized callback */ -export function memoFn any>(callback: () => T): () => T { +export function memoFn any>(callback: () => T): Accessor { return derived(callback); } diff --git a/src/core/signals/signal.ts b/src/core/signals/signal.ts index dc942eb..1b11ef7 100644 --- a/src/core/signals/signal.ts +++ b/src/core/signals/signal.ts @@ -3,8 +3,25 @@ import type { ReactiveSignal } from "../../reactivity/signal"; import { notifySubscribers, recordDependency } from "../../reactivity/track"; import { isDev } from "../dev"; +// Phantom brand symbol — exists only in the type system, never at runtime. +declare const __accessor: unique symbol; + +/** + * A reactive signal getter returned by signal(), derived(), memo(), and similar primitives. + * + * Pass an Accessor directly into reactive prop positions — never call it there: + * ```ts + * const [count, setCount] = signal(0); + * + * div({ nodes: count }) // ✓ reactive — Accessor passed directly + * div({ nodes: () => count() }) // ✓ reactive — explicit arrow wrapper + * div({ nodes: count() }) // ✗ static — evaluated once, not reactive + * ``` + */ +export type Accessor = (() => T) & { readonly [__accessor]?: never }; + type SetState = (next: T | ((prev: T) => T)) => void; -type StateTuple = [() => T, SetState]; +type StateTuple = [Accessor, SetState]; /** Options for signal */ export interface SignalOptions { @@ -70,5 +87,5 @@ export function signal(initial: T, options?: SignalOptions): StateTuple if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial }); } - return [get, set]; + return [get as Accessor, set]; } diff --git a/src/core/signals/writable.ts b/src/core/signals/writable.ts index c164429..743a43a 100644 --- a/src/core/signals/writable.ts +++ b/src/core/signals/writable.ts @@ -1,5 +1,6 @@ import { batch } from "../../reactivity/batch"; import { derived } from "./derived"; +import type { Accessor } from "./signal"; /** * Creates a writable computed value — a derived getter paired with @@ -38,7 +39,7 @@ export function writable( get: () => T, set: (value: T) => void, options?: { name?: string }, -): [() => T, (value: T) => void] { +): [Accessor, (value: T) => void] { const getter = derived(get, options); const setter = (value: T): void => { diff --git a/src/patterns/globalStore.ts b/src/patterns/globalStore.ts index 8870957..c9f312f 100644 --- a/src/patterns/globalStore.ts +++ b/src/patterns/globalStore.ts @@ -39,7 +39,14 @@ export function globalStore< const execute = () => { const current = getState(); - const patch = actionFn(current, payload); + const rawPatch = actionFn(current, payload); + // Strip prototype-pollution keys before merging to prevent __proto__ / constructor attacks + const patch: Partial = {}; + for (const key of Object.keys(rawPatch)) { + if (key !== "__proto__" && key !== "constructor" && key !== "prototype") { + (patch as Record)[key] = (rawPatch as Record)[key]; + } + } setState({ ...current, ...patch } as S); // Notify listeners const newState = getState(); diff --git a/src/platform/worker.ts b/src/platform/worker.ts index 9980c9d..f304caa 100644 --- a/src/platform/worker.ts +++ b/src/platform/worker.ts @@ -20,6 +20,15 @@ export interface UseWorkerReturn { * and should call `postMessage` to send results back. It is serialized * into a Blob URL, so it must be self-contained (no closures). * + * **CSP Warning:** This function serializes the provided function via `.toString()` + * and executes it inside a `blob:` URL Worker. This is equivalent to `eval()` and + * is incompatible with strict Content Security Policies that restrict + * `worker-src 'self'` or block `blob:` URLs. Additionally: + * - Minifiers may break captured variable references (closures silently fail). + * - Module-level imports are NOT accessible inside the worker. + * - Never pass user-controlled or dynamically constructed functions — this + * would be equivalent to `eval()` on untrusted input. + * * @param workerFn The function body to run inside the worker. * It receives `self` as the worker global scope. * @returns An object with post, result, error, loading, and terminate. From e2195e3e74e10d481784e8e81733dfe5c725119e Mon Sep 17 00:00:00 2001 From: hexplus Date: Mon, 6 Apr 2026 21:05:34 -0600 Subject: [PATCH 5/7] Updated version, added changelog input --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e046d55..0095b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ This project follows [Semantic Versioning](https://semver.org/). --- +## [1.1.0] — 2026-04-06 + +### Added + +- **`Accessor` brand type** — All reactive getters returned by `signal()`, `derived()`, `memo()`, `memoFn()`, `writable()`, `array()`, and `reactiveArray()` are now typed as `Accessor` instead of the plain `() => T`. The brand is purely a compile-time phantom (zero runtime cost) and makes signal getters clearly distinguishable from regular functions in IDE hover tooltips and type signatures. `NodeChildren` and `NodeChild` have been updated to explicitly list `Accessor` alongside the plain arrow-function form. + +### Fixed + +- **`isDev()` unsafe default** — The fallback when neither `globalThis.__SIBU_DEV__` nor the compile-time `__SIBU_DEV__` constant is set now evaluates `process.env.NODE_ENV !== "production"` instead of hard-coding `true`. In a browser environment without a Vite build (where `process` is undefined), this resolves to `false`, preventing DevTools from being silently active in production. +- **Prototype pollution in `globalStore`** — The `dispatch()` function now strips `__proto__`, `constructor`, and `prototype` keys from the action patch before spreading it into state. Previously a malicious or malformed action could pollute `Object.prototype` via `{ "__proto__": { isAdmin: true } }`. +- **`workerFn` / `worker()` CSP documentation** — Added a prominent JSDoc warning documenting that the inline worker pattern serializes functions via `.toString()` into a `blob:` URL (equivalent to `eval()`), is incompatible with strict `worker-src 'self'` CSP directives, and must never receive user-controlled or dynamically constructed function arguments. + +--- + ## [1.0.9] — 2026-04-03 ### Fixed diff --git a/package.json b/package.json index 29c4439..2ee80ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sibujs", - "version": "1.0.9", + "version": "1.1.0", "description": "A lightweight, function-based frontend framework that combines the best of React, Svelte, and Vue — with zero VDOM and maximum simplicity. Designed for developers who want fine-grained reactivity and full control without compilation or magic.", "keywords": [ "frontend", From d160a7825fe2457024c34868f806f7c5afcb627b Mon Sep 17 00:00:00 2001 From: hexplus Date: Mon, 6 Apr 2026 21:10:01 -0600 Subject: [PATCH 6/7] Updated types --- src/core/rendering/types.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core/rendering/types.ts b/src/core/rendering/types.ts index 105fbd8..2f6901b 100644 --- a/src/core/rendering/types.ts +++ b/src/core/rendering/types.ts @@ -1,5 +1,3 @@ -import type { Accessor } from "../signals/signal"; - export type NodeChild = | Node | Element @@ -8,13 +6,13 @@ export type NodeChild = | string | number | boolean - | Accessor // reactive signal getter — pass directly, do not call - | (() => NodeChild) // explicit arrow wrapper — also reactive + // Reactive: pass an Accessor directly or wrap in an arrow function. + // Accessor extends () => T so both forms are covered by this union member. + | (() => NodeChild) | null | undefined; export type NodeChildren = | NodeChild | NodeChild[] | NodeChild[][] - | Accessor | (() => NodeChild | NodeChild[]); From afa2b27e567e0b4ccb2264f3c64b2a05ec6219c3 Mon Sep 17 00:00:00 2001 From: hexplus Date: Mon, 6 Apr 2026 21:12:57 -0600 Subject: [PATCH 7/7] Fixed linter issues --- src/core/rendering/types.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core/rendering/types.ts b/src/core/rendering/types.ts index 2f6901b..413f494 100644 --- a/src/core/rendering/types.ts +++ b/src/core/rendering/types.ts @@ -11,8 +11,4 @@ export type NodeChild = | (() => NodeChild) | null | undefined; -export type NodeChildren = - | NodeChild - | NodeChild[] - | NodeChild[][] - | (() => NodeChild | NodeChild[]); +export type NodeChildren = NodeChild | NodeChild[] | NodeChild[][] | (() => NodeChild | NodeChild[]);