diff --git a/base/src/main.ts b/base/src/main.ts index 2270d557..be7b7fe8 100644 --- a/base/src/main.ts +++ b/base/src/main.ts @@ -81,6 +81,7 @@ export type { Delta, ObjectDelta, ArrayDelta, Edit } from './diff' export type { FormatNumberOptions } from './formatNumber' export type { StorageAdapter, BPlusIndexOptions, RangeOptions, BPlusIndexStats } from './BPlusIndex' +export { SpatialHash, Quadtree } from './spatial' export type { SpatialPoint, SpatialRect } from './spatial' export type { EasingFn } from './easing' export type { Rect } from './Vec2' diff --git a/base/tsconfig.json b/base/tsconfig.json index bd11dc90..a5894624 100644 --- a/base/tsconfig.json +++ b/base/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "./lib", - "rootDir": "./src" + "rootDir": "./src", + "types": ["node"] }, "include": ["src"] } diff --git a/examples/public/base/SKILL.md b/examples/public/base/SKILL.md index 150c35f6..6f0d0a92 100644 --- a/examples/public/base/SKILL.md +++ b/examples/public/base/SKILL.md @@ -1,6 +1,6 @@ --- name: base -description: Use when reaching for @toolcase/base — zero-dep TypeScript helpers + data structures (Cache, PriorityQueue, RingBuffer, Stack, Deque, VectorClock, State, AdjacencyMatrix, ObjectPool, WeightedRandom, BiMap, BloomFilter, MultiMap), events (EventEmitter, Broadcast), pathfinding (Dijkstra, AStar — class-based, step()-controlled, event-emitting), rectangle/atlas packing (Packing.Packer + MaxRects/Guillotine/Shelf/Skyline/BinaryTree algorithms, multi-page, POT, trim/extrude), spatial partitioning (Spatial.SpatialHash grid + Spatial.Quadtree — insert/remove/update/range-query/nearest-neighbour), async utilities (Deferred, Semaphore, Mutex, pLimit, withTimeout, sleep, debounce, throttle, AsyncQueue — backpressure-aware producer/consumer channel), easing functions (30 easeIn*/easeOut*/easeInOut* functions for Sine/Quad/Cubic/Quart/Quint/Expo/Circ/Back/Elastic/Bounce families plus a CSS-compatible cubicBezier sampler — all exported individually and as Easing namespace), scalar math helpers (clamp, lerp, inverseLerp, mapRange, smoothstep, approximately), string helpers (slugify — URL-safe slug; truncate — length-limited string with suffix; escapeHtml — XSS-safe HTML escaping for & < > \" '), utilities (generateId, retry, hex/byte/range helpers, diff — structural delta for plain objects/arrays, patch — apply delta so patch(a,diff(a,b)) deep-equals b), timing (Stopwatch — start/stop/lap/elapsed with injectable clock; Ticker — fixed-step/variable-step update dispatcher driven by tick(delta)), JSONSchema validation, LSystem, Color palette, HTTP REST primitives, and tagged-union helpers Result (ok/err constructors, isOk/isErr, map/mapErr, andThen/flatMap, unwrap/unwrapOr/unwrapErr) and Option (some/none constructors, isSome/isNone, map, andThen/flatMap, unwrap/unwrapOr). +description: Use when reaching for @toolcase/base — zero-dep TypeScript helpers + data structures (Cache, PriorityQueue, RingBuffer, Stack, Deque, VectorClock, State, AdjacencyMatrix, ObjectPool, WeightedRandom, BiMap, BloomFilter, MultiMap), events (EventEmitter, Broadcast), pathfinding (Dijkstra, AStar — class-based, step()-controlled, event-emitting), rectangle/atlas packing (Packing.Packer + MaxRects/Guillotine/Shelf/Skyline/BinaryTree algorithms, multi-page, POT, trim/extrude), spatial partitioning (Spatial.SpatialHash grid + Spatial.Quadtree — insert/remove/update/range-query/nearest-neighbour), async utilities (Deferred, Semaphore, Mutex, pLimit, withTimeout, sleep, debounce, throttle, AsyncQueue — backpressure-aware producer/consumer channel), easing functions (30 easeIn*/easeOut*/easeInOut* functions for Sine/Quad/Cubic/Quart/Quint/Expo/Circ/Back/Elastic/Bounce families plus a CSS-compatible cubicBezier sampler — all exported individually and as Easing namespace), scalar math helpers (clamp, lerp, inverseLerp, mapRange, smoothstep, approximately), string helpers (slugify — URL-safe slug; truncate — length-limited string with suffix; escapeHtml — XSS-safe HTML escaping for & < > \" '), utilities (generateId, retry, hex/byte/range helpers, diff — structural delta for plain objects/arrays, patch — apply delta so patch(a,diff(a,b)) deep-equals b), timing (Stopwatch — start/stop/lap/elapsed with injectable clock; Ticker — fixed-step/variable-step update dispatcher driven by tick(delta)), JSONSchema validation, LSystem, Color palette, HTTP REST primitives, tagged-union helpers Result (ok/err constructors, isOk/isErr, map/mapErr, andThen/flatMap, unwrap/unwrapOr/unwrapErr) and Option (some/none constructors, isSome/isNone, map, andThen/flatMap, unwrap/unwrapOr), and BPlusIndex — persistent ordered B+ tree key-value index with MemoryAdapter/FsAdapter/OpfsAdapter/LocalStorageAdapter storage backends and PageCache LRU buffer pool (set/get/has/delete, setMany/getMany/deleteMany, first/last/floor/ceil/rank/nth/count/range, entries/keys/values, flush/close/clear/compact/stats). --- # base — API Reference @@ -38,9 +38,10 @@ import { Stopwatch, Ticker, diff, - patch + patch, + BPlusIndex, MemoryAdapter, FsAdapter, OpfsAdapter, LocalStorageAdapter, PageCache, } from '@toolcase/base' -import type { Result, Option, Delta } from '@toolcase/base' +import type { Result, Option, Delta, StorageAdapter, BPlusIndexOptions, RangeOptions, BPlusIndexStats } from '@toolcase/base' ``` --- @@ -114,6 +115,14 @@ import type { Result, Option, Delta } from '@toolcase/base' - [AsyncQueue](#asyncqueue) - [TokenBucket](#tokenbucket) - [diff / patch](#diff--patch) +- [BPlusIndex](#bplusindex) + - [BPlusIndex\](#bplusindexk-v) + - [Storage adapters](#storage-adapters) + - [MemoryAdapter](#memoryadapter) + - [FsAdapter](#fsadapter) + - [OpfsAdapter](#opfsadapter) + - [LocalStorageAdapter](#localstorageadapter) + - [PageCache](#pagecache) --- @@ -2602,6 +2611,306 @@ patch(x, diff(x, y)) // [1, 99, 3, 4] --- +## BPlusIndex + +Persistent ordered key-value index backed by a B+ tree. Isomorphic — works in Node.js, the browser main thread, and Web Workers. Supports ordered queries, rank/nth access by sorted position, and large values stored across overflow pages. All writes are copy-on-write with an atomic dual-superblock checkpoint so a crash mid-write never corrupts the existing data. + +```ts +import { + BPlusIndex, + MemoryAdapter, FsAdapter, OpfsAdapter, LocalStorageAdapter, PageCache, +} from '@toolcase/base' +import type { StorageAdapter, BPlusIndexOptions, RangeOptions, BPlusIndexStats } from '@toolcase/base' +``` + +### BPlusIndex\ + +Generic B+ tree index. The constructor is private — open with `BPlusIndex.open()` (resume or create) or `BPlusIndex.bulkLoad()` (pre-sorted initial load). + +```ts +BPlusIndex.open(opts: BPlusIndexOptions): Promise> +``` + +**`BPlusIndexOptions`:** + +| Field | Type | Notes | +|---|---|---| +| `adapter` | `StorageAdapter` | Backing store (required) | +| `compare` | `(a: K, b: K) => number` | Key ordering — negative / zero / positive (required) | +| `serializeKey` | `(k: K) => Uint8Array` | Key encoder (required) | +| `deserializeKey` | `(b: Uint8Array) => K` | Key decoder (required) | +| `serializeValue` | `(v: V) => Uint8Array` | Value encoder (required) | +| `deserializeValue` | `(b: Uint8Array) => V` | Value decoder (required) | +| `pageSize?` | `number` | Default `4096` | +| `order?` | `number` | Fan-out; derived from `pageSize` when omitted | +| `keyEncoding?` | `string` | Stored in the superblock; reopening an existing file must pass the same value | +| `overflowThreshold?` | `number` | Fraction of `pageSize` above which a value is stored in overflow pages; default `0.25` | + +**`BPlusIndex.keyPreset`** bundles `{compare, serializeKey, deserializeKey, keyEncoding}` for the four built-in key types — pass one as a spread and supply only the value codecs: + +```ts +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, // string keys + adapter: new MemoryAdapter(), + serializeValue: (v: string) => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) +``` + +Available presets: `string`, `number`, `bigint`, `uint8Array`. The raw components are also available individually as `BPlusIndex.comparators.*`, `BPlusIndex.serializers.*`, and `BPlusIndex.deserializers.*`. + +**Properties:** +- `size: number` — number of stored entries (read-only). + +**Core CRUD:** +- `get(key: K): Promise` — look up by key; `undefined` when absent. +- `has(key: K): Promise` — existence check. +- `set(key: K, value: V): Promise` — insert or overwrite; flushes the superblock. Chainable-style (returns `this`). +- `delete(key: K): Promise` — remove key; returns `true` if it existed. + +**Batch:** +- `setMany(entries: [K, V][]): Promise` — sort by key (last-wins on duplicates), then insert all with a single superblock flush. +- `getMany(keys: K[]): Promise<(V | undefined)[]>` — look up each key in order; preserves input index. +- `deleteMany(keys: K[]): Promise` — delete each key; returns count of keys that actually existed. + +**Ordered queries:** +- `first(): Promise<[K, V] | undefined>` — O(log n) minimum entry; `undefined` if empty. +- `last(): Promise<[K, V] | undefined>` — O(log n) maximum entry; `undefined` if empty. +- `floor(target: K): Promise<[K, V] | undefined>` — O(log n) largest entry whose key ≤ `target`; `undefined` if none. +- `ceil(target: K): Promise<[K, V] | undefined>` — O(log n) smallest entry whose key ≥ `target`; `undefined` if none. +- `rank(key: K): Promise` — O(log n) 0-based position in sorted order (= count of entries strictly less than `key`). Returns the insertion rank even when `key` is absent. +- `nth(i: number): Promise<[K, V] | undefined>` — O(log n) entry at 0-based sorted position `i`; `undefined` when out of range. +- `count(opts?: RangeOptions): Promise` — O(log n) entries in range; O(1) when called with no bounds (returns `size` directly). +- `range(opts?: RangeOptions): AsyncGenerator<[K, V]>` — ordered async iteration; yields `[key, value]` pairs. + +**`RangeOptions`:** + +```ts +interface RangeOptions { + gte?: K // include entries with key >= gte + gt?: K // include entries with key > gt + lte?: K // include entries with key <= lte + lt?: K // include entries with key < lt + reverse?: boolean // default false; iterate from high to low + limit?: number // stop after yielding this many entries +} +``` + +**Iteration (full ordered scan):** +- `entries(): AsyncGenerator<[K, V]>` — all entries in sorted order (equivalent to `range()`). +- `keys(): AsyncGenerator` — key-only forward scan. +- `values(): AsyncGenerator` — value-only forward scan. + +**Lifecycle:** +- `flush(): Promise` — call the adapter's `flush()` (e.g. `fsync`). No-op when the adapter omits `flush`. +- `close(): Promise` — flush then close the adapter. +- `clear(): Promise` — reset to an empty tree; sets `size` to 0 and shrinks page count. +- `compact(): Promise` — pack all live tree pages to the front of the file and truncate the tail. Requires exclusive access — no concurrent reads, writes, or open iterators. Invalidates outstanding in-memory page IDs; re-open or discard held references after returning. +- `stats(): Promise` — read tree health metadata. + +**`BPlusIndexStats`:** + +```ts +interface BPlusIndexStats { + height: number // levels in the tree (1 = root is a leaf) + pageCount: number // total allocated pages (including the two superblock slots) + freePages: number // page IDs on the free list — reusable without growing the file + fillFactor: number // live-page fraction of non-superblock pages; 1.0 = no waste +} +``` + +**Static factory methods:** +- `BPlusIndex.open(opts): Promise>` — create-if-new, resume-if-existing. +- `BPlusIndex.bulkLoad(opts, entries: [K, V][]): Promise>` — build a balanced tree bottom-up from pre-sorted, de-duplicated `entries`. Throws when entries are out-of-order or contain duplicate keys, or when the adapter is not fresh (non-empty). + +--- + +### Storage adapters + +All adapters implement `StorageAdapter`: + +```ts +interface StorageAdapter { + read(pageId: number): Promise + write(pageId: number, data: Uint8Array): Promise + truncate?(pageCount: number): Promise // optional + flush?(): Promise // optional + close?(): Promise // optional +} +``` + +#### MemoryAdapter + +In-memory adapter. Default for development and testing. Not persistent across restarts. + +```ts +new MemoryAdapter() +``` + +Implements `truncate`, `flush` (no-op), and `close` (no-op). + +#### FsAdapter + +Single-file Node.js adapter. Stores page N at byte offset `N × pageSize`. Opens with `O_RDWR | O_CREAT` — creates the file on first use and never truncates on open. The `node:fs` import is deferred so browser bundlers can tree-shake it when `FsAdapter` is never instantiated. + +```ts +new FsAdapter(path: string, pageSize = 4096) +``` + +#### OpfsAdapter + +Browser Web Worker adapter backed by the Origin Private File System (OPFS). **Must run in a dedicated Web Worker** — `FileSystemSyncAccessHandle` is unavailable on the main thread. Acquires an exclusive lock for the lifetime of the handle, enforcing the single-writer guarantee without additional locking. The OPFS API is accessed lazily so bundlers can tree-shake it from non-browser builds. + +```ts +new OpfsAdapter(name: string, pageSize = 4096) +// name — OPFS file name relative to the OPFS root directory +``` + +#### LocalStorageAdapter + +Browser main-thread adapter backed by `localStorage`. Each page is stored under `page:` encoded as a latin-1 string (1 byte → 1 char), staying within the ~5 MB origin quota without base64 overhead. Synchronous under the hood; single-tab only. `truncate` is intentionally absent — `localStorage` provides no way to shrink the key namespace atomically. + +```ts +new LocalStorageAdapter(prefix = 'bplus:') +// prefix — namespace prepended to every key; use a unique prefix per index to avoid collisions +``` + +--- + +### PageCache + +LRU buffer pool that wraps any `StorageAdapter`. Read hits are served from memory; misses delegate to the inner adapter and cache the result. Writes are write-through: the inner adapter receives the write and the cache is updated immediately. Truncate evicts cached entries outside the new page boundary. Implements the full `StorageAdapter` interface and delegates `flush` and `close` to the inner adapter. + +```ts +new PageCache(inner: StorageAdapter, maxSize = 64) +// maxSize — maximum pages to keep cached; throws if < 1 +``` + +```ts +import { BPlusIndex, FsAdapter, PageCache } from '@toolcase/base' + +const adapter = new PageCache(new FsAdapter('/data/index.bin'), 128) +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, + adapter, + serializeValue: v => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) +``` + +--- + +### Examples + +**In-memory index — development / testing:** + +```ts +import { BPlusIndex, MemoryAdapter } from '@toolcase/base' + +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, + adapter: new MemoryAdapter(), + serializeValue: (v: string) => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) + +await idx.set('hello', 'world') +await idx.get('hello') // 'world' +await idx.has('missing') // false +idx.size // 1 + +await idx.setMany([['a', '1'], ['b', '2'], ['c', '3']]) +for await (const [k, v] of idx.range({ gte: 'a', lt: 'c' })) { + console.log(k, v) // 'a' '1' then 'b' '2' +} + +await idx.delete('hello') +await idx.close() +``` + +**Node.js persistent index with FsAdapter + PageCache:** + +```ts +import { BPlusIndex, FsAdapter, PageCache } from '@toolcase/base' + +const adapter = new PageCache(new FsAdapter('/data/myindex.bin'), 128) +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.number, + adapter, + serializeValue: (v: object) => new TextEncoder().encode(JSON.stringify(v)), + deserializeValue: b => JSON.parse(new TextDecoder().decode(b)), +}) + +// Ordered queries +await idx.first() // smallest [key, value] or undefined +await idx.last() // largest [key, value] or undefined +await idx.floor(500) // largest entry with key <= 500 +await idx.ceil(500) // smallest entry with key >= 500 +await idx.rank(42) // 0-based sorted position of key 42 +await idx.nth(0) // entry at position 0 (the minimum) +await idx.count({ gte: 100, lt: 200 }) // entries in [100, 200) + +const s = await idx.stats() +console.log(s.height, s.fillFactor) + +// Compact when fill factor drops below 70% +if (s.fillFactor < 0.7) await idx.compact() + +await idx.close() +``` + +**Bulk-load pre-sorted data:** + +```ts +import { BPlusIndex, MemoryAdapter } from '@toolcase/base' + +const entries: [string, number][] = [['a', 1], ['b', 2], ['c', 3]] // must be sorted + unique +const idx = await BPlusIndex.bulkLoad( + { + ...BPlusIndex.keyPreset.string, + adapter: new MemoryAdapter(), + serializeValue: (v: number) => { const b = new Uint8Array(8); new DataView(b.buffer).setFloat64(0, v, true); return b }, + deserializeValue: b => new DataView(b.buffer, b.byteOffset).getFloat64(0, true), + }, + entries, +) +await idx.first() // ['a', 1] +await idx.size // 3 +``` + +**Browser OPFS index inside a Web Worker:** + +```ts +import { BPlusIndex, OpfsAdapter, PageCache } from '@toolcase/base' + +// Must run inside a dedicated Web Worker +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, + adapter: new PageCache(new OpfsAdapter('myapp.idx'), 64), + serializeValue: v => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) +await idx.set('key', 'value') +await idx.flush() +``` + +**Browser localStorage index (main thread):** + +```ts +import { BPlusIndex, LocalStorageAdapter } from '@toolcase/base' + +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, + adapter: new LocalStorageAdapter('myapp:config:'), + serializeValue: v => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) +await idx.set('theme', 'dark') +await idx.get('theme') // 'dark' +``` + +--- + ## Notes - Package is `sideEffects: false` — tree-shakable. diff --git a/examples/public/logging/SKILL.md b/examples/public/logging/SKILL.md index 80d5ac81..00e059f2 100644 --- a/examples/public/logging/SKILL.md +++ b/examples/public/logging/SKILL.md @@ -26,7 +26,20 @@ import { LogReporter, // base class for transports ConsoleLogReporter, JSONLineReporter, // structured JSON-line transport - BufferedReporter // batching/debounce wrapper for slow sinks + BufferedReporter, // batching/debounce wrapper for slow sinks + isKnownLevel, // (level: string) => level is LoggerLevel + KNOWN_LEVELS, // LoggerLevel[] — all valid level strings + textFormatter, // LogFormatter — human-readable text line + jsonFormatter, // LogFormatter — JSON record (default for JSONLineReporter) + logfmtFormatter, // LogFormatter — logfmt key=value line +} from '@toolcase/logging' + +// Types: +import type { + LogFormatter, // (level, scope, time, fields, messages) => string + ClockFn, // () => number (epoch ms) + JSONLineReporterOptions, + JSONLineWriter, } from '@toolcase/logging' // Node-only transports live on the /node subpath (uses fs): @@ -81,13 +94,21 @@ class Logger { verbose(...args: any[]): void log(level: LoggerLevel, ...args: any[]): void - setLevel(level: LoggerLevel | null): void // per-logger override; null = inherit factory - getLevel(): LoggerLevel | null // current override, or null + isEnabled(level: LoggerLevel): boolean // true when the level would be dispatched + setLevel(level: LoggerLevel | null): void // per-logger override; null = inherit factory + getLevel(): LoggerLevel | null // current override, or null withContext(ctx: Record): Logger // child logger that prepends ctx } ``` -Each call timestamps with `new Date().toISOString()` and forwards `(level, scope, time, args)` to every reporter (gated by per-logger override if set, otherwise factory level). +Each call timestamps with `this.clock()` (defaulting to `Date.now()`) and forwards `(level, scope, time, fields, messages)` to every reporter (gated by per-logger override if set, otherwise factory level). The `time` value is epoch milliseconds; ISO formatting happens inside reporters and formatters, not in the logger itself. + +Any arg that is a function is called lazily — the function is invoked only when the level is enabled: + +```ts +log.debug(() => JSON.stringify(heavySnapshot)) // only evaluated when debug is enabled +log.verbose(() => computeStats(), 'stats') // mixed: thunk + plain arg +``` ```ts const auth = logging.getLogger('auth') @@ -160,9 +181,15 @@ async function handle(req: Request) { ```ts class LoggerFactory { - constructor(reporters?: LogReporter[]) - level: LoggerLevel // get/set; setter writes underlying numeric order + constructor(reporters?: LogReporter[], clock?: ClockFn) // clock defaults to Date.now + level: LoggerLevel // get/set; throws RangeError on unknown level getLogger(scope: string = 'default'): Logger + addReporter(reporter: LogReporter): void // append a reporter after construction + removeReporter(reporter: LogReporter): void // detach a reporter by reference + flush(): void // synchronously flush all reporters + close(): Promise // flush + close all reporters (await on shutdown) + setLevel(pattern: string, level: LoggerLevel): void // scope-pattern override (see below) + parseEnv(env: Record): void // reads LOG_LEVEL + DEBUG (see below) } ``` @@ -176,7 +203,7 @@ audit.level = 'verbose' audit.getLogger('audit').verbose('login', { userId }) ``` -There is **no** `addReporter()` method — pass reporters via the constructor. (To extend the default singleton, build your own factory.) +Add or remove reporters after construction with `addReporter` / `removeReporter`. Call `flush()` to synchronously drain all reporters, and `await close()` on shutdown to flush and close every reporter in the factory's list. --- @@ -186,11 +213,13 @@ Base class. Override `log()` to ship messages to any sink. ```ts class LogReporter { - log(level: LoggerLevel, scope: string, time: string, messages: any[]): void + log(level: LoggerLevel, scope: string, time: number, fields: Record, messages: any[]): void + flush(): void + close(): void | Promise } ``` -The default base does nothing — subclass it. +`time` is epoch milliseconds (`Date.now()` by default). The default base does nothing — subclass it. ### ConsoleLogReporter @@ -209,6 +238,7 @@ Constructor options (`ConsoleLogReporterOptions`): | `timestamp` | `boolean` | `true` | Include the ISO timestamp in the default prefix. | | `prefix` | `string \| (level, scope, time) => string` | — | Override the prefix entirely (string or function). | | `objects` | `'compact' \| 'pretty'` | `'compact'` | `'compact'` passes values to console as-is; `'pretty'` serializes objects/arrays to indented JSON and errors to a string. | +| `formatter` | `LogFormatter` | — | When set, bypasses the prefix/objects pipeline; calls `formatter(level, scope, time, fields, messages)` and writes the returned string as a single colored line. | ```ts new ConsoleLogReporter() // auto-detect color, default prefix @@ -234,12 +264,14 @@ const factory = new LoggerFactory([ // optional: where to write each line (default: console.log) write: line => process.stdout.write(line + '\n'), // optional: static fields merged into every record - extra: { service: 'api', region: 'eu' } + extra: { service: 'api', region: 'eu' }, + // optional: custom formatter (default: jsonFormatter) + // formatter: logfmtFormatter, }) ]) const log = factory.getLogger('auth') log.info('login ok', { userId: 42 }) -// {"service":"api","region":"eu","level":"info","scope":"auth","time":"…","messages":["login ok",{"userId":42}]} +// {"service":"api","region":"eu","level":"info","scope":"auth","time":1748950800000,"messages":["login ok",{"userId":42}]} ``` Errors are serialized to `{ name, message, stack }`. Circular references fall back to `''`. @@ -640,8 +672,8 @@ factory.level = 'info' ```ts class RingBuffer extends LogReporter { private events: any[] = [] - log(level: any, scope: string, time: string, messages: any[]) { - this.events.push({ level, scope, time, messages }) + log(level: any, scope: string, time: number, fields: Record, messages: any[]) { + this.events.push({ level, scope, time, fields, messages }) if (this.events.length > 500) this.events.shift() } snapshot() { return this.events.slice() } @@ -678,9 +710,9 @@ new BufferedReporter(null, { ```ts class ScopedReporter extends LogReporter { constructor(private prefix: string, private inner: LogReporter) { super() } - log(level, scope, time, messages) { + log(level, scope, time, fields, messages) { if (!scope.startsWith(this.prefix)) return - this.inner.log(level, scope, time, messages) + this.inner.log(level, scope, time, fields, messages) } } @@ -691,7 +723,7 @@ new LoggerFactory([new ScopedReporter('http.', new ConsoleLogReporter())]) ```ts class GroupedReporter extends LogReporter { - log(level, scope, time, messages) { + log(level, scope, time, fields, messages) { const fn = level === 'error' ? console.error : level === 'warning' ? console.warn : console.log console.groupCollapsed(`${level.toUpperCase()} ${scope} ${time}`) @@ -1111,7 +1143,7 @@ API: class MemoryReporter extends LogReporter { entries(): LogEntry[] // all retained entries, oldest → newest (copy) find(level: LoggerLevel): LogEntry[] // entries filtered by exact level - clear(): void // reset; capacity is preserved + clear(): void // reset all entries } ``` @@ -1119,9 +1151,222 @@ class MemoryReporter extends LogReporter { --- +## Formatters + +Three ready-made `LogFormatter` implementations are exported. A `LogFormatter` has the signature: + +```ts +type LogFormatter = (level: LoggerLevel, scope: string, time: number, fields: Record, messages: any[]) => string +``` + +Pass one to `ConsoleLogReporter` or `JSONLineReporter` via the `formatter` option, or call them directly in a custom reporter. + +| Export | Output style | +|--------|-------------| +| `textFormatter` | `LEVEL [ISO-timestamp] \| scope: msg1 msg2 …` | +| `jsonFormatter` | JSON object: `{ ...fields, level, scope, time, messages }` (default for `JSONLineReporter`) | +| `logfmtFormatter` | `level=info scope=auth ts=ISO msg="login ok"` with per-field pairs from `fields` | + +```ts +import { textFormatter, jsonFormatter, logfmtFormatter, ConsoleLogReporter, JSONLineReporter } from '@toolcase/logging' + +// Human-readable text via ConsoleLogReporter: +new ConsoleLogReporter({ formatter: textFormatter }) + +// logfmt lines via JSONLineReporter: +new JSONLineReporter({ formatter: logfmtFormatter, write: line => process.stdout.write(line + '\n') }) + +// Custom formatter: +const myFmt: LogFormatter = (level, scope, time, fields, messages) => + `[${scope}] ${level}: ${messages.join(' ')}` +new ConsoleLogReporter({ formatter: myFmt }) +``` + +--- + ## Notes - Package is `sideEffects: false` and zero-dependency. - Importing the default export instantiates one `LoggerFactory` + one `ConsoleLogReporter` — safe in both Node and browser. - No async / no buffering — every `logger.x()` call dispatches synchronously. -- ISO timestamps are generated per call (one `new Date()` per log). +- Timestamps are epoch milliseconds from `this.clock()` (default `Date.now()`). ISO formatting happens in reporters and formatters, not in the logger. + +--- + +## @toolcase/logging/node + +Node-only exports. Uses `node:fs` (write streams) and `node:async_hooks` (`AsyncLocalStorage`). Import from the `/node` subpath: + +```ts +import { + StreamReporter, // writable-stream transport (base of FileLogReporter) + FileLogReporter, // file-backed reporter with rotation + defaultStreamFormatter, // textFormatter alias — default for StreamReporter + AsyncContext, // AsyncLocalStorage wrapper for per-request fields + ContextualReporter, // reporter decorator that injects AsyncContext fields +} from '@toolcase/logging/node' + +// Types: +import type { + StreamLogFormatter, // alias for LogFormatter + StreamReporterOptions, + FileLogFormatter, // alias for StreamLogFormatter + FileLogReporterOptions, +} from '@toolcase/logging/node' +``` + +### `StreamReporter` + +Base class for writable-stream transports. Writes one formatted line per entry. Supports optional size-based rotation via `maxBytes` — when the accumulated byte count of the current stream exceeds the threshold, new lines are queued while `openRotatedStream()` runs asynchronously, then flushed to the new stream. + +```ts +import { createWriteStream } from 'node:fs' +import { StreamReporter, defaultStreamFormatter } from '@toolcase/logging/node' +import { LoggerFactory } from '@toolcase/logging' + +const reporter = new StreamReporter( + createWriteStream('./app.log', { flags: 'a' }), + { + formatter: defaultStreamFormatter, // default; textFormatter output + maxBytes: 5 * 1024 * 1024, // rotate at 5 MB (0 = never rotate) + onError: err => console.error('stream error', err), + } +) + +const factory = new LoggerFactory([reporter]) +factory.getLogger('boot').info('started') + +await reporter.close() // flushes pending rotation, then ends the stream +``` + +API: + +```ts +interface StreamReporterOptions { + formatter?: LogFormatter // default: defaultStreamFormatter (textFormatter) + maxBytes?: number // rotation threshold in bytes; 0 = disabled (default) + onError?: (err: Error) => void +} + +type StreamLogFormatter = LogFormatter // alias + +class StreamReporter extends LogReporter { + constructor(stream: Writable, options?: StreamReporterOptions) + log(level, scope, time, fields, messages): void + close(): Promise // waits for any in-progress rotation, then ends the stream +} +``` + +`defaultStreamFormatter` is the same as `textFormatter` from `@toolcase/logging` — produces `LEVEL [ISO-timestamp] | scope: …messages`. + +Subclass `StreamReporter` and override the protected `openRotatedStream(): Promise` hook to swap in a new destination when `maxBytes` is exceeded. `FileLogReporter` uses this hook to rename archive files and open a fresh `createWriteStream`. + +### `FileLogReporter` + +File-backed reporter that extends `StreamReporter`. Opens `filePath` for writing and rotates when the file grows past `maxBytes`. During rotation the current file is renamed to `filePath.1`, older archives shift up (`filePath.1` → `filePath.2`, …), and any archive numbered above `maxFiles` is deleted. + +```ts +import { FileLogReporter } from '@toolcase/logging/node' +import { LoggerFactory } from '@toolcase/logging' + +const reporter = new FileLogReporter('./logs/app.log', { + append: true, // default — append; false truncates on open + maxBytes: 10 * 1024 * 1024, // rotate at 10 MB + maxFiles: 5, // keep app.log.1 … app.log.5 (default) + formatter: defaultStreamFormatter, + onError: err => console.error(err), +}) + +const factory = new LoggerFactory([reporter]) +factory.getLogger('app').info('boot complete') + +await reporter.close() +``` + +API: + +```ts +interface FileLogReporterOptions extends StreamReporterOptions { + append?: boolean // default true (append); false → truncate on open + maxFiles?: number // default 5; archives kept are filePath.1 … filePath.maxFiles +} + +type FileLogFormatter = StreamLogFormatter // alias + +class FileLogReporter extends StreamReporter { + constructor(filePath: string, options?: FileLogReporterOptions) +} +``` + +Rotation sequence: end current stream → delete `filePath.maxFiles` → rename `filePath.N` → `filePath.N+1` (from highest down) → rename `filePath` → `filePath.1` → open new `filePath` in append mode. + +### `AsyncContext` + +Thin wrapper around Node.js `AsyncLocalStorage`. Stores a `Record` that is visible throughout an `async_hooks` continuation. Each `run()` call merges new fields on top of the parent store — deeper calls extend, not replace. + +```ts +import { AsyncContext } from '@toolcase/logging/node' + +const ctx = new AsyncContext() + +// Wrap an incoming request handler: +ctx.run({ requestId: 'r-42', userId: 7 }, async () => { + ctx.getFields() // → { requestId: 'r-42', userId: 7 } + + // Nested run merges on top: + ctx.run({ route: 'POST /orders' }, async () => { + ctx.getFields() // → { requestId: 'r-42', userId: 7, route: 'POST /orders' } + }) +}) + +ctx.getFields() // → {} (outside any run) +``` + +API: + +```ts +class AsyncContext { + constructor() + run(fields: Record, fn: () => T): T + getFields(): Record // current store, or {} when called outside a run +} +``` + +### `ContextualReporter` + +Reporter decorator that reads the current `AsyncContext` fields on every log call and merges them into `fields` before forwarding to an inner reporter. Lets you inject per-request or per-task context into log entries without threading a logger through every call site. + +```ts +import { AsyncContext, ContextualReporter } from '@toolcase/logging/node' +import { LoggerFactory, ConsoleLogReporter } from '@toolcase/logging' + +const ctx = new AsyncContext() +const factory = new LoggerFactory([ + new ContextualReporter(new ConsoleLogReporter(), ctx), +]) +factory.level = 'debug' + +const log = factory.getLogger('http') + +async function handleRequest(req: Request) { + await ctx.run({ requestId: req.headers.get('x-request-id'), path: new URL(req.url).pathname }, async () => { + log.info('start') // ConsoleLogReporter receives fields: { requestId, path } + await processRequest(req, log) + log.info('done') + }) +} +``` + +API: + +```ts +class ContextualReporter extends LogReporter { + constructor(inner: LogReporter, context: AsyncContext) + log(level, scope, time, fields, messages): void + // merges context.getFields() with fields (own fields win on collision) + flush(): void + close(): void | Promise +} +``` + +`ContextualReporter` does not buffer — it calls `inner.log()` synchronously. `flush()` and `close()` are forwarded to `inner`. diff --git a/examples/public/node-service/SKILL.md b/examples/public/node-service/SKILL.md index 5a4c7be8..d881489d 100644 --- a/examples/public/node-service/SKILL.md +++ b/examples/public/node-service/SKILL.md @@ -30,7 +30,7 @@ Optional packages that slot into this scaffold. Use what fits. - `HTTP.RESTResponse` / `HTTP.RESTError` / `HTTP.Status` for every router response. Optional: `Cache`, `EventEmitter`, `Broadcast`, `generateId`, `retry`, `ObjectPool`, `JSONSchema` validation, `PriorityQueue`, `VectorClock`, `Color`, `Dijkstra`, `AStar`, `WeightedRandom`, `Packing.Packer`. Live in `util/` or `services/`. -- **`@toolcase/node`** — Backend helpers. Mandatory: `env(name, default, type)` for every env var read in `src/env.ts`. Recommended for the db layer: `BaseRepository` (engine-agnostic — ships no queries; you implement the verbs with raw SQL, see `db/`) + `EntityService` (before/after hooks) + `RESTRouteHandler` (auto-CRUD over an `EntityService`). Also: `HttpServer` wrapper, `KVService` (Redis), `ImageProcessor` (sharp), `AtlasBuilder`, `OAuth2` helpers. Peer-deps `@toolcase/base`, `fastify`, `redis`, `sharp`, `jose` declared optional — install only what you use. `BaseRepository` is driver-free: pass it your query runner (the `pg` `Pool`/`PoolClient`, or the `node:sqlite` `DatabaseSync`); the driver stays the project's own dep. +- **`@toolcase/node`** — Backend helpers. Mandatory: `env(name, default, type)` for every env var read in `src/env.ts`. Recommended for the db layer: `BaseRepository` (engine-agnostic — ships no queries; you implement the verbs with raw SQL, see `db/`) + `EntityService` (before/after hooks) + `RESTRouteHandler` (auto-CRUD over an `EntityService`). Also: `HttpServer` wrapper, `KVService` (Redis), `NodeStore` + `BlockStore` (durable file-based key-value store: B+ tree index + append-mostly heap, no extra peer deps — useful for embedded caches or local worker state that must survive process restarts without a Redis dependency), `ImageProcessor` (sharp), `AtlasBuilder`, `OAuth2` helpers. Peer-deps `@toolcase/base`, `fastify`, `redis`, `sharp`, `jose` declared optional — install only what you use. `BaseRepository` is driver-free: pass it your query runner (the `pg` `Pool`/`PoolClient`, or the `node:sqlite` `DatabaseSync`); the driver stays the project's own dep. - **`@toolcase/logging`** — Isomorphic logger. Mandatory. One named logger per class: `logging.getLogger('Database')`, `logging.getLogger('Http')`, `logging.getLogger('UserService')`. Configure level + reporters once at boot in `index.ts` (default reporter is console; swap via custom `LogReporter`, `JSONLineReporter`, `BufferedReporter`, or `FileLogReporter` from `@toolcase/logging/node`). diff --git a/examples/public/node/SKILL.md b/examples/public/node/SKILL.md index 938bb53a..3a9116b9 100644 --- a/examples/public/node/SKILL.md +++ b/examples/public/node/SKILL.md @@ -1,6 +1,6 @@ --- name: node -description: Use when reaching for @toolcase/node — backend helpers for Node.js. Single entrypoint exposing Fastify endpoints (RouteHandler, RESTRouteHandler, Router, HttpServer), engine-agnostic repositories (BaseRepository, EntityService — you implement the verbs for your engine), Redis KV service (KVService — Locker, RateLimiter, Leaderboard, ValueStore, Versioned, SubscriberPool, KeyBuilder, LuaScriptCache), OAuth2/OIDC helpers, ImageProcessor + AtlasBuilder (sharp), typed env() loader, plus isomorphic sanitize / pagination / filter / sort / domain-error helpers and FieldSchema → JSON Schema derivation. +description: Use when reaching for @toolcase/node — backend helpers for Node.js. Single entrypoint exposing Fastify endpoints (RouteHandler, RESTRouteHandler, Router, HttpServer), engine-agnostic repositories (BaseRepository, EntityService — you implement the verbs for your engine), Redis KV service (KVService — Locker, RateLimiter, Leaderboard, ValueStore, Versioned, SubscriberPool, KeyBuilder, LuaScriptCache), NodeStore + BlockStore (two-file persistent key-value store: B+ tree index + append-mostly heap), OAuth2/OIDC helpers, ImageProcessor + AtlasBuilder (sharp), typed env() loader, plus isomorphic sanitize / pagination / filter / sort / domain-error helpers and FieldSchema → JSON Schema derivation. --- # @toolcase/node — API Reference @@ -24,6 +24,9 @@ import { type Filter, type Sort, FILTER_OP_SET, encodeCursor, decodeCursor, // redis peers: redis, @toolcase/serializer (serializer optional) KVService, KeyBuilder, LuaScriptCache, KV_LUA_SCRIPTS, + // file store — no extra peers beyond node:fs (built-in) + NodeStore, BlockStore, type NodeStoreOptions, type BlockRef, + serializeBlockRef, deserializeBlockRef, BLOCK_REF_SIZE, } from '@toolcase/node' ``` @@ -34,6 +37,7 @@ import { | `BaseRepository`, `EntityService` | `@toolcase/base` — engine-agnostic; you implement the verbs against any driver | | `KVService` (string surface only) | `@toolcase/base`, `redis` | | `KVService.*Value*` methods (typed binary) | adds `@toolcase/serializer` | +| `NodeStore`, `BlockStore` | `@toolcase/base`, `node:fs` (Node built-in) | | `ImageProcessor`, `AtlasBuilder` | `@toolcase/base`, `sharp` | --- @@ -1209,6 +1213,57 @@ const nonce = generateNonce() // base64url, 32 bytes `byteLength < 16` throws. `generatePKCE('plain')` returns `codeChallenge === codeVerifier`. +### `callback.ts` — CSRF state verification + +```ts +import { verifyCallback, type VerifyCallbackInput } from '@toolcase/node' + +interface VerifyCallbackInput { + stored: string // state your server generated and saved before the authorize redirect + received: string // state returned by the authorization server in the callback URL +} + +// Constant-time CSRF guard. +// Throws OAuth2CallbackError('state mismatch') when lengths differ or values do not match. +function verifyCallback(input: VerifyCallbackInput): void +``` + +`state` is the CSRF token for the Authorization Code flow. A naive `===` check is vulnerable to timing side-channels: an attacker who can measure response latency may infer characters of the stored value one byte at a time. `verifyCallback` wraps Node's `crypto.timingSafeEqual` to close that window. + +Call it as the first action inside the callback handler — before `exchangeCode` — so a forged `state` is rejected before any token request is made. + +```ts +// authorize handler — store state inside the session blob alongside the rest of the flow data +const state = generateState() +const nonce = generateNonce() +const pkce = generatePKCE() + +await kv.set(`oauth2:session:${sessionId}`, JSON.stringify({ + state, nonce, codeVerifier: pkce.codeVerifier, redirectUri +}), { EX: 600 }) + +reply.redirect(303, buildAuthorizeURL(provider, { + state, nonce, + codeChallenge: pkce.codeChallenge, codeChallengeMethod: pkce.method, + redirectUri, scope: ['openid', 'email', 'profile'] +})) + +// callback handler +const { code, state: receivedState, error, error_description } = req.query +if (error) throw new OAuth2CallbackError(error, error_description) + +const raw = await kv.getDel(`oauth2:session:${sessionId}`) +if (!raw) throw new OAuth2ProtocolError('session_missing_or_expired') +const flow = JSON.parse(raw) + +verifyCallback({ stored: flow.state, received: receivedState }) // CSRF guard — before exchangeCode + +const tokens = await exchangeCode(provider, { code, codeVerifier: flow.codeVerifier, redirectUri: flow.redirectUri }) +// then verifyIdToken(..., { nonce: flow.nonce, ... }) +``` + +> **Nonce replay protection.** `verifyCallback` guards only `state`. Always also pass `nonce` to `verifyIdToken` — omitting it silently skips the nonce check and leaves the OIDC flow open to replay attacks. + ### `flow.ts` — Authorization Code ```ts @@ -1410,3 +1465,149 @@ const discord = defineOAuth2Provider({ defaultScope: ['identify', 'email'] }) ``` + +--- + +## Store + +Two-file persistent key-value store backed by a B+ tree index and an append-mostly data heap. Node-only (uses `FileHandle` from `node:fs/promises`). No extra peer deps beyond `@toolcase/base`. Storage is split across: + +- `.idx` — `BPlusIndex` from `@toolcase/base` (managed by `FsAdapter`), maps keys to 12-byte `BlockRef` pointers. +- `.dat.N` — append-mostly heap segments; each block is a raw serialised value blob. + +### BlockRef / helpers + +```ts +interface BlockRef { + segment: number // uint32 — which .dat.N segment file + offset: number // uint32 — byte offset within that segment + length: number // uint32 — byte length of the block +} + +const BLOCK_REF_SIZE = 12 // bytes (fixed-size struct) + +function serializeBlockRef(ref: BlockRef): Uint8Array +function deserializeBlockRef(buf: Uint8Array): BlockRef +``` + +### NodeStoreOptions + +```ts +interface NodeStoreOptions { + path: string // base path; .idx and .dat.N are appended + compare: (a: K, b: K) => number // key ordering function + serializeKey: (k: K) => Uint8Array + deserializeKey: (b: Uint8Array) => K + serializeValue: (v: V) => Uint8Array + deserializeValue: (b: Uint8Array) => V + keyEncoding?: string // forwarded to BPlusIndex for guard checks + pageSize?: number // B+ tree page size in bytes (default 4096) +} +``` + +`BPlusIndex.keyPreset` from `@toolcase/base` supplies pre-wired `{ compare, serializeKey, deserializeKey, keyEncoding }` bundles for `'string'`, `'number'`, and `'bigint'` key types — spread one into the options to skip the boilerplate: + +```ts +const store = await NodeStore.open({ + path: '/data/mystore', + ...BPlusIndex.keyPreset.string, + serializeValue: v => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) +``` + +### NodeStore + +```ts +class NodeStore { + static open(opts: NodeStoreOptions): Promise> + + // Accessors + get size(): number // live entry count + get liveRatio(): number // live heap bytes / total heap bytes; 1.0 = no dead space + + // CRUD + get(key: K): Promise + set(key: K, value: V): Promise + delete(key: K): Promise + + // Iteration — all in B+ tree key order + entries(): AsyncGenerator<[K, V]> + range(opts?: RangeOptions): AsyncGenerator<[K, V]> // bounded scan; RangeOptions from @toolcase/base + keys(): AsyncGenerator + values(): AsyncGenerator + + // Maintenance + optimize(): Promise // compact: copy live blocks to new segment, reclaim dead space + flush(): Promise // fsync both heap and index + close(): Promise +} +``` + +`RangeOptions` from `@toolcase/base`: `{ gte?, gt?, lte?, lt? }` compared with the store's `compare` function. + +```ts +import { NodeStore } from '@toolcase/node' +import { BPlusIndex } from '@toolcase/base' + +const store = await NodeStore.open({ + path: '/data/mystore', + ...BPlusIndex.keyPreset.string, + serializeValue: v => new TextEncoder().encode(v), + deserializeValue: b => new TextDecoder().decode(b), +}) + +await store.set('hello', 'world') +const v = await store.get('hello') // 'world' +await store.delete('hello') + +// Bounded range scan +for await (const [key, value] of store.range({ gte: 'a', lte: 'z' })) { + console.log(key, value) +} + +// Compaction metrics and maintenance +console.log(store.liveRatio) // < 1 means dead heap space; run optimize() to reclaim +await store.optimize() +await store.flush() +await store.close() +``` + +**Crash safety.** `set()` fsyncs the heap segment before committing the index entry — a crash between the two steps leaves at most a harmless orphan block in `.dat.N`. The index stays consistent. On the next `open()`, segment files not referenced by the index are detected as crash orphans and deleted automatically. + +**Compaction (`optimize()`).** Copies all live blocks to a fresh segment in key order, atomically updates the index with a single `setMany` call (one superblock commit), then deletes the old segments. A crash before the `setMany` leaves the new segment as an orphan, cleaned up on `open()`. A crash after `setMany` but before old-segment deletion leaves extra files that are cleaned up on `open()` — both paths are safe. + +**`NodeStore` vs `KVService`.** Use `NodeStore` for durable file-based storage with no infrastructure dependency (embedded caches, local feature stores, offline-capable worker state). Use `KVService` (Redis) for distributed in-memory state with TTL, pub/sub, and horizontal scale. + +### BlockStore + +Append-mostly heap manager that `NodeStore` wraps internally. Exported as a public API for advanced use cases that need raw segment-level access (e.g. a standalone append-only log). + +```ts +class BlockStore { + constructor(basePath: string) + + segmentPath(segId: number): string + + get activeSegment(): number + get activeOffset(): number + + // Discovery — returns { segId → file size } for all .dat.N files found on disk + scanSegments(): Promise> + setActive(segId: number, writeOffset: number): void + nextSegmentId(): number // returns activeSegment + 1 (for a fresh compaction segment) + + // I/O + append(data: Uint8Array): Promise // write to active segment + read(ref: BlockRef): Promise // read any block by ref + writeAt(segId: number, offset: number, data: Uint8Array): Promise // used during compaction + + // Durability + fsyncActive(): Promise + fsyncSegment(segId: number): Promise + + // Lifecycle + deleteSegment(segId: number): Promise // closes handle + unlinks file; unlink errors swallowed + close(): Promise +} +``` diff --git a/examples/public/phaser-game-dev/SKILL.md b/examples/public/phaser-game-dev/SKILL.md index e413e340..76f461c3 100644 --- a/examples/public/phaser-game-dev/SKILL.md +++ b/examples/public/phaser-game-dev/SKILL.md @@ -10,7 +10,7 @@ Opinionated blueprint for Phaser 4 games. Layered, registry-driven, ESM. Every s Stack baseline: - Phaser 4 (`phaser ^4.x`). -- `@toolcase/phaser-plus` ^4.x — **required** runtime layer. Top-level exports: `Engine`, `Scene`, `GameObject`, `Feature`, `FeatureRegistry`, `ServiceRegistry`, `GameObjectPool`, `Layer`, `ObjectLayer`, `HTMLFeature`, `SplitScreen`, `LogLevel`, plus `Events`, `Flow`, `Structs` namespaces. Re-exports from `effects/` (`installEffects`, shader effects), `perspective2d/` (`Scene2D`, `World`, `GameObject2D`, `Grid`), `cinema/`, `input/`, `ai/`, `flow/`, `debugger/`. +- `@toolcase/phaser-plus` ^4.x — **required** runtime layer. Top-level exports: `Engine`, `Scene`, `GameObject`, `Feature`, `FeatureRegistry`, `ServiceRegistry`, `GameObjectPool`, `Layer`, `ObjectLayer`, `HTMLFeature`, `SplitScreen`, `LogLevel`, plus `Events`, `Flow`, `Structs` namespaces. Re-exports from `effects/` (`installEffects`, shader effects), `perspective2d/` (`Scene2D`, `World`, `GameObject2D`, `Grid`), `cinema/` (`CameraDirector`, `ScreenShake`, `CameraFlash`, `DialogCameraCue`, `LetterboxFeature`, `ParallaxLayer`), `input/`, `ai/`, `flow/`, `debugger/`, `audio/` (`AudioFeature`), `persistence/` (`PersistenceFeature`, `SaveService`, `LocalStorageBackend`, `IndexedDBBackend`, `MemoryBackend`), `assets/` (`AssetFeature`), `particles/` (`ParticleFeature`), `net/` (`NetFeature`, `WebSocketTransport`, `LoopbackTransport`), `tilemap/` (`TilemapFeature`). - `@toolcase/web-components` ^4.x — **required** UI toolkit. Framework-free `tc-*` Web Components for HUDs, menus, inventories, dialogs, settings, screens, minimaps. Every `HTMLFeature` composes these instead of hand-rolling markup. - `@toolcase/base` ^4.x — peer of `phaser-plus` and `web-components`. Helpers and data structures (`Broadcast`, `ObjectPool`, etc.). - `@toolcase/logging` ^4.x — peer of `phaser-plus`. Scoped loggers wired through `Engine`. @@ -979,7 +979,7 @@ For enemy AI, dialog flow, scene state — use `phaser-plus`'s `StateMachine` an ### Cinema / camera -`phaser-plus/cinema` ships `CameraDirector`, `ScreenShake`, `CameraFlash`, `LetterboxFeature`, `ParallaxLayer`. Register as features in your scene; expose triggers via the bus (`cinema:shake`, `cinema:flash`). +`phaser-plus/cinema` ships `CameraDirector`, `ScreenShake`, `CameraFlash`, `DialogCameraCue`, `LetterboxFeature`, `ParallaxLayer`. Register as features in your scene; expose triggers via the bus (`cinema:shake`, `cinema:flash`). `DialogCameraCue` dims everything outside a supplied `DialogFrame` rectangle and optionally applies a vignette — call `open(frame, opts?)` / `close()` around dialogue sequences. ### Input @@ -1011,6 +1011,30 @@ save.save(0, { score: this.score }) `resolve(class)` lazy-instantiates on first call. Use `bind(class, factory)` for constructors that need args, or `provide(class, instance)` to inject a prebuilt singleton. +### Audio + +`AudioFeature` is a scene-lifetime `Feature` wrapping Phaser's `Sound` manager with named buses (`music`, `sfx`, `ui`, `ambience`), crossfade, per-voice SFX pools, spatial positioning, and ducking. Register it, then drive it entirely through bus events or direct calls (`play`, `playMusic`, `crossfade`, `duck`, `setVolume`). Three bus events are emitted: `AUDIO_MUSIC_START`, `AUDIO_MUSIC_END`, `AUDIO_BUS_CHANGE`. + +### Persistence / save data + +`PersistenceFeature` wraps a `SaveService` (game-lifetime) with an async slot API (`save`, `load`, `delete`, `list`, `has`) and emits `SAVE_DONE`, `LOAD_DONE`, `SAVE_DELETED` on the feature bus after each operation. Choose a backend at service binding time: `LocalStorageBackend` (synchronous, browser), `IndexedDBBackend` (async, larger payloads), or `MemoryBackend` (ephemeral / testing). Register `SaveService` in `services/` with the desired backend, then register `PersistenceFeature` in the scene that needs save/load. + +### Asset loading (runtime bundles) + +`AssetFeature` is a `Feature` for declarative, retry-aware asset loading at runtime (after the initial boot preload). Feed it an `AssetManifest` (named bundles of images, atlases, audio, bitmap fonts) and call `loadBundle(name)`. It emits `ASSET_PROGRESS` per-file, `ASSET_LOAD_COMPLETE` on bundle finish, and `ASSET_LOAD_ERROR` on failure. Useful for lazy-loading level art or DLC content mid-session without restarting the scene. + +### Particles + +`ParticleFeature` is a `Feature` that manages a registry of named `ParticlePreset` configurations wrapping Phaser's particle emitters. Register presets once in `onCreate`, then call `burst(preset, x, y)` for one-shot FX or `stream(preset, x, y)` for a continuous emitter (returns a `StreamHandle` with `stop()`). Events: `PARTICLE_BURST`, `PARTICLE_STREAM_START`, `PARTICLE_STREAM_STOP`. Presets can optionally attach a per-particle GLSL `Effect`. + +### Networking + +`NetFeature` drives a pluggable `Transport` (WebSocket or the in-process `LoopbackTransport` for local testing) with ping/pong RTT measurement, snapshot + delta entity sync (`syncEntity`, `interpolateEntity`), and optional client-side prediction (`predict`, `getPredicted`). Wire RTT/loss stats to the `NetPanel` debugger via `bindDebugger(dbg)`. Events: `NET_CONNECTED`, `NET_DISCONNECTED`, `NET_RTT_UPDATE`, `NET_ENTITY_STATE`. Swap the wire format by replacing `_sendMessage`/`_onMessage` with a `@toolcase/serializer` codec — the rest of the class is agnostic. + +### Tilemaps + +`TilemapFeature` is a `Feature` that loads Tiled (`.tmj`) or LDtk (`.ldtk`) maps, adds tilesets, renders layers, wires arcade-physics colliders via `buildColliders`, and generates a `TilemapNavMesh` for A\* pathfinding via `buildNavMesh({ walkable: [...] })`. Convert a Tiled object layer to an `ObjectLayer` with `toObjectLayer(key, layerName, poolKey)`, or read raw objects with `getObjects(layerName)`. + --- ## Recipes diff --git a/examples/public/phaser-plus/SKILL.md b/examples/public/phaser-plus/SKILL.md index 4666d253..bc03b3d0 100644 --- a/examples/public/phaser-plus/SKILL.md +++ b/examples/public/phaser-plus/SKILL.md @@ -14,7 +14,7 @@ import { Layer, ObjectLayer, HTMLFeature, SplitScreen, GameObjectPool, Flow, // { Event, TimeEvent, CollisionEvent, Job, FlowEngine, StateMachine, BehaviorTreeProcessor, ReplayRecorder, Timer, Tween, Timeline, TweenProcessor, EASE, resolveEase, Parallel, throttle, debounce, BT: {...} } - Structs, // { Matrix2, SpatialHash, Quadtree } + Structs, // { Vec2, AABB, Transform, Easing, Random, Matrix2, SpatialHash, Quadtree } LogLevel, // Debugger Debugger, Panel, @@ -56,7 +56,7 @@ import type { Transport } from '@toolcase/phaser-plus' ``` -Peers: `phaser@4.x`, `@toolcase/base@3.x`, `@toolcase/logging@3.x`. Optional peers: `react` / `react-dom` >=18. +Peers: `phaser@4.x`, `@toolcase/base@5.x`, `@toolcase/logging@5.x`. Optional peers: `react` / `react-dom` >=18. --- @@ -87,6 +87,11 @@ Peers: `phaser@4.x`, `@toolcase/base@3.x`, `@toolcase/logging@3.x`. Optional pee - [World](#world) - [GameObject2D](#gameobject2d) - [Grid](#grid) + - [Vec2](#vec2) + - [AABB](#aabb) + - [Transform](#transform) + - [Easing](#easing) + - [Random](#random) - [Matrix2](#matrix2) - [SpatialHash](#spatialhash) - [Quadtree](#quadtree) @@ -376,7 +381,19 @@ class Hud extends HTMLFeature { } ``` -> Note: a `ReactFeature` exists in source (`features/ReactFeature.ts`) for mounting a React tree into the DOM overlay, but it is **not** re-exported from the package root, so `import { ReactFeature } from '@toolcase/phaser-plus'` does not resolve. Until that export gap is closed, mount React manually inside an `HTMLFeature.onCreate()` (`createRoot(this.node).render(...)`). +Use `ReactFeature` (extends `HTMLFeature`) to mount a React tree into the DOM overlay: + +```ts +import { ReactFeature } from '@toolcase/phaser-plus' + +class Hud extends ReactFeature { + onCreate() { + this.render() + } +} +``` + +`render(element)` is safe to call before `react-dom` has loaded — the latest element is buffered and flushed once the root is ready. Use `renderAsync(element)` when you need to `await` the initial mount. Call `unmount()` to tear down the React tree early (also called automatically on feature destroy). ### SplitScreen @@ -821,6 +838,115 @@ qt.clear() - `SpatialHash` — uniform object sizes, high throughput, frequent moves. - `Quadtree` — varying object sizes or sparse density; culling and AI perception over irregular distributions. +Demo: `broad-phase` example scene (`examples/src/phaser-plus/scenes/BroadPhaseDemo.js`) — 260 moving bodies re-indexed in both a `SpatialHash` and a `Quadtree` every frame; the cursor drives a range `query()` (highlighting overlapping bodies) and a `nearest()` lookup (ringed + connected to the cursor), with `S` switching which structure answers. Registered under category `Structs`. + +### Vec2 + +Immutable 2D vector. All operations return a new `Vec2`; `Vec2.ZERO` and `Vec2.ONE` are shared singletons. + +```ts +const a = new Structs.Vec2(3, 4) +const b = new Structs.Vec2(1, 2) + +a.add(b) // Vec2(4, 6) +a.subtract(b) // Vec2(2, 2) +a.scale(2) // Vec2(6, 8) +a.dot(b) // 11 +a.length // 5 +a.lengthSq // 25 +a.normalize() // unit vector +a.lerp(b, 0.5) // midpoint +a.rotate(Math.PI) // 180° rotation +a.negate() // Vec2(-3, -4) +a.distanceTo(b) // Euclidean distance +a.equals(b) // false +a.toArray() // [3, 4] +``` + +### AABB + +Axis-aligned bounding box for broad-phase collision checks and region queries. Mutable — `set` / `reset` update in place; `clone` makes a copy. + +```ts +const box = new Structs.AABB(10, 20, 100, 80) // x, y, width, height + +// Derived edges +box.left; box.right; box.top; box.bottom // 10, 110, 20, 100 +box.centerX; box.centerY // 60, 60 + +// Predicates +box.contains(50, 50) // true — point inside +box.intersects(otherBox) // AABB overlap test +box.containsRect(otherBox) // other fully inside this + +// Mutation +box.set(0, 0, 200, 200) +box.reset() // → (0, 0, 0, 0) +box.copyFrom(otherBox) + +// Factory helpers +const b2 = Structs.AABB.from({ x: 0, y: 0, width: 64, height: 64 }) +const b3 = Structs.AABB.fromCenter(cx, cy, width, height) +``` + +### Transform + +Mutable 2D transform snapshot (position, rotation, scale). Useful for pooled objects that need to cache or diff state without allocating new objects. + +```ts +const t = new Structs.Transform() +t.set(100, 200, Math.PI / 4, 1.5, 1.5) // x, y, rotation, scaleX, scaleY + +t.x; t.y; t.rotation; t.scaleX; t.scaleY + +// Apply to any Phaser-compatible target +t.applyTo(sprite) // sets sprite.x, .y, .rotation, .scaleX, .scaleY + +t.copyFrom(other) // in-place update +t.clone() // new Transform with same values +t.reset() // → (0, 0, 0, 1, 1) +``` + +### Easing + +Namespace of named easing functions (`(t: number) => number`, t ∈ [0, 1]). Sourced from `@toolcase/base`; identical to the keys available via `Flow.EASE`. Use directly when you need an easing function outside of a Tween/Timeline context. + +```ts +import type { EasingFn } from '@toolcase/phaser-plus' + +Structs.Easing.easeOutCubic(0.5) // 0.875 +Structs.Easing.easeInBack(0.8) +Structs.Easing.easeOutBounce(1) // 1 + +// Custom bezier (equivalent to CSS cubic-bezier) +const ease: EasingFn = Structs.Easing.cubicBezier(0.25, 0.1, 0.25, 1) +ease(0.5) +``` + +Families available: `Sine`, `Quad`, `Cubic`, `Quart`, `Quint`, `Expo`, `Circ`, `Back`, `Elastic`, `Bounce` — each in `easeIn`, `easeOut`, `easeInOut` variants, plus `cubicBezier`. + +### Random + +Seeded, deterministic PRNG (mulberry32). Reproducible across platforms — pass the same seed to replay a sequence. + +```ts +const rng = new Structs.Random(42) + +rng.next() // float [0, 1) +rng.int(1, 6) // integer in [min, max] +rng.float(0.5, 1.5) // float in [min, max] +rng.bool(0.3) // true with probability 0.3 +rng.pick(['a', 'b', 'c']) // random element +rng.shuffle([1, 2, 3, 4]) // new shuffled array (original untouched) + +// Weighted selection +rng.weighted([ + { item: 'common', weight: 70 }, + { item: 'rare', weight: 25 }, + { item: 'epic', weight: 5 }, +]) +``` + --- ## Effects diff --git a/examples/public/serializer/SKILL.md b/examples/public/serializer/SKILL.md index b7d98832..1963004d 100644 --- a/examples/public/serializer/SKILL.md +++ b/examples/public/serializer/SKILL.md @@ -48,13 +48,14 @@ serializer.define(key: string, fields: FieldType[]): void interface FieldType { key: string - type: string // one of Serializer.FieldType.* + type: FieldTypeRef // string literal, EnumMarker, MapMarker, or PackedMarker rule: 'required' | 'optional' | 'repeated' default?: any + tag?: number // explicit protobuf field number; defaults to index + 1 } ``` -- Field tags are auto-assigned by array order (1-indexed). **Order matters** — once data is on the wire, reordering fields will break decoding for old buffers. Append-only is safe. +- Field tags are auto-assigned by array order (1-indexed) unless `tag` is supplied explicitly. **Order matters** — inserting, removing, or reordering a field shifts every subsequent positional tag and silently corrupts existing buffers. Append-only is safe. Protobuf reserves tags 19000–19999 for internal use; positional schemas with ≥ 18999 fields will hit this range and be rejected by `protobufjs`. - `default` is applied on decode when the field is absent. `null` is used internally if `default` is undefined. - `'repeated'` ⇒ field is encoded as `T[]`. @@ -130,7 +131,7 @@ serializer.fields('Player') // → [{ key: 'id', type: 'string', rule: 'opti ### Versioning — `version()`, `getVersion()`, `defineVersion()`, `migrate()`, `encodeVersioned()`, `decodeVersioned()` -Stamps a 2-byte version header (`major`, `minor`) onto encoded frames and dispatches per-type migrations on decode when the frame is older than current. Both `major` and `minor` are byte-sized (0–255). +Stamps a 3-byte version header (magic byte `0xB5`, `major`, `minor`) onto encoded frames and dispatches per-type migrations on decode when the frame is older than current. Both `major` and `minor` are byte-sized (0–255). ```ts const s = new Serializer() @@ -142,7 +143,7 @@ s.define('Player', [ s.migrate('Player', 1, msg => ({ name: msg.name, level: 1 })) // hop v1 → v2 const frame = s.encodeVersioned('Player', { name: 'Alice', level: 7 }) -// frame[0] = 2, frame[1] = 0, frame[2..] = protobuf body +// frame[0] = 0xB5 (MAGIC_VERSIONED), frame[1] = 2 (major), frame[2] = 0 (minor), frame[3..] = body const out = s.decodeVersioned('Player', incomingV1Frame) // out.version = { major: 1, minor: 0 } @@ -154,6 +155,11 @@ Decode rules: - `major < current.major` → walk single-step migrations from `major` → `major+1` → … → `current.major`. Throws if any hop is missing. - `major > current.major` → throws (frame is from the future). +`decodeVersioned` throws on: +- `Serializer.decodeVersioned[] failed: missing versioned magic byte` — buffer does not start with `0xB5`. +- `Serializer.decodeVersioned[] failed: buffer too small for version header (bytes=)` — buffer is fewer than 3 bytes. +- `Serializer.decodeVersioned[] failed: migrated message invalid: ` — after all migrations, the result fails re-verification against the current schema. + `getVersion(): { major, minor }` returns the current marker. #### Breaking schema changes — `defineVersion()` @@ -184,16 +190,16 @@ const out = s.decodeVersioned('Item', v1Frame) ### Streaming — `fragment()`, `reassemble()` -Split an encoded buffer into transport-sized chunks and reassemble out-of-order on the receiving side. Each chunk carries an 8-byte header: 4-byte random `frameId`, 2-byte `index`, 2-byte `total`. Up to 65535 chunks per frame. +Split an encoded buffer into transport-sized chunks and reassemble out-of-order on the receiving side. Each chunk carries a 9-byte header: byte 0 = `MAGIC_FRAGMENT` (`0xF5`), 4-byte random `frameId` [1–4], 2-byte `index` [5–6], 2-byte `total` [7–8]; payload begins at offset 9. Up to 65535 chunks per frame. ```ts const buf = serializer.encode('Snapshot', big) // e.g. 80 KB const chunks = serializer.fragment(buf, 16384) // payload bytes per chunk; default 16384 -for (const chunk of chunks) ws.send(chunk) // each chunk has the 8-byte header +for (const chunk of chunks) ws.send(chunk) // each chunk has the 9-byte header -// on the receiver — total lives at bytes [6,7] of every chunk header: -const peekTotal = (chunk: Uint8Array) => (chunk[6] << 8) | chunk[7] +// on the receiver — total lives at bytes [7,8] of every chunk header: +const peekTotal = (chunk: Uint8Array) => (chunk[7] << 8) | chunk[8] const incoming: Uint8Array[] = [] ws.addEventListener('message', e => { @@ -206,7 +212,52 @@ ws.addEventListener('message', e => { }) ``` -`reassemble()` validates: header length, matching `frameId` across all chunks, exact chunk count, no duplicate indexes. Use it as the membrane between unreliable transports and `decode()`. +`fragment(buffer, maxChunkSize?)` throws: +- `Serializer.fragment: maxChunkSize must be a positive integer, got ` — value is zero, negative, or non-integer. +- `Serializer.fragment: chunks exceeds the 65535 limit; raise maxChunkSize` — the buffer requires more than 65535 chunks at the given size. + +`reassemble(chunks)` throws on: +- `Serializer.reassemble: missing fragment magic byte` — a chunk does not start with `0xF5`. +- `Serializer.reassemble: chunk header truncated (need 9 bytes)` — a chunk is shorter than 9 bytes. +- `Serializer.reassemble: chunk count mismatch — got , header says ` — wrong number of chunks supplied. +- `Serializer.reassemble: total mismatch — chunk says , expected ` — chunks disagree on total count. +- `Serializer.reassemble: index out of range [0, )` — an index falls outside the expected range. +- `Serializer.reassemble: frameId mismatch — chunks belong to different frames` — mixed-frame interleaving. +- `Serializer.reassemble: duplicate chunk at index ` — same index received twice. + +Use `reassemble` as the membrane between unreliable transports and `decode()`. + +--- + +## Public types + +The following interfaces and type aliases are exported from `@toolcase/serializer` and can be imported directly: + +```ts +import type { + FieldType, + FieldTypeRef, + EnumMarker, + MapMarker, + PackedMarker, + Version, + VersionedFrame, + MigrationFn, + SafeResult, +} from '@toolcase/serializer' +``` + +| Type | Description | +|------|-------------| +| `FieldType` | Field descriptor passed to `define()` / `defineVersion()` | +| `FieldTypeRef` | Union of `string \| EnumMarker \| MapMarker \| PackedMarker` — the `type` property of `FieldType` | +| `EnumMarker` | Opaque marker returned by `Serializer.FieldType.ENUM(...)` | +| `MapMarker` | Opaque marker returned by `Serializer.FieldType.MAP(...)` | +| `PackedMarker` | Opaque marker returned by `Serializer.FieldType.PACKED_ARRAY(...)` | +| `Version` | `{ major: number, minor: number }` | +| `VersionedFrame` | `{ version: Version, message: T }` — return type of `decodeVersioned()` | +| `MigrationFn` | `(message: any) => Record` — handler passed to `migrate()` | +| `SafeResult` | `{ ok: boolean, value?: T, error?: Error }` — return type of `safeEncode()` / `safeDecode()` | --- diff --git a/examples/public/taskforge/SKILL.md b/examples/public/taskforge/SKILL.md new file mode 100644 index 00000000..c9ca427a --- /dev/null +++ b/examples/public/taskforge/SKILL.md @@ -0,0 +1,884 @@ +--- +name: taskforge +description: Use when reading or modifying the TaskForge application source (taskforge/) or when understanding how a TaskForge-managed project workspace is structured. Covers the full architecture: Next.js 15 + node:sqlite backend, 16 SQLite repositories and their schema, ~60 API routes by area, three-tier role model, multi-account LRU orchestration (account-repo / account-lru / account-secrets, per-task **Account:**, DEFAULT_ACCOUNT, cooldown/failover), cron scheduler (5-field subset), reviewer pass (review.ts, needs-review status), four bundled skills (task-creator / knowledge-writer / note-writer / commit-message), notes vs knowledge distinction, telemetry + usage gate, warm sessions, Git surface, audit / health / backups / search, webhook notifications, per-project settings, and GitHub allow-list / org membership gating. +--- + +# taskforge — Architecture Reference + +TaskForge is a self-hosted autonomous task runner: a Next.js 15 (App Router) full-stack application that queues engineering tasks as Markdown files, delegates them one at a time to the Claude Code CLI, and surfaces progress through a real-time dashboard. All state persists in a single SQLite database via Node's built-in `node:sqlite`. + +Stack baseline: + +- Next.js 15 (App Router), React 19, TypeScript `strict`. +- `node:sqlite` (`DatabaseSync`) — synchronous, WAL journal, foreign keys on, 5 000 ms busy timeout. +- Prepared-statement cache (`Map`) per `Database` singleton; re-used across calls. +- Claude Code CLI (`claude`) as the agent binary — spawned as child processes via `server/infrastructure/agent.ts`. +- GitHub OAuth for authentication; session tokens are `.`. +- Stateful singletons cached on `globalThis` (survive Next.js dev hot-reload) rather than a DI container. + +--- + +## Table of contents + +1. [Directory layout](#directory-layout) +2. [Data layer — SQLite](#data-layer--sqlite) +3. [The 16 repositories](#the-16-repositories) +4. [Schema highlights](#schema-highlights) +5. [Server layering](#server-layering) +6. [Three-tier role model](#three-tier-role-model) +7. [Multi-account orchestration](#multi-account-orchestration) +8. [Scheduler](#scheduler) +9. [Reviewer pass](#reviewer-pass) +10. [Four bundled skills](#four-bundled-skills) +11. [Notes vs knowledge](#notes-vs-knowledge) +12. [Telemetry](#telemetry) +13. [Usage tracking and usage gate](#usage-tracking-and-usage-gate) +14. [Warm sessions](#warm-sessions) +15. [Git surface](#git-surface) +16. [Audit log](#audit-log) +17. [Health](#health) +18. [Backups](#backups) +19. [Search](#search) +20. [Notifications](#notifications) +21. [Per-project settings](#per-project-settings) +22. [GitHub allow-list and org gating](#github-allow-list-and-org-gating) +23. [API routes by area](#api-routes-by-area) +24. [Task file format](#task-file-format) +25. [Environment variables](#environment-variables) +26. [SSE event types](#sse-event-types) +27. [Invariants](#invariants) + +--- + +## Directory layout + +``` +taskforge/ +├── app/ # Next.js App Router +│ ├── api/ # ~60 route handlers +│ │ ├── accounts/ # admin: account CRUD + health-check +│ │ ├── admin/ # admin: DB backup + project backup +│ │ ├── agent-defs/ # custom agent kinds +│ │ ├── audit/ # audit log (admin) +│ │ ├── auth/ # GitHub OAuth + logout +│ │ ├── health/ # liveness + readiness +│ │ ├── me/ # current user profile +│ │ ├── projects/ # project + task + run + git + knowledge + notes + … +│ │ ├── prompt-templates/ +│ │ ├── skills/ # read installed skills +│ │ ├── telemetry/ # global cross-project cost +│ │ ├── usage/ # usage snapshot +│ │ └── users/ # admin: user CRUD +│ └── projects/[project]/ # dashboard pages +│ ├── page.tsx # project overview +│ ├── tasks/ # task list +│ ├── run/ # run control +│ ├── runs/ # run history +│ ├── git/ # git panel +│ ├── knowledge/ # knowledge docs +│ ├── notes/ # notes +│ ├── agents/ # agent session panel +│ └── settings/ # project settings +├── server/ # backend logic (no React, no Next APIs) +│ ├── config.ts # all env vars → typed config object +│ ├── data/ +│ │ ├── db.ts # DatabaseSync wrapper + migration runner +│ │ ├── repositories/ # 16 repository files +│ │ ├── accounts.ts # account service facade +│ │ ├── agent-sessions.ts # agent session manager singleton +│ │ └── auth.ts # OAuth flow + session mint/verify +│ ├── domain/ +│ │ └── types.ts # shared types + constants (no I/O) +│ ├── infrastructure/ # agent spawn, stream parser, fs-workspace, git, notify +│ ├── services/ # stateful logic (execution-manager, scheduler, review, …) +│ └── templates/ # CLAUDE.md and other text templates +└── skills/ # four bundled Claude Code skills + ├── task-creator/SKILL.md + ├── knowledge-writer/SKILL.md + ├── note-writer/SKILL.md + └── commit-message/SKILL.md +``` + +--- + +## Data layer — SQLite + +`server/data/db.ts` owns a single `DatabaseSync` instance. Boot-time initialization: + +```ts +db.exec(` + PRAGMA journal_mode = WAL; + PRAGMA foreign_keys = ON; + PRAGMA busy_timeout = 5000; +`) +``` + +Schema migrations are an append-only `MIGRATIONS: string[][]` array. Each entry is a list of SQL statements; its position + 1 is its version number. A `schema_migrations` table tracks the applied version. On init, outstanding migrations run one at a time inside individual `tx()` calls. + +`tx(fn: () => T)` wraps synchronous work in `BEGIN / COMMIT / ROLLBACK`. + +Prepared statements are cached by SQL text: `stmts: Map`. The first call for a given SQL string calls `db.prepare(sql)` and caches the result; subsequent calls reuse the compiled statement. + +--- + +## The 16 repositories + +| File | Table(s) | Purpose | +|---|---|---| +| `account-repo.ts` | `account` | Register / list / pick accounts; atomic LRU pick + stamp | +| `agent-def-repo.ts` | `agent_def` | Custom agent kind definitions | +| `agent-prompt-repo.ts` | `agent_prompt`, `agent_prompt_history` | Per-project per-agent saved prompts + history | +| `audit-repo.ts` | `audit` | Append-only audit trail | +| `project-repo.ts` | `project` | Project registry; explicit cascading deletes | +| `prompt-history-repo.ts` | `prompt_template` | Shared reusable prompt templates | +| `run-event-repo.ts` | `run_event` | Durable event + log frames per run | +| `run-repo.ts` | `run` | Per-run lifecycle records | +| `schedule-repo.ts` | `schedule` | Per-project cron schedule | +| `search-repo.ts` | FTS5 virtual table | Full-text search over tasks, knowledge, notes | +| `settings-repo.ts` | `project_setting` | Per-project KV overrides | +| `task-repo.ts` | `task` | Task metadata synced from Markdown | +| `telemetry-repo.ts` | `telemetry` | Per-attempt execution records | +| `usage-repo.ts` | `usage_snapshot` | Cached `claude /usage` snapshots | +| `user-repo.ts` | `app_user` | Users, roles, first-admin bootstrap | +| `warm-session-repo.ts` | `warm_session` | Per-project warm session IDs | + +--- + +## Schema highlights + +All column names are `snake_case`. The schema was built across 6 migration versions. + +**`project`** — `name` PK, `git_url`, `branch`, `created_at`. + +**`task`** — composite PK `(project, id)`; `title`, `status` ∈ `'open'|'done'|'error'`, `severity`, `facet_project`, `model`, `account`, `depends` (CSV of task numbers), `last_error` (500-char cap), `synced_mtime`, `updated_at`. Indexed on `(project, status)`. + +**`telemetry`** — one row per task attempt; `task`, `status` ∈ `'done'|'failed'|…`, `elapsed` (ms), `model`, `commit_sha`, `error`, `created_at`, `tokens_in`, `tokens_out`, `cost_usd`, `review` ∈ `'pass'|'fail'`, `review_note` (500-char cap). Indexed for latest-by-task lookups. + +**`run`** — `id` (autoincrement), `project`, `started_at`, `finished_at`, `reason`, `options_json`, `done`, `total`, `error`, `started_by`, `branch`, `pr_url`. + +**`run_event`** — `id` (autoincrement), `project`, `type`, `payload` (JSON), `run_id`, `created_at`. Indexed on `(run_id, id)`. + +**`app_user`** — `github_id` PK, `login`, `name`, `avatar_url`, `role` ∈ `'admin'|'standard'|'guest'`, `added_at`. + +**`account`** — `alias` PK (regex `/^[a-z0-9][a-z0-9-]{0,40}$/`); `auth` ∈ `'oauth'|'apikey'`; `dir` (workspace path); `label`; `api_key_env` (env-var **name** — never the key value); `last_used_at`; `cooling_until` (ISO-8601 or null). + +**`schedule`** — `project` PK; `cron`, `options_json`, `enabled` (0|1), `only_if_pending` (0|1), `skip_above_usage` (percent or null), `last_fired_at`. + +**`project_setting`** — composite PK `(project, key)`; `value` is JSON-encoded string. + +**`agent_def`** — `kind` PK (regex `/^[a-z0-9][a-z0-9-]{1,40}$/`); `label`, `prompt_preamble`, `target` ∈ `'repo'|'tasks'|'knowledge'|'notes'|'project'`, `post` ∈ `'none'|'tasks'|'knowledge'|'notes'`, `created_at`. + +**`audit`** — `id` (autoincrement), `at` (ISO-8601), `github_id`, `login`, `action`, `project`, `detail` (500-char cap). + +**`warm_session`** — `project` PK, `session_id`, `ts` (epoch milliseconds). + +**`usage_snapshot`** — `id` (autoincrement), `fetched_at`, `note`, `raw`, `max_percent`, `entries` (JSON). + +--- + +## Server layering + +``` +app/api/**/route.ts # auth-guard → parse body → call service/repo → HTTP response +server/services/ # stateful business logic — accounts, runs, scheduling, review +server/data/repositories/ # all SQL — one file per table cluster +server/data/db.ts # single DatabaseSync handle + migration runner +server/infrastructure/ # pure helpers — agent spawn, stream parser, fs-workspace, git, notify +server/domain/types.ts # shared TS types + constants (no I/O, innermost) +``` + +Route handlers use `guard(minRole)` from `server/web/http.ts`, which re-reads the calling user's role from the database on every request — the cookie role field is not trusted for authorization decisions. + +Services call repositories; they never call `db.prepare()` directly. Repositories never call services. + +--- + +## Three-tier role model + +| Role | Rank | Capabilities | +|---|---|---| +| `guest` | 0 | Read-only access; cannot start runs or create tasks | +| `standard` | 1 | Full project operations (tasks, runs, knowledge, notes, settings) | +| `admin` | 2 | + user management, accounts, audit, backups, health details | + +**Bootstrap**: the first user to sign in when no admin exists is automatically promoted to `admin` (`user-repo.ts: adminCount()` check at insert time). + +**Last-admin guard**: `setRole()` throws if the target is the last `admin` and the new role would remove admin status. + +**Per-request re-auth**: `authorize(minRole)` in `server/data/auth.ts` queries the live `app_user` row each request. An admin can promote/demote users and the change is effective on their next API call without requiring re-login. + +--- + +## Multi-account orchestration + +### Overview + +TaskForge manages a pool of Claude accounts. The execution engine selects accounts via LRU rotation and cools them down on rate-limit (429) signals. + +### Files + +| File | Responsibility | +|---|---| +| `server/data/repositories/account-repo.ts` | Persist accounts; `pickLeastRecentlyUsed()` — atomic select + stamp | +| `server/services/account-lru.ts` | Pure LRU logic: `isEligible`, `compareLru`, `selectLru` — no I/O | +| `server/services/account-secrets.ts` | Secret redaction: `scrubSecrets`, `looksLikeApiKey` | +| `server/data/accounts.ts` | Service facade: `registerAccount`, `pickAccount`, `resolveAccount`, `verifyAccount`, `coolDownAccount` | + +### Selection algorithm + +`pickLeastRecentlyUsed(opts, now)` runs as a single SQLite transaction: +1. Load all eligible accounts (not currently cooling, matching optional `auth` / pool filter). +2. Sort by `last_used_at` ascending — never-used accounts first, ties broken by `alias`. +3. Stamp `last_used_at = now` on the winner atomically. +4. Return the selected account. + +The pure logic lives in `account-lru.ts` (`selectLru`) and is exercised by unit tests without touching the database. + +### Secret boundary + +API key values are never stored. The `api_key_env` column holds only the **name** of the environment variable. `resolveAccount(alias)` reads `process.env[apiKeyEnv]` at spawn time and passes it to the child process as `ANTHROPIC_API_KEY`. `scrubSecrets` redacts any `sk-ant-*` pattern from agent stdout/stderr before logging. + +### Per-task account override + +A task file may include `**Account:** ` in its metadata block. The execution engine reads this and selects that specific account instead of calling the LRU pool. If the named account is in a cooling period, the engine falls back to LRU selection. + +### Default account + +`config.defaultAccount` (env `DEFAULT_ACCOUNT`) names the account to use when no per-task override is set and only one account is registered. With multiple accounts, LRU rotation applies unconditionally. + +### Cooldown and failover + +On a `limit` / 429 signal from the CLI: +1. The current account is marked cooling: `coolDownAccount(alias, until)` sets `cooling_until` to the parsed reset time (or `now + limitSleepFallback` when no reset time is available). +2. The engine may retry the current task with a different LRU-eligible account. +3. If no eligible account is available, the engine sleeps (`computeLimitSleep`) — exponential back-off bounded by `config.limitSleepMax` (default 21 600 s / 6 h). +4. After at most `config.limitMaxRetries` (default 5) attempts, the task is marked `error`. + +### Account health check + +`verifyAccount(alias)` runs a cheap one-shot probe with `--permission-mode plan` and read-only output flags, then returns `AccountHealth { ok: boolean, detail: string }`. Secrets are scrubbed from the output before returning. + +Account directories are created with mode `0o700` (owner read-write-execute only). `ensureAccountsDirSecure()` (called at boot) hard-sets the accounts directory and all children to `0o700`. + +--- + +## Scheduler + +`server/services/scheduler.ts`. Singleton on `globalThis`; started once at app boot. + +### Cron expression subset + +Five space-separated fields: `minute hour dom month dow`. Supported operators per field: + +| Operator | Example | Meaning | +|---|---|---| +| `*` | `* * * * *` | any value | +| integer | `30` | exact match | +| list | `0,15,30,45` | any listed value | +| range | `1-5` | inclusive range | +| step | `*/15` or `0-59/5` | every N values | + +`dow` value `7` is normalized to `0` (Sunday). When both `dom` and `dow` are non-`*`, standard cron semantics apply: a minute fires if **either** condition matches. + +`parseCron(expr)` validates the expression and returns a parsed structure; throws `InvalidCronError` on malformed input. + +### Tick loop + +A `setInterval` (60 000 ms, `unref`'d) fires each minute. For each row where `enabled = 1`, the scheduler: + +1. **De-dupe**: skips if `last_fired_at` is already within the current minute. +2. **Pattern match**: evaluates the cron expression against the current minute. +3. **Pre-flight guards** (all must pass): + - Project execution lock held? → skip + log. + - Agent session running for the project? → skip + log. + - `skip_above_usage` configured? → probe `claude /usage`, parse `maxPercent`, skip if `maxPercent >= threshold`. + - `only_if_pending`? → count open tasks, skip if zero. +4. **Merge options**: combines stored `options_json` with effective settings (`defaultModel`) and calls `executionManager.start(project, mergedOpts)`. +5. Stamps `last_fired_at = now`. + +--- + +## Reviewer pass + +`server/services/review.ts`. Optional post-task verification. + +### When it runs + +After a task finishes with status `done`, if the project's `review` setting is `true`, the execution engine invokes the reviewer before advancing to the next task. + +### Mechanics + +- **Model**: `config.reviewModel` (defaults to a haiku-class model). +- **Timeout**: `config.reviewTimeoutMs` (default 120 000 ms). +- **Input**: task body (capped to 6 000 chars) + staged diff (capped to 12 000 chars). +- **Prompt**: adversarial criteria — missing requirements, wrong files touched, incomplete implementation, contradicting changes. +- **Output parse**: first line must match `/^\s*(PASS|FAIL)\b/i`; remaining lines become the review note (capped to 500 chars). +- Returns `null` on timeout or spawn error — the reviewer is advisory and never blocks the run. + +### Effect on task status + +The verdict is stored via `telemetry.setReview(project, task, verdict, note)`: + +- `review = 'pass'` → runtime status `'done'`. +- `review = 'fail'` → runtime status `'needs-review'` (the `task` row stays `done` in SQLite; the distinction is resolved in `projects.ts: getTasks()`). + +A `needs-review` task must be manually reopened before it will be re-executed. + +--- + +## Four bundled skills + +Skills are SKILL.md files consumed by the Claude Code CLI (`--add-dir` makes the skills directory available in each agent run). Project workspaces get a symlink `.claude/skills → config.skillsDir` at creation time. + +### task-creator + +Creates numbered `.md` task files inside the project's `tasks/` directory. + +- **Filename**: `-.md` — NNN continues from the highest existing number (counting archived files in the A5 subdirectory via `nextTaskNumber()`). +- **Required metadata**: H1 title, `**Status:** open`. +- **Optional metadata**: `**Severity:**`, `**Project:**`, `**Model:**` (per-task model override), `**Depends:**` (comma-separated task numbers). +- **Sections**: `## Problem` + `## Task`. +- **Post-processing**: new files are reconciled into the `task` table and indexed in the FTS5 search table. + +### knowledge-writer + +Writes a single technical knowledge document into the project's `knowledge/` directory. + +- **Filename**: kebab-case slug. +- **Required format**: H1 title + one-sentence summary on the immediately-following line (lifted verbatim into `knowledge/index.md`). +- `knowledge/index.md` is rebuilt automatically after each add/remove — the skill must not touch it. +- Triggered automatically after each successful task when `knowledgeAutoUpdate` is `true`. + +### note-writer + +Creates or edits a single free-form Markdown note in the project's `notes/` directory. + +- **Edit mode**: changes only what the instruction specifies; no other files. +- **Create mode**: exactly one new `notes/.md`. +- May read `repo/` and `knowledge/` for context. Never modifies application source or commits. + +### commit-message + +Generates a Conventional Commits message from a staged diff. + +- **Output**: plain message text only — no preamble, no code fences. +- **Format**: `type(scope): summary` — subject ≤ 72 chars, imperative mood, no trailing period. +- **Body**: optional 1–3-line explanation when the why is non-obvious. +- **Model**: `config.commitModel` (defaults to a haiku-class model). +- **Timeout**: `config.generateTimeoutMs` (default 120 000 ms). +- Returns `null` on timeout or non-zero exit; the engine falls back to the task filename as the commit message. + +--- + +## Notes vs knowledge + +| | `knowledge/` | `notes/` | +|---|---|---| +| **Purpose** | Source-anchored technical reference docs | Free-form context (decisions, scratchpad, meeting notes) | +| **Structure** | H1 + one-sentence summary + body; `index.md` auto-rebuilt | H1 + free Markdown | +| **Auto-update** | `knowledge-writer` runs after task when `knowledgeAutoUpdate = true` | Never auto-updated | +| **Written by** | `knowledge-writer` skill | `note-writer` skill | +| **Consumed by** | Agent at run time via `knowledge/index.md` | Explicit agent reference | + +--- + +## Telemetry + +One row per task attempt in the `telemetry` table. + +Key `telemetry-repo.ts` methods: + +| Method | Returns | +|---|---| +| `record(project, rec)` | Insert new attempt | +| `setReview(project, task, verdict, note)` | Stamp `review` + `review_note` on latest row | +| `latestByTask(project)` | `Map` — latest attempt per task by autoincrement id | +| `costBetween(project, fromIso, toIso)` | Sum `cost_usd` for time window | +| `summary(project, days=30)` | `TelemetrySummary` — per-day series, per-model stats, top-5 slowest/most-expensive, retried tasks, totals | +| `globalCostPerDay(days)` | `[{date, costUsd}]` — cross-project daily cost | + +`TelemetrySummary` is served by `GET /api/projects/[p]/telemetry/summary` and shown on the project dashboard. `globalCostPerDay` is served by `GET /api/telemetry/global` (admin telemetry page). + +--- + +## Usage tracking and usage gate + +`server/services/usage.ts` wraps `claude /usage` — a local, read-only command that reports Anthropic account usage without spending tokens. + +- `refreshUsage(now)` — spawns `claude /usage`, parses output with `/^(.+?):\s*(\d+)%\s*used/i`, persists the result to `usage_snapshot`. +- `readUsageCache()` — returns the latest snapshot without spawning. +- Snapshot shape: `{ fetchedAt, note, entries: [{label, percent, resets?}], maxPercent }`. +- `UsageError` is thrown on timeout or unparseable output. + +### Usage gate + +When `config.usageGateEnabled` is `true` (default), the platform checks usage before allowing new runs: + +- **Execution engine**: checks `maxPercent` against the effective `usageGateThreshold` before starting a run. If `maxPercent >= threshold`, the run is rejected with an error. +- **Scheduler**: checks the same threshold as a pre-flight guard before firing a scheduled run. +- **Threshold source**: project setting `usageGateThreshold` (1–100) overrides `config.usageGateThreshold` (default 95). +- **Background poll**: usage is refreshed on a `config.usageGatePollSeconds` (default 1 800 s) cadence independent of the scheduler. + +--- + +## Warm sessions + +The Claude Code CLI supports `--resume ` to continue a prior conversation context. + +- Stored in `warm_session (project PK, session_id, ts)`. +- When a task finishes, the execution engine writes the session ID emitted by the CLI to the warm-session store. +- On the next run, if `warmSession` setting is `true` and the stored session is not older than `config.warmSessionMaxAge` (default 14 400 s / 4 h), the CLI is launched with `--resume `. +- A stale or absent session ID causes a cold start — not an error. + +--- + +## Git surface + +### Engine-side + +`server/infrastructure/git.ts` provides: + +| Helper | Description | +|---|---| +| `clone(name, url, branch)` | `git clone`; optionally check out a specific branch | +| `setOriginUrlWithToken(name, url)` | Embed `GIT_REMOTE_TOKEN` in the origin URL for credential-less push | +| `isClean(name)` | Returns `true` if working tree is clean | +| `dirtyFiles(name)` | Returns list of modified/untracked files | +| `assertSafeGitUrl(url)` | Rejects local file paths at project-creation time | + +`config.canPush()` returns `true` if `GIT_REMOTE_TOKEN` is set or `GIT_SSH_CONFIGURED=1`. + +Before each run, the engine calls `isClean`. A dirty working tree returns HTTP 412 to the caller. + +When `commitAfter` is enabled, the engine commits after each successful task. When `commitMessageMode = 'ai'`, the commit-message skill is invoked; otherwise the task filename is used. When `pushAfter` is enabled, the commit is pushed to origin. When `branchPerRun + openPr` are both enabled, a new branch is created at run start and a PR is opened at run end via the GitHub REST API (`server/infrastructure/github.ts`). + +### API routes (Git) + +| Method | Route | Description | +|---|---|---| +| GET | `/api/projects/[p]/git` | Status (clean flag + dirty files) | +| GET | `/api/projects/[p]/git/diff` | Staged + unstaged diff | +| GET | `/api/projects/[p]/git/files` | File tree | +| GET | `/api/projects/[p]/git/branch` | Current branch name | +| GET | `/api/projects/[p]/git/branches` | All local branches | +| GET | `/api/projects/[p]/git/commits` | Commit log | +| GET | `/api/projects/[p]/git/commits/[sha]` | Single commit detail | +| GET | `/api/projects/[p]/git/issues` | Open GitHub issues | +| POST | `/api/projects/[p]/git/commit` | Create a commit | +| POST | `/api/projects/[p]/git/push` | Push to origin | +| POST | `/api/projects/[p]/git/op` | Generic git operation (e.g. checkout) | +| POST | `/api/projects/[p]/git/revert` | Revert a commit | +| POST | `/api/projects/[p]/git/exec` | Raw git command (admin) | +| POST | `/api/projects/[p]/git/stash` | Stash changes | +| POST | `/api/projects/[p]/git/discard-paths` | Discard specific paths | +| POST | `/api/projects/[p]/git/pr` | Create a pull request | + +--- + +## Audit log + +`server/data/repositories/audit-repo.ts`. Append-only; writes are best-effort (never throws). Project deletion cascades task/run/telemetry rows but preserves audit entries. + +Fields: `at` (ISO-8601), `github_id`, `login`, `action`, `project`, `detail` (500-char cap). + +Audited actions: `task.create`, `run.start`, `run.stop`, `run.force`, `account.create`, `account.remove`, `backup.db`, `backup.project`, `user.role`, `project.create`, `project.delete`. + +API: `GET /api/audit` — paginated (cursor: `beforeId`), filterable by `project`, `login`, and `action` prefix. Admin only. + +`audit-repo.ts` helpers: `actions()` returns all distinct action names for filter dropdowns; `count()` returns total row count. + +--- + +## Health + +`GET /api/health` — lightweight liveness; always returns 200 OK. No auth required. + +`GET /api/health/details` — detailed readiness. Admin only. Returns: + +- Agent binary version (`claude --version` output). +- Git version. +- Disk free bytes on the workspace mount. +- SQLite DB file size + applied migration version. +- Per-project engine state snapshots (`IDLE` / `RESUMING` / `RUNNING` / `PAUSED` / `SLEEPING`). +- FTS5 availability flag (`searchAvailable()`). +- Non-sensitive account summaries (alias, auth, label, lastUsedAt). +- Config snapshot — operational values only; secrets redacted. + +--- + +## Backups + +| Route | Description | +|---|---| +| `GET /api/admin/backup/db` | `VACUUM INTO` a temp file, stream as binary download (`taskforge-YYYY-MM-DD.db`) | +| `GET /api/admin/backup/project/[project]` | Archive the project workspace tree and stream as download | + +Both routes require `admin` role and emit an audit entry (`backup.db` / `backup.project`). + +--- + +## Search + +`server/data/repositories/search-repo.ts`. FTS5 virtual table lazily created at first use. + +| Method | Description | +|---|---| +| `indexDoc(project, type, id, title, body)` | Delete-then-insert (upsert pattern); `body` capped at 200 000 chars | +| `removeDoc(project, type, id)` | Remove a single document | +| `removeProject(project)` | Remove all documents for a project | +| `search(project, query, limit)` | Tokenize query as quoted prefix terms + FTS5 `MATCH`; returns snippets with `` tags | +| `searchAvailable()` | Returns `false` if the SQLite build lacks FTS5 | + +FTS5 fields: `project` (UNINDEXED), `type` (UNINDEXED), `doc_id` (UNINDEXED), `title`, `body`. + +Search degrades gracefully: if FTS5 is unavailable, `search()` returns empty results and no boot failure or error surface occurs. + +--- + +## Notifications + +`server/infrastructure/notify.ts`. + +Two sinks: + +1. **Slack** — `SLACK_WEBHOOK_URL` env var. +2. **Generic JSON webhook** — `NOTIFY_WEBHOOK_URL` (global config) or `notifyWebhookUrl` (per-project setting override). + +`notifyEvents` (comma-separated env var `NOTIFY_EVENTS`, or per-project override) controls which SSE event types trigger a notification. Known trigger event types: `task:done`, `task:error`, `completed`, `stopped`, `limit`. + +--- + +## Per-project settings + +Stored in `project_setting (project, key)`. Unset keys fall back to global config defaults. + +| Key | Type | Default | Description | +|---|---|---|---| +| `defaultModel` | string | `config.defaultModel` | Model for all tasks (unless per-task override) | +| `defaultAccount` | string | `config.defaultAccount` | Account alias for LRU selection | +| `commitAfter` | boolean | `config.commitAfterTask` | Commit after each successful task | +| `commitMessageMode` | `'taskname'` \| `'ai'` | `config.commitMessageMode` | Commit message strategy | +| `commitModel` | string | `config.commitModel` | Model for commit-message skill | +| `warmSession` | boolean | `config.warmSession` | Resume prior Claude session | +| `knowledgeAutoUpdate` | boolean | `config.knowledgeAutoUpdate` | Run knowledge-writer after task | +| `usageGateThreshold` | number (1–100) | `config.usageGateThreshold` | Usage percent threshold to pause runs | +| `pushAfter` | boolean | `false` | Push commit to origin after task | +| `branchPerRun` | boolean | `false` | Create a branch per run | +| `review` | boolean | `false` | Run reviewer pass after task | +| `openPr` | boolean | `false` | Open a pull request at run end | +| `notifyEvents` | string[] | `config.notifyEvents` | Which events trigger webhook | +| `notifyWebhookUrl` | string | `config.notifyWebhookUrl` | Per-project webhook override | + +`server/services/settings.ts` validates values on write: `defaultModel` must be in `config.modelCatalog`; `defaultAccount` must be a registered alias; `commitMessageMode` ∈ `{'taskname', 'ai'}`; `usageGateThreshold` ∈ [1, 100]; `notifyEvents` must be a subset of the `NOTIFY_EVENTS` constant. + +--- + +## GitHub allow-list and org gating + +Two optional guards in `server/data/auth.ts`, evaluated in `checkAllowlist(profile, token)` during the OAuth callback: + +**Login allow-list** — `config.allowedLogins` (env `ALLOWED_LOGINS`, comma-separated). If non-empty, the authenticating user's `login` must appear in the list. + +**Org membership** — `config.allowedOrg` (env `ALLOWED_ORG`). When set, the OAuth authorize URL requests the `'read:user read:org'` scope and the callback verifies the user is an active member of the named org via the GitHub API. + +A user that fails either guard is redirected to `/no-access`. A user that passes both proceeds to session minting. + +--- + +## API routes by area + +### Authentication + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/auth/github` | — | Redirect to GitHub OAuth authorize; set CSRF state cookie | +| GET | `/api/auth/github/callback` | — | Exchange code, verify allowlist/org, mint session cookie | +| POST | `/api/auth/logout` | any | Clear session cookie | + +### Current user + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/me` | any | Current user profile + role | + +### Projects + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects` | standard | List all projects | +| POST | `/api/projects` | standard | Create project (clone + provision workspace) | +| GET | `/api/projects/[p]` | standard | Project detail | +| DELETE | `/api/projects/[p]` | admin | Delete project + workspace | + +### Tasks + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects/[p]/tasks` | standard | List tasks with runtime status | +| POST | `/api/projects/[p]/tasks` | standard | Create task manually | +| GET / PATCH / DELETE | `/api/projects/[p]/tasks/[...id]` | standard | Read / update / delete single task | +| POST | `/api/projects/[p]/tasks/generate` | standard | Run task-creator agent | +| POST | `/api/projects/[p]/tasks/bulk` | standard | Bulk status change | +| POST | `/api/projects/[p]/tasks/archive` | standard | Archive completed tasks | +| POST | `/api/projects/[p]/tasks/reorder` | standard | Reorder task queue | +| POST | `/api/projects/[p]/tasks/reset-errors` | standard | Reopen errored tasks | +| POST | `/api/projects/[p]/tasks/feedback` | standard | Submit feedback on a task | +| POST | `/api/projects/[p]/tasks/import-issues` | standard | Import GitHub issues as tasks | + +### Run control + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects/[p]/status` | standard | Current engine state snapshot | +| POST | `/api/projects/[p]/run/start` | standard | Start a run | +| POST | `/api/projects/[p]/run/stop` | standard | Stop running agent (SIGTERM) | +| POST | `/api/projects/[p]/run/force` | admin | Force-stop (SIGKILL) | +| POST | `/api/projects/[p]/run/skip` | standard | Skip the current task | +| GET | `/api/projects/[p]/stream` | standard | SSE stream of run events | + +### Run history + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects/[p]/runs` | standard | List past runs (newest first, limit 50) | +| GET | `/api/projects/[p]/runs/[id]` | standard | Single run detail + event log | + +### Agent sessions + +| Method | Route | Auth | Description | +|---|---|---|---| +| POST | `/api/projects/[p]/agents/[agent]/start` | standard | Start an agent session (task-creator, knowledge-writer, etc.) | +| POST | `/api/projects/[p]/agents/[agent]/stop` | standard | Stop an agent session | +| GET | `/api/projects/[p]/agents/[agent]/prompts` | standard | Get saved prompts for agent | + +### Knowledge / Notes + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects/[p]/knowledge` | standard | List knowledge docs | +| GET / PUT / DELETE | `/api/projects/[p]/knowledge/[...id]` | standard | Read / write / delete a doc | +| GET | `/api/projects/[p]/notes` | standard | List notes | +| GET / PUT / DELETE | `/api/projects/[p]/notes/[...id]` | standard | Read / write / delete a note | + +### Schedule + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET / PUT | `/api/projects/[p]/schedule` | standard | Read / update project schedule | + +### Settings + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET / PUT | `/api/projects/[p]/settings` | standard | Read / update project settings | + +### Search + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects/[p]/search` | standard | Full-text search (tasks + knowledge + notes) | + +### Telemetry + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/projects/[p]/telemetry/summary` | standard | Per-project telemetry summary (30-day default) | +| GET | `/api/telemetry/global` | standard | Cross-project daily cost series (`?days=` 1–365) | + +### CLAUDE.md + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET / PUT | `/api/projects/[p]/claude-md` | standard | Read / write project CLAUDE.md | + +### Usage + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/usage` | standard | Latest cached usage snapshot (no spawn) | +| POST | `/api/usage` | standard | Refresh usage (spawns `claude /usage`); 502 on `UsageError` | + +### Users (admin) + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/users` | admin | List all users | +| GET / PATCH / DELETE | `/api/users/[githubId]` | admin | Read / update role / remove user | + +### Accounts (admin) + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/accounts` | admin | List accounts (non-secret view) | +| POST | `/api/accounts` | admin | Register account; returns optional `guidance` for OAuth | +| GET / PATCH / DELETE | `/api/accounts/[alias]` | admin | Read / update / remove account | +| POST | `/api/accounts/[alias]/verify` | admin | Run health-check probe | + +### Agent definitions (admin) + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/agent-defs` | standard | List custom agent kinds | +| POST | `/api/agent-defs` | admin | Create custom agent kind | +| GET / PATCH / DELETE | `/api/agent-defs/[kind]` | admin | Read / update / remove | + +### Prompt templates + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/prompt-templates` | standard | List shared prompt templates | +| POST | `/api/prompt-templates` | standard | Create template | +| GET / PATCH / DELETE | `/api/prompt-templates/[id]` | standard | Read / update / delete | + +### Skills + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/skills` | standard | List installed skills | +| GET | `/api/skills/[name]` | standard | Read a skill's SKILL.md content | + +### Audit (admin) + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/audit` | admin | Paginated audit log; filter by project / login / action prefix / `beforeId` cursor | + +### Backup (admin) + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/admin/backup/db` | admin | Stream full SQLite backup (`taskforge-YYYY-MM-DD.db`) | +| GET | `/api/admin/backup/project/[p]` | admin | Stream project workspace archive | + +### Health + +| Method | Route | Auth | Description | +|---|---|---|---| +| GET | `/api/health` | — | Liveness check (always 200) | +| GET | `/api/health/details` | admin | Readiness + config snapshot | + +--- + +## Task file format + +Tasks live in the project's `tasks/` directory. Filename: `-.md` (three-digit zero-padded prefix). + +```markdown +# Task title + +**Status:** open +**Severity:** medium +**Project:** api +**Model:** deep +**Account:** prod-account +**Depends:** 001, 003 + +## Problem +… + +## Task +… +``` + +Metadata lines parsed by `server/infrastructure/fs-workspace.ts`: + +| Key | Values | Meaning | +|---|---|---| +| `Status` | `open` \| `done` \| `error` | Engine-written on completion; must be `open` to be picked up | +| `Severity` | `low` \| `medium` \| `high` \| `critical` | Priority hint | +| `Project` | string | Sub-project facet label | +| `Model` | alias or full model ID | Overrides run-wide model for this task only | +| `Account` | alias | Overrides LRU account selection for this task only | +| `Depends` | `NNN[, NNN…]` | Task numbers that must be `done` before this task is eligible | + +Model aliases resolved in `server/infrastructure/agent.ts`: `fast` → haiku-class, `mid` → sonnet-class, `deep` → opus-class. + +--- + +## Environment variables + +All environment variables are read once in `server/config.ts` and exported as a typed config object. Nothing reads `process.env` outside this file. + +| Variable | Default | Description | +|---|---|---| +| `GITHUB_CLIENT_ID` | required | GitHub OAuth app client ID | +| `GITHUB_CLIENT_SECRET` | required | GitHub OAuth app secret | +| `OAUTH_REDIRECT_URI` | required | OAuth callback URL | +| `PUBLIC_ORIGIN` | required | App public origin (cookie domain) | +| `AUTH_SECRET` | required | HMAC signing key for session tokens | +| `SESSION_TTL` | `86400` | Session lifetime (seconds) | +| `ALLOWED_LOGINS` | — | Comma-separated GitHub logins allow-list | +| `ALLOWED_ORG` | — | GitHub org name for membership gate | +| `AGENT_BIN` | `claude` | Path or name of Claude Code CLI | +| `DEFAULT_ACCOUNT` | — | Default account alias | +| `DEFAULT_MODEL` | `claude-sonnet-4-6` | Default agent model | +| `MODEL_CATALOG` | (built-in list) | Comma-separated valid model names + aliases | +| `AGENT_EXTRA_ARGS` | — | Extra args appended to every `claude` spawn | +| `WORKSPACE_DIR` | `/workspace` | Root directory for all project workspaces | +| `DB_PATH` | under `WORKSPACE_DIR` | SQLite file path | +| `SKILLS_DIR` | — | Path to bundled skills directory | +| `LIMIT_AUTO_SLEEP` | `true` | Sleep on 429 instead of aborting | +| `LIMIT_SLEEP_BUFFER` | `60` | Extra seconds added to rate-limit cooldown | +| `LIMIT_SLEEP_FALLBACK` | `1800` | Default sleep when no reset time is available (seconds) | +| `LIMIT_SLEEP_MAX` | `21600` | Maximum sleep cap (seconds) | +| `LIMIT_MAX_RETRIES` | `5` | Max 429-triggered retries per task | +| `TRANSIENT_MAX_RETRIES` | `3` | Max non-429 error retries | +| `TRANSIENT_BASE_DELAY` | `10` | Base delay for exponential back-off (seconds) | +| `USAGE_GATE_ENABLED` | `true` | Enable usage gate | +| `USAGE_GATE_THRESHOLD` | `95` | Percent threshold to pause new runs | +| `USAGE_GATE_POLL_SECONDS` | `1800` | Background usage refresh cadence | +| `WARM_SESSION` | `false` | Enable warm session resume globally | +| `WARM_SESSION_MAX_AGE` | `14400` | Max session age before cold start (seconds) | +| `FORCE_KILL_GRACE_MS` | `5000` | SIGTERM → SIGKILL grace period | +| `GENERATE_TIMEOUT_MS` | `120000` | task-creator agent timeout | +| `KNOWLEDGE_TIMEOUT_MS` | `300000` | knowledge-writer / note-writer agent timeout | +| `REVIEW_TIMEOUT_MS` | `120000` | reviewer pass timeout | +| `KNOWLEDGE_AUTO_UPDATE` | `true` | Run knowledge-writer after each successful task | +| `COMMIT_AFTER_TASK` | `false` | Commit working tree after each successful task | +| `COMMIT_MESSAGE_MODE` | `taskname` | `'taskname'` or `'ai'` (commit-message skill) | +| `COMMIT_MODEL` | haiku-class | Model for commit-message skill | +| `GIT_AUTHOR_NAME` | — | Git author name for engine commits | +| `GIT_AUTHOR_EMAIL` | — | Git author email for engine commits | +| `GIT_REMOTE_TOKEN` | — | Token embedded in origin URL for credential-less push | +| `GIT_SSH_CONFIGURED` | — | Set to `1` if SSH push is pre-configured | +| `LOG_RETENTION_HOURS` | `168` | Run log-frame retention (1 week) | +| `SLACK_WEBHOOK_URL` | — | Slack notification sink | +| `NOTIFY_EVENTS` | — | Comma-separated event types that trigger webhooks | +| `NOTIFY_WEBHOOK_URL` | — | Generic JSON webhook sink | +| `REVIEW_MODEL` | haiku-class | Model for reviewer pass | + +--- + +## SSE event types + +`GET /api/projects/[p]/stream` emits a stream of NDJSON frames. The execution engine keeps a ring buffer of the latest 1 000 frames; agent sessions keep 500 frames. Latecomers receive a replay of the buffer on connect. + +| Type | Emitted by | Payload | +|---|---|---| +| `log` | Execution engine | Terminal output line | +| `task:begin` | Execution engine | `{ task }` | +| `task:done` | Execution engine | `{ task, elapsed, tokensIn, tokensOut, costUsd }` | +| `task:error` | Execution engine | `{ task, error }` | +| `commit` | Execution engine | `{ sha, message }` | +| `limit` | Execution engine | `{ account, resets? }` | +| `completed` | Execution engine | `{ done, total }` | +| `stopped` | Execution engine | — | +| `agent:log` | Agent session manager | Terminal output line | +| `agent:state` | Agent session manager | `{ status: 'idle' | 'running' }` | +| `agent:done` | Agent session manager | `{ kind }` | +| `knowledge` | Post-processing | `{ updated: string[] }` | +| `notes` | Post-processing | `{ updated: string[] }` | + +--- + +## Invariants + +- **One DB handle per process.** A single `DatabaseSync` instance owned by the `Database` singleton; nothing else calls `new DatabaseSync(...)`. +- **One execution engine per project.** `server/data/agent-sessions.ts` and the execution engine share a per-project lock; only one Claude process runs per project at any time. +- **No SQL in services.** Services call repositories; they never call `db.prepare()` directly. +- **API key values never persist.** Only `api_key_env` (the env-var name) is stored; the key is resolved from `process.env` at spawn time. +- **Audit entries are never deleted.** Project deletion cascades task/run/telemetry rows but preserves audit rows. +- **Review is advisory.** A `fail` verdict marks the task `needs-review` but never blocks commit or push. +- **Warm session age is checked before resume.** A session older than `warmSessionMaxAge` causes a cold start, not an error. +- **FTS5 is optional.** Search degrades to empty results if the SQLite build lacks the FTS5 extension; no boot failure. +- **`canPush()` gates push and PR creation.** Attempting to push without a token or SSH config configured will fail at the git layer; the engine surfaces the error as a task error. diff --git a/examples/public/web-components/SKILL.md b/examples/public/web-components/SKILL.md index 5727af7d..40362572 100644 --- a/examples/public/web-components/SKILL.md +++ b/examples/public/web-components/SKILL.md @@ -1,6 +1,6 @@ --- name: web-components -description: Use when building UI with @toolcase/web-components — framework-free HTML5 Web Components (`tc-*` custom elements) with from-scratch toolcase styling and a Bootstrap-compatible class API. Covers layout (BasicLayout, DashboardLayout, DashboardContent, DashboardSidebar, Login, Container, Row, Col, Spacer, Stack), content (ActionHeader, ActionItems, ActionRowList, Alert, AnnouncementBar, ApiReferenceTable, AssetRow, AssetRowList, Avatar, Badge, BadgeRow, Banner, Brand, Build, BriefCard, BundleBar, CdnMap, CalloutQuote, Changelog, ChartContainer, Sparkline, TrendIndicator, Leaderboard, LeaderboardTrend, CodeLabelCell, CodeSnippet, CodeWithOutput, CommunityLinks, ConfigPreview, ContributorWall, CookbookGrid, CoolButton, ActivityCard, BasicCard, Button, ButtonGroup, Card, Carousel, CloseButton, Collapse, Divider, Dropdown, DownloadStats, EcosystemMap, EmptyState, GameShowcaseCard, GithubStarsCard, GoodFirstIssues, Group, Hero, HeroStatsBar, Heading, Image, InfiniteScroll, Kbd, ListCard, ListGroup, LogoCloud, MaintainerCard, Marquee, MetricTile, MetricGrid, MigrationGuide, PageFooter, Panel, PhaseGrid, Pipeline, PinnedFeatureShowcase, PluginGrid, PricingCard, File, UserPanel, QueuedFile, Placeholder, Progress, PulseIndicator, ScoringRules, ScoreDisplay, SectionCard, SectionFlag, Skeleton, Spinner, SprintChain, Stamp, MetricCard, StatCard, StateMachine, StatusCard, StatusDot, Stepper, Tag, DataList, TierLadder, Timeline, UsageSummaryPanel, WelcomeGuide, CommandReference, Comparator, CompatibilityMatrix, CountdownTimer, FAQList, FeatureMatrix, Text, VersionLabel, VisuallyHidden), navigation (Breadcrumb, CoolNav, Nav, Navbar, Pagination, Scrollspy, SocialLinks, Stepper), overlays & feedback (ContextMenu, DebugOverlay, Modal, Offcanvas, Popover, Toast, Tooltip), and forms (CardOptions, Check, CheckboxGroup, Chip, ChipGroup, ColorPicker, IconPicker, DatePicker, EarlySignupForm, EditableText, FloatingLabel, Form, HelperText, Input, InputGroup, InputGroupText, Label, MultiCardSelect, NewsletterSignup, Option, Radio, RadioGroup, Range, RangeSlider, DeadzoneSlider, Select, Switch, Textarea). Consumable from any stack — React, Vue, Svelte, or plain HTML. +description: Use when building UI with @toolcase/web-components — framework-free HTML5 Web Components (`tc-*` custom elements) with from-scratch toolcase styling and a Bootstrap-compatible class API. Covers layout (BasicLayout, DashboardLayout, DashboardContent, DashboardSidebar, Login, Container, Row, Col, Spacer, Stack), content (Accordion, AccordionItem, ActionHeader, ActionItems, ActionRowList, Alert, AnnouncementBar, ApiReferenceTable, AssetRow, AssetRowList, Avatar, Badge, BadgeRow, Banner, Brand, Build, BriefCard, BundleBar, CdnMap, CalloutQuote, Changelog, ChartContainer, Sparkline, TrendIndicator, Leaderboard, LeaderboardTrend, CodeLabelCell, CodeSnippet, CodeWithOutput, CommunityLinks, ConfigPreview, ContributorWall, CookbookGrid, CoolButton, ActivityCard, BasicCard, Button, ButtonGroup, Card, Carousel, CloseButton, Collapse, Divider, Dropdown, DownloadStats, EcosystemMap, EmptyState, GameShowcaseCard, GithubStarsCard, GoodFirstIssues, Group, Hero, HeroStatsBar, Heading, Image, InfiniteScroll, Kbd, ListCard, ListGroup, LogoCloud, MaintainerCard, Marquee, MetricTile, MetricGrid, MigrationGuide, PageFooter, Panel, PhaseGrid, Pipeline, PinnedFeatureShowcase, PluginGrid, PricingCard, File, UserPanel, QueuedFile, Placeholder, Progress, PulseIndicator, ScoringRules, ScoreDisplay, SectionCard, SectionFlag, Skeleton, Spinner, SprintChain, Stamp, MetricCard, StatCard, StateMachine, StatusCard, StatusDot, Stepper, Tag, DataList, TierLadder, Timeline, UsageSummaryPanel, WelcomeGuide, CommandReference, Comparator, CompatibilityMatrix, CountdownTimer, FAQList, FeatureMatrix, Text, VersionLabel, VisuallyHidden), navigation (Breadcrumb, CoolNav, Nav, Navbar, Pagination, Scrollspy, SocialLinks, Stepper), overlays & feedback (ContextMenu, DebugOverlay, Modal, Offcanvas, Popover, Toast, Tooltip), and forms (CardOptions, Check, CheckboxGroup, Chip, ChipGroup, ColorPicker, IconPicker, DatePicker, EarlySignupForm, EditableText, FloatingLabel, Form, HelperText, Input, InputGroup, InputGroupText, Label, MultiCardSelect, NewsletterSignup, Option, Radio, RadioGroup, Range, RangeSlider, DeadzoneSlider, Select, Switch, Textarea). Consumable from any stack — React, Vue, Svelte, or plain HTML. --- # web-components — API Reference @@ -59,7 +59,10 @@ After `register()` you can author markup directly: - [tc-resizable-panel](#tc-resizable-panel) - [tc-scroll-area](#tc-scroll-area) - [tc-artboard-backdrop](#tc-artboard-backdrop) + - [tc-theme](#tc-theme) - [Content](#content) + - [tc-accordion](#tc-accordion) + - [tc-accordion-item](#tc-accordion-item) - [tc-action-header](#tc-action-header) - [tc-action-items](#tc-action-items) - [tc-action-row-list](#tc-action-row-list) @@ -1276,8 +1279,114 @@ None. `tc-artboard-backdrop` is a purely presentational surface. --- +### tc-theme + +Theming host element — the `--tc-*` token override container. Every `tc-*` component drives its cosmetics through `--bs--*` custom properties whose defaults resolve to the design-system `--tc-*` tokens (e.g. `--bs-panel-bg: var(--tc-surface)`). Wrapping a subtree in `` and re-pointing those tokens on it therefore re-skins **every nested component at once** — no per-component overrides needed. The default tokens are already applied globally at `:root`, so components are themed out of the box; reach for `tc-theme` only when you want a *subtree* to differ from the ambient skin. + +`tc-theme` is `display: contents` — it adds no layout box. Inherited properties (the `--tc-*` / `--bs-*` custom properties, plus `color` and `font-family`) pass straight through to descendants, but box properties (`background`, `padding`, `border`) will **not** paint because there is no box. For a themed backdrop, wrap the content in [`tc-artboard-backdrop`](#tc-artboard-backdrop) or set the background on your own container. + +Two ways to theme a subtree: + +1. **Named theme** via the `name` attribute — opt into a bundled skin. `default` is the product (slate) voice applied globally; `dungeon` (gilded fantasy) and `aurora` (dark "production-AI") are opt-in skins that stay inert until a `tc-theme` wrapper requests them. Each named skin is scoped under `tc-theme[name="…"]` (a plain wrapper carrying `[data-tc-theme="…"]` is matched too). The `dungeon` and `aurora` skins reference display fonts (Cinzel / EB Garamond for dungeon) that are **not** bundled — load them on the host page for the full look; both degrade to system serifs/sans. +2. **Ad-hoc token overrides** — set `--tc-*` (or the finer-grained `--bs--*`) custom properties directly on the `tc-theme` element via `style` or a class. Because the tokens inherit through the `display: contents` box, every descendant component picks them up. + +**Tag:** `tc-theme` + +**Attributes** + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `default\|dungeon\|aurora` | — | Selects a bundled named theme for the wrapped subtree. Absent → the subtree inherits the ambient (global `:root`) theme. Unrecognised values simply match no theme scope, so the subtree keeps the inherited skin. | + +**JS Properties** + +| Property | Type | Description | +|----------|------|-------------| +| `name` | `string` | Reflects the `name` attribute. Returns `''` when absent. Setting a truthy value writes the attribute; setting `''`/falsy removes it. | + +**Events** + +None. + +**Slots** + +| Slot | Description | +|------|-------------| +| *(default)* | The themed subtree. Rendered in place (the host is `display: contents`, so children participate in the parent's layout directly — no projection, no wrapper box). | + +**CSS Custom Properties** + +`tc-theme` defines no custom properties of its own — it is a passthrough host for the design-system tokens. Override any `--tc-*` token (or `--bs--*` contract variable) on the element to re-skin its descendants. Common roots: `--tc-surface`, `--tc-surface-muted`, `--tc-border`, `--tc-text`, `--tc-text-muted`, `--tc-accent`, `--tc-app-accent`, `--tc-success`/`--tc-info`/`--tc-warning`/`--tc-danger`, `--tc-font-sans`, `--tc-font-mono`. + +```html + + + + + Accept + + + + + + + Themed action + New + + + + +Default action +``` + +--- + ## Content +### tc-accordion + +Collapsible accordion container. Wrap `tc-accordion-item` children inside to build a group where only one item can be open at a time (unless `always-open` is set). + +**Attributes** + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `flush` | boolean | false | Removes borders and rounded corners so the accordion sits edge-to-edge with its parent | +| `always-open` | boolean | false | Allows multiple items to be expanded simultaneously | + +```html + + Body of the first item. + Body of the second item. + Body of the third item. + +``` + +--- + +### tc-accordion-item + +Single panel inside a `tc-accordion`. Renders a clickable header button and a collapsible body region; default slot content becomes the body. + +**Attributes** + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `header` | string | — | Text for the clickable header button | +| `open` | boolean | false | Expanded state of this item | + +**Events:** `tc-show`, `tc-shown`, `tc-hide`, `tc-hidden` + +```html + + +

Any body content goes here.

+
+
+``` + +--- + ### tc-action-header Flex header row with slotted title content on the left and a row of action buttons on the right. Dispatches a `tc-exec` custom event when an action button is clicked. diff --git a/examples/src/base/BPlusIndexDemo.tsx b/examples/src/base/BPlusIndexDemo.tsx new file mode 100644 index 00000000..6b47b9c6 --- /dev/null +++ b/examples/src/base/BPlusIndexDemo.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react' +import { BPlusIndex, MemoryAdapter } from '@toolcase/base' +import { captureConsoleAsync, DemoSection, type LogEntry } from './_demo/ConsoleDemo' + +const code = `const enc = new TextEncoder() +const dec = new TextDecoder() + +const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, + adapter: new MemoryAdapter(), + serializeValue: v => enc.encode(v), + deserializeValue: b => dec.decode(b), +}) + +// Insert / lookup / delete +await idx.set('banana', 'yellow fruit') +await idx.set('apple', 'red fruit') +await idx.set('cherry', 'red berry') + +console.log('has apple:', await idx.has('apple')) // true +console.log('get apple:', await idx.get('apple')) // 'red fruit' +console.log('has mango:', await idx.has('mango')) // false + +await idx.delete('banana') +console.log('has banana after delete:', await idx.has('banana')) // false + +// Batch ops +await idx.setMany([ + ['date', 'sweet fruit'], + ['elderberry', 'dark berry'], + ['fig', 'sweet fruit'], +]) +const batch = await idx.getMany(['fig', 'date', 'mango']) +console.log('getMany [fig, date, mango]:', batch) +// ['sweet fruit', 'sweet fruit', undefined] + +// Ordered queries +console.log('first:', await idx.first()) // ['apple', 'red fruit'] +console.log('last:', await idx.last()) // ['fig', 'sweet fruit'] + +// range scan c → e (inclusive) +const keys = [] +for await (const [k] of idx.range({ gte: 'c', lte: 'e' })) keys.push(k) +console.log('range c–e:', keys) // ['cherry', 'date', 'elderberry'] + +// rank (0-based position in sorted order) +console.log('rank of cherry:', await idx.rank('cherry')) // 1 + +// stats +const s = await idx.stats() +console.log('stats:', s)` + +export const BPlusIndexDemo = () => { + const [logs, setLogs] = useState([]) + const [running, setRunning] = useState(false) + + const run = async () => { + setRunning(true) + setLogs([]) + await captureConsoleAsync(async () => { + const enc = new TextEncoder() + const dec = new TextDecoder() + + const idx = await BPlusIndex.open({ + ...BPlusIndex.keyPreset.string, + adapter: new MemoryAdapter(), + serializeValue: v => enc.encode(v), + deserializeValue: b => dec.decode(b), + }) + + // Individual ops + await idx.set('banana', 'yellow fruit') + await idx.set('apple', 'red fruit') + await idx.set('cherry', 'red berry') + + console.log('has apple:', await idx.has('apple')) + console.log('get apple:', await idx.get('apple')) + console.log('has mango:', await idx.has('mango')) + + await idx.delete('banana') + console.log('has banana after delete:', await idx.has('banana')) + + // Batch ops + await idx.setMany([ + ['date', 'sweet fruit'], + ['elderberry', 'dark berry'], + ['fig', 'sweet fruit'], + ]) + const batch = await idx.getMany(['fig', 'date', 'mango']) + console.log('getMany [fig, date, mango]:', JSON.stringify(batch)) + + // Ordered queries + const first = await idx.first() + const last = await idx.last() + console.log('first:', JSON.stringify(first)) + console.log('last:', JSON.stringify(last)) + + // range scan c → e (inclusive) + const rangeKeys: string[] = [] + for await (const [k] of idx.range({ gte: 'c', lte: 'e' })) { + rangeKeys.push(k) + } + console.log('range c–e:', JSON.stringify(rangeKeys)) + + // rank (0-based count of entries strictly before the key) + const cherryRank = await idx.rank('cherry') + console.log('rank of cherry:', cherryRank) + + // stats + const s = await idx.stats() + console.log( + 'stats:', + `height=${s.height} pages=${s.pageCount} free=${s.freePages} fill=${s.fillFactor.toFixed(2)}` + ) + }, setLogs) + setRunning(false) + } + + return ( + + ) +} + +export default BPlusIndexDemo diff --git a/examples/src/base/index.tsx b/examples/src/base/index.tsx index 765517df..6dfeea20 100644 --- a/examples/src/base/index.tsx +++ b/examples/src/base/index.tsx @@ -38,6 +38,7 @@ import TokenBucketDemo from './TokenBucketDemo' import StopwatchDemo from './StopwatchDemo' import TickerDemo from './TickerDemo' import DiffPatchDemo from './DiffPatchDemo' +import BPlusIndexDemo from './BPlusIndexDemo' export type BaseCategory = | 'Events & State' @@ -81,6 +82,7 @@ export const baseExamples: BaseExampleDef[] = [ { key: 'multimap', label: 'MultiMap', category: 'Data Structures', element: }, { key: 'vec2', label: 'Vec2', category: 'Data Structures', element: }, { key: 'spatial', label: 'Spatial', category: 'Data Structures', element: }, + { key: 'bplus-index', label: 'BPlusIndex', category: 'Data Structures', element: }, { key: 'weighted-random', label: 'WeightedRandom', category: 'Data Structures', element: }, { key: 'random', label: 'Random', category: 'Data Structures', element: }, { key: 'dijkstra', label: 'Dijkstra', category: 'Data Structures', element: }, diff --git a/examples/src/node/EnvDemo.tsx b/examples/src/node/EnvDemo.tsx new file mode 100644 index 00000000..610f32a6 --- /dev/null +++ b/examples/src/node/EnvDemo.tsx @@ -0,0 +1,54 @@ +import { NodeDemoCard } from './_demo/NodeDemo' + +const code = `// Server-side only — env reads from globalThis.process.env and throws +// 'env works only with NodeJS' when process is undefined (e.g. the browser). + +import { env } from '@toolcase/node' + +// ── Define a typed config schema ───────────────────────────────────────────── +// env is overloaded: the return type is narrowed by defaultValue + type. +// Pass null as the default to opt into a nullable result. +const config = { + host: env('HOST', 'localhost'), // string — passthrough, default if unset + port: env('PORT', 3000, 'number'), // number — parseInt(v, 10) + debug: env('DEBUG', false, 'boolean'), // boolean — case-insensitive 'true' | 'false' + sentry: env('SENTRY_DSN'), // string | null — no default given + workers: env('WORKERS', null, 'number'), // number | null — nullable numeric + readonly: env('READ_ONLY', null, 'boolean'), // boolean | null — nullable boolean +} + +// Given this environment: +// HOST=api.internal PORT=8080 DEBUG=TRUE WORKERS=4 +// config resolves to: +// { host: 'api.internal', port: 8080, debug: true, +// sentry: null, workers: 4, readonly: null } + +// ── Coercion rules ─────────────────────────────────────────────────────────── +// 'number' — parseInt(v, 10), then re-checked: kept only if String(n) === v. +// 'boolean' — lowercased; 'true' → true, 'false' → false; anything else → default. +// 'string' — passed through unchanged; default used only when the var is unset. + +// ── Validation-failure case ────────────────────────────────────────────────── +// A non-integer numeric value fails the round-trip check (String(n) !== v), so +// env warns and falls back to the default rather than returning a partial parse. +// +// PORT=8080abc → parseInt → 8080, but String(8080) !== '8080abc' +// console.warn: [env] PORT="8080abc" is not a valid integer; using default +const port = env('PORT', 3000, 'number') // → 3000 (default; warning emitted) + +// Floats and leading zeros are rejected the same way — only clean integers pass. +// PORT=8080.5 → 3000 PORT=007 → 3000 +// +// Booleans never warn: an unrecognised value silently yields the default. +// DEBUG=yes → false (default) +` + +export const EnvDemo = () => ( + +) + +export default EnvDemo diff --git a/examples/src/node/ImagingDemo.tsx b/examples/src/node/ImagingDemo.tsx new file mode 100644 index 00000000..a3661dc4 --- /dev/null +++ b/examples/src/node/ImagingDemo.tsx @@ -0,0 +1,100 @@ +import { NodeDemoCard } from './_demo/NodeDemo' + +const processorCode = `// Everything below runs on the server — requires Node.js + the optional 'sharp' peer. +// sharp is loaded lazily on the first terminal call (metadata / toBuffer / toFile). + +import { ImageProcessor, ImageProcessorError } from '@toolcase/node' + +// ── 1. Inspect — metadata() is a terminal call that decodes the header only ── +const meta = await ImageProcessor.fromPath('./uploads/avatar.png').metadata() +// → { format: 'png', width: 1024, height: 1024, channels: 4, hasAlpha: true, size: 248_513 } + +// ── 2. Transform — resize / crop queue ops on an immutable ImageProcessor ──── +// Each builder method returns a NEW instance, so a base pipeline forks safely. +const base = ImageProcessor.fromPath('./uploads/avatar.png') + .crop({ left: 112, top: 112, width: 800, height: 800 }) // square out the centre + .resize({ width: 256, height: 256, fit: 'cover', withoutEnlargement: true }) + +// ── 3. Encode + optimise — fork the base into two output formats ───────────── +// format() picks the encoder; optimize() tunes it (mozjpeg / palette / effort) +// and strips metadata by default. The sharp pipeline is rebuilt fresh per chain. +const webp = await base + .format({ format: 'webp', quality: 80 }) + .optimize({ format: 'webp', quality: 80, effort: 6, stripMetadata: true }) + .toFile('./public/avatar.webp') +// → { format: 'webp', width: 256, height: 256, channels: 4, hasAlpha: true, size: 9_204 } + +const avif: Buffer = await base + .format({ format: 'avif', quality: 50 }) + .optimize({ format: 'avif', quality: 50 }) + .toBuffer() + +// ── 4. Errors funnel through ImageProcessorError ───────────────────────────── +// invalid-buffer / invalid-path / crop-invalid throw eagerly from the builder; +// decode-failed / crop-out-of-bounds / metadata-failed / encode-failed / +// write-failed surface lazily on the terminal call (with the offending path). +try { + await ImageProcessor.fromPath('./uploads/avatar.png') + .crop({ left: 9000, top: 9000, width: 64, height: 64 }) // outside the image + .toBuffer() +} catch (error) { + if (error instanceof ImageProcessorError) { + console.error(error.reason, error.path) // 'crop-out-of-bounds' './uploads/avatar.png' + } +}` + +const atlasCode = `// Server-side only — sharp + node:fs. Composes the Packer from @toolcase/base +// with sharp compositing to produce atlas page images plus a JSON manifest. + +import { AtlasBuilder, type AtlasResult } from '@toolcase/node' + +const result: AtlasResult = await new AtlasBuilder({ + output: { directory: './dist/atlases', baseName: 'characters', format: 'webp', quality: 80 }, + packer: { algorithm: 'max-rects', allowRotation: true, padding: 2, pot: 'page' }, + optimize: true, // run ImageProcessor.optimize on each composed page + useAlphaTrimming: true, // trim transparent borders before packing (default) + continueOnError: true, // skip undecodable inputs → result.failures (don't throw) +}).build([ + { id: 'hero-idle-0', path: './sprites/hero-idle-0.png' }, + { id: 'hero-idle-1', path: './sprites/hero-idle-1.png' }, + { id: 'hero-run-0', path: './sprites/hero-run-0.png' }, + { id: 'hero-run-1', path: './sprites/hero-run-1.png' }, +]) + +// One page (small set), packed into a single power-of-two atlas image. +const page = result.pages[0] +console.log(page.file) // absolute path → .../characters.webp +console.log(page.width, page.height) // e.g. 256 128 +console.log(page.occupancy) // packing efficiency, 0..1 + +// Per-sprite frames carry the atlas rect + original source geometry (offset from +// alpha trimming), so a loader can re-expand each frame to its untrimmed size. +for (const frame of page.frames) { + console.log(frame.id, frame.rect, 'trim:', frame.source.offsetX, frame.source.offsetY) + // hero-idle-0 { x: 2, y: 2, width: 60, height: 92 } trim: 2 2 +} + +// Sprites the packer couldn't place (over budget / too large) are reported, not dropped. +for (const u of result.unpacked) console.warn('unpacked:', u.id, u.width, u.height) + +// Inputs that failed to decode when continueOnError is set (e.g. a corrupt file). +for (const f of result.failures) console.warn('failed:', f.input.id, f.stage, f.error.message) + +console.log(result.manifestPath) // absolute path → .../characters.json (null if writeManifest:false)` + +export const ImagingDemo = () => ( +
+ + +
+) + +export default ImagingDemo diff --git a/examples/src/node/KVServiceDemo.tsx b/examples/src/node/KVServiceDemo.tsx index 22f176f0..dfc7f94e 100644 --- a/examples/src/node/KVServiceDemo.tsx +++ b/examples/src/node/KVServiceDemo.tsx @@ -1,8 +1,47 @@ -import { NodeDemoCard } from './_demo/NodeDemo' +import { useState } from 'react' +// KeyBuilder / KV_KEY_SEPARATOR are pure (no Redis driver, no node:crypto), so the +// examples site can import and run them. They live on the Node-only `@toolcase/node` +// entry, which the site aliases to `main.iso.ts` — reach the real source directly. +import { KeyBuilder, KV_KEY_SEPARATOR } from '../../../node/src/kv/keys' +import { captureLogs, NodeDemoCard, type LogEntry } from './_demo/NodeDemo' -const code = `import { createClient } from 'redis' +// ── Runnable: the real KeyBuilder primitive that KVService composes every key with ── + +const keyBuilderCode = `import { KeyBuilder, KV_KEY_SEPARATOR } from '@toolcase/node' + +// KVService routes every Redis key through a KeyBuilder. It is pure (no driver), +// so the real class can run here unchanged — this is the exact composition logic +// behind kv.set(...), kv.scoped(...), and pub/sub channel stripping. +const keys = new KeyBuilder('app:prod', KV_KEY_SEPARATOR) + +keys.build('session', 'alice') // 'app:prod:session:alice' +keys.build('rate', 'login', 42) // 'app:prod:rate:login:42' + +// A separator inside a single part is percent-escaped so it can't split the key: +keys.build('leaderboard:weekly') // 'app:prod:leaderboard%3Aweekly' + +// scope() returns a child builder — this is exactly what kv.scoped('tenant-42') does: +const tenant = keys.scope('tenant-42') +tenant.build('player', 'alice') // 'app:prod:tenant-42:player:alice' + +// stripNamespace() reverses build() — used when pub/sub channels arrive prefixed: +keys.stripNamespace('app:prod:players.scored') // 'players.scored'` + +// ── Annotated only: the rest of the KV surface needs Node + a live Redis driver ── + +const serviceCode = `// Everything below needs a live Redis connection (node-redis v5+) and runs only on +// the server, so it is shown as a reference rather than executed here. + +import { createClient } from 'redis' import Serializer from '@toolcase/serializer' -import { KVService } from '@toolcase/node' +import { + KVService, + // The standalone primitives KVService wires together (all driver-backed): + Locker, RateLimiter, Leaderboard, ValueStore, Versioned, SubscriberPool, + // Lua machinery — LuaScript hashes its source with node:crypto (SHA1) and + // dispatches via EVALSHA/EVAL; KV_LUA_SCRIPTS holds the raw script sources. + LuaScript, LuaScriptCache, KV_LUA_SCRIPTS, +} from '@toolcase/node' const client = await createClient({ url: 'redis://localhost:6379' }).connect() @@ -21,43 +60,72 @@ const kv = new KVService({ await kv.warmScripts() -// Distributed lock (auto-released, optional retry) +// Distributed lock (Locker) — auto-released, optional retry await kv.withLock('order:42', 5_000, async () => { await processOrder(42) }) -// Sliding-window rate limit +// Sliding-window rate limit (RateLimiter; also fixedWindow / tokenBucket) const rl = await kv.slidingWindow('login:1.2.3.4', 5, 60_000) if (!rl.allowed) throw new RateLimitedError('login', rl.resetInSeconds) -// Leaderboard +// Leaderboard (sorted sets) await kv.addScore('weekly', 'alice', 1200) const top = await kv.topN('weekly', 10) -// Typed value store (uses Serializer for binary encode) +// Typed value store (ValueStore — uses Serializer for binary encode/decode) await kv.setValue('Player', 'p:alice', { id: 'alice', score: 1200 }) const player = await kv.getValue<{ id: string; score: number }>('Player', 'p:alice') -// Optimistic-versioned set +// Optimistic-versioned set (Versioned — CAS on a version field) await kv.versionedSetValue('Player', 'p:alice', /* expectedVersion */ 0, { id: 'alice', score: 1300 }) -// Pub/sub +// Pub/sub (SubscriberPool manages the dedicated subscriber connection) const sub = await kv.subscribeValue<{ id: string; score: number }>( 'Player', 'players.scored', async (msg) => track(msg)) // later: await sub.close() -// Scoped child for a tenant -const tenant = kv.scoped('t:42') // keys become app:prod:t:42:* +// Scoped child for a tenant — keys become app:prod:tenant-42:* +const tenant = kv.scoped('tenant-42') await kv.close()` -export const KVServiceDemo = () => ( - -) +export const KVServiceDemo = () => { + const [logs, setLogs] = useState([]) + const run = () => setLogs(captureLogs(() => { + const keys = new KeyBuilder('app:prod', KV_KEY_SEPARATOR) + const show = (expr: string, value: string) => console.log(expr, '->', value) + + show("build('session', 'alice')", keys.build('session', 'alice')) + show("build('rate', 'login', 42)", keys.build('rate', 'login', 42)) + show("build('leaderboard:weekly') // separator escaped", keys.build('leaderboard:weekly')) + + const tenant = keys.scope('tenant-42') + show("scope('tenant-42').build('player', 'alice')", tenant.build('player', 'alice')) + + show( + "stripNamespace('app:prod:players.scored')", + keys.stripNamespace('app:prod:players.scored'), + ) + })) + + return ( +
+ + +
+ ) +} export default KVServiceDemo diff --git a/examples/src/node/NodeStoreDemo.tsx b/examples/src/node/NodeStoreDemo.tsx new file mode 100644 index 00000000..7998bff6 --- /dev/null +++ b/examples/src/node/NodeStoreDemo.tsx @@ -0,0 +1,71 @@ +import { NodeDemoCard } from './_demo/NodeDemo' + +const code = `import { BPlusIndex } from '@toolcase/base' +import { + NodeStore, + BlockStore, + serializeBlockRef, + deserializeBlockRef, + BLOCK_REF_SIZE, +} from '@toolcase/node' + +// ── NodeStore: put / get a record ─────────────────────────────────────────── + +type User = { id: string; name: string; score: number } + +const enc = new TextEncoder() +const dec = new TextDecoder() + +const store = await NodeStore.open({ + path: '/data/users', + ...BPlusIndex.keyPreset.string, // compare + serializeKey + deserializeKey + serializeValue: v => enc.encode(JSON.stringify(v)), + deserializeValue: b => JSON.parse(dec.decode(b)) as User, +}) + +await store.set('alice', { id: 'alice', name: 'Alice', score: 1_200 }) +await store.set('bob', { id: 'bob', name: 'Bob', score: 800 }) + +const alice = await store.get('alice') // { id: 'alice', name: 'Alice', score: 1200 } +const size = store.size // 2 + +// Range scan — results in B+ tree key order +for await (const [key, user] of store.range({ gte: 'a', lte: 'z' })) { + console.log(key, user.score) +} + +// Compaction: reclaim dead heap blocks left by deletes / overwrites +console.log('live ratio:', store.liveRatio) // 1.0 when no dead blocks +await store.optimize() + +await store.close() + +// ── BlockRef: serialize / deserialize round-trip ──────────────────────────── +// BLOCK_REF_SIZE === 12 (segment u32 + offset u32 + length u32) + +const ref = { segment: 0, offset: 128, length: 64 } +const buf = serializeBlockRef(ref) // Uint8Array(12) +const back = deserializeBlockRef(buf) // { segment: 0, offset: 128, length: 64 } + +console.log(buf.byteLength === BLOCK_REF_SIZE) // true +console.log(JSON.stringify(back) === JSON.stringify(ref)) // true + +// ── BlockStore: direct heap access (advanced) ─────────────────────────────── + +const bs = new BlockStore('/data/raw') +await bs.scanSegments() // discover existing .dat.N files +bs.setActive(0, 0) // designate segment 0 as the write target +const written = await bs.append(enc.encode('hello')) // → BlockRef +await bs.fsyncActive() +const raw = await bs.read(written) // Uint8Array containing 'hello' +await bs.close()` + +export const NodeStoreDemo = () => ( + +) + +export default NodeStoreDemo diff --git a/examples/src/node/OAuth2Demo.tsx b/examples/src/node/OAuth2Demo.tsx new file mode 100644 index 00000000..660de8ab --- /dev/null +++ b/examples/src/node/OAuth2Demo.tsx @@ -0,0 +1,190 @@ +import { useState } from 'react' +import { + defineOAuth2Provider, + buildAuthorizeURL, + parseGitHubProfile, + parseStandardOIDCProfile, +} from '@toolcase/node' +import { captureLogs, NodeDemoCard, type LogEntry } from './_demo/NodeDemo' + +const providerCode = `import { defineOAuth2Provider, buildAuthorizeURL } from '@toolcase/node' + +// Define a static provider once at app startup. +// For OIDC providers use oidcProvider() instead — it auto-fills +// endpoints from the discovery document (server-side; needs network). +const github = defineOAuth2Provider({ + clientId: 'Iv1.abc123', + clientSecret: process.env.GITHUB_CLIENT_SECRET, + authorizationEndpoint: 'https://github.com/login/oauth/authorize', + tokenEndpoint: 'https://github.com/login/oauth/access_token', + defaultScope: ['read:user', 'user:email'], +}) + +// Build the authorization redirect URL. +// state + codeVerifier are generated server-side via generateState() / generatePKCE() +// and stored in session/KV before the redirect. +const url = buildAuthorizeURL(github, { + state: 'csrf-token-abc', + redirectUri: 'https://app.example.com/auth/callback', + scope: ['read:user', 'user:email'], +}) +// → https://github.com/login/oauth/authorize?response_type=code&client_id=Iv1.abc123&...` + +const profileCode = `import { parseGitHubProfile, parseStandardOIDCProfile } from '@toolcase/node' + +// GitHub: two-call API — /user + /user/emails. +// Parser selects the verified primary email and maps to the common OAuth2Profile shape. +const ghProfile = parseGitHubProfile({ + user: { id: 1234567, login: 'alice', name: 'Alice Dev', + avatar_url: 'https://avatars.githubusercontent.com/u/1234567' }, + emails: [ + { email: 'alice@personal.com', primary: false, verified: true }, + { email: 'alice@company.com', primary: true, verified: true }, + ], +}) +// → { subject: '1234567', email: 'alice@company.com', emailVerified: true, ... } + +// Standard OIDC: merge verified ID-token claims over userinfo. +// ID-token claims win for security-sensitive fields (sub, email, email_verified) +// because they are cryptographically signed; userinfo is not. +const oidcProfile = parseStandardOIDCProfile({ + idTokenClaims: { sub: 'user-uuid', email: 'alice@idp.example', email_verified: true, name: 'Alice' }, + userinfo: { picture: 'https://photos.example/alice.jpg', groups: ['admin', 'dev'] }, +}) +// → { subject: 'user-uuid', email: 'alice@idp.example', emailVerified: true, +// name: 'Alice', avatarUrl: 'https://...', groups: ['admin', 'dev'], ... }` + +const serverCode = `// Everything below runs on the server — requires Node.js + network. + +import { + oidcProvider, generateState, generatePKCE, + buildAuthorizeURL, verifyCallback, + exchangeCode, refreshToken, verifyIdToken, + type OAuth2Tokens, +} from '@toolcase/node' + +// ── 1. Boot ────────────────────────────────────────────────────────────── +// oidcProvider() hits the OIDC discovery document once and auto-fills +// authorizationEndpoint, tokenEndpoint, jwksUri, issuer, etc. +const google = await oidcProvider({ + issuer: 'https://accounts.google.com', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + defaultScope: ['openid', 'email', 'profile'], +}) + +// ── 2. Login initiation handler ─────────────────────────────────────────── +// Generate state + PKCE pair; persist them in session/KV before redirecting. +const state = generateState() // crypto-random, base64url +const { codeVerifier, codeChallenge } = generatePKCE() // S256 by default +const redirectUri = 'https://app.example.com/auth/callback' +await kv.set(\`oauth:\${state}\`, { state, codeVerifier, redirectUri }, { ttlMs: 10 * 60_000 }) + +reply.redirect(buildAuthorizeURL(google, { state, redirectUri, codeChallenge })) + +// ── 3. Callback handler (/auth/callback?code=…&state=…) ────────────────── +const ctx = await kv.get(\`oauth:\${req.query.state}\`) + +// Constant-time CSRF check — throws OAuth2CallbackError on mismatch. +verifyCallback({ stored: ctx.state, received: req.query.state }) + +const tokens: OAuth2Tokens = await exchangeCode(google, { + code: req.query.code, + redirectUri: ctx.redirectUri, + codeVerifier: ctx.codeVerifier, // proves this server initiated the flow +}) + +// ── 4. Verify the OIDC ID token (JWKS fetched + cached automatically) ──── +const verified = await verifyIdToken(tokens.idToken!, { + issuer: google.issuer!, + audience: google.clientId, + jwksUri: google.jwksUri!, +}) +// verified.payload → { sub, email, email_verified, iat, exp, ... } + +// ── 5. Refresh when the access token expires ────────────────────────────── +const refreshed = await refreshToken(google, { refreshToken: tokens.refreshToken! })` + +export const OAuth2Demo = () => { + const [providerLogs, setProviderLogs] = useState([]) + const [profileLogs, setProfileLogs] = useState([]) + + const runProvider = () => setProviderLogs(captureLogs(() => { + const github = defineOAuth2Provider({ + clientId: 'Iv1.abc123', + clientSecret: 'secret', + authorizationEndpoint: 'https://github.com/login/oauth/authorize', + tokenEndpoint: 'https://github.com/login/oauth/access_token', + defaultScope: ['read:user', 'user:email'], + }) + console.log('provider.clientId:', github.clientId) + console.log('provider.clientAuthMethod:', github.clientAuthMethod) + console.log('provider.pkceMethod:', github.pkceMethod) + + const url = buildAuthorizeURL(github, { + state: 'csrf-token-abc', + redirectUri: 'https://app.example.com/auth/callback', + scope: ['read:user', 'user:email'], + }) + console.log('authorize URL ->', url) + + const params = Object.fromEntries(new URL(url).searchParams) + console.log('params ->', params) + })) + + const runProfiles = () => setProfileLogs(captureLogs(() => { + const ghProfile = parseGitHubProfile({ + user: { + id: 1234567, + login: 'alice', + name: 'Alice Dev', + avatar_url: 'https://avatars.githubusercontent.com/u/1234567', + }, + emails: [ + { email: 'alice@personal.com', primary: false, verified: true }, + { email: 'alice@company.com', primary: true, verified: true }, + ], + }) + console.log('GitHub profile ->', ghProfile) + + const oidcProfile = parseStandardOIDCProfile({ + idTokenClaims: { + sub: 'user-uuid', + email: 'alice@idp.example', + email_verified: true, + name: 'Alice', + }, + userinfo: { + picture: 'https://photos.example/alice.jpg', + groups: ['admin', 'dev'], + }, + }) + console.log('OIDC profile ->', oidcProfile) + })) + + return ( +
+ + + +
+ ) +} + +export default OAuth2Demo diff --git a/examples/src/node/index.tsx b/examples/src/node/index.tsx index b2aef457..f2d3e2c2 100644 --- a/examples/src/node/index.tsx +++ b/examples/src/node/index.tsx @@ -6,6 +6,10 @@ import ErrorsDemo from './ErrorsDemo' import RepositoryDemo from './RepositoryDemo' import EndpointDemo from './EndpointDemo' import KVServiceDemo from './KVServiceDemo' +import NodeStoreDemo from './NodeStoreDemo' +import OAuth2Demo from './OAuth2Demo' +import ImagingDemo from './ImagingDemo' +import EnvDemo from './EnvDemo' export type NodeExampleDef = { key: string @@ -21,4 +25,8 @@ export const nodeExamples: NodeExampleDef[] = [ { key: 'repository', label: 'Repository', element: }, { key: 'endpoint', label: 'Endpoint', element: }, { key: 'kv', label: 'KVService', element: }, + { key: 'node-store', label: 'NodeStore', element: }, + { key: 'oauth2', label: 'OAuth2 / OIDC', element: }, + { key: 'imaging', label: 'Imaging', element: }, + { key: 'env', label: 'env (typed loader)', element: }, ] diff --git a/examples/src/pages/NginxPilotPage.tsx b/examples/src/pages/NginxPilotPage.tsx index ee263dd9..4bb8caea 100644 --- a/examples/src/pages/NginxPilotPage.tsx +++ b/examples/src/pages/NginxPilotPage.tsx @@ -11,30 +11,122 @@ const configExample = `# /etc/nginxpilot/config.yml — globals + includes data_dir: /var/lib/nginxpilot admin: listen: 127.0.0.1:9090 # health + status + manual sync trigger + # token_env: NGINXPILOT_TOKEN # bearer auth on all admin routes; omit to disable + defaults: interval: 5m keep_releases: 3 + include: - - sites.d/*.yml # drop a file in, kill -HUP — that's onboarding + - sites.d/*.yml # fragments may contain only sites: lists + # duplicate domains across files are a validation error + # drop a file in, kill -HUP — that's onboarding` -# /etc/nginxpilot/sites.d/example.com.yml +const gitSourceExample = `# /etc/nginxpilot/sites.d/example.com.yml sites: - domain: example.com source: - type: git # or: http-zip (CI artifact URL) + type: git url: git@github.com:acme/example-site.git - branch: gh-pages + branch: main + interval: 2m # min 30s; defaults to defaults.interval + auth: + method: ssh-key # ssh-key | https-token | none + key_file: /etc/nginxpilot/keys/example_ed25519 + # known_hosts: /etc/nginxpilot/known_hosts # strict check; default: accept-new (TOFU) + subdir: dist/ # serve only this subtree of the repo + require_file: [index.html] # post-fetch gate: reject release if file is absent + exclude: ["*.map"] # extends defaults strip list: + # .env*, .htaccess, .DS_Store (.git* always stripped)` + +const httpZipSourceExample = `sites: + - domain: blog.example.com + source: + type: http-zip + url: https://ci.example.com/artifacts/blog/latest.zip # https required + # allow_insecure: true # permit http:// URLs (not recommended) + interval: 10m auth: - method: ssh-key - key_file: /etc/nginxpilot/keys/example_ed25519` + method: bearer # bearer | basic | header | none + token_env: BLOG_ARTIFACT_TOKEN + checksum_url: https://ci.example.com/artifacts/blog/latest.zip.sha256 # optional + # strip_components: 1 # explicit; a single shared root dir is auto-stripped + limits: # zip-bomb guards (these are the defaults) + max_archive_size: 512MiB + max_uncompressed_size: 2GiB + max_entries: 100000 + max_compression_ratio: 100 + +# Downloads use conditional GET (ETag / Last-Modified) — unchanged content is a cheap no-op. +# Extraction rejects zip-slip paths and symlinks outright.` -const cliReference = `nginxpilot run # the daemon (default) +const secretsExample = `# Inline secrets are a PARSE-TIME ERROR — only _env / _file refs are accepted. +# Config files stay safe to commit. +auth: + method: https-token + token_env: MY_TOKEN # reads $MY_TOKEN at runtime + # token_file: /run/secrets/my_token # or point at a file + +# Secret files must be 0600 or 0640 and owned by the daemon user or root. +# The daemon refuses to start if permissions are wrong. + +# systemd LoadCredential integration: +# [Service] +# LoadCredential=my_token:/etc/nginxpilot/tokens/my_token +# then in config: +# token_file: /run/credentials/nginxpilot.service/my_token` + +const cliReference = `nginxpilot run [--config PATH] [--log-format logfmt|json] [--prune-orphans] + # the daemon; --prune-orphans deletes content dirs for sites + # that were removed from config (orphans are warned, not deleted, by default) nginxpilot validate # parse + validate merged config, CI exit codes nginxpilot sync # one-shot sync, no daemon needed (onboarding) nginxpilot print-vhost # nginx server-block starting snippet nginxpilot status [--json] # per-site table from the daemon nginxpilot version # build info` +const adminEndpoints = `GET /healthz — liveness probe +GET /status — per-site JSON: deployed ref, last success/error, + failure_streak, never_synced, next sync time +POST /sync/ — force an immediate out-of-schedule sync + +# admin.token_env enables bearer auth on all three routes: +# admin: +# listen: 127.0.0.1:9090 +# token_env: NGINXPILOT_TOKEN + +curl -H "Authorization: Bearer $NGINXPILOT_TOKEN" http://127.0.0.1:9090/status +curl -X POST -H "Authorization: Bearer $NGINXPILOT_TOKEN" http://127.0.0.1:9090/sync/example.com` + +const signalsSnippet = `SIGHUP — diff-based reload + added sites: start and sync immediately + removed sites: stop the watcher; content stays on disk (orphan, warned) + remove orphaned content by restarting with --prune-orphans + invalid config: rejected wholesale; running config stays active + +SIGTERM / SIGINT — graceful shutdown + in-flight symlink swaps finish; downloads abort cleanly` + +const releasesSnippet = `data_dir/sites// + releases/ + 20240601T120000-abc1234/ # - + 20240601T120512-def5678/ + 20240602T080000-ghi9012/ # newest successful sync + current -> releases/20240602T080000-ghi9012 # atomic rename(2) swap + +# keep_releases: 3 (default) — oldest release dirs pruned after each sync +# Manual rollback: point current at an older release directory (no restart needed)` + +const backoffSnippet = `# Retry formula: interval × 2^streak — capped at 4× interval +# streak resets to 0 on success; visible in GET /status as failure_streak +# current symlink is never overwritten on failure — last good content stays live +# +# Example with defaults.interval: 5m +# streak 0 → next retry in 5m +# streak 1 → next retry in 10m +# streak 2 → next retry in 20m +# streak 3+ → retry in 20m (4 × 5m cap)` + const nginxSnippet = `server { listen 80; server_name example.com; @@ -48,6 +140,23 @@ const nginxSnippet = `server { } }` +const systemdSnippet = `# packaging/nginxpilot.service (ships in the repo) +[Service] +Type=notify +Restart=on-failure +User=nginxpilot +UMask=0027 +ProtectSystem=strict +NoNewPrivileges=true +PrivateTmp=true +# Run as a dedicated nginxpilot user owning data_dir; +# the nginx worker user joins the nginxpilot group. +# Dirs 0750, files 0640 (enforced by umask 027). + +# SELinux — RHEL-family only: +semanage fcontext -a -t httpd_sys_content_t '/var/lib/nginxpilot/sites(/.*)?' \\ + && restorecon -R /var/lib/nginxpilot/sites` + const features = [ { title: 'Atomic deploys', @@ -132,22 +241,70 @@ export const NginxPilotPage = () => {

Configuration

- declarative YAML + sites.d/ fragments + declarative YAML · strict unknown-key errors · sites.d/ fragments
+
+

git source

+ shallow single-branch · subdir · require_file · TOFU or known_hosts +
+ + +
+

http-zip source

+ conditional GET · checksum · zip-bomb limits · bearer / basic / header auth +
+ + +
+

Secrets

+ _env / _file refs only · 0600/0640 enforcement · systemd LoadCredential +
+ +

CLI

single binary
+
+

Admin endpoint

+ loopback HTTP · /healthz · /status · /sync/<domain> +
+ + +
+

Signals

+ SIGHUP diff-reload · SIGTERM/SIGINT graceful shutdown +
+ + +
+

Release retention

+ keep_releases · timestamped dirs · atomic current symlink +
+ + +
+

Failure & backoff

+ interval × 2^streak · capped at 4× · streak visible in /status +
+ +

nginx integration

the daemon never touches nginx config
+
+

systemd & SELinux

+ Type=notify · hardening · 0750/0640 umask · httpd_sys_content_t +
+ +

Run it

binary, systemd or Docker diff --git a/examples/src/pages/NodePage.tsx b/examples/src/pages/NodePage.tsx index 6b10b2c7..7a951ea2 100644 --- a/examples/src/pages/NodePage.tsx +++ b/examples/src/pages/NodePage.tsx @@ -13,10 +13,13 @@ npm install redis @toolcase/serializer` const subpathTable = `// One entrypoint — peers are optional, install only what you use import { - createAPISanitizer, NotFoundError, ValidationError, // utils + errors + createSanitizer, NotFoundError, ValidationError, // utils + errors RouteHandler, HttpServer, // peers: fastify, @fastify/cors BaseRepository, EntityService, // peer: pg (or any SQL executor) KVService, // peers: redis, @toolcase/serializer + OAuth2Flow, verifyIdToken, // OAuth2 / OIDC + ImageProcessor, AtlasBuilder, // imaging: sharp + NodeStore, BlockStore, // persistent store } from '@toolcase/node'` export const NodePage = () => { @@ -29,10 +32,10 @@ export const NodePage = () => { name: 'node', eyebrow: 'Library · Backend · Node', tagline: - 'Backend helpers — Fastify endpoints, raw-SQL repositories, Redis KV service, isomorphic sanitize / pagination / domain-error helpers. One entrypoint, peers are optional so you only pay for what you import.', + 'Backend helpers — Fastify endpoints, raw-SQL repositories, Redis KV service, OAuth2/OIDC, image processing, persistent store, and isomorphic sanitize / pagination / domain-error helpers. One entrypoint, peers are optional so you only pay for what you import.', version: versions.node, examples: nodeExamples.length, - chips: ['Fastify', 'Postgres', 'Redis', 'ESM + CJS'], + chips: ['Fastify', 'Postgres', 'Redis', 'OAuth2 / OIDC', 'Imaging', 'Store', 'ESM + CJS'], }} /> diff --git a/examples/src/pages/TaskForgePage.tsx b/examples/src/pages/TaskForgePage.tsx index 73394916..d57381f9 100644 --- a/examples/src/pages/TaskForgePage.tsx +++ b/examples/src/pages/TaskForgePage.tsx @@ -1,62 +1,132 @@ import { Link as RouterLink } from 'react-router' import { CodeBlock, CopyLine } from './_chrome' -const archDiagram = `GitHub OAuth ──► TaskForge (Next.js, single process) +const archDiagram = `GitHub OAuth ──► TaskForge (Next.js 14, single process) │ - │ per-project queue of *.md task files + │ per-project queue of *.md task files ▼ - execution engine ──spawns──► claude CLI ──edits──► repo/ (git) - │ │ - │ live stdout commit · push - ▼ │ - SSE stream ──────────────────► browser UI` + execution engine ──spawns──► claude CLI ──edits──► repo/ (git) + │ │ + │ live SSE stream commit · push · PR + ▼ + browser UI ◄───────────────────────────────────── + │ + SQLite (node:sqlite, Node ≥ 22.5) + tasks · runs · telemetry · users · schedules + audit · accounts · search · prompt-history` const fsContract = `/workspace ├── projects// │ ├── repo/ managed git repository (cloned from a git URL) -│ ├── tasks/ *.md task files + .status + logs/ (hourly-rotated) -│ ├── knowledge/ durable project notes the agent reads/writes -│ ├── CLAUDE.md project-root instructions for the agent +│ ├── tasks/ *.md task files + logs/ (hourly-rotated) +│ ├── knowledge/ durable project docs the agent reads and maintains +│ ├── notes/ free-form markdown notes (manual or agent-written) +│ ├── CLAUDE.md project-root instructions for the agent (editable in UI) │ └── .claude/skills symlink → bundled app skills (read-only) +├── .claude-accounts/ +│ ├── / isolated CLAUDE_CONFIG_DIR per registered account +│ └── … ├── skills// user-level skills -└── .auth/roles.json GitHub user → role map` +└── taskforge.db SQLite system of record (node:sqlite)` -const envExample = `# required — GitHub OAuth2 + session signing +const envExample = `# Required — GitHub OAuth2 + session signing GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=... OAUTH_REDIRECT_URI=http://localhost:3000/api/auth/github/callback -AUTH_SECRET=... # random secret for session cookies +AUTH_SECRET=... # HMAC key for session + OAuth state + +# No ANTHROPIC_API_KEY — the claude CLI authenticates itself + +# Optional: access control +GITHUB_ALLOWED_LOGINS=octocat,hubot # login allowlist +GITHUB_ALLOWED_ORG=my-org # org membership gate +SESSION_TTL=86400 # cookie lifetime (seconds) + +# Optional: multiple accounts (see README "Multiple Claude accounts") +# DEFAULT_ACCOUNT=alpha +# TASKFORGE_CIBOT_KEY=sk-ant-… # API key for an api-key alias -# the claude CLI authenticates itself — no API key env +# Git push (NOT the OAuth login) +GIT_REMOTE_TOKEN=... # HTTPS, GitHub scope: repo +# …or mount an SSH deploy key with GIT_SSH_CONFIGURED=1 -# git push needs a container-level credential (NOT the OAuth login): -GIT_REMOTE_TOKEN=... # HTTPS, GitHub scope: repo -# …or a mounted SSH deploy key with GIT_SSH_CONFIGURED=1` +# Notifications +SLACK_WEBHOOK_URL=... +NOTIFY_WEBHOOK_URL=... # generic JSON webhook +NOTIFY_EVENTS=run:completed,run:errored,task:error,limit,agent:done` const features = [ { - title: 'Project-centric workspace', - body: 'Each project bundles its git repo, a task queue, and a knowledge store under /workspace/projects/. The agent runs at the project root, so every path it touches stays inside the sandbox. Create a project by cloning a git URL.', + title: 'SQLite system of record', + body: 'All runtime state lives in a single SQLite database (node:sqlite built-in, requires Node ≥ 22.5) at WORKSPACE_DIR/taskforge.db: task queue, run history, telemetry, users and roles, schedules, audit log, account registry, warm sessions, and FTS5 full-text search. Task and knowledge Markdown bodies stay on disk (the agent reads them); the DB mirrors their metadata so the queue renders from one query. On first boot against an existing workspace, legacy filesystem files (.status, roles.json, telemetry-*.jsonl, project.json, etc.) are imported into SQLite automatically.', + }, + { + title: 'Three-tier roles: admin · standard · guest', + body: 'GitHub OAuth2 authentication with three roles (admin | standard | guest). The first user to sign in becomes admin; everyone else lands as guest until promoted via the Users panel. Admins manage users, account registry, agent definitions, prompt templates, and global settings. Standard users can run tasks and use all per-project features.', }, { title: 'One task at a time, streamed live', - body: 'The engine runs claude over queued *.md task files sequentially, streaming stdout to the browser over Server-Sent Events. Logs rotate hourly on disk; the UI reconnects to the live run.', + body: 'The execution engine runs claude over queued *.md task files sequentially, streaming stdout to the browser over Server-Sent Events. Logs rotate hourly on disk; the browser UI reconnects automatically to the live run. Runs can be stopped gracefully or force-killed.', }, { title: 'Survives the usage wall', - body: 'When claude hits a usage-limit reset, the engine sleeps until the window reopens and resumes — no lost queue. Transient failures retry; runs can be stopped gracefully or forced.', + body: 'When claude hits a usage-limit reset the engine sleeps until the window reopens — no lost queue. A pre-emptive usage gate polls /usage after each task and pauses automatically when any bucket is at or above a configurable threshold (default 95%), so the next task never fails immediately on the limit. Transient failures retry with exponential backoff.', + }, + { + title: 'Multiple Claude accounts', + body: 'Register several Claude Code identities — OAuth subscription seats and/or API-key credentials — as short named aliases. Each alias gets an isolated CLAUDE_CONFIG_DIR; no credential-file races so aliases can run concurrently. The dispatcher round-robins across OAuth aliases to spread usage limits, marks a cooling alias on quota errors, and fails over to the next available one. Account selection: per-task **Account:** facet → per-project default → global DEFAULT_ACCOUNT fallback.', + }, + { + title: 'Scheduled cron runs', + body: 'Per-project cron scheduler with 5-field expressions (supports wildcards, ranges, lists, and step values). Each schedule carries the full RunOptions — model, account, warmSession, commitAfter, pushAfter, branchPerRun, reviewer pass — and can be configured to fire only when tasks are pending, or only when usage is below a configurable threshold.', + }, + { + title: 'Reviewer pass', + body: 'Optional post-task reviewer step: after each task completes, a cheap claude run checks the commit diff against the task spec and returns pass/fail with a short note. Failed tasks surface as needs-review status and are excluded from follow-on pushes and PR creation. Reviewer verdict and notes are stored per telemetry row.', + }, + { + title: 'Rich Git surface', + body: 'Branch, commit, push, fetch, pull, and revert from the UI. Browse unpushed commits and recent history; inspect per-commit diffs; discard specific working-tree paths; manage stash (push, pop, drop). Create GitHub pull requests after a push (branchPerRun + openPr flags). Import GitHub issues directly as task Markdown files. Push requires a container-level credential — GIT_REMOTE_TOKEN or a mounted SSH deploy key — deliberately decoupled from the OAuth login.', + }, + { + title: 'Four bundled skills', + body: 'Read-only app skills the agent can invoke: task-creator (generates new task files from a natural-language prompt), knowledge-writer (creates or updates project knowledge docs), note-writer (creates free-form notes), commit-message (drafts a commit message from the staged diff). Admins can define custom agent kinds — each with a prompt preamble, a target directory (repo, tasks, knowledge, notes, or project), and a post-processing hook — and save reusable prompt templates.', }, { - title: 'GitHub OAuth + roles', - body: 'Auth is GitHub OAuth2. The first user to sign in becomes admin; everyone else lands as guest until promoted. A thin edge middleware gates routes; the full verify happens in the Node layer.', + title: 'Notes vs. knowledge', + body: 'Each project has two separate document stores. knowledge/ holds durable, structured analysis docs that the agent actively reads and maintains; they are indexed and linked via an index.md entry point. notes/ holds free-form Markdown pages that are written manually or by the note-writer agent and are not automatically maintained. Both stores are full-text searchable and editable from the UI.', }, { - title: 'Git built in', - body: 'Branch, commit, and push straight from the UI. Push requires a container-level credential — GIT_REMOTE_TOKEN or a mounted SSH deploy key — deliberately decoupled from the OAuth login.', + title: 'Telemetry and usage dashboards', + body: 'Per-task telemetry records elapsed time, model, token counts (in/out), cost in USD, and reviewer verdict. Aggregated into a dashboard: per-day completion and cost charts, per-model breakdown (count, average elapsed, cost), slowest tasks, most expensive tasks, and retry leaders. A usage panel shows the current claude /usage snapshot with bucket fill percentages and reset times.', }, { - title: 'Database-free', - body: 'All durable state lives on the filesystem under WORKSPACE_DIR. Ships read-only app skills the agent uses: task-creator, knowledge-writer, commit-message.', + title: 'Warm sessions', + body: 'Optionally reuse the claude session ID between tasks in a run to skip cold-start setup time (WARM_SESSION=1). The session ID is persisted per project in SQLite and reused while it is within the configured max-age window. Warm sessions are opt-in per run or per project setting.', + }, + { + title: 'Run history and event ledger', + body: 'Every run is recorded — start/finish time, outcome (completed, stopped, force-stopped, usage-limit), task counts, branch, PR URL, triggered by (user or schedule), and summed cost. A fine-grained run-event ledger captures task:begin, task:done, task:error, commit, limit, and transient events for audit and replay. Run history is browsable per project.', + }, + { + title: 'Deep task management', + body: 'Bulk actions (re-run, reset to open, archive), manual task reordering, per-task feedback, error reset, and the full task drawer with last-run telemetry. Task Markdown facets: Severity, Project, Model, Account, Depends. GitHub issues import directly as task files via the Import Issues modal.', + }, + { + title: 'Per-project settings and CLAUDE.md editing', + body: 'Each project exposes a settings panel for: default model, default account alias, commit behavior and commit-message mode, warm session, knowledge auto-update, usage gate threshold, reviewer pass, auto-push and PR creation, and per-event notification routing. The project CLAUDE.md is editable directly from the UI without touching the filesystem.', + }, + { + title: 'Audit log, health page, and backups', + body: 'Tamper-evident audit log records every significant user action: login, role changes, run start/stop, task CRUD, settings changes. The health page shows agent binary version, git version, disk free, DB path / size / migration version, engine state per project, and account health summaries. One-click DB download and per-project workspace archive backups from the admin panel.', + }, + { + title: 'Full-text search and notifications', + body: 'SQLite FTS5 full-text search (degrades gracefully when unavailable) across tasks, knowledge, and notes with highlighted result snippets. Notifications via Slack webhook and a generic outbound JSON webhook (ntfy, Discord, custom automations), configurable globally and per project across five event types: run:completed, run:errored, task:error, limit, agent:done.', + }, + { + title: 'Access control and session gating', + body: 'Optional GITHUB_ALLOWED_LOGINS (comma-separated login allowlist) and GITHUB_ALLOWED_ORG (org membership check) env vars restrict who can sign in at all — checked at the OAuth callback before a user record is created. SESSION_TTL controls the signed cookie lifetime. A thin edge middleware gates every page route; the full role verification happens in the Node layer.', }, ] @@ -71,17 +141,18 @@ export const TaskForgePage = () => {
-
App · Next.js · Claude Code
+
App · Next.js 14 · Claude Code CLI

TaskForge

A self-hosted, single-process Next.js control panel that drives the Claude Code CLI over a set of local git repositories. Each project has a queue of Markdown task files; TaskForge runs claude over them one at a time, streams the output live, survives usage-limit walls, retries transient failures, and - can commit and push the result. + can commit, push, and open pull requests. All state is backed by SQLite via the + Node built-in node:sqlite module (Node ≥ 22.5).

- {['Next.js 14', 'Claude Code CLI', 'GitHub OAuth', 'SSE', 'Docker', 'database-free'].map((chip) => ( + {['Next.js 14', 'SQLite · node:sqlite', 'Claude Code CLI', 'GitHub OAuth', 'SSE', 'Docker', 'multi-account', 'cron scheduler'].map((chip) => ( {chip} ))}
@@ -92,13 +163,21 @@ export const TaskForgePage = () => {
Next.js 14
-
Durable state
-
Filesystem
+
Persistence
+
SQLite (node:sqlite)
+
+
+
Node requirement
+
≥ 22.5 (24 recommended)
Auth
GitHub OAuth2
+
+
Roles
+
admin · standard · guest
+
License
MIT
@@ -108,7 +187,7 @@ export const TaskForgePage = () => {

How it works

- queue → claude → stream → commit + queue → claude → stream → commit → SQLite
@@ -126,8 +205,8 @@ export const TaskForgePage = () => {
-

Filesystem contract

- no database — state on disk under WORKSPACE_DIR +

Filesystem layout

+ task + knowledge bodies on disk; runtime state in SQLite
@@ -139,12 +218,12 @@ export const TaskForgePage = () => {

Run it

- Docker, or local dev + Docker (Node 24 image), or local dev on Node ≥ 22.5
- +
@@ -161,7 +240,7 @@ export const TaskForgePage = () => {

kalevski/toolcase

-

taskforge/ — Next.js app, engine, README, Dockerfile, bundled skills

+

taskforge/ — Next.js app, execution engine, SQLite repositories, bundled skills, README, Dockerfile

diff --git a/examples/src/phaser-plus/index.tsx b/examples/src/phaser-plus/index.tsx index fb4a0ddf..7890fa58 100644 --- a/examples/src/phaser-plus/index.tsx +++ b/examples/src/phaser-plus/index.tsx @@ -41,6 +41,8 @@ import ReplayRecorderDemo from './scenes/ReplayRecorderDemo.js' import ReplayRecorderTimelineDemo from './scenes/ReplayRecorderTimelineDemo.js' import CameraDirectorDemo from './scenes/CameraDirectorDemo.js' import ScreenShakeDemo from './scenes/ScreenShakeDemo.js' +import CameraFlashDemo from './scenes/CameraFlashDemo.js' +import DialogCameraCueDemo from './scenes/DialogCameraCueDemo.js' import ParallaxLayerDemo from './scenes/ParallaxLayerDemo.js' import LetterboxDemo from './scenes/LetterboxDemo.js' import InputFeatureDemo from './scenes/InputFeatureDemo.js' @@ -58,6 +60,7 @@ import EffectsGalleryDemo from './scenes/EffectsGalleryDemo.js' import FlowLandingDemo from './scenes/FlowLandingDemo.js' import InputLandingDemo from './scenes/InputLandingDemo.js' import CinemaLandingDemo from './scenes/CinemaLandingDemo.js' +import BroadPhaseDemo from './scenes/BroadPhaseDemo.js' export type PhaserCategory = | 'Core' @@ -75,6 +78,7 @@ export type PhaserCategory = | 'Net' | 'Persistence' | 'Assets' + | 'Structs' export type PhaserExampleDef = { key: string @@ -99,6 +103,7 @@ export const phaserCategories: PhaserCategory[] = [ 'Assets', 'Persistence', 'AI', + 'Structs', 'Perspective2D', 'Debugging' ] @@ -466,6 +471,22 @@ export const phaserExamples: PhaserExampleDef[] = [ sceneFile: 'LetterboxDemo.js', element: }, + { + key: 'camera-flash', + title: 'Camera Flash', + category: 'Cinema', + description: 'CameraFlash overlay: click for a white hit, stack additive color flashes, hold frames. 1: red, 2: cyan, 3: stack, C: clear.', + sceneFile: 'CameraFlashDemo.js', + element: + }, + { + key: 'dialog-camera-cue', + title: 'Dialog Camera Cue', + category: 'Cinema', + description: 'DialogCameraCue dims everything outside a focus frame around an NPC, with optional vignette ring. SPACE: toggle, F: full dim, V: vignette, C: clear.', + sceneFile: 'DialogCameraCueDemo.js', + element: + }, { key: 'input-feature', @@ -573,5 +594,14 @@ export const phaserExamples: PhaserExampleDef[] = [ description: 'Landing demo: whole Cinema surface — CameraDirector shots, ScreenShake (impact/rumble/sine), CameraFlash, ParallaxLayer, LetterboxFeature, DialogCameraCue.', sceneFile: 'CinemaLandingDemo.js', element: + }, + + { + key: 'broad-phase', + title: 'Broad-phase Queries', + category: 'Structs', + description: 'Structs.SpatialHash + Structs.Quadtree: 260 moving bodies indexed in both broad-phases; cursor drives a range query() and a nearest() lookup. Press S to switch which structure answers.', + sceneFile: 'BroadPhaseDemo.js', + element: } ] diff --git a/examples/src/phaser-plus/scenes/BroadPhaseDemo.js b/examples/src/phaser-plus/scenes/BroadPhaseDemo.js new file mode 100644 index 00000000..7494692a --- /dev/null +++ b/examples/src/phaser-plus/scenes/BroadPhaseDemo.js @@ -0,0 +1,186 @@ +import { Scene, Structs } from '@toolcase/phaser-plus' + +// Broad-phase demo: a field of moving bodies indexed in BOTH a SpatialHash and a +// Quadtree every frame. The cursor drives a range query() (highlighting overlapping +// bodies) and a nearest() lookup (ringed + connected to the cursor). Press S to switch +// which structure answers the queries — the highlights are identical, proving both +// broad-phases agree while exercising their different internals. + +const CELL = 80 // SpatialHash cell size (px) +const QT_CAPACITY = 8 // Quadtree items-per-node before split +const QT_MAX_DEPTH = 8 // Quadtree max subdivision depth +const BODIES = 260 +const SEED = 1337 + +class BroadPhaseDemo extends Scene { + + /** @type {InstanceType} */ hash = null + /** @type {InstanceType} */ quadtree = null + /** @type {Array<{x:number,y:number,vx:number,vy:number,r:number}>} */ bodies = [] + /** @type {import('phaser').GameObjects.Graphics} */ gfx = null + + mode = 'hash' // 'hash' | 'quadtree' + paused = false + queryRadius = 110 // half-extent of the cursor query rectangle + pointer = { x: 640, y: 360 } + hitCount = 0 + nearestDist = -1 + + onCreate() { + const { width, height } = this.scale + + const rng = new Structs.Random(SEED) + this.bodies = [] + for (let i = 0; i < BODIES; i++) { + const r = rng.float(6, 18) + this.bodies.push({ + x: rng.float(r, width - r), + y: rng.float(r, height - r), + vx: rng.float(-90, 90), + vy: rng.float(-90, 90), + r + }) + } + + // Build both broad-phases over the same world bounds. + this.hash = new Structs.SpatialHash(CELL) + this.quadtree = new Structs.Quadtree( + { x: 0, y: 0, width, height }, + QT_CAPACITY, + QT_MAX_DEPTH + ) + for (const b of this.bodies) { + const bounds = this.boundsOf(b) + this.hash.insert(b, bounds) + this.quadtree.insert(b, bounds) + } + + this.gfx = this.add.graphics() + + this.input.on('pointermove', (p) => { this.pointer.x = p.x; this.pointer.y = p.y }) + this.input.on('wheel', (_p, _o, _dx, dy) => { + const next = this.queryRadius + (dy > 0 ? -12 : 12) + this.queryRadius = Math.max(30, Math.min(320, next)) + }) + this.input.keyboard.on('keydown-S', () => { + this.mode = this.mode === 'hash' ? 'quadtree' : 'hash' + }) + this.input.keyboard.on('keydown-SPACE', () => { this.paused = !this.paused }) + + this.add.text(8, 8, + 'Move cursor: range query + nearest. [S] switch structure · [Wheel] query size · [Space] pause', + { color: '#ffffff', fontSize: '14px', backgroundColor: '#000000aa', padding: { x: 8, y: 6 } } + ).setDepth(10) + + this.statusText = this.add.text(8, height - 30, '', + { color: '#ffeb3b', fontSize: '13px', backgroundColor: '#000000aa', padding: { x: 6, y: 4 } } + ).setDepth(10) + } + + /** Axis-aligned bounds rect for a body (SpatialRect shape). */ + boundsOf(b) { + return { x: b.x - b.r, y: b.y - b.r, width: b.r * 2, height: b.r * 2 } + } + + onUpdate(_time, delta) { + const { width, height } = this.scale + const dt = delta / 1000 + + if (!this.paused) { + for (const b of this.bodies) { + b.x += b.vx * dt + b.y += b.vy * dt + if (b.x < b.r) { b.x = b.r; b.vx = -b.vx } + else if (b.x > width - b.r) { b.x = width - b.r; b.vx = -b.vx } + if (b.y < b.r) { b.y = b.r; b.vy = -b.vy } + else if (b.y > height - b.r) { b.y = height - b.r; b.vy = -b.vy } + // Re-index the moved body in BOTH structures. + const bounds = this.boundsOf(b) + this.hash.update(b, bounds) + this.quadtree.update(b, bounds) + } + } + + this.draw() + } + + draw() { + const g = this.gfx + const { width, height } = this.scale + const struct = this.mode === 'hash' ? this.hash : this.quadtree + + const qr = this.queryRadius + const region = { + x: this.pointer.x - qr, + y: this.pointer.y - qr, + width: qr * 2, + height: qr * 2 + } + + const hits = struct.query(region) + const hitSet = new Set(hits) + const nearest = struct.nearest({ x: this.pointer.x, y: this.pointer.y }) + this.hitCount = hits.length + this.nearestDist = nearest + ? Math.round(Math.hypot(nearest.x - this.pointer.x, nearest.y - this.pointer.y)) + : -1 + + g.clear() + g.fillStyle(0x0d141b, 1) + g.fillRect(0, 0, width, height) + + // Structure overlay: uniform grid for the hash, world frame for the quadtree. + if (this.mode === 'hash') { + g.lineStyle(1, 0x1d2b38, 0.8) + for (let x = 0; x <= width; x += CELL) { + g.beginPath(); g.moveTo(x, 0); g.lineTo(x, height); g.strokePath() + } + for (let y = 0; y <= height; y += CELL) { + g.beginPath(); g.moveTo(0, y); g.lineTo(width, y); g.strokePath() + } + } else { + g.lineStyle(1, 0x1d2b38, 0.9) + g.strokeRect(0.5, 0.5, width - 1, height - 1) + } + + // Bodies — dim by default, bright when inside the query region. + for (const b of this.bodies) { + if (hitSet.has(b)) g.fillStyle(0xffd54f, 1) + else g.fillStyle(0x3a4d5e, 1) + g.fillCircle(b.x, b.y, b.r) + } + + // Query rectangle. + g.lineStyle(2, 0xffd54f, 0.9) + g.strokeRect(region.x, region.y, region.width, region.height) + + // Nearest body — cyan ring + connector from the cursor. + if (nearest) { + g.lineStyle(2, 0x29e3ff, 0.95) + g.strokeCircle(nearest.x, nearest.y, nearest.r + 5) + g.beginPath() + g.moveTo(this.pointer.x, this.pointer.y) + g.lineTo(nearest.x, nearest.y) + g.strokePath() + } + + // Cursor crosshair. + g.fillStyle(0x29e3ff, 1) + g.fillCircle(this.pointer.x, this.pointer.y, 3) + + const label = this.mode === 'hash' + ? `SpatialHash (cell ${CELL}px)` + : `Quadtree (cap ${QT_CAPACITY}, depth ${QT_MAX_DEPTH})` + this.statusText.setText( + `structure: ${label} | indexed=${struct.size} | query hits=${this.hitCount} | nearest=${this.nearestDist}px | radius=${this.queryRadius}px${this.paused ? ' | PAUSED' : ''}` + ) + } + + onDestroy() { + this.hash?.clear() + this.quadtree?.clear() + } + +} + +export default BroadPhaseDemo diff --git a/examples/src/phaser-plus/scenes/CameraFlashDemo.js b/examples/src/phaser-plus/scenes/CameraFlashDemo.js new file mode 100644 index 00000000..7f25ee3e --- /dev/null +++ b/examples/src/phaser-plus/scenes/CameraFlashDemo.js @@ -0,0 +1,71 @@ +import { Scene, CameraFlash } from '@toolcase/phaser-plus' +import { Input } from 'phaser' + +class CameraFlashDemo extends Scene { + + /** @type {CameraFlash} */ flash = null + /** @type {Phaser.GameObjects.Text} */ label = null + + onCreate() { + const { width, height } = this.game.config + + // Backdrop content so the flash overlay has something to wash over. + const g = this.add.graphics() + g.lineStyle(1, 0x334155, 0.8) + for (let x = 0; x <= width; x += 60) { + g.lineBetween(x, 0, x, height) + } + for (let y = 0; y <= height; y += 60) { + g.lineBetween(0, y, width, y) + } + this.add.rectangle(width / 2, height / 2, 180, 180, 0x3b82f6) + this.add.circle(width / 2 - 240, height / 2, 70, 0xec4899) + this.add.circle(width / 2 + 240, height / 2, 70, 0x22c55e) + + this.add.text(width / 2, 60, 'Click for a hit flash', { color: '#fff', fontSize: '24px' }).setOrigin(0.5) + this.label = this.add.text(width / 2, 96, 'idle', { color: '#94a3b8', fontSize: '16px' }).setOrigin(0.5) + + // The feature owns a screen-space overlay; flashes stack additively. + this.flash = this.features.register('flash', CameraFlash) + + // Click: quick white hit. Shift+Click: white hit with a hold frame. + this.input.on('pointerdown', p => { + if (p.event.shiftKey) { + this.flash.flash(0xffffff, 0.4, 0.06) + this.label.setText('white hold flash (0.4 s + hold)') + } else { + this.flash.flash(0xffffff, 0.25) + this.label.setText('white hit (0.25 s)') + } + }) + + const codes = Input.Keyboard.KeyCodes + const kb = this.input.keyboard + kb.addKey(codes.ONE).on('down', () => { + this.flash.flash(0xff2244, 0.5, 0.05) + this.label.setText('red damage flash') + }) + kb.addKey(codes.TWO).on('down', () => { + this.flash.flash(0x38bdf8, 0.6) + this.label.setText('cyan pickup flash') + }) + kb.addKey(codes.THREE).on('down', () => { + // Stack several flashes in one frame to show additive blending. + this.flash.flash(0xffffff, 0.3) + this.flash.flash(0xfbbf24, 0.5) + this.label.setText('stacked white + amber') + }) + kb.addKey(codes.C).on('down', () => { + this.flash.clear() + this.label.setText('cleared') + }) + + this.add.text(20, height - 30, + 'Click: white hit • Shift+Click: hold • 1: red • 2: cyan • 3: stack • C: clear', + { color: '#94a3b8', fontSize: '14px' } + ).setScrollFactor(0) + } + +} + +export default CameraFlashDemo diff --git a/examples/src/phaser-plus/scenes/DialogCameraCueDemo.js b/examples/src/phaser-plus/scenes/DialogCameraCueDemo.js new file mode 100644 index 00000000..b4c03fca --- /dev/null +++ b/examples/src/phaser-plus/scenes/DialogCameraCueDemo.js @@ -0,0 +1,74 @@ +import { Scene, DialogCameraCue } from '@toolcase/phaser-plus' +import { Input } from 'phaser' + +class DialogCameraCueDemo extends Scene { + + /** @type {DialogCameraCue} */ cue = null + /** @type {Phaser.GameObjects.Text} */ label = null + /** @type {boolean} */ vignette = true + + onCreate() { + const { width, height } = this.game.config + const cx = width / 2 + const cy = height / 2 + + // Busy scene so the dim/focus frame is visible against the surroundings. + const g = this.add.graphics() + g.lineStyle(1, 0x334155, 0.7) + for (let x = 0; x <= width; x += 48) g.lineBetween(x, 0, x, height) + for (let y = 0; y <= height; y += 48) g.lineBetween(0, y, width, y) + + for (let i = 0; i < 8; i++) { + const a = (i / 8) * Math.PI * 2 + this.add.circle(cx + Math.cos(a) * 220, cy + Math.sin(a) * 150, 26, 0x1e293b) + .setStrokeStyle(2, 0x475569) + } + + // NPC stand-in centred where the focus frame lands. + this.add.rectangle(cx, cy, 60, 110, 0x8b5cf6) + this.add.text(cx, cy - 78, 'NPC', { color: '#a78bfa', fontSize: '15px' }).setOrigin(0.5) + + this.add.text(cx, 56, 'Press SPACE to focus the NPC', { color: '#fff', fontSize: '22px' }).setOrigin(0.5) + this.label = this.add.text(cx, 90, 'cue: closed', { color: '#94a3b8', fontSize: '16px' }).setOrigin(0.5) + + // The cue dims everything outside a supplied screen-space frame. + this.cue = this.features.register('cue', DialogCameraCue) + this.cue.setDimColor(0x000000).setDimAlpha(0.65).setDefaultFade(0.25) + + // Screen-space frame around the NPC. + const frame = { x: cx - 150, y: cy - 90, width: 300, height: 200 } + + const codes = Input.Keyboard.KeyCodes + const kb = this.input.keyboard + kb.addKey(codes.SPACE).on('down', () => { + if (this.cue.isOpen()) { + this.cue.clear() + this.label.setText('cue: closing') + } else { + this.cue.focus(frame, { vignette: this.vignette, fadeSec: 0.3 }) + this.label.setText(`cue: focus NPC${this.vignette ? ' + vignette' : ''}`) + } + }) + kb.addKey(codes.F).on('down', () => { + // Full-screen dim (no frame) — letterbox-free attention pull. + this.cue.focus(null, { fadeSec: 0.3 }) + this.label.setText('cue: full-screen dim') + }) + kb.addKey(codes.V).on('down', () => { + this.vignette = !this.vignette + this.label.setText(`vignette: ${this.vignette ? 'on' : 'off'}`) + }) + kb.addKey(codes.C).on('down', () => { + this.cue.clear() + this.label.setText('cue: closing') + }) + + this.add.text(20, height - 30, + 'SPACE: toggle focus • F: full dim • V: toggle vignette • C: clear', + { color: '#94a3b8', fontSize: '14px' } + ).setScrollFactor(0) + } + +} + +export default DialogCameraCueDemo diff --git a/examples/src/serializer/AdvancedFieldsDemo.tsx b/examples/src/serializer/AdvancedFieldsDemo.tsx new file mode 100644 index 00000000..d981acff --- /dev/null +++ b/examples/src/serializer/AdvancedFieldsDemo.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react' +import Serializer from '@toolcase/serializer' +import { captureLogs, SerializerDemoCard, type LogEntry } from './_demo/SerializerDemo' + +const code = `const serializer = new Serializer() + +// Shared enum registered by name +serializer.enum('Status', ['pending', 'active', 'banned']) + +serializer.define('Account', [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' }, + // Reference the shared enum by name + { key: 'status', type: 'Status', rule: 'optional' }, + // Inline enum (no shared registration needed) + { key: 'role', type: Serializer.FieldType.ENUM(['guest', 'member', 'admin']), rule: 'optional' }, + // Map: string key → int32 value + { key: 'scores', type: Serializer.FieldType.MAP('string', 'int32'), rule: 'optional' }, + // Packed array: tightly-encoded repeated int32 + { key: 'levels', type: Serializer.FieldType.PACKED_ARRAY('int32'), rule: 'repeated' }, +]) + +const buf = serializer.encode('Account', { + name: 'Alice', + status: 'active', + role: 'admin', + scores: { math: 95, science: 88 }, + levels: [1, 2, 3, 5, 8], +}) + +const decoded = serializer.decode('Account', buf) +// decoded.status → 'active' +// decoded.role → 'admin' +// decoded.scores → { math: 95, science: 88 } +// decoded.levels → [1, 2, 3, 5, 8]` + +export const AdvancedFieldsDemo = () => { + const [logs, setLogs] = useState([]) + const run = () => setLogs(captureLogs(() => { + const serializer = new Serializer() + + serializer.enum('Status', ['pending', 'active', 'banned']) + + serializer.define('Account', [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' }, + { key: 'status', type: 'Status', rule: 'optional' }, + { key: 'role', type: Serializer.FieldType.ENUM(['guest', 'member', 'admin']), rule: 'optional' }, + { key: 'scores', type: Serializer.FieldType.MAP('string', 'int32'), rule: 'optional' }, + { key: 'levels', type: Serializer.FieldType.PACKED_ARRAY('int32'), rule: 'repeated' }, + ]) + + const original = { + name: 'Alice', + status: 'active', + role: 'admin', + scores: { math: 95, science: 88 }, + levels: [1, 2, 3, 5, 8], + } + console.log('Original:', JSON.stringify(original)) + + const buf = serializer.encode('Account', original) + console.log('Encoded size:', buf.length, 'bytes') + + const decoded = serializer.decode('Account', buf) + console.log('Decoded:', JSON.stringify(decoded)) + console.log('') + console.log('status round-trip ok:', decoded.status === 'active') + console.log('role round-trip ok: ', decoded.role === 'admin') + console.log('scores round-trip ok:', decoded.scores.math === 95 && decoded.scores.science === 88) + console.log('levels round-trip ok:', JSON.stringify(decoded.levels) === JSON.stringify([1, 2, 3, 5, 8])) + })) + return ( + + Demonstrates s.enum() shared enum registration,{' '} + FieldType.ENUM() inline enum,{' '} + FieldType.MAP(keyType, valueType) map fields, and{' '} + FieldType.PACKED_ARRAY(type) efficiently packed repeated numerics. + All four survive a full encode → decode round-trip. + + } + code={code} + onRun={run} + logs={logs} + /> + ) +} + +export default AdvancedFieldsDemo diff --git a/examples/src/serializer/FieldTypesDemo.tsx b/examples/src/serializer/FieldTypesDemo.tsx index 83e4c924..b77dcea5 100644 --- a/examples/src/serializer/FieldTypesDemo.tsx +++ b/examples/src/serializer/FieldTypesDemo.tsx @@ -36,17 +36,26 @@ export const FieldTypesDemo = () => { console.log('Decoded:', JSON.stringify(dec)) console.log('') - console.log('Available FieldType constants:') + console.log('Scalar FieldType constants (15):') for (const [name, value] of Object.entries(Serializer.FieldType)) { - console.log(` ${name}: '${value}'`) + if (typeof value === 'string') { + console.log(` ${name}: '${value}'`) + } } + console.log('') + console.log('FieldType constructors (advanced types):') + console.log(' ENUM(values) — inline enum marker') + console.log(' MAP(keyType, valueType) — map field marker') + console.log(' PACKED_ARRAY(type) — packed repeated marker') })) return ( - All 15 protobuf field types are supported via Serializer.FieldType constants. + Serializer.FieldType provides 15 scalar field-type constants plus three + constructor helpers (ENUM, MAP, PACKED_ARRAY) for + advanced field descriptors. } code={code} diff --git a/examples/src/serializer/StreamingDemo.tsx b/examples/src/serializer/StreamingDemo.tsx new file mode 100644 index 00000000..91c576b1 --- /dev/null +++ b/examples/src/serializer/StreamingDemo.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react' +import Serializer from '@toolcase/serializer' +import { captureLogs, SerializerDemoCard, type LogEntry } from './_demo/SerializerDemo' + +const fields = [ + { key: 'id', type: Serializer.FieldType.STRING, rule: 'required' as const }, + { key: 'payload', type: Serializer.FieldType.STRING, rule: 'required' as const }, +] + +const code = `import Serializer from '@toolcase/serializer' + +const s = new Serializer('streaming-demo') +s.define('Packet', [ + { key: 'id', type: Serializer.FieldType.STRING, rule: 'required' }, + { key: 'payload', type: Serializer.FieldType.STRING, rule: 'required' }, +]) + +// Encode a message +const buf = s.encode('Packet', { + id: 'msg-001', + payload: 'A'.repeat(200), +}) + +// Fragment into small chunks — use a tiny maxChunkSize to force multiple chunks +const chunks = s.fragment(buf, 32) +console.log('original size:', buf.length, 'bytes') +console.log('chunk count:', chunks.length) + +// Each chunk carries a 9-byte header: 0xF5 | 4-byte frame ID | 2-byte index | 2-byte total +for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i] + const magic = chunk[0].toString(16) + const index = (chunk[5] << 8) | chunk[6] + const total = (chunk[7] << 8) | chunk[8] + console.log(\` chunk[\${i}] magic=0x\${magic} index=\${index} total=\${total} size=\${chunk.length}\`) +} + +// Reassemble (order-independent) +const reassembled = s.reassemble(chunks) +const message = s.decode('Packet', reassembled) +console.log('round-trip ok:', message.id === 'msg-001' && message.payload === 'A'.repeat(200))` + +export const StreamingDemo = () => { + const [logs, setLogs] = useState([]) + const run = () => setLogs(captureLogs(() => { + const s = new Serializer('streaming-demo') + s.define('Packet', fields) + + const original = { id: 'msg-001', payload: 'A'.repeat(200) } + const buf = s.encode('Packet', original) + + // Small maxChunkSize forces several fragments so the demo is illustrative + const maxChunkSize = 32 + const chunks = s.fragment(buf, maxChunkSize) + + console.log('original size:', buf.length, 'bytes') + console.log('chunk count:', chunks.length) + console.log('max chunk size:', maxChunkSize, 'bytes') + console.log('') + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i] + const magic = chunk[0].toString(16).toUpperCase() + const index = (chunk[5] << 8) | chunk[6] + const total = (chunk[7] << 8) | chunk[8] + console.log(`chunk[${i}] magic=0x${magic} index=${index} total=${total} size=${chunk.length}`) + } + + console.log('') + console.log('--- reassembling (shuffled order) ---') + // Shuffle to prove order-independence + const shuffled = [...chunks].reverse() + const reassembled = s.reassemble(shuffled) + const message = s.decode('Packet', reassembled) + + console.log('reassembled size:', reassembled.length, 'bytes') + console.log('id ok:', message.id === original.id) + console.log('payload ok:', message.payload === original.payload) + console.log('round-trip ok:', message.id === original.id && message.payload === original.payload) + })) + return ( + + Encodes a message, calls fragment() with a small maxChunkSize to + split the buffer into multiple chunks, then inspects the 9-byte header on each chunk + (magic 0xF5, frame ID, 2-byte index, 2-byte total). The chunks are passed + to reassemble() in reverse order to prove order-independence, and the + result is decoded to confirm the complete round-trip. + + } + code={code} + onRun={run} + logs={logs} + /> + ) +} + +export default StreamingDemo diff --git a/examples/src/serializer/ValidationDemo.tsx b/examples/src/serializer/ValidationDemo.tsx new file mode 100644 index 00000000..0706cdbe --- /dev/null +++ b/examples/src/serializer/ValidationDemo.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react' +import Serializer from '@toolcase/serializer' +import { captureLogs, SerializerDemoCard, type LogEntry } from './_demo/SerializerDemo' + +const code = `// validate — returns error string or null (no encode step) +serializer.validate('Event', { id: 'e1', score: 10 }) // → null (valid) +serializer.validate('Event', { id: 42, score: 10 }) // → 'id: string expected' + +// safeEncode / safeDecode — return SafeResult instead of throwing +const enc = serializer.safeEncode('Event', { id: 'e1', score: 10 }) +// { ok: true, value: Uint8Array } + +const dec = serializer.safeDecode('Event', enc.value!) +// { ok: true, value: Message } + +const fail = serializer.safeDecode('Event', new Uint8Array([0xff, 0xff, 0xff])) +// { ok: false, error: Error } + +// types() / fields(key) — introspect the registered schema +serializer.types() // → ['Event'] +serializer.fields('Event') // → [{ key: 'id', type: 'string', ... }, ...]` + +export const ValidationDemo = () => { + const [logs, setLogs] = useState([]) + const run = () => setLogs(captureLogs(() => { + const serializer = new Serializer() + serializer.define('Event', [ + { key: 'id', type: Serializer.FieldType.STRING, rule: 'required' }, + { key: 'score', type: Serializer.FieldType.INT32, rule: 'optional', default: 0 }, + ]) + + console.log('--- validate ---') + const valid = serializer.validate('Event', { id: 'e1', score: 10 }) + console.log('valid message →', valid === null ? 'null (ok)' : valid) + + const invalid = serializer.validate('Event', { id: 42 as any, score: 10 }) + console.log('invalid message →', invalid) + + console.log('') + console.log('--- safeEncode / safeDecode ---') + + const enc = serializer.safeEncode('Event', { id: 'e1', score: 10 }) + console.log('safeEncode ok:', enc.ok, '— bytes:', enc.value?.length) + + const dec = serializer.safeDecode('Event', enc.value!) + console.log('safeDecode ok:', dec.ok, '— value:', JSON.stringify(dec.value)) + + const fail = serializer.safeDecode('Event', new Uint8Array([0xff, 0xff, 0xff])) + console.log('safeDecode fail:', fail.ok, '— error:', fail.error?.message) + + console.log('') + console.log('--- introspection ---') + console.log('types():', JSON.stringify(serializer.types())) + const fieldDescs = serializer.fields('Event').map(f => ({ key: f.key, type: f.type })) + console.log("fields('Event'):", JSON.stringify(fieldDescs)) + })) + return ( + + validate checks a message against the schema without encoding it.{' '} + safeEncode/safeDecode return a{' '} + {'{ ok, value?, error? }'} result instead of throwing.{' '} + types()/fields(key) expose the registered schema at runtime. + + } + code={code} + onRun={run} + logs={logs} + /> + ) +} + +export default ValidationDemo diff --git a/examples/src/serializer/VersioningDemo.tsx b/examples/src/serializer/VersioningDemo.tsx new file mode 100644 index 00000000..36b1b538 --- /dev/null +++ b/examples/src/serializer/VersioningDemo.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react' +import Serializer from '@toolcase/serializer' +import { captureLogs, SerializerDemoCard, type LogEntry } from './_demo/SerializerDemo' + +const v1Fields = [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' as const }, + { key: 'score', type: Serializer.FieldType.INT32, rule: 'optional' as const, default: 0 }, +] + +const v2Fields = [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' as const }, + { key: 'score', type: Serializer.FieldType.INT32, rule: 'optional' as const, default: 0 }, + { key: 'level', type: Serializer.FieldType.INT32, rule: 'optional' as const, default: 1 }, +] + +const code = `import Serializer from '@toolcase/serializer' + +// --- Produce a legacy v1 buffer (simulates data from an older client) --- +const legacy = new Serializer('save-v1') +legacy.define('SaveGame', [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' }, + { key: 'score', type: Serializer.FieldType.INT32, rule: 'optional', default: 0 }, +]) +legacy.version(1, 0) +const v1Buf = legacy.encodeVersioned('SaveGame', { name: 'Hero', score: 500 }) + +// --- Current serializer: v2 schema + migration from v1 --- +const s = new Serializer('save-v2') +s.define('SaveGame', [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' }, + { key: 'score', type: Serializer.FieldType.INT32, rule: 'optional', default: 0 }, + { key: 'level', type: Serializer.FieldType.INT32, rule: 'optional', default: 1 }, +]) +// Register the old v1 shape so decodeVersioned can parse it +s.defineVersion('SaveGame', 1, [ + { key: 'name', type: Serializer.FieldType.STRING, rule: 'required' }, + { key: 'score', type: Serializer.FieldType.INT32, rule: 'optional', default: 0 }, +]) +// Migration: v1 → v2 fills in the new 'level' field +s.migrate('SaveGame', 1, (msg) => ({ ...msg, level: 1 })) +s.version(2, 0) + +// Decode the old buffer — migration runs automatically +const { version, message } = s.decodeVersioned('SaveGame', v1Buf) +// version → { major: 1, minor: 0 } +// message → { name: 'Hero', score: 500, level: 1 } + +// Encode a fresh v2 message and decode it (no migration needed) +const v2Buf = s.encodeVersioned('SaveGame', { name: 'Hero', score: 750, level: 3 }) +const { version: v2, message: m2 } = s.decodeVersioned('SaveGame', v2Buf) +// v2 → { major: 2, minor: 0 } +// m2 → { name: 'Hero', score: 750, level: 3 }` + +export const VersioningDemo = () => { + const [logs, setLogs] = useState([]) + const run = () => setLogs(captureLogs(() => { + // Produce a legacy v1 buffer (simulates data from an older client) + const legacy = new Serializer('save-v1') + legacy.define('SaveGame', v1Fields) + legacy.version(1, 0) + const v1Buf = legacy.encodeVersioned('SaveGame', { name: 'Hero', score: 500 }) + console.log('v1 buffer size:', v1Buf.length, 'bytes') + console.log('v1 buffer header (magic, major, minor):', v1Buf[0].toString(16), v1Buf[1], v1Buf[2]) + + // Current serializer: v2 schema + migration from v1 + const s = new Serializer('save-v2') + s.define('SaveGame', v2Fields) + s.defineVersion('SaveGame', 1, v1Fields) + s.migrate('SaveGame', 1, (msg: Record) => ({ ...msg, level: 1 })) + s.version(2, 0) + + console.log('') + console.log('--- decoding v1 buffer (migration path) ---') + const { version, message } = s.decodeVersioned('SaveGame', v1Buf) + console.log('decoded version:', JSON.stringify(version)) + console.log('decoded message:', JSON.stringify(message)) + console.log('migration added level:', message.level === 1) + + console.log('') + console.log('--- encoding + decoding v2 message (no migration) ---') + const v2Buf = s.encodeVersioned('SaveGame', { name: 'Hero', score: 750, level: 3 }) + console.log('v2 buffer size:', v2Buf.length, 'bytes') + const { version: v2, message: m2 } = s.decodeVersioned('SaveGame', v2Buf) + console.log('decoded version:', JSON.stringify(v2)) + console.log('decoded message:', JSON.stringify(m2)) + console.log('level preserved:', m2.level === 3) + })) + return ( + + Uses version() / getVersion(), defineVersion(), and{' '} + migrate() to show how a v1 buffer — produced by a legacy client — is transparently + decoded and upgraded to the v2 schema via decodeVersioned(). + The 3-byte versioned frame header (0xB5 magic + major + minor) is printed + so you can see the wire format directly. + + } + code={code} + onRun={run} + logs={logs} + /> + ) +} + +export default VersioningDemo diff --git a/examples/src/serializer/index.tsx b/examples/src/serializer/index.tsx index 59978562..b151332a 100644 --- a/examples/src/serializer/index.tsx +++ b/examples/src/serializer/index.tsx @@ -4,6 +4,10 @@ import FieldTypesDemo from './FieldTypesDemo' import RepeatedFieldsDemo from './RepeatedFieldsDemo' import MultipleTypesDemo from './MultipleTypesDemo' import ErrorHandlingDemo from './ErrorHandlingDemo' +import AdvancedFieldsDemo from './AdvancedFieldsDemo' +import VersioningDemo from './VersioningDemo' +import StreamingDemo from './StreamingDemo' +import ValidationDemo from './ValidationDemo' export type SerializerExampleDef = { key: string @@ -17,4 +21,8 @@ export const serializerExamples: SerializerExampleDef[] = [ { key: 'repeated', label: 'Repeated Fields', element: }, { key: 'multiple', label: 'Multiple Types', element: }, { key: 'errors', label: 'Error Handling', element: }, + { key: 'advanced-fields', label: 'Advanced Fields', element: }, + { key: 'versioning', label: 'Versioning & Migration', element: }, + { key: 'streaming', label: 'Streaming', element: }, + { key: 'validation', label: 'Validation & Introspection', element: }, ] diff --git a/examples/src/web-components/ThemeDemo.tsx b/examples/src/web-components/ThemeDemo.tsx new file mode 100644 index 00000000..e9a549f3 --- /dev/null +++ b/examples/src/web-components/ThemeDemo.tsx @@ -0,0 +1,117 @@ +import React from 'react' + +// A small cluster of nested components reused under each theme wrapper, so the +// effect of a token swap is obvious side-by-side. +const Cluster: React.FC = () => ( +
+ {/* @ts-ignore */} + + {/* @ts-ignore */} + +

+ Every component below inherits its colours, borders, and type from the wrapping{' '} + tc-theme — no per-element overrides. +

+
+ +
+ Primary + {/* @ts-ignore */} + + Secondary + + New + Stable +
+ + Tokens cascade to nested components through inheritance. + +
+ {/* @ts-ignore */} + + {/* @ts-ignore */} + +
+
+) + +const ThemeDemo: React.FC = () => { + return ( +
+
+
+
+ + + Web Components + + + +
+ +

+ The default toolcase tokens apply globally at :root, + so components are themed out of the box with no{' '} + tc-theme wrapper. +

+ +
+ + +

+ The simplest re-skin: set --tc-* design tokens + directly on a tc-theme. They inherit through the{' '} + display:contents box, so every descendant picks + them up. +

+ {/* @ts-ignore */} + + + +
+ + +

+ Opt into a bundled skin via the name attribute. The + fantasy dungeon palette stays inert until a wrapper + requests it. (Display fonts are not bundled — they degrade to + system serifs.) +

+ {/* @ts-ignore */} + + + +
+ + +

+ The dark aurora skin re-skins the same subtree + without touching any nested markup. +

+ {/* @ts-ignore */} + + + +
+
+
+
+
+
+ ) +} + +export default ThemeDemo diff --git a/examples/src/web-components/index.tsx b/examples/src/web-components/index.tsx index 3632e024..97b079f9 100644 --- a/examples/src/web-components/index.tsx +++ b/examples/src/web-components/index.tsx @@ -4,6 +4,7 @@ import AspectRatioBoxDemo from './AspectRatioBoxDemo' import GildedFrameDemo from './GildedFrameDemo' import AvatarDemo from './AvatarDemo' import ArtboardBackdropDemo from './ArtboardBackdropDemo' +import ThemeDemo from './ThemeDemo' import BlurOverlayDemo from './BlurOverlayDemo' import ActionHeaderDemo from './ActionHeaderDemo' import ActionItemsDemo from './ActionItemsDemo' @@ -362,6 +363,7 @@ export const webComponentExamples: WebComponentDef[] = [ { key: 'col', complexity: 'Simple', element: }, { key: 'scroll-area', complexity: 'Primitives', element: }, { key: 'artboard-backdrop', complexity: 'Primitives', element: }, + { key: 'theme', complexity: 'Primitives', element: }, { key: 'accordion', complexity: 'Primitives', element: }, { key: 'alert', complexity: 'Primitives', element: }, { key: 'badge', complexity: 'Primitives', element: }, diff --git a/logging/package.json b/logging/package.json index 8f831a7c..436d35b0 100644 --- a/logging/package.json +++ b/logging/package.json @@ -28,6 +28,9 @@ "dev": "tsup --watch", "build": "tsup" }, + "devDependencies": { + "@types/node": "^20.0.0" + }, "author": { "name": "Daniel Kalevski", "url": "https://kalevski.dev" diff --git a/logging/tsconfig.json b/logging/tsconfig.json index bd11dc90..a5894624 100644 --- a/logging/tsconfig.json +++ b/logging/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "./lib", - "rootDir": "./src" + "rootDir": "./src", + "types": ["node"] }, "include": ["src"] } diff --git a/nginxpilot/README.md b/nginxpilot/README.md index ba423d12..804cee6b 100644 --- a/nginxpilot/README.md +++ b/nginxpilot/README.md @@ -57,7 +57,7 @@ Onboarding more sites later: drop a file into `sites.d/` and `kill -HUP $(pidof ## Configuration -YAML, strict (unknown keys are errors). Default path `/etc/nginxpilot/config.yml`, override with `--config`. Fragments pulled in via `include:` globs may contain **only** `sites:` lists; duplicate domains across files are a validation error. +YAML, strict (unknown keys are errors). Default path `/etc/nginxpilot/config.yml`, override with `--config`. Fragments pulled in via `include:` globs may contain `sites:`, `upstreams:` and/or `proxies:` lists; duplicate domains across files (sites **and** proxies share one domain namespace) are a validation error. ### git source @@ -165,6 +165,42 @@ Downloads use conditional GET (ETag / Last-Modified); unchanged content is a che Inline secrets are a **parse-time error** — only `*_env` / `*_file` references are accepted, so config files stay safe to commit. Secret files must be `0600`/`0640` and owned by the daemon user or root, or the daemon refuses to start. systemd `LoadCredential` works via `*_file` + `$CREDENTIALS_DIRECTORY`. +### Reverse proxies and upstreams + +`sites:` are content the daemon syncs; `upstreams:` and `proxies:` are **config-only entities** — the daemon never syncs, serves, or proxies them. They exist so `print-vhost` (and the admin `/vhost/` endpoint) can **generate** the nginx `upstream {}` + `server { … proxy_pass … }` config for you to paste and adapt. nginxpilot stays out of the request path and never writes nginx config; nginx is still the proxy. + +```yaml +upstreams: + - name: api_pool # [A-Za-z0-9_]+ (referenced as proxy_pass http://api_pool) + balancer: least_conn # round_robin (default) | least_conn | ip_hash + keepalive: 32 # optional keepalive connection cache + servers: + - address: 10.0.0.1:8080 # host:port | ip:port | unix:/path.sock + weight: 2 # optional + max_fails: 3 # optional + fail_timeout: 30s # optional + - address: 10.0.0.2:8080 + backup: true # backup | down flags + +proxies: + - domain: api.example.com + upstream: api_pool # reference a named upstream … + # pass: http://127.0.0.1:9000 # … OR a single inline target (exactly one) + listen: 80 # optional, default 80 + client_max_body_size: 20MiB # optional + connect_timeout: 60s # optional proxy_connect_timeout + read_timeout: 60s # optional proxy_read_timeout + send_timeout: 60s # optional proxy_send_timeout + locations: # optional; default is a single "/" → upstream/pass + - path: / + upstream: api_pool # per-location override (else inherits the proxy default) + - path: /ws + upstream: api_pool + websocket: true # adds the Upgrade/Connection upgrade headers + HTTP/1.1 +``` + +Generate the nginx config: `nginxpilot print-vhost api.example.com`. The output is **self-contained** — it emits each referenced named upstream `upstream {}` block followed by the `server {}` block. If you share one upstream across several proxies, emit it once and drop the duplicate. Standard forwarding headers (`Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`) are always set; TLS stays a commented certbot hint (nginxpilot does not manage certificates). + ## CLI | Subcommand | Purpose | @@ -172,7 +208,7 @@ Inline secrets are a **parse-time error** — only `*_env` / `*_file` references | `run` | The daemon (default). Flags: `--config`, `--log-format logfmt\|json`, `--prune-orphans`. | | `validate` | Parse + validate merged config, check `git` presence, verify secret refs resolve. CI-friendly exit codes. | | `sync ` | One-shot in-process sync, no daemon needed; non-zero exit on failure. | -| `print-vhost ` | Print a commented nginx server-block starting snippet. | +| `print-vhost ` | Print a commented nginx snippet — a content-serving block for a static site, or `upstream {}` + `proxy_pass` blocks for a reverse proxy. | | `status [--json]` | Human table (or raw JSON) from the daemon's `/status` endpoint. | | `version` | Build info. | @@ -183,6 +219,7 @@ Loopback HTTP (default `127.0.0.1:9090`; `admin.listen: ""` disables; `admin.tok - `GET /healthz` — liveness - `GET /status` — per-site JSON: deployed ref, last success/error, failure streak, `never_synced`, next sync - `POST /sync/` — force an immediate sync +- `GET /vhost/` — `text/plain` generated nginx config for a site or reverse proxy (same output as `print-vhost`) ## Signals diff --git a/nginxpilot/cmd/nginxpilot/main.go b/nginxpilot/cmd/nginxpilot/main.go index 0870344a..4b6f3994 100644 --- a/nginxpilot/cmd/nginxpilot/main.go +++ b/nginxpilot/cmd/nginxpilot/main.go @@ -20,7 +20,7 @@ Usage: nginxpilot [run] [flags] run the daemon (default) nginxpilot validate [flags] parse + validate the merged config nginxpilot sync [flags] one-shot sync for one site - nginxpilot print-vhost print an nginx server-block snippet + nginxpilot print-vhost print an nginx snippet (static site or reverse proxy) nginxpilot status [--json] show per-site status from the daemon nginxpilot version print build info diff --git a/nginxpilot/cmd/nginxpilot/printvhost.go b/nginxpilot/cmd/nginxpilot/printvhost.go index a1740a72..a6a3e79c 100644 --- a/nginxpilot/cmd/nginxpilot/printvhost.go +++ b/nginxpilot/cmd/nginxpilot/printvhost.go @@ -4,14 +4,15 @@ import ( "flag" "fmt" "os" - "path/filepath" "github.com/kalevski/toolcase/nginxpilot/internal/config" + "github.com/kalevski/toolcase/nginxpilot/internal/nginxconf" ) -// cmdPrintVhost prints a correct, commented nginx server-block starting -// snippet for one domain (Model A helper, spec §5). The daemon never -// touches nginx config — paste and adapt this. +// cmdPrintVhost prints a correct, commented nginx snippet for one domain +// (Model A helper, spec §5): a content-serving server{} block for a static +// site, or upstream{} + proxy_pass server{} blocks for a reverse proxy. The +// daemon never touches nginx config — paste and adapt this. func cmdPrintVhost(args []string) int { fs := flag.NewFlagSet("print-vhost", flag.ExitOnError) configPath := fs.String("config", config.DefaultPath, "config file path") @@ -25,53 +26,12 @@ func cmdPrintVhost(args []string) int { fmt.Fprintf(os.Stderr, "config error: %v\n", err) return 1 } - found := false - for _, site := range res.Config.Sites { - if site.Domain == domain { - found = true - break - } - } - if !found { - fmt.Fprintf(os.Stderr, "domain %q is not configured\n", domain) + + out, err := nginxconf.Vhost(res.Config, domain) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) return 1 } - - root := filepath.Join(res.Config.DataDir, "sites", domain, "current") - fmt.Printf(`# nginx vhost for %[1]s — generated by nginxpilot print-vhost -# Paste into /etc/nginx/sites-available/%[1]s.conf (or conf.d/), adapt, then: -# nginx -t && nginx -s reload -# -# Onboarding tip: run `+"`nginxpilot sync %[1]s`"+` once BEFORE -# enabling this vhost so `+"`current`"+` exists and nginx doesn't 404. - -server { - listen 80; - listen [::]:80; - server_name %[1]s; - - # Content is deployed atomically by nginxpilot; the `+"`current`"+` - # symlink is swapped via rename(2) — no nginx reload needed on updates. - root %[2]s; - index index.html; - - location / { - try_files $uri $uri/ =404; - # For single-page apps use instead: - # try_files $uri $uri/ /index.html; - } - - # If your CI pre-compresses assets (.gz next to each file): - # gzip_static on; - - # Keep directory listings off — deployed content is source-controlled, - # anything unexpected should 404, not enumerate. - autoindex off; -} - -# TLS: terminate with certbot/acme.sh (nginxpilot does not manage -# certificates), e.g.: -# certbot --nginx -d %[1]s -`, domain, root) + fmt.Print(out) return 0 } diff --git a/nginxpilot/cmd/nginxpilot/status.go b/nginxpilot/cmd/nginxpilot/status.go index 31f7ac02..559492a1 100644 --- a/nginxpilot/cmd/nginxpilot/status.go +++ b/nginxpilot/cmd/nginxpilot/status.go @@ -40,10 +40,8 @@ func cmdStatus(args []string) int { fmt.Fprintln(os.Stderr, err) return 1 } - if res.Config.Admin.TokenEnv != "" { - if token := os.Getenv(res.Config.Admin.TokenEnv); token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } + if token := clientAdminToken(res.Config.Admin); token != "" { + req.Header.Set("Authorization", "Bearer "+token) } client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) @@ -87,6 +85,23 @@ func cmdStatus(args []string) int { return 0 } +// clientAdminToken resolves the bearer token the status client should present, +// matching the daemon's own resolution (admin.token_env OR admin.token_file). +// Returns "" when no auth is configured, or when a configured ref can't be +// resolved here — in which case we still issue the request and let the daemon's +// 401 surface a clear message rather than failing silently. +func clientAdminToken(a config.Admin) string { + if a.TokenEnv == "" && a.TokenFile == "" { + return "" + } + token, err := config.ResolveSecret(a.TokenEnv, a.TokenFile) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: cannot resolve admin token (%v); requesting without auth\n", err) + return "" + } + return token +} + func fmtTime(t *time.Time) string { if t == nil || t.IsZero() { return "-" diff --git a/nginxpilot/cmd/nginxpilot/status_test.go b/nginxpilot/cmd/nginxpilot/status_test.go index 94af8fb6..660dc2b4 100644 --- a/nginxpilot/cmd/nginxpilot/status_test.go +++ b/nginxpilot/cmd/nginxpilot/status_test.go @@ -1,6 +1,35 @@ package main -import "testing" +import ( + "os" + "path/filepath" + "testing" + + "github.com/kalevski/toolcase/nginxpilot/internal/config" +) + +// TestClientAdminToken verifies the status client resolves the bearer token +// from BOTH admin.token_env and admin.token_file (matching the daemon), not +// just token_env — otherwise `status` against a token_file-secured daemon +// always 401s. +func TestClientAdminToken(t *testing.T) { + if got := clientAdminToken(config.Admin{}); got != "" { + t.Fatalf("no auth configured: want empty, got %q", got) + } + + t.Setenv("NP_TEST_ADMIN_TOKEN", "envtok") + if got := clientAdminToken(config.Admin{TokenEnv: "NP_TEST_ADMIN_TOKEN"}); got != "envtok" { + t.Fatalf("token_env: want %q, got %q", "envtok", got) + } + + f := filepath.Join(t.TempDir(), "tok") + if err := os.WriteFile(f, []byte("filetok\n"), 0o600); err != nil { + t.Fatal(err) + } + if got := clientAdminToken(config.Admin{TokenFile: f}); got != "filetok" { + t.Fatalf("token_file: want %q, got %q", "filetok", got) + } +} func TestTruncate(t *testing.T) { tests := []struct { diff --git a/nginxpilot/cmd/nginxpilot/validate.go b/nginxpilot/cmd/nginxpilot/validate.go index 774c8dbd..af08e519 100644 --- a/nginxpilot/cmd/nginxpilot/validate.go +++ b/nginxpilot/cmd/nginxpilot/validate.go @@ -74,9 +74,20 @@ func cmdValidate(args []string) int { if failed { return 1 } - fmt.Printf("OK: %d site(s), data_dir %s\n", len(cfg.Sites), cfg.DataDir) + fmt.Printf("OK: %d site(s), %d upstream(s), %d proxy(ies), data_dir %s\n", + len(cfg.Sites), len(cfg.Upstreams), len(cfg.Proxies), cfg.DataDir) for _, site := range cfg.Sites { fmt.Printf(" %-30s %-9s %s\n", site.Domain, site.Source.Type, site.Source.URL) } + for _, u := range cfg.Upstreams { + fmt.Printf(" %-30s %-9s %d server(s)\n", u.Name, "upstream", len(u.Servers)) + } + for _, p := range cfg.Proxies { + target := p.Upstream + if target == "" { + target = p.Pass + } + fmt.Printf(" %-30s %-9s %s\n", p.Domain, "proxy", target) + } return 0 } diff --git a/nginxpilot/internal/admin/admin.go b/nginxpilot/internal/admin/admin.go index ded208f8..cc98dcca 100644 --- a/nginxpilot/internal/admin/admin.go +++ b/nginxpilot/internal/admin/admin.go @@ -3,6 +3,7 @@ // GET /healthz daemon liveness // GET /status per-site status JSON // POST /sync/ force an immediate sync (tick-now) +// GET /vhost/ generated nginx config for a site or reverse proxy // // Loopback only by default; an optional bearer token (admin.token_env) // guards reverse-proxied setups. @@ -19,6 +20,7 @@ import ( "time" "github.com/kalevski/toolcase/nginxpilot/internal/manager" + "github.com/kalevski/toolcase/nginxpilot/internal/nginxconf" ) // Server is the admin HTTP endpoint. @@ -76,6 +78,7 @@ func (s *Server) routes() http.Handler { }) mux.HandleFunc("GET /status", s.auth(s.handleStatus)) mux.HandleFunc("POST /sync/{domain}", s.auth(s.handleSync)) + mux.HandleFunc("GET /vhost/{domain}", s.auth(s.handleVhost)) return mux } @@ -105,11 +108,39 @@ func (s *Server) handleStatus(w http.ResponseWriter, _ *http.Request) { func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) { domain := r.PathValue("domain") - if !s.mgr.Kick(domain) { - http.Error(w, "unknown domain", http.StatusNotFound) + if s.mgr.Kick(domain) { + s.log.Info("manual sync triggered", "domain", domain) + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write([]byte("sync scheduled\n")) return } - s.log.Info("manual sync triggered", "domain", domain) - w.WriteHeader(http.StatusAccepted) - _, _ = w.Write([]byte("sync scheduled\n")) + // Not a managed site. Distinguish a configured reverse proxy (which has no + // content to sync) from a genuinely unknown domain so the caller isn't told + // a domain it can see in /vhost is "unknown". + cfg := s.mgr.Config() + for i := range cfg.Proxies { + if cfg.Proxies[i].Domain == domain { + http.Error(w, "domain is a reverse proxy, not a synced site", http.StatusBadRequest) + return + } + } + http.Error(w, "unknown domain", http.StatusNotFound) +} + +// handleVhost renders the nginx config for a site or reverse proxy. The +// daemon only generates text here — it never writes nginx config or reloads. +func (s *Server) handleVhost(w http.ResponseWriter, r *http.Request) { + domain := r.PathValue("domain") + out, err := nginxconf.Vhost(s.mgr.Config(), domain) + if err != nil { + if errors.Is(err, nginxconf.ErrUnknownDomain) { + http.Error(w, "unknown domain", http.StatusNotFound) + return + } + s.log.Warn("vhost generation failed", "domain", domain, "error", err) + http.Error(w, "vhost generation failed", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + _, _ = w.Write([]byte(out)) } diff --git a/nginxpilot/internal/admin/admin_test.go b/nginxpilot/internal/admin/admin_test.go new file mode 100644 index 00000000..ad340c56 --- /dev/null +++ b/nginxpilot/internal/admin/admin_test.go @@ -0,0 +1,79 @@ +package admin + +import ( + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/kalevski/toolcase/nginxpilot/internal/config" + "github.com/kalevski/toolcase/nginxpilot/internal/manager" + "github.com/kalevski/toolcase/nginxpilot/internal/state" +) + +func newTestServer(t *testing.T, cfg *config.Config) http.Handler { + t.Helper() + cfg.DataDir = t.TempDir() + store, err := state.NewStore(cfg.DataDir) + if err != nil { + t.Fatalf("state store: %v", err) + } + log := slog.New(slog.NewTextHandler(io.Discard, nil)) + mgr := manager.New(cfg, store, log) + return New(mgr, "", log).routes() +} + +func TestVhostEndpointProxy(t *testing.T) { + cfg := &config.Config{ + Proxies: []config.Proxy{{Domain: "api.example.com", Pass: "http://127.0.0.1:9000"}}, + } + h := newTestServer(t, cfg) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/vhost/api.example.com", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("want 200, got %d", rec.Code) + } + if ct := rec.Header().Get("Content-Type"); ct != "text/plain; charset=utf-8" { + t.Errorf("want text/plain, got %q", ct) + } + body := rec.Body.String() + if !strings.Contains(body, "proxy_pass http://127.0.0.1:9000;") || !strings.Contains(body, "server_name api.example.com;") { + t.Errorf("unexpected body:\n%s", body) + } +} + +func TestVhostEndpointUnknownDomain404(t *testing.T) { + h := newTestServer(t, &config.Config{}) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/vhost/nope.example.com", nil)) + if rec.Code != http.StatusNotFound { + t.Fatalf("want 404, got %d", rec.Code) + } +} + +// A proxy domain is configured but has no content to sync — the caller must +// get a precise 400, not a misleading "unknown domain" 404 for a domain it can +// see via /vhost. +func TestSyncEndpointProxyIsBadRequest(t *testing.T) { + cfg := &config.Config{ + Proxies: []config.Proxy{{Domain: "api.example.com", Pass: "http://127.0.0.1:9000"}}, + } + h := newTestServer(t, cfg) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/sync/api.example.com", nil)) + if rec.Code != http.StatusBadRequest { + t.Fatalf("want 400 for a proxy domain, got %d (%s)", rec.Code, rec.Body.String()) + } +} + +func TestSyncEndpointUnknownDomain404(t *testing.T) { + h := newTestServer(t, &config.Config{}) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/sync/nope.example.com", nil)) + if rec.Code != http.StatusNotFound { + t.Fatalf("want 404, got %d", rec.Code) + } +} diff --git a/nginxpilot/internal/config/parse.go b/nginxpilot/internal/config/parse.go index 5be07195..9c577a0f 100644 --- a/nginxpilot/internal/config/parse.go +++ b/nginxpilot/internal/config/parse.go @@ -15,10 +15,12 @@ import ( // DefaultPath is where the daemon looks for its config unless --config is given. const DefaultPath = "/etc/nginxpilot/config.yml" -// fragment is the only shape an included file may have: a sites: list. -// No globals, no nested include:. +// fragment is the shape an included file may have: sites:, upstreams: and/or +// proxies: lists. No globals, no nested include:. type fragment struct { - Sites []Site `yaml:"sites"` + Sites []Site `yaml:"sites"` + Upstreams []Upstream `yaml:"upstreams"` + Proxies []Proxy `yaml:"proxies"` } // LoadResult carries the parsed config plus non-fatal warnings (e.g. an @@ -47,6 +49,12 @@ func Load(path string) (*LoadResult, error) { for i := range cfg.Sites { cfg.Sites[i].File = abs } + for i := range cfg.Upstreams { + cfg.Upstreams[i].File = abs + } + for i := range cfg.Proxies { + cfg.Proxies[i].File = abs + } applyDefaults(&cfg) @@ -100,12 +108,20 @@ func loadIncludes(cfg *Config, res *LoadResult) error { } var frag fragment if err := strictDecode(raw, &frag); err != nil { - return fmt.Errorf("%s: %w (fragments may only contain a sites: list)", file, err) + return fmt.Errorf("%s: %w (fragments may only contain sites:, upstreams: and/or proxies: lists)", file, err) } for i := range frag.Sites { frag.Sites[i].File = file } + for i := range frag.Upstreams { + frag.Upstreams[i].File = file + } + for i := range frag.Proxies { + frag.Proxies[i].File = file + } cfg.Sites = append(cfg.Sites, frag.Sites...) + cfg.Upstreams = append(cfg.Upstreams, frag.Upstreams...) + cfg.Proxies = append(cfg.Proxies, frag.Proxies...) } } return nil diff --git a/nginxpilot/internal/config/types.go b/nginxpilot/internal/config/types.go index f44c416d..efe430e2 100644 --- a/nginxpilot/internal/config/types.go +++ b/nginxpilot/internal/config/types.go @@ -31,6 +31,19 @@ const ( AuthHeader = "header" ) +// Upstream load-balancing methods (nginx upstream{} directives). The empty +// string is treated as round-robin (nginx's default, no directive emitted). +const ( + BalancerRoundRobin = "round_robin" + BalancerLeastConn = "least_conn" + BalancerIPHash = "ip_hash" +) + +// DefaultProxyListen is the listen port used for a reverse proxy when the +// entity does not set one. Mirrors the static print-vhost output (:80, plus +// a commented certbot/TLS hint). +const DefaultProxyListen = 80 + // Config is the merged result of the main file and all included fragments. type Config struct { DataDir string `yaml:"data_dir"` @@ -40,10 +53,88 @@ type Config struct { Include []string `yaml:"include"` Sites []Site `yaml:"sites"` + // Upstreams and Proxies are config-only entities: nginx upstream{} pools + // and reverse-proxy server{} blocks. The daemon never syncs or serves + // them — they exist purely so `print-vhost` / the admin /vhost endpoint + // can generate nginx configuration to paste and adapt (the daemon stays + // out of the request path and never writes nginx config). + Upstreams []Upstream `yaml:"upstreams"` + Proxies []Proxy `yaml:"proxies"` + // Path is the main config file path the config was loaded from. Path string `yaml:"-"` } +// Upstream is a named nginx upstream{} block — a pool of backend servers a +// reverse proxy can proxy_pass to by name. +type Upstream struct { + Name string `yaml:"name"` + // Balancer selects the load-balancing method: "" / round_robin (default), + // least_conn, or ip_hash. + Balancer string `yaml:"balancer"` + // Keepalive sets the keepalive connection cache size (0 = omit). + Keepalive int `yaml:"keepalive"` + Servers []UpstreamServer `yaml:"servers"` + + // File records which config file declared this upstream (provenance for + // duplicate-name errors and logs). + File string `yaml:"-"` +} + +// UpstreamServer is one backend in an upstream pool. +type UpstreamServer struct { + // Address is "host:port", an IP:port, or "unix:/path/to.sock". + Address string `yaml:"address"` + Weight int `yaml:"weight"` + MaxFails *int `yaml:"max_fails"` + FailTimeout Duration `yaml:"fail_timeout"` + Backup bool `yaml:"backup"` + Down bool `yaml:"down"` +} + +// Proxy is a reverse-proxy vhost: an nginx server{} block whose locations +// proxy_pass to a named upstream or a single inline target. +type Proxy struct { + Domain string `yaml:"domain"` + // Listen is the HTTP port (default DefaultProxyListen). + Listen int `yaml:"listen"` + // Upstream / Pass set the default backend for all locations. Exactly one + // of them is required unless every location sets its own. Upstream names + // an entry in Config.Upstreams; Pass is an inline scheme://host:port. + Upstream string `yaml:"upstream"` + Pass string `yaml:"pass"` + Locations []ProxyLocation `yaml:"locations"` + + // Optional proxy tuning applied at the server level. + ConnectTimeout Duration `yaml:"connect_timeout"` + ReadTimeout Duration `yaml:"read_timeout"` + SendTimeout Duration `yaml:"send_timeout"` + ClientMaxBodySize ByteSize `yaml:"client_max_body_size"` + + // File records which config file declared this proxy (provenance). + File string `yaml:"-"` +} + +// ProxyLocation maps a location path to a backend, overriding the proxy's +// default Upstream/Pass when set. +type ProxyLocation struct { + // Path defaults to "/". + Path string `yaml:"path"` + Upstream string `yaml:"upstream"` + Pass string `yaml:"pass"` + // Websocket adds the Upgrade/Connection headers + HTTP/1.1 for WebSocket + // and other connection-upgrade traffic. + Websocket bool `yaml:"websocket"` +} + +// ListenPort returns the effective listen port for the proxy. +func (p Proxy) ListenPort() int { + if p.Listen > 0 { + return p.Listen + } + return DefaultProxyListen +} + // Admin configures the loopback admin HTTP endpoint. type Admin struct { // Listen is the address for the admin endpoint. nil means the default diff --git a/nginxpilot/internal/config/validate.go b/nginxpilot/internal/config/validate.go index 4c5cd9d3..b65ec3aa 100644 --- a/nginxpilot/internal/config/validate.go +++ b/nginxpilot/internal/config/validate.go @@ -3,6 +3,7 @@ package config import ( "fmt" "path" + "regexp" "strings" "time" @@ -12,6 +13,10 @@ import ( // MinInterval protects upstreams from over-eager polling (spec §3). const MinInterval = 30 * time.Second +// upstreamNameRe restricts upstream names to a safe nginx identifier so the +// generated `upstream ` / `proxy_pass http://` are unambiguous. +var upstreamNameRe = regexp.MustCompile(`^[A-Za-z0-9_]+$`) + // Validate checks the merged configuration. It normalizes domains to their // punycode/ASCII form in place. func Validate(cfg *Config) error { @@ -31,7 +36,7 @@ func Validate(cfg *Config) error { return fmt.Errorf("admin.token_env and admin.token_file are mutually exclusive") } - seen := map[string]string{} // domain -> file + seen := map[string]string{} // domain -> file (sites + proxies share the namespace) for i := range cfg.Sites { site := &cfg.Sites[i] if err := validateSite(site); err != nil { @@ -42,21 +47,166 @@ func Validate(cfg *Config) error { } seen[site.Domain] = site.File } + + upstreams, err := validateUpstreams(cfg) + if err != nil { + return err + } + if err := validateProxies(cfg, upstreams, seen); err != nil { + return err + } return nil } -func validateSite(site *Site) error { - if site.Domain == "" { - return fmt.Errorf("domain is required") +// normalizeDomain validates and rewrites a domain to its punycode/ASCII +// (IDNA2008) form — filesystem-safe and identical to what nginx sees in +// SNI/Host (spec Q12). Wildcards are rejected (no v1 support). +func normalizeDomain(domain string) (string, error) { + if domain == "" { + return "", fmt.Errorf("domain is required") } - if strings.Contains(site.Domain, "*") { - return fmt.Errorf("wildcard domains are not supported in v1") + if strings.Contains(domain, "*") { + return "", fmt.Errorf("wildcard domains are not supported in v1") } - // Normalize to punycode/ASCII (IDNA2008) — filesystem-safe, identical to - // what nginx sees in SNI/Host (spec Q12). - ascii, err := idna.Lookup.ToASCII(site.Domain) + ascii, err := idna.Lookup.ToASCII(domain) if err != nil { - return fmt.Errorf("invalid domain: %w", err) + return "", fmt.Errorf("invalid domain: %w", err) + } + return ascii, nil +} + +// validateUpstreams checks every upstream and returns the set of declared +// names (post-validation) for proxy reference resolution. +func validateUpstreams(cfg *Config) (map[string]bool, error) { + names := map[string]string{} // name -> file + for i := range cfg.Upstreams { + u := &cfg.Upstreams[i] + if u.Name == "" { + return nil, fmt.Errorf("upstream (%s): name is required", u.File) + } + if !upstreamNameRe.MatchString(u.Name) { + return nil, fmt.Errorf("upstream %q (%s): name must match [A-Za-z0-9_]+", u.Name, u.File) + } + if prev, dup := names[u.Name]; dup { + return nil, fmt.Errorf("duplicate upstream %q declared in %s and %s", u.Name, prev, u.File) + } + names[u.Name] = u.File + + switch u.Balancer { + case "", BalancerRoundRobin, BalancerLeastConn, BalancerIPHash: + default: + return nil, fmt.Errorf("upstream %q: balancer %q must be round_robin | least_conn | ip_hash", u.Name, u.Balancer) + } + if u.Keepalive < 0 { + return nil, fmt.Errorf("upstream %q: keepalive must be >= 0", u.Name) + } + if len(u.Servers) == 0 { + return nil, fmt.Errorf("upstream %q: at least one server is required", u.Name) + } + for j := range u.Servers { + s := u.Servers[j] + if s.Address == "" { + return nil, fmt.Errorf("upstream %q: server[%d].address is required", u.Name, j) + } + if s.Weight < 0 { + return nil, fmt.Errorf("upstream %q: server %q weight must be >= 0", u.Name, s.Address) + } + if s.MaxFails != nil && *s.MaxFails < 0 { + return nil, fmt.Errorf("upstream %q: server %q max_fails must be >= 0", u.Name, s.Address) + } + } + } + out := make(map[string]bool, len(names)) + for n := range names { + out[n] = true + } + return out, nil +} + +// validateProxies checks reverse-proxy entities, resolving upstream references +// against declared names and guarding the shared domain namespace. +func validateProxies(cfg *Config, upstreams map[string]bool, seen map[string]string) error { + for i := range cfg.Proxies { + p := &cfg.Proxies[i] + ascii, err := normalizeDomain(p.Domain) + if err != nil { + return fmt.Errorf("proxy %q (%s): %w", p.Domain, p.File, err) + } + p.Domain = ascii + + if p.Listen < 0 || p.Listen > 65535 { + return fmt.Errorf("proxy %q: listen %d must be 1..65535", p.Domain, p.Listen) + } + + if err := validateProxyTargets(p, upstreams); err != nil { + return fmt.Errorf("proxy %q (%s): %w", p.Domain, p.File, err) + } + + if prev, dup := seen[p.Domain]; dup { + return fmt.Errorf("duplicate domain %q declared in %s and %s", p.Domain, prev, p.File) + } + seen[p.Domain] = p.File + } + return nil +} + +// validateProxyTargets enforces the upstream/pass exactly-one rule at the +// proxy and location levels, with locations inheriting the proxy default. +func validateProxyTargets(p *Proxy, upstreams map[string]bool) error { + if err := checkTarget("", p.Upstream, p.Pass, upstreams, true); err != nil { + return err + } + for j := range p.Locations { + loc := &p.Locations[j] + if loc.Path == "" { + loc.Path = "/" + } + if !strings.HasPrefix(loc.Path, "/") { + return fmt.Errorf("location[%d] path %q must start with /", j, loc.Path) + } + // A location may inherit the proxy default; resolve the effective pair. + up, pass := loc.Upstream, loc.Pass + if up == "" && pass == "" { + up, pass = p.Upstream, p.Pass + } + if err := checkTarget(fmt.Sprintf("location %q ", loc.Path), up, pass, upstreams, false); err != nil { + return err + } + } + // With no locations the proxy default must itself be a complete target. + if len(p.Locations) == 0 && p.Upstream == "" && p.Pass == "" { + return fmt.Errorf("a proxy needs upstream/pass (or at least one location that sets one)") + } + return nil +} + +// checkTarget validates a single (upstream, pass) pair. optional allows the +// pair to be empty (a proxy default that locations override); when false +// exactly one of the two must be set. +func checkTarget(prefix, upstream, pass string, upstreams map[string]bool, optional bool) error { + switch { + case upstream != "" && pass != "": + return fmt.Errorf("%supstream and pass are mutually exclusive", prefix) + case upstream != "": + if !upstreams[upstream] { + return fmt.Errorf("%sreferences unknown upstream %q", prefix, upstream) + } + case pass != "": + if !strings.HasPrefix(pass, "http://") && !strings.HasPrefix(pass, "https://") { + return fmt.Errorf("%spass %q must be an http:// or https:// URL", prefix, pass) + } + default: + if !optional { + return fmt.Errorf("%supstream or pass is required", prefix) + } + } + return nil +} + +func validateSite(site *Site) error { + ascii, err := normalizeDomain(site.Domain) + if err != nil { + return err } site.Domain = ascii diff --git a/nginxpilot/internal/config/validate_test.go b/nginxpilot/internal/config/validate_test.go index 7505390d..6de6e461 100644 --- a/nginxpilot/internal/config/validate_test.go +++ b/nginxpilot/internal/config/validate_test.go @@ -560,3 +560,148 @@ func TestValidateHTTPZipInsecureWithFlagOK(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +// ---- upstreams & reverse proxies ------------------------------------------ + +func validUpstream() Upstream { + return Upstream{ + Name: "api_pool", + Servers: []UpstreamServer{{Address: "10.0.0.1:8080"}}, + File: "test", + } +} + +func TestValidateProxyReferencingUpstreamOK(t *testing.T) { + cfg := minValidConfig() + cfg.Upstreams = []Upstream{validUpstream()} + cfg.Proxies = []Proxy{{Domain: "api.example.com", Upstream: "api_pool", File: "test"}} + if err := Validate(&cfg); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestValidateUpstreamNameRequired(t *testing.T) { + cfg := minValidConfig() + u := validUpstream() + u.Name = "" + cfg.Upstreams = []Upstream{u} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "name is required") { + t.Fatalf("expected name required error, got: %v", err) + } +} + +func TestValidateUpstreamBadName(t *testing.T) { + cfg := minValidConfig() + u := validUpstream() + u.Name = "api-pool" // hyphen not allowed + cfg.Upstreams = []Upstream{u} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "[A-Za-z0-9_]") { + t.Fatalf("expected name charset error, got: %v", err) + } +} + +func TestValidateUpstreamBadBalancer(t *testing.T) { + cfg := minValidConfig() + u := validUpstream() + u.Balancer = "magic" + cfg.Upstreams = []Upstream{u} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "balancer") { + t.Fatalf("expected balancer error, got: %v", err) + } +} + +func TestValidateUpstreamNoServers(t *testing.T) { + cfg := minValidConfig() + u := validUpstream() + u.Servers = nil + cfg.Upstreams = []Upstream{u} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "at least one server") { + t.Fatalf("expected no-servers error, got: %v", err) + } +} + +func TestValidateUpstreamDuplicateName(t *testing.T) { + cfg := minValidConfig() + cfg.Upstreams = []Upstream{validUpstream(), validUpstream()} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "duplicate upstream") { + t.Fatalf("expected duplicate upstream error, got: %v", err) + } +} + +func TestValidateProxyUnknownUpstream(t *testing.T) { + cfg := minValidConfig() + cfg.Proxies = []Proxy{{Domain: "api.example.com", Upstream: "missing", File: "test"}} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "unknown upstream") { + t.Fatalf("expected unknown upstream error, got: %v", err) + } +} + +func TestValidateProxyUpstreamAndPassMutuallyExclusive(t *testing.T) { + cfg := minValidConfig() + cfg.Upstreams = []Upstream{validUpstream()} + cfg.Proxies = []Proxy{{Domain: "api.example.com", Upstream: "api_pool", Pass: "http://x:1", File: "test"}} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "mutually exclusive") { + t.Fatalf("expected mutual-exclusion error, got: %v", err) + } +} + +func TestValidateProxyNoTarget(t *testing.T) { + cfg := minValidConfig() + cfg.Proxies = []Proxy{{Domain: "api.example.com", File: "test"}} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "upstream/pass") { + t.Fatalf("expected no-target error, got: %v", err) + } +} + +func TestValidateProxyInlinePassBadScheme(t *testing.T) { + cfg := minValidConfig() + cfg.Proxies = []Proxy{{Domain: "api.example.com", Pass: "10.0.0.1:8080", File: "test"}} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "http://") { + t.Fatalf("expected pass scheme error, got: %v", err) + } +} + +func TestValidateProxyDomainCollidesWithSite(t *testing.T) { + cfg := minValidConfig() + cfg.Sites = []Site{{ + Domain: "example.com", + Source: Source{Type: SourceHTTPZip, URL: "https://x/a.zip"}, + File: "site.yml", + }} + cfg.Proxies = []Proxy{{Domain: "example.com", Pass: "http://127.0.0.1:9000", File: "proxy.yml"}} + if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "duplicate domain") { + t.Fatalf("expected duplicate domain error, got: %v", err) + } +} + +func TestValidateProxyLocationInheritsDefault(t *testing.T) { + cfg := minValidConfig() + cfg.Upstreams = []Upstream{validUpstream()} + cfg.Proxies = []Proxy{{ + Domain: "api.example.com", + Upstream: "api_pool", + Locations: []ProxyLocation{{Path: "/static"}}, // no target → inherits api_pool + File: "test", + }} + if err := Validate(&cfg); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Proxies[0].Locations[0].Path != "/static" { + t.Fatalf("path mangled: %q", cfg.Proxies[0].Locations[0].Path) + } +} + +func TestValidateProxyLocationPathDefaulted(t *testing.T) { + cfg := minValidConfig() + cfg.Proxies = []Proxy{{ + Domain: "api.example.com", + Locations: []ProxyLocation{{Pass: "http://127.0.0.1:9000"}}, // empty path → "/" + File: "test", + }} + if err := Validate(&cfg); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := cfg.Proxies[0].Locations[0].Path; got != "/" { + t.Fatalf("want default path /, got %q", got) + } +} diff --git a/nginxpilot/internal/manager/manager.go b/nginxpilot/internal/manager/manager.go index ad074d26..079c51b3 100644 --- a/nginxpilot/internal/manager/manager.go +++ b/nginxpilot/internal/manager/manager.go @@ -135,7 +135,7 @@ func (m *Manager) loop(ctx context.Context, sl *siteLoop) { // Startup jitter avoids a thundering herd when many sites share an // interval (spec §2). first := jitter(sl.interval) - sl.setNext(time.Now().Add(first)) + sl.setNext(time.Now().UTC().Add(first)) timer := time.NewTimer(first) defer timer.Stop() @@ -161,7 +161,7 @@ func (m *Manager) loop(ctx context.Context, sl *siteLoop) { // Exponential backoff on failure streaks: interval × 2^streak, // capped at 4× (spec Q19). delay := backoff(sl.interval, streak) - sl.setNext(time.Now().Add(delay)) + sl.setNext(time.Now().UTC().Add(delay)) timer.Reset(delay) } } @@ -278,6 +278,15 @@ func (m *Manager) Status() []SiteStatus { return out } +// Config returns the live config under lock. Reload swaps the pointer +// wholesale, so callers always observe the current sites/upstreams/proxies — +// used by the admin /vhost endpoint to generate nginx config on demand. +func (m *Manager) Config() *config.Config { + m.mu.Lock() + defer m.mu.Unlock() + return m.cfg +} + // Reload applies a freshly validated config: diff-based per spec §6. // The caller guarantees newCfg passed validation — a config that fails // validation must never reach here (reload rejected wholesale). diff --git a/nginxpilot/internal/manager/manager_test.go b/nginxpilot/internal/manager/manager_test.go index 7e8f2e91..99dbf16d 100644 --- a/nginxpilot/internal/manager/manager_test.go +++ b/nginxpilot/internal/manager/manager_test.go @@ -303,3 +303,61 @@ func TestReconcileStateSkipsWhenCurrentExists(t *testing.T) { t.Errorf("DeployedRef must not be cleared when current/ exists; got %q", got.DeployedRef) } } + +// TestStatusNextSyncIsUTC guards the /status JSON timezone consistency: the +// loop must record next_sync in UTC so it matches last_success / last_error_time +// (both stored as UTC) rather than emitting a local-offset timestamp in the +// same payload. +func TestStatusNextSyncIsUTC(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + store, err := state.NewStore(dir) + if err != nil { + t.Fatal(err) + } + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + // A long interval keeps the first tick far in the future: the loop sets + // next_sync at startup and then blocks, so syncFn never runs in this window. + site := config.Site{ + Domain: "example.com", + Source: config.Source{Type: "git", URL: "fake://v1", Interval: config.Duration(time.Hour)}, + } + m := &Manager{ + log: logger, + store: store, + cfg: &config.Config{DataDir: dir, Sites: []config.Site{site}}, + deployer: deploy.New(dir, logger), + loops: map[string]*siteLoop{}, + syncFn: func(context.Context, config.Site, config.Defaults, string, *state.Store, *deploy.Deployer, *slog.Logger) (*state.SiteState, error) { + return &state.SiteState{Domain: "example.com"}, nil + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + m.mu.Lock() + m.ctx = ctx + m.startLoop(site) + m.mu.Unlock() + + var ns *time.Time + for i := 0; i < 200; i++ { + s := m.Status() + if len(s) == 1 && s[0].NextSync != nil { + ns = s[0].NextSync + break + } + time.Sleep(time.Millisecond) + } + if ns == nil { + t.Fatal("next_sync was never set") + } + if loc := ns.Location(); loc != time.UTC { + t.Fatalf("next_sync must be UTC for consistency with last_success/last_error_time, got %v", loc) + } + + cancel() + m.wg.Wait() +} diff --git a/nginxpilot/internal/nginxconf/nginxconf.go b/nginxpilot/internal/nginxconf/nginxconf.go new file mode 100644 index 00000000..4f374857 --- /dev/null +++ b/nginxpilot/internal/nginxconf/nginxconf.go @@ -0,0 +1,217 @@ +// Package nginxconf generates nginx server-block / upstream snippets from +// the daemon's config entities. It is a pure text generator: the daemon never +// writes nginx config nor reloads nginx (zero request-path coupling). Both the +// `print-vhost` CLI and the admin GET /vhost/ endpoint render through +// here so the two surfaces never drift. +package nginxconf + +import ( + "errors" + "fmt" + "path/filepath" + "strconv" + "strings" + + "github.com/kalevski/toolcase/nginxpilot/internal/config" +) + +// ErrUnknownDomain is returned by Vhost when no site or proxy owns the domain. +var ErrUnknownDomain = errors.New("domain is not configured") + +// Vhost renders the nginx snippet for one domain. A static site yields a +// content-serving server{} block; a reverse proxy yields the upstream{} +// block(s) it references plus a proxy_pass server{} block. +func Vhost(cfg *config.Config, domain string) (string, error) { + for i := range cfg.Sites { + if cfg.Sites[i].Domain == domain { + return StaticVhost(cfg.DataDir, domain), nil + } + } + for i := range cfg.Proxies { + if cfg.Proxies[i].Domain == domain { + return ProxyVhost(cfg, &cfg.Proxies[i]), nil + } + } + return "", fmt.Errorf("%q: %w", domain, ErrUnknownDomain) +} + +// StaticVhost renders the content-serving server block for a static site. +func StaticVhost(dataDir, domain string) string { + root := filepath.Join(dataDir, "sites", domain, "current") + return fmt.Sprintf(`# nginx vhost for %[1]s — generated by nginxpilot print-vhost +# Paste into /etc/nginx/sites-available/%[1]s.conf (or conf.d/), adapt, then: +# nginx -t && nginx -s reload +# +# Onboarding tip: run `+"`nginxpilot sync %[1]s`"+` once BEFORE +# enabling this vhost so `+"`current`"+` exists and nginx doesn't 404. + +server { + listen 80; + listen [::]:80; + server_name %[1]s; + + # Content is deployed atomically by nginxpilot; the `+"`current`"+` + # symlink is swapped via rename(2) — no nginx reload needed on updates. + root %[2]s; + index index.html; + + location / { + try_files $uri $uri/ =404; + # For single-page apps use instead: + # try_files $uri $uri/ /index.html; + } + + # If your CI pre-compresses assets (.gz next to each file): + # gzip_static on; + + # Keep directory listings off — deployed content is source-controlled, + # anything unexpected should 404, not enumerate. + autoindex off; +} + +# TLS: terminate with certbot/acme.sh (nginxpilot does not manage +# certificates), e.g.: +# certbot --nginx -d %[1]s +`, domain, root) +} + +// ProxyVhost renders the upstream{} block(s) a proxy references followed by +// its server{} block. The snippet is self-contained (paste-and-go); when an +// upstream is shared across proxies, emit it once and drop the duplicate. +func ProxyVhost(cfg *config.Config, p *config.Proxy) string { + var b strings.Builder + port := p.ListenPort() + + fmt.Fprintf(&b, "# nginx reverse proxy for %[1]s — generated by nginxpilot print-vhost\n", p.Domain) + fmt.Fprintf(&b, "# Paste into /etc/nginx/sites-available/%[1]s.conf (or conf.d/), adapt, then:\n", p.Domain) + b.WriteString("# nginx -t && nginx -s reload\n\n") + + // Referenced named upstreams, in first-seen order, emitted self-contained. + byName := map[string]*config.Upstream{} + for i := range cfg.Upstreams { + byName[cfg.Upstreams[i].Name] = &cfg.Upstreams[i] + } + for _, name := range referencedUpstreams(p) { + if u := byName[name]; u != nil { + b.WriteString(UpstreamBlock(u)) + b.WriteByte('\n') + } + } + + fmt.Fprintf(&b, "server {\n listen %d;\n listen [::]:%d;\n server_name %s;\n", port, port, p.Domain) + if p.ClientMaxBodySize > 0 { + fmt.Fprintf(&b, " client_max_body_size %s;\n", p.ClientMaxBodySize) + } + + for _, loc := range effectiveLocations(p) { + writeLocation(&b, p, loc) + } + + b.WriteString("}\n\n") + fmt.Fprintf(&b, "# TLS: terminate with certbot/acme.sh (nginxpilot does not manage\n# certificates), e.g.:\n# certbot --nginx -d %s\n", p.Domain) + return b.String() +} + +// UpstreamBlock renders one nginx upstream{} block. +func UpstreamBlock(u *config.Upstream) string { + var b strings.Builder + fmt.Fprintf(&b, "upstream %s {\n", u.Name) + switch u.Balancer { + case config.BalancerLeastConn: + b.WriteString(" least_conn;\n") + case config.BalancerIPHash: + b.WriteString(" ip_hash;\n") + } + for _, s := range u.Servers { + b.WriteString(" server " + s.Address) + if s.Weight > 0 { + b.WriteString(" weight=" + strconv.Itoa(s.Weight)) + } + if s.MaxFails != nil { + b.WriteString(" max_fails=" + strconv.Itoa(*s.MaxFails)) + } + if s.FailTimeout > 0 { + b.WriteString(" fail_timeout=" + s.FailTimeout.String()) + } + if s.Backup { + b.WriteString(" backup") + } + if s.Down { + b.WriteString(" down") + } + b.WriteString(";\n") + } + if u.Keepalive > 0 { + fmt.Fprintf(&b, " keepalive %d;\n", u.Keepalive) + } + b.WriteString("}\n") + return b.String() +} + +// writeLocation renders one location{} block with proxy_pass + the standard +// forwarding headers (and WebSocket upgrade headers when requested). +func writeLocation(b *strings.Builder, p *config.Proxy, loc config.ProxyLocation) { + target := proxyPassTarget(p, loc) + fmt.Fprintf(b, "\n location %s {\n", loc.Path) + fmt.Fprintf(b, " proxy_pass %s;\n", target) + b.WriteString(" proxy_http_version 1.1;\n") + b.WriteString(" proxy_set_header Host $host;\n") + b.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n") + b.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n") + b.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n") + if loc.Websocket { + b.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") + b.WriteString(" proxy_set_header Connection \"upgrade\";\n") + } + if p.ConnectTimeout > 0 { + fmt.Fprintf(b, " proxy_connect_timeout %s;\n", p.ConnectTimeout) + } + if p.ReadTimeout > 0 { + fmt.Fprintf(b, " proxy_read_timeout %s;\n", p.ReadTimeout) + } + if p.SendTimeout > 0 { + fmt.Fprintf(b, " proxy_send_timeout %s;\n", p.SendTimeout) + } + b.WriteString(" }\n") +} + +// proxyPassTarget resolves the proxy_pass argument for a location: a named +// upstream becomes http://, an inline pass is used verbatim. Location +// settings override the proxy default. +func proxyPassTarget(p *config.Proxy, loc config.ProxyLocation) string { + up, pass := loc.Upstream, loc.Pass + if up == "" && pass == "" { + up, pass = p.Upstream, p.Pass + } + if up != "" { + return "http://" + up + } + return pass +} + +// effectiveLocations returns the proxy's locations, defaulting to a single +// "/" location using the proxy default when none are declared. +func effectiveLocations(p *config.Proxy) []config.ProxyLocation { + if len(p.Locations) == 0 { + return []config.ProxyLocation{{Path: "/"}} + } + return p.Locations +} + +// referencedUpstreams returns the distinct named upstreams a proxy uses, +// in first-seen order (proxy default first, then per-location overrides). +func referencedUpstreams(p *config.Proxy) []string { + var out []string + seen := map[string]bool{} + add := func(name string) { + if name != "" && !seen[name] { + seen[name] = true + out = append(out, name) + } + } + add(p.Upstream) + for _, loc := range p.Locations { + add(loc.Upstream) + } + return out +} diff --git a/nginxpilot/internal/nginxconf/nginxconf_test.go b/nginxpilot/internal/nginxconf/nginxconf_test.go new file mode 100644 index 00000000..670302de --- /dev/null +++ b/nginxpilot/internal/nginxconf/nginxconf_test.go @@ -0,0 +1,139 @@ +package nginxconf + +import ( + "errors" + "strings" + "testing" + + "github.com/kalevski/toolcase/nginxpilot/internal/config" +) + +func TestVhostStaticSite(t *testing.T) { + cfg := &config.Config{ + DataDir: "/var/lib/nginxpilot", + Sites: []config.Site{{Domain: "example.com"}}, + } + out, err := Vhost(cfg, "example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for _, want := range []string{ + "server_name example.com;", + "root /var/lib/nginxpilot/sites/example.com/current;", + "try_files $uri $uri/ =404;", + } { + if !strings.Contains(out, want) { + t.Errorf("static vhost missing %q\n%s", want, out) + } + } +} + +func TestVhostUnknownDomain(t *testing.T) { + cfg := &config.Config{} + _, err := Vhost(cfg, "nope.com") + if !errors.Is(err, ErrUnknownDomain) { + t.Fatalf("want ErrUnknownDomain, got %v", err) + } +} + +func TestProxyVhostNamedUpstream(t *testing.T) { + maxFails := 3 + cfg := &config.Config{ + Upstreams: []config.Upstream{{ + Name: "api_pool", + Balancer: config.BalancerLeastConn, + Keepalive: 32, + Servers: []config.UpstreamServer{ + {Address: "10.0.0.1:8080", Weight: 2, MaxFails: &maxFails, FailTimeout: config.Duration(30e9)}, + {Address: "10.0.0.2:8080", Backup: true}, + }, + }}, + Proxies: []config.Proxy{{ + Domain: "api.example.com", + Upstream: "api_pool", + }}, + } + out, err := Vhost(cfg, "api.example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for _, want := range []string{ + "upstream api_pool {", + "least_conn;", + "server 10.0.0.1:8080 weight=2 max_fails=3 fail_timeout=30s;", + "server 10.0.0.2:8080 backup;", + "keepalive 32;", + "server_name api.example.com;", + "location / {", + "proxy_pass http://api_pool;", + "proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;", + } { + if !strings.Contains(out, want) { + t.Errorf("proxy vhost missing %q\n%s", want, out) + } + } +} + +func TestProxyVhostInlinePass(t *testing.T) { + cfg := &config.Config{ + Proxies: []config.Proxy{{ + Domain: "svc.example.com", + Pass: "http://127.0.0.1:9000", + }}, + } + out, err := Vhost(cfg, "svc.example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(out, "proxy_pass http://127.0.0.1:9000;") { + t.Errorf("missing inline proxy_pass\n%s", out) + } + if strings.Contains(out, "upstream ") { + t.Errorf("inline pass must not emit an upstream block\n%s", out) + } +} + +func TestProxyVhostWebsocketAndLocations(t *testing.T) { + cfg := &config.Config{ + Proxies: []config.Proxy{{ + Domain: "app.example.com", + Locations: []config.ProxyLocation{ + {Path: "/", Pass: "http://127.0.0.1:3000"}, + {Path: "/ws", Pass: "http://127.0.0.1:3001", Websocket: true}, + }, + }}, + } + out, err := Vhost(cfg, "app.example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(out, "location /ws {") { + t.Errorf("missing /ws location\n%s", out) + } + if !strings.Contains(out, `proxy_set_header Connection "upgrade";`) { + t.Errorf("websocket location missing Connection upgrade header\n%s", out) + } + if strings.Count(out, "proxy_set_header Upgrade $http_upgrade;") != 1 { + t.Errorf("Upgrade header should appear only in the websocket location\n%s", out) + } +} + +func TestReferencedUpstreamsDedup(t *testing.T) { + p := &config.Proxy{ + Upstream: "pool_a", + Locations: []config.ProxyLocation{ + {Path: "/", Upstream: "pool_a"}, + {Path: "/api", Upstream: "pool_b"}, + }, + } + got := referencedUpstreams(p) + want := []string{"pool_a", "pool_b"} + if len(got) != len(want) { + t.Fatalf("want %v, got %v", want, got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("want %v, got %v", want, got) + } + } +} diff --git a/nginxpilot/internal/source/httpzip/extract.go b/nginxpilot/internal/source/httpzip/extract.go index 104649b7..7d7ecf79 100644 --- a/nginxpilot/internal/source/httpzip/extract.go +++ b/nginxpilot/internal/source/httpzip/extract.go @@ -85,8 +85,12 @@ func (s *Syncer) extract(archivePath string, archiveSize int64, stagingDir strin return fmt.Errorf("extract %q: %w", f.Name, err) } writtenTotal += n - // Belt-and-braces: actual bytes may exceed declared sizes on a - // crafted archive; enforce the cap on real output too. + // Belt-and-braces backstop: the archive (CD-declared sizes) is + // pre-flighted above, and the stdlib zip reader already rejects an + // entry whose real content disagrees with its declared size/CRC, so + // a size-lying archive fails inside writeEntry before reaching here. + // This cap on actual output is the last line of defence in case that + // ever changes (e.g. a zip64 edge case). if writtenTotal > maxUncompressed { return fmt.Errorf("limit exceeded: max_uncompressed_size (%s)", s.limits.MaxUncompressedSize) } diff --git a/nginxpilot/internal/source/httpzip/extract_test.go b/nginxpilot/internal/source/httpzip/extract_test.go index 5137e7cb..30c9a351 100644 --- a/nginxpilot/internal/source/httpzip/extract_test.go +++ b/nginxpilot/internal/source/httpzip/extract_test.go @@ -358,16 +358,22 @@ func TestExtractOK(t *testing.T) { } } -// TestExtractBeltAndBraces verifies the second uncompressed-size guard -// (extract.go:88-92) fires when a crafted archive declares a small -// uncompressed size in the central directory header but actually contains -// more bytes — the declared-total pre-flight passes, but actual writes exceed -// the cap. -func TestExtractBeltAndBraces(t *testing.T) { +// TestExtractRejectsSizeTamperedArchive verifies the security property the +// uncompressed-size guards exist to enforce: an archive that lies about its +// uncompressed size — declaring a tiny size in the central directory to slip +// past the declared-total pre-flight while actually holding far more bytes — +// cannot smuggle oversized content into staging. +// +// In practice the stdlib zip reader detects the size/CRC mismatch on read and +// fails the copy (so the extract.go belt-and-braces cap is an unreachable +// backstop, not the active defence here); either way extraction must error and +// no oversized file may land in staging. +func TestExtractRejectsSizeTamperedArchive(t *testing.T) { const maxUncomp = 10 const fakeSize = 5 // claimed in central directory — passes pre-flight const realSize = 100 // actual decompressed bytes — exceeds cap + dest := t.TempDir() path, size := buildZipWithFakeDeclaredSize(t, "evil.bin", bytes.Repeat([]byte("X"), realSize), fakeSize) @@ -378,11 +384,12 @@ func TestExtractBeltAndBraces(t *testing.T) { MaxArchiveSize: config.DefaultMaxArchiveSize, }.Effective(), nil) - err := s.extract(path, size, t.TempDir()) + err := s.extract(path, size, dest) if err == nil { - t.Fatal("expected belt-and-braces size error, got nil") + t.Fatal("expected size-tampered archive to be rejected, got nil") } - if !strings.Contains(err.Error(), "max_uncompressed_size") { - t.Fatalf("expected max_uncompressed_size error, got: %v", err) + // Whatever the failure mode, no oversized content may have been committed. + if fi, statErr := os.Stat(filepath.Join(dest, "evil.bin")); statErr == nil && fi.Size() > maxUncomp { + t.Fatalf("oversized file written despite rejection: %d bytes", fi.Size()) } } diff --git a/node/src/AtlasBuilder.ts b/node/src/AtlasBuilder.ts index 76529b38..c91d908d 100644 --- a/node/src/AtlasBuilder.ts +++ b/node/src/AtlasBuilder.ts @@ -240,8 +240,10 @@ export class AtlasBuilder { height: page.height, channels: 4, background: this.options.background ?? { r: 0, g: 0, b: 0, alpha: 0 } - } - }, { limitInputPixels: this.options.maxInputPixels ?? 50_000_000, failOn: 'error' }).composite(composites) + }, + limitInputPixels: this.options.maxInputPixels ?? 50_000_000, + failOn: 'error' + }).composite(composites) const composedBuffer = await canvas.png().toBuffer() let writer = ImageProcessor.fromBuffer(composedBuffer).format({ diff --git a/node/src/main.iso.ts b/node/src/main.iso.ts index 6d7de144..2836b5e6 100644 --- a/node/src/main.iso.ts +++ b/node/src/main.iso.ts @@ -8,3 +8,6 @@ export * from './utils/pagination' export * from './utils/sanitize' export * from './BaseRepository' export * from './EntityService' +export { defineOAuth2Provider, type OAuth2ProviderConfig, type OAuth2Tokens, type OAuth2Profile, type OAuth2FlowState, type ClientAuthMethod } from './oauth2/types' +export { buildAuthorizeURL, type BuildAuthorizeURLInput } from './oauth2/flow' +export { parseGitHubProfile, parseStandardOIDCProfile, parseDiscordProfile, type ParseGitHubProfileInput, type ParseStandardOIDCProfileInput, type ParseDiscordProfileInput } from './oauth2/profiles' diff --git a/node/src/oauth2/oidc.ts b/node/src/oauth2/oidc.ts index 2a38c6d1..500c60d3 100644 --- a/node/src/oauth2/oidc.ts +++ b/node/src/oauth2/oidc.ts @@ -98,8 +98,9 @@ let jwksCache = new Cache(async (jwksUri: string) => createJwksGetter(jwksU async function createJwksGetter(jwksUri: string, opts: HttpOptions): Promise { const j = await loadJose() + // jose v5's createRemoteJWKSet has no custom-fetch hook (customFetch is a v6 export), + // so opts.fetchImpl cannot be threaded into JWKS retrieval here — it uses global fetch. return j.createRemoteJWKSet(new URL(jwksUri), { - [j.customFetch]: opts.fetchImpl, timeoutDuration: opts.timeoutMs, }) } diff --git a/package-lock.json b/package-lock.json index 848fa3a0..5a1a61ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,19 +47,6 @@ "node": ">=18" } }, - "base/node_modules/@types/node": { - "version": "20.19.43", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "base/node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - }, "examples": { "name": "@toolcase/examples", "license": "MIT", @@ -88,6 +75,9 @@ "name": "@toolcase/logging", "version": "5.0.0", "license": "MIT", + "devDependencies": { + "@types/node": "^20.0.0" + }, "engines": { "node": ">=18" } @@ -141,8 +131,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { @@ -156,8 +144,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -166,8 +152,6 @@ }, "node_modules/@babel/core": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { @@ -197,8 +181,6 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -207,8 +189,6 @@ }, "node_modules/@babel/generator": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -224,8 +204,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { @@ -241,8 +219,6 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -251,8 +227,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -261,8 +235,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { @@ -275,8 +247,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { @@ -293,8 +263,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -303,8 +271,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -313,8 +279,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -323,8 +287,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -333,8 +295,6 @@ }, "node_modules/@babel/helpers": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { @@ -347,8 +307,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { @@ -363,8 +321,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "dev": true, "license": "MIT", "dependencies": { @@ -379,8 +335,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -395,8 +349,6 @@ }, "node_modules/@babel/template": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { @@ -410,8 +362,6 @@ }, "node_modules/@babel/traverse": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { @@ -429,8 +379,6 @@ }, "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { @@ -441,3535 +389,1653 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array": { + "version": "0.23.5", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/core": { + "version": "1.2.1", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/js": { + "version": "10.0.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/object-schema": { + "version": "3.0.5", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], - "engines": { - "node": ">=18" + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.20.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "license": "MIT" + }, + "node_modules/@fastify/cors": { + "version": "11.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], - "engines": { - "node": ">=18" + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@fastify/error": { + "version": "4.2.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], - "engines": { - "node": ">=18" + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", - "optional": true, - "os": [ - "linux" + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], + "node_modules/@humanfs/node": { + "version": "0.16.8", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/types": { + "version": "0.15.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", "license": "MIT", "optional": true, - "os": [ - "netbsd" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "openbsd" + "darwin" ], "engines": { - "node": ">=18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "openbsd" + "darwin" ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "16.2.9", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.9", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.9.tgz", + "integrity": "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", "optional": true, "os": [ - "sunos" + "darwin" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.9.tgz", + "integrity": "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==", "cpu": [ "arm64" ], - "dev": true, - "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.9.tgz", + "integrity": "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==", "cpu": [ - "ia32" + "arm64" ], - "dev": true, - "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.9.tgz", + "integrity": "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.9.tgz", + "integrity": "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">= 10" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.9.tgz", + "integrity": "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 10" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.9.tgz", + "integrity": "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">= 10" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "node_modules/@pinojs/redact": { + "version": "0.4.0", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "node_modules/@publint/pack": { + "version": "0.1.5", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^1.2.1" + "tinyexec": "^1.2.4" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@redis/bloom": { + "version": "5.12.1", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@redis/client": { + "version": "5.12.1", "dev": true, "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "dependencies": { + "cluster-key-slot": "1.1.2" }, - "funding": { - "url": "https://eslint.org/donate" + "engines": { + "node": ">= 18.19.0" }, "peerDependencies": { - "eslint": "^10.0.0" + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" }, "peerDependenciesMeta": { - "eslint": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { "optional": true } } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "node_modules/@redis/json": { + "version": "5.12.1", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" + "node": ">= 18.19.0" }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "peerDependencies": { + "@redis/client": "^5.12.1" } }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", - "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "node_modules/@redis/search": { + "version": "5.12.1", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" } }, - "node_modules/@fastify/ajv-compiler/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/@redis/time-series": { + "version": "5.12.1", "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "engines": { + "node": ">= 18.19.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "@redis/client": "^5.12.1" } }, - "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", "dev": true, "license": "MIT" }, - "node_modules/@fastify/cors": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", - "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "toad-cache": "^3.7.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], "license": "MIT" }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", - "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "node_modules/@swc/helpers": { + "version": "0.5.15", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@toolcase/base": { + "resolved": "base", + "link": true + }, + "node_modules/@toolcase/examples": { + "resolved": "examples", + "link": true + }, + "node_modules/@toolcase/logging": { + "resolved": "logging", + "link": true + }, + "node_modules/@toolcase/node": { + "resolved": "node", + "link": true + }, + "node_modules/@toolcase/phaser-plus": { + "resolved": "phaser-plus", + "link": true + }, + "node_modules/@toolcase/serializer": { + "resolved": "serializer", + "link": true + }, + "node_modules/@toolcase/taskforge": { + "resolved": "taskforge", + "link": true + }, + "node_modules/@toolcase/web-components": { + "resolved": "web-components", + "link": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], "license": "MIT", "dependencies": { - "fast-json-stringify": "^6.0.0" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@fastify/forwarded": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", - "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", - "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "node_modules/@types/babel__template": { + "version": "7.4.4", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], "license": "MIT", "dependencies": { - "dequal": "^2.0.3" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@fastify/proxy-addr": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", - "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], "license": "MIT", "dependencies": { - "@fastify/forwarded": "^3.0.0", - "ipaddr.js": "^2.1.0" + "@babel/types": "^7.28.2" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@types/chai": { + "version": "5.2.3", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "dev": true, + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" + "undici-types": "~6.21.0" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@types/react": { + "version": "19.2.17", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@types/react-dom": { + "version": "19.2.3", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.0", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/type-utils": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=18.18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.62.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, "license": "MIT", - "optional": true, "engines": { - "node": ">=18" + "node": ">= 4" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/parser": { + "version": "8.62.0", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.0", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.0", + "@typescript-eslint/types": "^8.62.0", + "debug": "^4.4.3" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/types": { + "version": "8.62.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.0", + "@typescript-eslint/tsconfig-utils": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.62.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/expect": { + "version": "4.1.9", "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], + "node_modules/@vitest/mocker": { + "version": "4.1.9", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/vitest" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@vitest/runner": { + "version": "4.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], + "node_modules/@vitest/spy": { + "version": "4.1.9", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, + "license": "MIT", "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.9", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], + "node_modules/abstract-logging": { + "version": "2.0.1", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], + "node_modules/ajv-formats": { + "version": "3.0.1", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "ajv": "^8.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, + "license": "MIT", "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=8" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], + "node_modules/ansi-styles": { + "version": "4.3.0", "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=12" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/atomic-sleep": { + "version": "1.0.0", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=8.0.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/avvio": { + "version": "9.2.0", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/balanced-match": { + "version": "4.0.4", "dev": true, "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/brace-expansion": { + "version": "5.0.6", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@next/env": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz", - "integrity": "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.9.tgz", - "integrity": "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==", - "cpu": [ - "arm64" + "node_modules/browserslist": { + "version": "4.28.4", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, "engines": { - "node": ">= 10" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.9.tgz", - "integrity": "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==", - "cpu": [ - "x64" - ], + "node_modules/bundle-require": { + "version": "5.1.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "load-tsconfig": "^0.2.3" + }, "engines": { - "node": ">= 10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.9.tgz", - "integrity": "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==", - "cpu": [ - "arm64" - ], + "node_modules/cac": { + "version": "6.7.14", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.9.tgz", - "integrity": "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==", - "cpu": [ - "arm64" + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.9.tgz", - "integrity": "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==", - "cpu": [ - "x64" - ], + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.9.tgz", - "integrity": "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==", - "cpu": [ - "x64" - ], + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.9.tgz", - "integrity": "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==", - "cpu": [ - "arm64" - ], + "node_modules/chokidar": { + "version": "4.0.3", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "readdirp": "^4.0.1" + }, "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.9.tgz", - "integrity": "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "node_modules/cluster-key-slot": { + "version": "1.1.2", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "node_modules/color": { + "version": "4.2.3", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" } }, - "node_modules/@publint/pack": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.5.tgz", - "integrity": "sha512-edgyN2pP07uXiP4tJs0s8KVmU8M8i60YPbbI0/WDeok1mIJHRXz+CgD8I0nelwDkoCh3EWL/G5kGfbuHjsdbvw==", + "node_modules/color-convert": { + "version": "2.0.1", "dev": true, "license": "MIT", "dependencies": { - "tinyexec": "^1.2.4" + "color-name": "~1.1.4" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://bjornlu.com/sponsor" + "node": ">=7.0.0" } }, - "node_modules/@redis/bloom": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", - "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/commander": { + "version": "4.1.1", "dev": true, "license": "MIT", "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "node": ">= 6" } }, - "node_modules/@redis/client": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", - "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", + "node_modules/concurrently": { + "version": "9.2.3", "dev": true, "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, - "engines": { - "node": ">= 18.19.0" + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, - "peerDependencies": { - "@node-rs/xxhash": "^1.1.0", - "@opentelemetry/api": ">=1 <2" + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "@node-rs/xxhash": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - } + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/@redis/json": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", - "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", + "node_modules/confbox": { + "version": "0.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", "dev": true, "license": "MIT", "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/@redis/search": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", - "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", + "node_modules/convert-source-map": { + "version": "2.0.0", "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", "license": "MIT", "engines": { - "node": ">= 18.19.0" + "node": ">=18" }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@redis/time-series": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", - "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", + "node_modules/cross-spawn": { + "version": "7.0.6", "dev": true, "license": "MIT", - "engines": { - "node": ">= 18.19.0" + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "engines": { + "node": ">= 8" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "node_modules/csstype": { + "version": "3.2.3", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], + "node_modules/debug": { + "version": "4.4.3", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], + "node_modules/deep-is": { + "version": "0.1.4", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], + "node_modules/dequal": { + "version": "2.0.3", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "engines": { + "node": ">=6" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "node_modules/detect-libc": { + "version": "2.1.2", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], + "node_modules/electron-to-chromium": { + "version": "1.5.378", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "license": "ISC" }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], + "node_modules/emoji-regex": { + "version": "8.0.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], + "node_modules/es-module-lexer": { + "version": "2.1.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@toolcase/base": { - "resolved": "base", - "link": true - }, - "node_modules/@toolcase/examples": { - "resolved": "examples", - "link": true - }, - "node_modules/@toolcase/logging": { - "resolved": "logging", - "link": true - }, - "node_modules/@toolcase/node": { - "resolved": "node", - "link": true - }, - "node_modules/@toolcase/phaser-plus": { - "resolved": "phaser-plus", - "link": true - }, - "node_modules/@toolcase/serializer": { - "resolved": "serializer", - "link": true - }, - "node_modules/@toolcase/taskforge": { - "resolved": "taskforge", - "link": true - }, - "node_modules/@toolcase/web-components": { - "resolved": "web-components", - "link": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", - "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/type-utils": "8.62.0", - "@typescript-eslint/utils": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.62.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", - "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", - "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.0", - "@typescript-eslint/types": "^8.62.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", - "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", - "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", - "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/utils": "8.62.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", - "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", - "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.62.0", - "@typescript-eslint/tsconfig-utils": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", - "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", - "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.62.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.9", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/avvio": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", - "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.38", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bootstrap-icons": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", - "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concurrently": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", - "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.4", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.378", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", - "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", - "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stringify": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", - "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0", - "json-schema-ref-resolver": "^3.0.0", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-json-stringify/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastify": { - "version": "5.8.5", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", - "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^4.0.5", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "@fastify/proxy-addr": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^6.0.0", - "find-my-way": "^9.0.0", - "light-my-request": "^6.0.0", - "pino": "^9.14.0 || ^10.1.0", - "process-warning": "^5.0.0", - "rfdc": "^1.3.1", - "secure-json-parse": "^4.0.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/esbuild": { + "version": "0.28.1", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=16.0.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/find-my-way": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", - "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "node_modules/escalade": { + "version": "3.2.0", "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^5.0.0" - }, "engines": { - "node": ">=20" + "node": ">=6" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, "engines": { "node": ">=10" }, @@ -3977,232 +2043,196 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/eslint": { + "version": "10.5.0", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/eslint-config-prettier": { + "version": "10.1.8", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/eslint-scope": { + "version": "9.1.2", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause", "dependencies": { - "is-glob": "^4.0.3" + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.7.tgz", - "integrity": "sha512-47Xb+LFbZ/ZIjQMj6Q5J3IfK7PJFuqRdFOC9FpGgRTK6U2dAEVmkR9hp58qU4FpYux5YXpneDwkj2EP6lppzFA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 10" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/espree": { + "version": "11.2.0", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + }, + "node_modules/esquery": { + "version": "1.7.0", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">=8" + "node": ">=0.10" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/esrecurse": { + "version": "4.3.0", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "is-extglob": "^2.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/estraverse": { + "version": "5.3.0", "dev": true, - "license": "ISC" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "node_modules/estree-walker": { + "version": "3.0.3", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "node_modules/esutils": { + "version": "2.0.3", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, + "node_modules/eventemitter3": { + "version": "5.0.4", "license": "MIT" }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/expect-type": { + "version": "1.3.0", "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": ">=12.0.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", "dev": true, "license": "MIT" }, - "node_modules/json-schema-ref-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", - "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", "dev": true, "funding": [ { @@ -4216,64 +2246,49 @@ ], "license": "MIT", "dependencies": { - "dequal": "^2.0.3" + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.20.0", "dev": true, "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } + "license": "MIT" }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" + "fast-decode-uri-component": "^1.0.1" } }, - "node_modules/light-my-request": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", - "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "node_modules/fast-uri": { + "version": "3.1.2", "dev": true, "funding": [ { @@ -4285,17 +2300,10 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" - } + "license": "BSD-3-Clause" }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "node_modules/fastify": { + "version": "5.8.5", "dev": true, "funding": [ { @@ -4307,1325 +2315,904 @@ "url": "https://opencollective.com/fastify" } ], - "license": "MIT" - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-static": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.21.0.tgz", - "integrity": "sha512-6248z2/4sEyKkYAPPUYxOPiB2RCfMmLdMHuoOhsTFnoD40ixAoHmTVhOPux8ADa1NTBmzpEKF7WNePm+Ms503Q==", - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" } }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "node_modules/fastify-plugin": { + "version": "5.1.0", + "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } + "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/fastq": { + "version": "1.20.1", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } }, - "node_modules/next": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.9.tgz", - "integrity": "sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==", + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, "license": "MIT", - "dependencies": { - "@next/env": "16.2.9", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.9", - "@next/swc-darwin-x64": "16.2.9", - "@next/swc-linux-arm64-gnu": "16.2.9", - "@next/swc-linux-arm64-musl": "16.2.9", - "@next/swc-linux-x64-gnu": "16.2.9", - "@next/swc-linux-x64-musl": "16.2.9", - "@next/swc-win32-arm64-msvc": "16.2.9", - "@next/swc-win32-x64-msvc": "16.2.9", - "sharp": "^0.34.5" + "node": ">=12.0.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { + "picomatch": { "optional": true } } }, - "node_modules/next/node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/next/node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/next/node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/next/node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=16.0.0" } }, - "node_modules/next/node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/find-my-way": { + "version": "9.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" } }, - "node_modules/next/node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/next/node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" } }, - "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, - "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" }, - "node_modules/next/node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/next/node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "node": ">=6.9.0" } }, - "node_modules/next/node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "engines": { + "node": ">=10.13.0" } }, - "node_modules/next/node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "node": ">=8" } }, - "node_modules/next/node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "node": ">= 4" } }, - "node_modules/next/node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/immutable": { + "version": "5.1.7", + "devOptional": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "node": ">=0.8.19" } }, - "node_modules/next/node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">= 10" } }, - "node_modules/next/node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/is-arrayish": { + "version": "0.3.4", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=0.10.0" } }, - "node_modules/next/node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=8" } }, - "node_modules/next/node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "is-extglob": "^2.1.1" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "5.10.0", + "dev": true, + "license": "MIT", "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "url": "https://github.com/sponsors/panva" } }, - "node_modules/node-releases": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", - "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "node_modules/joycon": { + "version": "3.1.1", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", "dev": true, "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", "dev": true, "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], "license": "MIT", - "engines": { - "node": ">=12.20.0" + "dependencies": { + "dequal": "^2.0.3" } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", "dev": true, "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">=14.0.0" + "node": ">=6" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "type-check": "~0.4.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/light-my-request": { + "version": "6.6.0", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, "engines": { - "node": ">=10" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "node_modules/lines-and-columns": { + "version": "1.2.4", "dev": true, "license": "MIT" }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/load-tsconfig": { + "version": "0.2.5", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/locate-path": { + "version": "6.0.0", "dev": true, "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" + "node_modules/long": { + "version": "5.3.2", + "license": "Apache-2.0" }, - "node_modules/phaser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/phaser/-/phaser-4.2.0.tgz", - "integrity": "sha512-9nSQZs4CJ9+V96i2mRC3BBStBcsQWGJJBRpdFYSbpL40/i/QM+wLofaCYMsxNyDk8WJOlHGZUh3uVRKa7lj6yg==", - "license": "MIT", + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", "dependencies": { - "eventemitter3": "^5.0.4" + "yallist": "^3.0.2" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/lucide-static": { + "version": "1.21.0", "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/magic-string": { + "version": "0.30.21", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/pino": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "node_modules/minimatch": { + "version": "10.2.5", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^4.0.0" + "brace-expansion": "^5.0.5" }, - "bin": { - "pino": "bin.js" + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "node_modules/mlly": { + "version": "1.8.2", "dev": true, "license": "MIT", "dependencies": { - "split2": "^4.0.0" + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" } }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "dev": true, - "license": "MIT" - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "node_modules/mri": { + "version": "1.2.0", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "node_modules/natural-compare": { + "version": "1.4.0", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.9", "license": "MIT", "dependencies": { - "lilconfig": "^3.1.1" + "@next/env": "16.2.9", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" }, "engines": { - "node": ">= 18" + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.9", + "@next/swc-darwin-x64": "16.2.9", + "@next/swc-linux-arm64-gnu": "16.2.9", + "@next/swc-linux-arm64-musl": "16.2.9", + "@next/swc-linux-x64-gnu": "16.2.9", + "@next/swc-linux-x64-musl": "16.2.9", + "@next/swc-win32-arm64-msvc": "16.2.9", + "@next/swc-win32-x64-msvc": "16.2.9", + "sharp": "^0.34.5" }, "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" }, "peerDependenciesMeta": { - "jiti": { + "@opentelemetry/api": { "optional": true }, - "postcss": { + "@playwright/test": { "optional": true }, - "tsx": { + "babel-plugin-react-compiler": { "optional": true }, - "yaml": { + "sass": { "optional": true } } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, + "node_modules/next/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "cpu": [ + "arm64" ], - "license": "MIT" - }, - "node_modules/protobufjs": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.5.tgz", - "integrity": "sha512-zeE5LPpencAGXvsxyOYmEgJhxzHY8IsmPAFzstZVhDSVT8QH03q6gMZwZRaQGApevZbAL6u28ugs4CC+YKB2jQ==", - "license": "BSD-3-Clause", - "dependencies": { - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/publint": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.21.tgz", - "integrity": "sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==", - "dev": true, - "license": "MIT", + "node_modules/next/node_modules/sharp": { + "version": "0.34.5", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@publint/pack": "^0.1.4", - "package-manager-detector": "^1.6.0", - "picocolors": "^1.1.1", - "sade": "^1.8.1" - }, - "bin": { - "publint": "src/cli.js" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { - "node": ">=18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://bjornlu.com/sponsor" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/node-releases": { + "version": "2.0.48", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "node_modules/object-assign": { + "version": "4.1.1", "dev": true, - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "node_modules/obug": { + "version": "2.1.3", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12.20.0" } }, - "node_modules/react-router": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", - "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "dev": true, "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } + "node": ">=14.0.0" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/optionator": { + "version": "0.9.4", "dev": true, "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "node_modules/p-limit": { + "version": "3.1.0", "dev": true, "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">= 12.13.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/redis": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", - "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", + "node_modules/p-locate": { + "version": "5.0.0", "dev": true, "license": "MIT", "dependencies": { - "@redis/bloom": "5.12.1", - "@redis/client": "5.12.1", - "@redis/json": "5.12.1", - "@redis/search": "5.12.1", - "@redis/time-series": "5.12.1" + "p-limit": "^3.0.2" }, "engines": { - "node": ">= 18.19.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/package-manager-detector": { + "version": "1.6.0", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/path-exists": { + "version": "4.0.0", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/path-key": { + "version": "3.1.1", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "node_modules/pathe": { + "version": "2.0.3", "dev": true, + "license": "MIT" + }, + "node_modules/phaser": { + "version": "4.2.0", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "eventemitter3": "^5.0.4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/pino": { + "version": "10.3.1", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" }, "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" + "pino": "bin.js" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "node_modules/pino-abstract-transport": { + "version": "3.0.0", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "split2": "^4.0.0" } }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pirates": { + "version": "4.0.7", "dev": true, "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, "engines": { - "node": ">=6" + "node": ">= 6" } }, - "node_modules/safe-regex2": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", - "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "node_modules/pkg-types": { + "version": "1.3.1", "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/fastify" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "opencollective", - "url": "https://opencollective.com/fastify" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "ret": "~0.5.0" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, - "bin": { - "safe-regex2": "bin/safe-regex2.js" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=10" + "node": "^10 || ^12 || >=14" } }, - "node_modules/sass": { - "version": "1.101.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", - "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", - "devOptional": true, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "chokidar": "^5.0.0", - "immutable": "^5.1.5", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "lilconfig": "^3.1.1" }, "engines": { - "node": ">=20.19.0" + "node": ">= 18" }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/sass/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "devOptional": true, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">= 0.8.0" } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "devOptional": true, + }, + "node_modules/prettier": { + "version": "3.8.4", + "dev": true, "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, "engines": { - "node": ">= 20.19.0" + "node": ">=14" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "node_modules/process-warning": { + "version": "5.0.0", "dev": true, "funding": [ { @@ -5637,1073 +3224,891 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "MIT" }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/protobufjs": { + "version": "8.6.5", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" } }, - "node_modules/server-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", - "license": "MIT" - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "node_modules/publint": { + "version": "0.3.21", "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@publint/pack": "^0.1.4", + "package-manager-detector": "^1.6.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "url": "https://bjornlu.com/sponsor" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/punycode": { + "version": "2.3.1", "dev": true, "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/quick-format-unescaped": { + "version": "4.0.4", "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", - "dev": true, + "node_modules/react-dom": { + "version": "19.2.7", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "scheduler": "^0.27.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": "^19.2.7" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "node_modules/react-refresh": { + "version": "0.17.0", "dev": true, "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "dev": true, + "node_modules/react-router": { + "version": "7.18.0", "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "node_modules/readdirp": { + "version": "4.1.2", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", + "node_modules/real-require": { + "version": "0.2.0", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 12.13.0" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "node_modules/redis": { + "version": "5.12.1", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.12.1", + "@redis/client": "5.12.1", + "@redis/json": "5.12.1", + "@redis/search": "5.12.1", + "@redis/time-series": "5.12.1" + }, "engines": { - "node": ">= 10.x" + "node": ">= 18.19.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "node_modules/require-directory": { + "version": "2.1.1", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/require-from-string": { + "version": "2.0.2", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/resolve-from": { + "version": "5.0.0", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { "node": ">=8" } }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "node_modules/ret": { + "version": "0.5.0", + "dev": true, "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } + "node": ">=10" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/rfdc": { + "version": "1.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/rxjs": { + "version": "7.8.2", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "any-promise": "^1.0.0" + "tslib": "^2.1.0" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/sade": { + "version": "1.8.1", "dev": true, "license": "MIT", "dependencies": { - "thenify": ">= 3.1.0 < 4" + "mri": "^1.1.0" }, "engines": { - "node": ">=0.8" + "node": ">=6" } }, - "node_modules/thread-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", - "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "node_modules/safe-regex2": { + "version": "5.1.1", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { - "real-require": "^1.0.0" + "ret": "~0.5.0" }, - "engines": { - "node": ">=20" + "bin": { + "safe-regex2": "bin/safe-regex2.js" } }, - "node_modules/thread-stream/node_modules/real-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", - "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "node_modules/safe-stable-stringify": { + "version": "2.5.0", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, + "node_modules/sass": { + "version": "1.101.0", + "devOptional": true, "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.19.0" }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "devOptional": true, "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, "engines": { - "node": ">=14.0.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/toad-cache": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", - "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", - "dev": true, + "node_modules/sass/node_modules/readdirp": { + "version": "5.0.0", + "devOptional": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "node_modules/secure-json-parse": { + "version": "4.1.0", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "peerDependencies": { - "typescript": ">=4.8.4" + "engines": { + "node": ">=10" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" + "node_modules/server-only": { + "version": "0.0.1", + "license": "MIT" }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "license": "MIT" }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "node_modules/sharp": { + "version": "0.33.5", "dev": true, - "license": "MIT", + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, - "node_modules/tsup/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/shebang-command": { + "version": "2.0.0", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "node_modules/shebang-regex": { + "version": "3.0.0", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", "engines": { - "node": ">=14.17" + "node": ">=8" } }, - "node_modules/typescript-eslint": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", - "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", + "node_modules/shell-quote": { + "version": "1.8.4", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.0", - "@typescript-eslint/parser": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/utils": "8.62.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "node_modules/siginfo": { + "version": "2.0.0", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "node_modules/simple-swizzle": { + "version": "0.2.4", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "is-arrayish": "^0.3.1" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/sonic-boom": { + "version": "4.2.1", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "punycode": "^2.1.0" + "atomic-sleep": "^1.0.0" } }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "node_modules/source-map": { + "version": "0.7.6", "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, + "license": "BSD-3-Clause", "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">= 12" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">= 10.x" } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], + "node_modules/strip-ansi": { + "version": "6.0.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], + "node_modules/sucrase": { + "version": "3.35.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], + "node_modules/supports-color": { + "version": "8.1.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], + "node_modules/thenify": { + "version": "3.3.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "any-promise": "^1.0.0" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], + "node_modules/thenify-all": { + "version": "1.6.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, "engines": { - "node": ">=18" + "node": ">=0.8" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], + "node_modules/thread-stream": { + "version": "4.2.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "real-require": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], + "node_modules/tinybench": { + "version": "2.9.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], + "node_modules/tinyexec": { + "version": "1.2.4", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], + "node_modules/tinyglobby": { + "version": "0.2.17", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], + "node_modules/tinyrainbow": { + "version": "3.1.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], + "node_modules/toad-cache": { + "version": "3.7.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], + "node_modules/tree-kill": { + "version": "1.2.2", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "bin": { + "tree-kill": "cli.js" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], + "node_modules/ts-api-utils": { + "version": "2.5.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], + "node_modules/tsup/node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], + "node_modules/typescript": { + "version": "6.0.2", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], + "node_modules/typescript-eslint": { + "version": "8.62.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], + "node_modules/ufo": { + "version": "1.6.4", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], + "node_modules/undici-types": { + "version": "6.21.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], + "node_modules/update-browserslist-db": { + "version": "1.2.3", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], - "engines": { - "node": ">=18" + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], + "node_modules/uri-js": { + "version": "4.4.1", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], + "node_modules/vite": { + "version": "6.4.3", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=18" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">=18" @@ -6711,8 +4116,6 @@ }, "node_modules/vite/node_modules/esbuild": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6753,8 +4156,6 @@ }, "node_modules/vitest": { "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6843,8 +4244,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -6859,8 +4258,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -6876,8 +4273,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -6886,8 +4281,6 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6904,8 +4297,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -6914,15 +4305,11 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -6940,8 +4327,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -6950,8 +4335,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -6969,11 +4352,6 @@ "undici-types": "~6.21.0" } }, - "node/node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - }, "phaser-plus": { "name": "@toolcase/phaser-plus", "version": "5.0.0", @@ -7039,14 +4417,14 @@ "@toolcase/base": "file:../base", "@toolcase/web-components": "file:../web-components", "next": "^16.2.9", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", "server-only": "^0.0.1" }, "devDependencies": { "@types/node": "^22.5", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^19", + "@types/react-dom": "^19", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", "typescript": "^5.5.0" @@ -7071,23 +4449,6 @@ "undici-types": "~6.21.0" } }, - "taskforge/node_modules/@types/react": { - "version": "18.3.31", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "taskforge/node_modules/@types/react-dom": { - "version": "18.3.7", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, "taskforge/node_modules/balanced-match": { "version": "1.0.2", "dev": true, @@ -7429,34 +4790,6 @@ "node": "*" } }, - "taskforge/node_modules/react": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "taskforge/node_modules/react-dom": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "taskforge/node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "taskforge/node_modules/semver": { "version": "6.3.1", "dev": true, @@ -7477,11 +4810,6 @@ "node": ">=14.17" } }, - "taskforge/node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - }, "web-components": { "name": "@toolcase/web-components", "version": "5.0.0", diff --git a/phaser-plus/src/index.ts b/phaser-plus/src/index.ts index 324bab8e..2b3c2795 100644 --- a/phaser-plus/src/index.ts +++ b/phaser-plus/src/index.ts @@ -13,6 +13,7 @@ import type { Disposable } from './features/ServiceRegistry' import Layer from './features/Layer' import ObjectLayer from './features/ObjectLayer' import HTMLFeature from './features/HTMLFeature' +import ReactFeature from './features/ReactFeature' import SplitScreen from './features/SplitScreen' import GameObjectPool from './pool/GameObjectPool' @@ -109,6 +110,7 @@ export { Layer, ObjectLayer, HTMLFeature, + ReactFeature, SplitScreen, LogLevel diff --git a/phaser-plus/src/net/Transport.ts b/phaser-plus/src/net/Transport.ts index e3025edf..1b076c8c 100644 --- a/phaser-plus/src/net/Transport.ts +++ b/phaser-plus/src/net/Transport.ts @@ -151,6 +151,6 @@ export class WebSocketTransport implements Transport { } send(data: Uint8Array): void { - if (this.connected) this._socket!.send(data) + if (this.connected) this._socket!.send(data as Uint8Array) } } diff --git a/phaser-plus/src/structs/index.ts b/phaser-plus/src/structs/index.ts index cd0e6379..f8517391 100644 --- a/phaser-plus/src/structs/index.ts +++ b/phaser-plus/src/structs/index.ts @@ -1,9 +1,6 @@ -import { Spatial, Vec2, Easing, Random } from '@toolcase/base' +import { SpatialHash, Quadtree, Vec2, Easing, Random } from '@toolcase/base' import AABB from './AABB' import Transform from './Transform' -const SpatialHash = Spatial.SpatialHash -const Quadtree = Spatial.Quadtree - export { SpatialHash, Quadtree, Vec2, Easing, Random, AABB, Transform } export type { SpatialPoint, SpatialRect, EasingFn, Rect } from '@toolcase/base' diff --git a/phaser-plus/src/tilemap/TilemapFeature.ts b/phaser-plus/src/tilemap/TilemapFeature.ts index 600b7374..6ce0134e 100644 --- a/phaser-plus/src/tilemap/TilemapFeature.ts +++ b/phaser-plus/src/tilemap/TilemapFeature.ts @@ -68,7 +68,7 @@ class TilemapFeature extends Feature { createLayer(layerName: string | number, tilesetName?: string): Tilemaps.TilemapLayer { if (this.activeMap === null) throw new Error('TilemapFeature: call create() or createFromData() first') const tileset = tilesetName !== undefined ? (this.tilesets.get(tilesetName) ?? []) : [] - const layer = this.activeMap.createLayer(layerName, tileset as any, 0, 0) + const layer = this.activeMap.createLayer(layerName, tileset as any, 0, 0) as Tilemaps.TilemapLayer | null if (layer === null) throw new Error(`TilemapFeature: layer "${layerName}" not found`) if (typeof layerName === 'string') this.layers.set(layerName, layer) return layer diff --git a/taskforge/app/api/accounts/[alias]/route.ts b/taskforge/app/api/accounts/[alias]/route.ts index 59557185..052791e6 100644 --- a/taskforge/app/api/accounts/[alias]/route.ts +++ b/taskforge/app/api/accounts/[alias]/route.ts @@ -7,7 +7,8 @@ import { removeAccount } from '@/server/services/accounts' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function DELETE(_req: Request, { params }: { params: { alias: string } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ alias: string }> }) { + const params = await ctx.params const auth = await guard('admin') if ('res' in auth) return auth.res diff --git a/taskforge/app/api/accounts/[alias]/verify/route.ts b/taskforge/app/api/accounts/[alias]/verify/route.ts index dc41d783..c0ab7c84 100644 --- a/taskforge/app/api/accounts/[alias]/verify/route.ts +++ b/taskforge/app/api/accounts/[alias]/verify/route.ts @@ -8,7 +8,8 @@ import { verifyAccount, getAccount, UnknownAccountError, MissingApiKeyError } fr export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { alias: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ alias: string }> }) { + const params = await ctx.params const auth = await guard('admin') if ('res' in auth) return auth.res diff --git a/taskforge/app/api/admin/backup/project/[project]/route.ts b/taskforge/app/api/admin/backup/project/[project]/route.ts index 6a173c81..1c5d78c5 100644 --- a/taskforge/app/api/admin/backup/project/[project]/route.ts +++ b/taskforge/app/api/admin/backup/project/[project]/route.ts @@ -11,7 +11,8 @@ import { projectExists, projectPath, UnsafePathError } from '@/server/infrastruc export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('admin') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/agent-defs/[kind]/route.ts b/taskforge/app/api/agent-defs/[kind]/route.ts index 12ba88af..529cbb71 100644 --- a/taskforge/app/api/agent-defs/[kind]/route.ts +++ b/taskforge/app/api/agent-defs/[kind]/route.ts @@ -4,7 +4,8 @@ import * as agentDefRepo from '@/server/data/repositories/agent-def-repo' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function DELETE(_req: Request, { params }: { params: { kind: string } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ kind: string }> }) { + const params = await ctx.params const auth = await guard('admin') if ('res' in auth) return auth.res if (!agentDefRepo.get(params.kind)) return error('unknown agent kind', 404) diff --git a/taskforge/app/api/projects/[project]/agents/[agent]/prompts/route.ts b/taskforge/app/api/projects/[project]/agents/[agent]/prompts/route.ts index 9d4a1315..6868ea65 100644 --- a/taskforge/app/api/projects/[project]/agents/[agent]/prompts/route.ts +++ b/taskforge/app/api/projects/[project]/agents/[agent]/prompts/route.ts @@ -7,7 +7,8 @@ import * as promptHistoryRepo from '@/server/data/repositories/prompt-history-re export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(req: Request, { params }: { params: { project: string; agent: string } }) { +export async function GET(req: Request, ctx: { params: Promise<{ project: string; agent: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/agents/[agent]/start/route.ts b/taskforge/app/api/projects/[project]/agents/[agent]/start/route.ts index 8fc058e2..6433c23d 100644 --- a/taskforge/app/api/projects/[project]/agents/[agent]/start/route.ts +++ b/taskforge/app/api/projects/[project]/agents/[agent]/start/route.ts @@ -8,7 +8,8 @@ import type { AgentKind } from '@/server/domain/types' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string; agent: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string; agent: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res diff --git a/taskforge/app/api/projects/[project]/agents/[agent]/stop/route.ts b/taskforge/app/api/projects/[project]/agents/[agent]/stop/route.ts index 83443b8a..8cc0d202 100644 --- a/taskforge/app/api/projects/[project]/agents/[agent]/stop/route.ts +++ b/taskforge/app/api/projects/[project]/agents/[agent]/stop/route.ts @@ -6,7 +6,8 @@ import type { AgentKind } from '@/server/domain/types' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { project: string; agent: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string; agent: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res if (!listAgentKinds().some((k) => k.kind === params.agent)) return error('unknown agent', 400) diff --git a/taskforge/app/api/projects/[project]/claude-md/route.ts b/taskforge/app/api/projects/[project]/claude-md/route.ts index eacc1cb9..18780b05 100644 --- a/taskforge/app/api/projects/[project]/claude-md/route.ts +++ b/taskforge/app/api/projects/[project]/claude-md/route.ts @@ -8,7 +8,8 @@ export const runtime = 'nodejs' export const dynamic = 'force-dynamic' /** Reset the root CLAUDE.md to the canonical template (§8 — instant, no agent). */ -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/branch/route.ts b/taskforge/app/api/projects/[project]/git/branch/route.ts index c059dc20..9275ca64 100644 --- a/taskforge/app/api/projects/[project]/git/branch/route.ts +++ b/taskforge/app/api/projects/[project]/git/branch/route.ts @@ -19,7 +19,8 @@ export const dynamic = 'force-dynamic' * - `{ switchTo }` → switch to an existing branch * - `{ delete, force? }` → delete a local branch */ -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res if (engine.isLocked(params.project) || agentSessions.isBusy(params.project)) { diff --git a/taskforge/app/api/projects/[project]/git/branches/route.ts b/taskforge/app/api/projects/[project]/git/branches/route.ts index 93a876bf..9bacc5e7 100644 --- a/taskforge/app/api/projects/[project]/git/branches/route.ts +++ b/taskforge/app/api/projects/[project]/git/branches/route.ts @@ -5,7 +5,8 @@ import { projectExists, UnsafePathError } from '@/server/infrastructure/fs-works export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/commit/route.ts b/taskforge/app/api/projects/[project]/git/commit/route.ts index 2b0738cf..2c8c74ed 100644 --- a/taskforge/app/api/projects/[project]/git/commit/route.ts +++ b/taskforge/app/api/projects/[project]/git/commit/route.ts @@ -13,7 +13,8 @@ export const dynamic = 'force-dynamic' export const maxDuration = 120 /** §6.2 — manual commit of the whole working tree, with optional AI message. */ -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/commits/[sha]/route.ts b/taskforge/app/api/projects/[project]/git/commits/[sha]/route.ts index d071e6c4..01bc9b04 100644 --- a/taskforge/app/api/projects/[project]/git/commits/[sha]/route.ts +++ b/taskforge/app/api/projects/[project]/git/commits/[sha]/route.ts @@ -5,7 +5,8 @@ import { projectExists, UnsafePathError } from '@/server/infrastructure/fs-works export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string; sha: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string; sha: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/commits/route.ts b/taskforge/app/api/projects/[project]/git/commits/route.ts index 2094ab4d..f62131ff 100644 --- a/taskforge/app/api/projects/[project]/git/commits/route.ts +++ b/taskforge/app/api/projects/[project]/git/commits/route.ts @@ -5,7 +5,8 @@ import { projectExists, UnsafePathError } from '@/server/infrastructure/fs-works export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(req: Request, { params }: { params: { project: string } }) { +export async function GET(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/diff/route.ts b/taskforge/app/api/projects/[project]/git/diff/route.ts index 786378f7..3f48103b 100644 --- a/taskforge/app/api/projects/[project]/git/diff/route.ts +++ b/taskforge/app/api/projects/[project]/git/diff/route.ts @@ -6,7 +6,8 @@ export const runtime = 'nodejs' export const dynamic = 'force-dynamic' /** Before (HEAD) / after (working tree) content of one repo file, for DiffViewer. */ -export async function GET(req: Request, { params }: { params: { project: string } }) { +export async function GET(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const path = new URL(req.url).searchParams.get('path') diff --git a/taskforge/app/api/projects/[project]/git/discard-paths/route.ts b/taskforge/app/api/projects/[project]/git/discard-paths/route.ts index 343f31b5..3fc03bd7 100644 --- a/taskforge/app/api/projects/[project]/git/discard-paths/route.ts +++ b/taskforge/app/api/projects/[project]/git/discard-paths/route.ts @@ -8,7 +8,8 @@ export const runtime = 'nodejs' export const dynamic = 'force-dynamic' /** Per-file discard (§6.4): restore tracked paths, delete untracked ones. Destructive. */ -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res if (engine.isLocked(params.project) || agentSessions.isBusy(params.project)) { diff --git a/taskforge/app/api/projects/[project]/git/exec/route.ts b/taskforge/app/api/projects/[project]/git/exec/route.ts index 5753fed5..a8c4e2b2 100644 --- a/taskforge/app/api/projects/[project]/git/exec/route.ts +++ b/taskforge/app/api/projects/[project]/git/exec/route.ts @@ -10,7 +10,8 @@ export const dynamic = 'force-dynamic' const COMMAND_MAX = 2000 /** Git-page terminal: run one user-typed git command inside the repo checkout. */ -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res // the command may touch the working tree or refs — never while a run/agent is active. diff --git a/taskforge/app/api/projects/[project]/git/files/route.ts b/taskforge/app/api/projects/[project]/git/files/route.ts index 47937d9b..b8f6389f 100644 --- a/taskforge/app/api/projects/[project]/git/files/route.ts +++ b/taskforge/app/api/projects/[project]/git/files/route.ts @@ -6,7 +6,8 @@ export const runtime = 'nodejs' export const dynamic = 'force-dynamic' /** Per-file working-tree status (feeds the Git page's dirty-files table). */ -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/issues/route.ts b/taskforge/app/api/projects/[project]/git/issues/route.ts index 11857589..1f703289 100644 --- a/taskforge/app/api/projects/[project]/git/issues/route.ts +++ b/taskforge/app/api/projects/[project]/git/issues/route.ts @@ -8,7 +8,8 @@ import { readProjectMeta } from '@/server/services/provision' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/op/route.ts b/taskforge/app/api/projects/[project]/git/op/route.ts index 409a747a..a9e88dc2 100644 --- a/taskforge/app/api/projects/[project]/git/op/route.ts +++ b/taskforge/app/api/projects/[project]/git/op/route.ts @@ -34,7 +34,8 @@ const OPS: Record Promise> = { 'stash-drop': (p, b) => stashDrop(p, typeof b.index === 'number' ? b.index : 0), } -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res // every op touches the working tree or refs — never while a run/agent is active. diff --git a/taskforge/app/api/projects/[project]/git/pr/route.ts b/taskforge/app/api/projects/[project]/git/pr/route.ts index 783b0e0e..4b895d32 100644 --- a/taskforge/app/api/projects/[project]/git/pr/route.ts +++ b/taskforge/app/api/projects/[project]/git/pr/route.ts @@ -12,7 +12,8 @@ import { canPush } from '@/server/config' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/push/route.ts b/taskforge/app/api/projects/[project]/git/push/route.ts index 3ce68f42..171c9cb2 100644 --- a/taskforge/app/api/projects/[project]/git/push/route.ts +++ b/taskforge/app/api/projects/[project]/git/push/route.ts @@ -8,7 +8,8 @@ import { UnsafePathError } from '@/server/infrastructure/fs-workspace' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res if (!canPush()) return error('no push credential configured', 412) diff --git a/taskforge/app/api/projects/[project]/git/revert/route.ts b/taskforge/app/api/projects/[project]/git/revert/route.ts index 88415dff..73139b7a 100644 --- a/taskforge/app/api/projects/[project]/git/revert/route.ts +++ b/taskforge/app/api/projects/[project]/git/revert/route.ts @@ -7,7 +7,8 @@ import { UnsafePathError } from '@/server/infrastructure/fs-workspace' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res if (engine.isLocked(params.project) || agentSessions.isBusy(params.project)) { diff --git a/taskforge/app/api/projects/[project]/git/route.ts b/taskforge/app/api/projects/[project]/git/route.ts index c8d2ffe7..e376e700 100644 --- a/taskforge/app/api/projects/[project]/git/route.ts +++ b/taskforge/app/api/projects/[project]/git/route.ts @@ -5,7 +5,8 @@ import { projectExists, UnsafePathError } from '@/server/infrastructure/fs-works export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/git/stash/route.ts b/taskforge/app/api/projects/[project]/git/stash/route.ts index 1bc405e6..4d6e3afc 100644 --- a/taskforge/app/api/projects/[project]/git/stash/route.ts +++ b/taskforge/app/api/projects/[project]/git/stash/route.ts @@ -5,7 +5,8 @@ import { projectExists, UnsafePathError } from '@/server/infrastructure/fs-works export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/knowledge/[...id]/route.ts b/taskforge/app/api/projects/[project]/knowledge/[...id]/route.ts index eb3e28e0..96ee3e6d 100644 --- a/taskforge/app/api/projects/[project]/knowledge/[...id]/route.ts +++ b/taskforge/app/api/projects/[project]/knowledge/[...id]/route.ts @@ -21,7 +21,8 @@ function knowledgeBusy(project: string): boolean { ) } -export async function GET(_req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') @@ -35,7 +36,8 @@ export async function GET(_req: Request, { params }: { params: { project: string } /** C2 — manual save of a knowledge doc; the index rebuilds so summaries propagate. */ -export async function PUT(req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') @@ -55,7 +57,8 @@ export async function PUT(req: Request, { params }: { params: { project: string; } } -export async function DELETE(_req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') diff --git a/taskforge/app/api/projects/[project]/knowledge/route.ts b/taskforge/app/api/projects/[project]/knowledge/route.ts index c113b9d7..3af71fd6 100644 --- a/taskforge/app/api/projects/[project]/knowledge/route.ts +++ b/taskforge/app/api/projects/[project]/knowledge/route.ts @@ -13,7 +13,8 @@ import { agentSessions } from '@/server/services/agent-sessions' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -28,7 +29,8 @@ export async function GET(_req: Request, { params }: { params: { project: string const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/ /** C2 — manual knowledge-doc creation: `{ slug, content }` → `.md`. */ -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/notes/[...id]/route.ts b/taskforge/app/api/projects/[project]/notes/[...id]/route.ts index 9f2782b0..6ec71246 100644 --- a/taskforge/app/api/projects/[project]/notes/[...id]/route.ts +++ b/taskforge/app/api/projects/[project]/notes/[...id]/route.ts @@ -19,7 +19,8 @@ function noteAgentBusy(project: string): boolean { return agentSessions.snapshot(project, 'note-writer').status === 'running' } -export async function GET(_req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') @@ -33,7 +34,8 @@ export async function GET(_req: Request, { params }: { params: { project: string } /** Manual save; creates the note (and notes/) if missing. */ -export async function PUT(req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') @@ -50,7 +52,8 @@ export async function PUT(req: Request, { params }: { params: { project: string; } } -export async function DELETE(_req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') diff --git a/taskforge/app/api/projects/[project]/notes/route.ts b/taskforge/app/api/projects/[project]/notes/route.ts index 1ee5329a..c07ac062 100644 --- a/taskforge/app/api/projects/[project]/notes/route.ts +++ b/taskforge/app/api/projects/[project]/notes/route.ts @@ -5,7 +5,8 @@ import { projectExists, UnsafePathError } from '@/server/infrastructure/fs-works export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/route.ts b/taskforge/app/api/projects/[project]/route.ts index a15033f1..4ec8b297 100644 --- a/taskforge/app/api/projects/[project]/route.ts +++ b/taskforge/app/api/projects/[project]/route.ts @@ -5,7 +5,8 @@ import { deleteProject, ProjectLockedError } from '@/server/services/provision' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function DELETE(_req: Request, { params }: { params: { project: string } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/run/force/route.ts b/taskforge/app/api/projects/[project]/run/force/route.ts index 58dc2207..44e9e623 100644 --- a/taskforge/app/api/projects/[project]/run/force/route.ts +++ b/taskforge/app/api/projects/[project]/run/force/route.ts @@ -4,7 +4,8 @@ import { engine } from '@/server/services/execution-manager' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res audit(auth, 'run.force', params.project) diff --git a/taskforge/app/api/projects/[project]/run/skip/route.ts b/taskforge/app/api/projects/[project]/run/skip/route.ts index 68af509b..f2615463 100644 --- a/taskforge/app/api/projects/[project]/run/skip/route.ts +++ b/taskforge/app/api/projects/[project]/run/skip/route.ts @@ -4,7 +4,8 @@ import { engine } from '@/server/services/execution-manager' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res if (engine.state(params.project) !== 'RUNNING') return error('no task is currently running', 409) diff --git a/taskforge/app/api/projects/[project]/run/start/route.ts b/taskforge/app/api/projects/[project]/run/start/route.ts index 51c61eec..d4005e4c 100644 --- a/taskforge/app/api/projects/[project]/run/start/route.ts +++ b/taskforge/app/api/projects/[project]/run/start/route.ts @@ -8,7 +8,8 @@ import type { RunOptions } from '@/server/domain/types' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res diff --git a/taskforge/app/api/projects/[project]/run/stop/route.ts b/taskforge/app/api/projects/[project]/run/stop/route.ts index acb29f89..1871590a 100644 --- a/taskforge/app/api/projects/[project]/run/stop/route.ts +++ b/taskforge/app/api/projects/[project]/run/stop/route.ts @@ -4,7 +4,8 @@ import { engine } from '@/server/services/execution-manager' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res audit(auth, 'run.stop', params.project) diff --git a/taskforge/app/api/projects/[project]/runs/[id]/route.ts b/taskforge/app/api/projects/[project]/runs/[id]/route.ts index 5ec4e0bf..1a2d05d7 100644 --- a/taskforge/app/api/projects/[project]/runs/[id]/route.ts +++ b/taskforge/app/api/projects/[project]/runs/[id]/route.ts @@ -7,7 +7,8 @@ import * as runRepo from '@/server/data/repositories/run-repo' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string; id: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string; id: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/runs/route.ts b/taskforge/app/api/projects/[project]/runs/route.ts index a19453b2..eb110345 100644 --- a/taskforge/app/api/projects/[project]/runs/route.ts +++ b/taskforge/app/api/projects/[project]/runs/route.ts @@ -7,7 +7,8 @@ import * as runRepo from '@/server/data/repositories/run-repo' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(req: Request, { params }: { params: { project: string } }) { +export async function GET(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/schedule/route.ts b/taskforge/app/api/projects/[project]/schedule/route.ts index 64f42125..da69f703 100644 --- a/taskforge/app/api/projects/[project]/schedule/route.ts +++ b/taskforge/app/api/projects/[project]/schedule/route.ts @@ -10,7 +10,8 @@ import type { RunOptions } from '@/server/domain/types' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -22,7 +23,8 @@ export async function GET(_req: Request, { params }: { params: { project: string } } -export async function PUT(req: Request, { params }: { params: { project: string } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -69,7 +71,8 @@ export async function PUT(req: Request, { params }: { params: { project: string } } -export async function DELETE(_req: Request, { params }: { params: { project: string } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/search/route.ts b/taskforge/app/api/projects/[project]/search/route.ts index 441e3062..de760567 100644 --- a/taskforge/app/api/projects/[project]/search/route.ts +++ b/taskforge/app/api/projects/[project]/search/route.ts @@ -7,7 +7,8 @@ import * as searchRepo from '@/server/data/repositories/search-repo' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(req: Request, { params }: { params: { project: string } }) { +export async function GET(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/settings/route.ts b/taskforge/app/api/projects/[project]/settings/route.ts index d106f7b1..02cb2c92 100644 --- a/taskforge/app/api/projects/[project]/settings/route.ts +++ b/taskforge/app/api/projects/[project]/settings/route.ts @@ -14,7 +14,8 @@ import type { ProjectSettings } from '@/server/domain/types' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -29,7 +30,8 @@ export async function GET(_req: Request, { params }: { params: { project: string } } -export async function PUT(req: Request, { params }: { params: { project: string } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/status/route.ts b/taskforge/app/api/projects/[project]/status/route.ts index 23842f35..fd5e8a51 100644 --- a/taskforge/app/api/projects/[project]/status/route.ts +++ b/taskforge/app/api/projects/[project]/status/route.ts @@ -4,7 +4,8 @@ import { engine } from '@/server/services/execution-manager' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res return json(engine.snapshot(params.project)) diff --git a/taskforge/app/api/projects/[project]/stream/route.ts b/taskforge/app/api/projects/[project]/stream/route.ts index c0983643..a8fe7ce2 100644 --- a/taskforge/app/api/projects/[project]/stream/route.ts +++ b/taskforge/app/api/projects/[project]/stream/route.ts @@ -5,7 +5,8 @@ import { error } from '@/server/web/http' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await authorize('standard') if (!auth.ok) return error(auth.status === 401 ? 'unauthorized' : 'forbidden', auth.status) return sseResponse(params.project) diff --git a/taskforge/app/api/projects/[project]/tasks/[...id]/route.ts b/taskforge/app/api/projects/[project]/tasks/[...id]/route.ts index 40355aec..7d805ca1 100644 --- a/taskforge/app/api/projects/[project]/tasks/[...id]/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/[...id]/route.ts @@ -16,7 +16,8 @@ import { config } from '@/server/config' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') @@ -36,7 +37,8 @@ const MODEL_ALIASES = ['fast', 'mid', 'deep'] * §9 — pin (or clear, with null/'') the task's preferred model, and/or * A1 — replace the task body (`{ content }`). Both 409 while the task runs. */ -export async function PATCH(req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function PATCH(req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') @@ -75,7 +77,8 @@ export async function PATCH(req: Request, { params }: { params: { project: strin } /** A3 — delete one task file (its telemetry history is removed with it). */ -export async function DELETE(_req: Request, { params }: { params: { project: string; id: string[] } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ project: string; id: string[] }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = params.id.join('/') diff --git a/taskforge/app/api/projects/[project]/tasks/archive/route.ts b/taskforge/app/api/projects/[project]/tasks/archive/route.ts index f923782c..a376f863 100644 --- a/taskforge/app/api/projects/[project]/tasks/archive/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/archive/route.ts @@ -19,7 +19,8 @@ import { getTasks } from '@/server/services/projects' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -42,7 +43,8 @@ export async function GET(_req: Request, { params }: { params: { project: string } } -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -60,7 +62,8 @@ export async function POST(_req: Request, { params }: { params: { project: strin } /** Restore one archived task: `{ id }`. */ -export async function PUT(req: Request, { params }: { params: { project: string } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/tasks/bulk/route.ts b/taskforge/app/api/projects/[project]/tasks/bulk/route.ts index 5a21d940..2f1e7942 100644 --- a/taskforge/app/api/projects/[project]/tasks/bulk/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/bulk/route.ts @@ -24,7 +24,8 @@ export const dynamic = 'force-dynamic' const MODEL_ALIASES = ['fast', 'mid', 'deep'] -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/tasks/feedback/route.ts b/taskforge/app/api/projects/[project]/tasks/feedback/route.ts index e2c23f9c..247c5eb2 100644 --- a/taskforge/app/api/projects/[project]/tasks/feedback/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/feedback/route.ts @@ -22,7 +22,8 @@ import { getTasks } from '@/server/services/projects' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/tasks/import-issues/route.ts b/taskforge/app/api/projects/[project]/tasks/import-issues/route.ts index a1250ba4..5482a8dd 100644 --- a/taskforge/app/api/projects/[project]/tasks/import-issues/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/import-issues/route.ts @@ -11,7 +11,8 @@ import { getTasks } from '@/server/services/projects' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/tasks/reorder/route.ts b/taskforge/app/api/projects/[project]/tasks/reorder/route.ts index 5ab603e5..b7fb6a37 100644 --- a/taskforge/app/api/projects/[project]/tasks/reorder/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/reorder/route.ts @@ -15,7 +15,8 @@ import { getTasks } from '@/server/services/projects' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/tasks/reset-errors/route.ts b/taskforge/app/api/projects/[project]/tasks/reset-errors/route.ts index 1613eae0..c4717edd 100644 --- a/taskforge/app/api/projects/[project]/tasks/reset-errors/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/reset-errors/route.ts @@ -7,7 +7,8 @@ import { slog } from '@/server/infrastructure/server-log' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function POST(_req: Request, { params }: { params: { project: string } }) { +export async function POST(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res diff --git a/taskforge/app/api/projects/[project]/tasks/route.ts b/taskforge/app/api/projects/[project]/tasks/route.ts index 8fe56545..ef73c656 100644 --- a/taskforge/app/api/projects/[project]/tasks/route.ts +++ b/taskforge/app/api/projects/[project]/tasks/route.ts @@ -7,7 +7,8 @@ import { config } from '@/server/config' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { project: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -22,7 +23,8 @@ export async function GET(_req: Request, { params }: { params: { project: string const MODEL_ALIASES = ['fast', 'mid', 'deep'] /** A1 — manual task creation (no agent round-trip). */ -export async function POST(req: Request, { params }: { params: { project: string } }) { +export async function POST(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/projects/[project]/telemetry/summary/route.ts b/taskforge/app/api/projects/[project]/telemetry/summary/route.ts index fc341343..bbd5b099 100644 --- a/taskforge/app/api/projects/[project]/telemetry/summary/route.ts +++ b/taskforge/app/api/projects/[project]/telemetry/summary/route.ts @@ -7,7 +7,8 @@ import * as telemetryRepo from '@/server/data/repositories/telemetry-repo' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(req: Request, { params }: { params: { project: string } }) { +export async function GET(req: Request, ctx: { params: Promise<{ project: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/prompt-templates/[id]/route.ts b/taskforge/app/api/prompt-templates/[id]/route.ts index e5ef0f48..8adaf934 100644 --- a/taskforge/app/api/prompt-templates/[id]/route.ts +++ b/taskforge/app/api/prompt-templates/[id]/route.ts @@ -4,7 +4,8 @@ import * as promptHistoryRepo from '@/server/data/repositories/prompt-history-re export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function DELETE(_req: Request, { params }: { params: { id: string } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ id: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const id = Number(params.id) diff --git a/taskforge/app/api/skills/[name]/route.ts b/taskforge/app/api/skills/[name]/route.ts index 7fec44cb..c8b672d9 100644 --- a/taskforge/app/api/skills/[name]/route.ts +++ b/taskforge/app/api/skills/[name]/route.ts @@ -4,7 +4,8 @@ import { readSkill, writeSkill, deleteSkill, skillExists, UnsafePathError } from export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export async function GET(_req: Request, { params }: { params: { name: string } }) { +export async function GET(_req: Request, ctx: { params: Promise<{ name: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { @@ -16,7 +17,8 @@ export async function GET(_req: Request, { params }: { params: { name: string } } } -export async function PUT(req: Request, { params }: { params: { name: string } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ name: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res const body = (await req.json().catch(() => ({}))) as { content?: string } @@ -30,7 +32,8 @@ export async function PUT(req: Request, { params }: { params: { name: string } } } } -export async function DELETE(_req: Request, { params }: { params: { name: string } }) { +export async function DELETE(_req: Request, ctx: { params: Promise<{ name: string }> }) { + const params = await ctx.params const auth = await guard('standard') if ('res' in auth) return auth.res try { diff --git a/taskforge/app/api/users/[githubId]/route.ts b/taskforge/app/api/users/[githubId]/route.ts index cd6df96f..e5bd2d11 100644 --- a/taskforge/app/api/users/[githubId]/route.ts +++ b/taskforge/app/api/users/[githubId]/route.ts @@ -7,7 +7,8 @@ export const dynamic = 'force-dynamic' const ROLES: Role[] = ['admin', 'standard', 'guest'] -export async function PUT(req: Request, { params }: { params: { githubId: string } }) { +export async function PUT(req: Request, ctx: { params: Promise<{ githubId: string }> }) { + const params = await ctx.params const auth = await guard('admin') if ('res' in auth) return auth.res diff --git a/taskforge/app/login/page.tsx b/taskforge/app/login/page.tsx index bc554b52..08c893e7 100644 --- a/taskforge/app/login/page.tsx +++ b/taskforge/app/login/page.tsx @@ -8,7 +8,8 @@ export const dynamic = 'force-dynamic' export const metadata: Metadata = { title: 'Sign in' } -export default function LoginPage({ searchParams }: { searchParams: { error?: string } }) { - if (getSession()) redirect('/') - return +export default async function LoginPage({ searchParams }: { searchParams: Promise<{ error?: string }> }) { + if (await getSession()) redirect('/') + const { error } = await searchParams + return } diff --git a/taskforge/app/projects/[project]/agents/page.tsx b/taskforge/app/projects/[project]/agents/page.tsx index 41053c69..7f205b45 100644 --- a/taskforge/app/projects/[project]/agents/page.tsx +++ b/taskforge/app/projects/[project]/agents/page.tsx @@ -6,7 +6,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Agents · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/git/page.tsx b/taskforge/app/projects/[project]/git/page.tsx index f7eda737..5cf860d2 100644 --- a/taskforge/app/projects/[project]/git/page.tsx +++ b/taskforge/app/projects/[project]/git/page.tsx @@ -4,7 +4,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Git · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/knowledge/page.tsx b/taskforge/app/projects/[project]/knowledge/page.tsx index 3425b55b..35f94f8b 100644 --- a/taskforge/app/projects/[project]/knowledge/page.tsx +++ b/taskforge/app/projects/[project]/knowledge/page.tsx @@ -5,7 +5,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Knowledge · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/layout.tsx b/taskforge/app/projects/[project]/layout.tsx index 0bdf4fca..bd09b9a4 100644 --- a/taskforge/app/projects/[project]/layout.tsx +++ b/taskforge/app/projects/[project]/layout.tsx @@ -20,12 +20,13 @@ export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export default async function ProjectLayout({ - params, + params: paramsPromise, children, }: { - params: { project: string } + params: Promise<{ project: string }> children: React.ReactNode }) { + const params = await paramsPromise const me = await requireRole('standard') if (!(await projectExists(params.project))) notFound() ensureSchedulerStarted() // belt-and-braces next to instrumentation.ts diff --git a/taskforge/app/projects/[project]/notes/page.tsx b/taskforge/app/projects/[project]/notes/page.tsx index 3ca227f2..910f1978 100644 --- a/taskforge/app/projects/[project]/notes/page.tsx +++ b/taskforge/app/projects/[project]/notes/page.tsx @@ -5,7 +5,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Notes · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/page.tsx b/taskforge/app/projects/[project]/page.tsx index 348429b6..9b83d986 100644 --- a/taskforge/app/projects/[project]/page.tsx +++ b/taskforge/app/projects/[project]/page.tsx @@ -4,7 +4,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: params.project } } diff --git a/taskforge/app/projects/[project]/run/page.tsx b/taskforge/app/projects/[project]/run/page.tsx index fe41c507..917cb6ad 100644 --- a/taskforge/app/projects/[project]/run/page.tsx +++ b/taskforge/app/projects/[project]/run/page.tsx @@ -4,7 +4,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Run · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/runs/page.tsx b/taskforge/app/projects/[project]/runs/page.tsx index b90f9920..c597bbe1 100644 --- a/taskforge/app/projects/[project]/runs/page.tsx +++ b/taskforge/app/projects/[project]/runs/page.tsx @@ -4,7 +4,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Runs · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/settings/page.tsx b/taskforge/app/projects/[project]/settings/page.tsx index cae4968b..0c940ce8 100644 --- a/taskforge/app/projects/[project]/settings/page.tsx +++ b/taskforge/app/projects/[project]/settings/page.tsx @@ -4,7 +4,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Settings · ${params.project}` } } diff --git a/taskforge/app/projects/[project]/tasks/page.tsx b/taskforge/app/projects/[project]/tasks/page.tsx index c8528270..e94d2989 100644 --- a/taskforge/app/projects/[project]/tasks/page.tsx +++ b/taskforge/app/projects/[project]/tasks/page.tsx @@ -5,7 +5,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { project: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ project: string }> }): Promise { + const params = await ctx.params return { title: `Tasks · ${params.project}` } } diff --git a/taskforge/app/skills/[name]/page.tsx b/taskforge/app/skills/[name]/page.tsx index 456117ba..14617879 100644 --- a/taskforge/app/skills/[name]/page.tsx +++ b/taskforge/app/skills/[name]/page.tsx @@ -9,7 +9,8 @@ import type { Metadata } from 'next' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' -export function generateMetadata({ params }: { params: { name: string } }): Metadata { +export async function generateMetadata(ctx: { params: Promise<{ name: string }> }): Promise { + const params = await ctx.params return { title: params.name === 'new' ? 'New skill' : params.name } } @@ -23,7 +24,8 @@ description: One-line summary of what this skill does. Describe how Claude should use this skill. ` -export default async function SkillEditorPage({ params }: { params: { name: string } }) { +export default async function SkillEditorPage(ctx: { params: Promise<{ name: string }> }) { + const params = await ctx.params const me = await requireRole('standard') const projects = await getProjectNav() diff --git a/taskforge/components/project/TasksClient.tsx b/taskforge/components/project/TasksClient.tsx index 941a178f..6bdbe35a 100644 --- a/taskforge/components/project/TasksClient.tsx +++ b/taskforge/components/project/TasksClient.tsx @@ -45,7 +45,7 @@ const ARCHIVE_COLUMNS = [ // Bare checkbox with its own change listener (React 18 won't fire onChange on tc-check). function Chk({ checked, indeterminate, onChange }: { checked: boolean; indeterminate?: boolean; onChange: (c: boolean) => void }) { const ref = useTcEvents({ change: (e) => onChange((e.target as HTMLInputElement).checked) }) - return + return } export function TasksClient() { diff --git a/taskforge/package.json b/taskforge/package.json index ec17f6f0..f8e2a827 100644 --- a/taskforge/package.json +++ b/taskforge/package.json @@ -14,14 +14,14 @@ "@toolcase/base": "file:../base", "@toolcase/web-components": "file:../web-components", "next": "^16.2.9", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", "server-only": "^0.0.1" }, "devDependencies": { "@types/node": "^22.5", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^19", + "@types/react-dom": "^19", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", "typescript": "^5.5.0" diff --git a/taskforge/server/services/auth.ts b/taskforge/server/services/auth.ts index 7fc7ffad..391e8b53 100644 --- a/taskforge/server/services/auth.ts +++ b/taskforge/server/services/auth.ts @@ -92,8 +92,8 @@ function verifySession(token: string | undefined): SessionPayload | null { } /** Read + verify the session from the request cookies (Node runtime). */ -export function getSession(): SessionPayload | null { - const token = cookies().get(SESSION_COOKIE)?.value +export async function getSession(): Promise { + const token = (await cookies()).get(SESSION_COOKIE)?.value return verifySession(token) } @@ -196,7 +196,7 @@ export type AuthzResult = * re-login. Returns a discriminated result for route handlers to act on. */ export async function authorize(minRole: Role): Promise { - const session = getSession() + const session = await getSession() if (!session) return { ok: false, status: 401 } const role = (await getRole(session.sub)) ?? 'guest' if (ROLE_RANK[role] < ROLE_RANK[minRole]) return { ok: false, status: 403 } diff --git a/taskforge/server/web/page-guards.ts b/taskforge/server/web/page-guards.ts index 3cb67111..c465ed49 100644 --- a/taskforge/server/web/page-guards.ts +++ b/taskforge/server/web/page-guards.ts @@ -9,7 +9,7 @@ import { ROLE_RANK, type MeResponse, type Role } from '@/server/domain/types' /** Require a valid session; returns the current user (`MeResponse`). */ export async function requireSession(): Promise { - const session = getSession() + const session = await getSession() if (!session) redirect('/login') const user = await getUser(session.sub) return { diff --git a/taskforge/tsconfig.json b/taskforge/tsconfig.json index f16af609..4ee33674 100644 --- a/taskforge/tsconfig.json +++ b/taskforge/tsconfig.json @@ -1,23 +1,41 @@ { - "compilerOptions": { - "target": "ES2020", - "lib": ["dom", "dom.iterable", "ES2020"], - "allowJs": false, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [{ "name": "next" }], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "ES2020", + "lib": [ + "dom", + "dom.iterable", + "ES2020" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/web-components/package.json b/web-components/package.json index 0907981a..feb44ed2 100644 --- a/web-components/package.json +++ b/web-components/package.json @@ -26,8 +26,8 @@ ], "scripts": { "dev": "tsup --watch", - "build": "tsup && sass style/index.scss:lib/index.css --no-source-map --style=compressed", - "build:css": "sass style/index.scss:lib/index.css --no-source-map --style=compressed", + "build": "tsup && sass style/index.scss:lib/index.css --no-source-map --no-charset --style=compressed", + "build:css": "sass style/index.scss:lib/index.css --no-source-map --no-charset --style=compressed", "gen:react-types": "node scripts/gen-react-types.mjs" }, "peerDependencies": { diff --git a/web-components/scripts/gen-react-types.mjs b/web-components/scripts/gen-react-types.mjs index 5f577cb7..68557ef3 100644 --- a/web-components/scripts/gen-react-types.mjs +++ b/web-components/scripts/gen-react-types.mjs @@ -10,7 +10,7 @@ // the global JSX namespace (React 18). // // Run: npm -w @toolcase/web-components run gen:react-types -import { readFileSync, writeFileSync } from 'node:fs' +import { readFileSync, writeFileSync, readdirSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -34,8 +34,10 @@ for (const m of register.matchAll(/import\s*\{([^}]+)\}\s*from\s*'\.\/([A-Za-z0- // define('tc-x', SomeClass) / define('tc-x', SomeClass as unknown as ...) // define('tc-x', class extends BaseClass {}) ← anonymous subclass, inherits attrs const tagToClass = [] +// register.ts wraps registration in a local `define(tag, ctor)` helper, so match +// both the wrapper calls `define('tc-x', Class)` and bare `customElements.define`. for (const m of register.matchAll( - /customElements\.define\(\s*'(tc-[a-z0-9-]+)'\s*,\s*(?:class\s+extends\s+([A-Za-z0-9_]+)|([A-Za-z0-9_]+))/g + /(?:customElements\.)?\bdefine\(\s*'(tc-[a-z0-9-]+)'\s*,\s*(?:class\s+extends\s+([A-Za-z0-9_]+)|([A-Za-z0-9_]+))/g )) { tagToClass.push({ tag: m[1], cls: m[2] || m[3] }) } @@ -54,6 +56,35 @@ const readInternal = (name) => { } } +// NAME -> [string members] for every `(export) const NAME = [ ... ]` array across +// all source files. Used to expand `...NAME` spreads inside observedAttributes() +// (e.g. `return ['type', ...TEXT_FIELD_ATTRIBUTES]`), which the per-class literal +// scan in attrsFor cannot resolve on its own. +const constMap = new Map() +const scanConsts = (src) => { + for (const m of src.matchAll(/const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\[[\s\S]*?\])/g)) { + const members = [] + for (const s of m[2].matchAll(/['"]([^'"]+)['"]/g)) members.push(s[1]) + if (members.length) constMap.set(m[1], members) + } +} +for (const dir of [srcDir, join(srcDir, 'internal')]) { + let files = [] + try { + files = readdirSync(dir) + } catch { + files = [] + } + for (const f of files) { + if (!f.endsWith('.ts')) continue + try { + scanConsts(readFileSync(join(dir, f), 'utf8')) + } catch { + // skip unreadable file + } + } +} + // Extract a class's observed attributes. Indexes every class declaration in the // file, slices the target class's body up to the next declaration, then reads its // `observedAttributes` return array. @@ -70,10 +101,16 @@ const attrsFor = (localName) => { const i = decls.findIndex(d => d[1] === entry.realName) if (i === -1) return [] const body = src.slice(decls[i].index, i + 1 < decls.length ? decls[i + 1].index : undefined) - const obs = body.match(/observedAttributes\s*\(\s*\)\s*:\s*[^{]*\{\s*return\s*(\[[\s\S]*?\])/) + const obs = body.match(/observedAttributes\s*\(\s*\)\s*:\s*[^{]*\{[\s\S]*?\breturn\s*(\[[\s\S]*?\])/) if (!obs) return [] const attrs = [] for (const s of obs[1].matchAll(/['"]([^'"]+)['"]/g)) attrs.push(s[1]) + // Expand `...CONST` spreads (shared/base-class attribute lists) the literal + // scan above can't see, e.g. `return ['type', ...TEXT_FIELD_ATTRIBUTES]`. + for (const sp of obs[1].matchAll(/\.\.\.([A-Za-z_][A-Za-z0-9_]*)/g)) { + const members = constMap.get(sp[1]) + if (members) attrs.push(...members) + } return [...new Set(attrs)].sort() } @@ -129,11 +166,16 @@ const booleanAttrsFor = (localName) => { } const boolSet = new Set() const collectBooleans = (text) => { - // Match simple getter pattern: `return this.hasAttribute('name')` - // and compound getter patterns: `return x?.y ?? this.hasAttribute('name')` - // The `[^\n;{}]*` allows arbitrary prefix expressions within the same return statement - // but stops at statement boundaries so `if (!this.hasAttribute(...))` guards don't match. - for (const m of text.matchAll(/return\s+[^\n;{}]*this\.hasAttribute\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) { + // A boolean attribute is one whose getter RETURNS `this.hasAttribute('name')`. + // Two return shapes occur; both stop at statement boundaries (`;{}`) so guards + // like `if (!this.hasAttribute(...))` after a bare `return` don't leak in: + // 1) single-line: `return … this.hasAttribute('name')` (no newline crossing) + // 2) parenthesised body: `return (\n … ?? this.hasAttribute('name')\n)` + // A bare `return` is never followed by `(`, so case 2 can't bridge into a later guard. + for (const m of text.matchAll(/return\s+[^\n;{}]*?this\.hasAttribute\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) { + boolSet.add(m[1]) + } + for (const m of text.matchAll(/return\s*\([^;{}]*?this\.hasAttribute\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) { boolSet.add(m[1]) } } diff --git a/web-components/src/react-types.ts b/web-components/src/react-types.ts index 35556bc4..2299d8d9 100644 --- a/web-components/src/react-types.ts +++ b/web-components/src/react-types.ts @@ -23,18 +23,18 @@ export type TcProps = React.DetailedHTMLProps< export interface ToolcaseIntrinsicElements { 'tc-ability-card': TcProps<{ 'ability-name'?: string | number; 'cooldown'?: string | number; 'cost'?: string | number; 'description'?: string | number; 'icon'?: string | number; 'keybind'?: string | number; 'range'?: string | number; 'rarity'?: string | number }> 'tc-accordion': TcProps<{ 'always-open'?: boolean; 'flush'?: boolean }> - 'tc-accordion-item': TcProps<{ 'header'?: string | number; 'open'?: boolean; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-accordion-item': TcProps<{ 'header'?: string | number; 'open'?: boolean; onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-action-header': TcProps<{ 'disabled'?: boolean; onTcExec?: (e: CustomEvent) => void }> 'tc-action-items': TcProps<{ 'label'?: string | number; onTcActionClick?: (e: CustomEvent) => void }> 'tc-action-row-list': TcProps<{ 'outline'?: boolean; 'trailing-icon'?: string | number; onTcActionClick?: (e: CustomEvent) => void }> 'tc-activity-card': TcProps<{ 'loading'?: boolean; 'loading-count'?: string | number; 'title'?: string | number }> - 'tc-advanced-table': TcProps<{ 'limit'?: string | number; 'loading'?: boolean; 'offset'?: string | number; 'total'?: string | number; onTcFilterChange?: (e: CustomEvent) => void; onTcSortChange?: (e: CustomEvent) => void; onTcPageChange?: (e: CustomEvent) => void }> + 'tc-advanced-table': TcProps<{ 'limit'?: string | number; 'loading'?: boolean; 'offset'?: string | number; 'total'?: string | number; onTcFilterChange?: (e: CustomEvent) => void; onTcPageChange?: (e: CustomEvent) => void; onTcSortChange?: (e: CustomEvent) => void }> 'tc-alert': TcProps<{ 'dismissible'?: boolean; 'variant'?: string | number; onTcClosed?: (e: CustomEvent) => void }> 'tc-ammo-counter': TcProps<{ 'mag'?: string | number; 'mag-max'?: string | number; 'reloading'?: boolean; 'reserve'?: string | number; 'weapon-name'?: string | number }> 'tc-anchor': TcProps<{ 'inset'?: string | number; 'position'?: string | number }> - 'tc-announcement-bar': TcProps<{ 'cta-href'?: string | number; 'cta-label'?: string | number; 'dismissible'?: boolean; 'icon'?: string | number; 'icon-name'?: string | number; 'persist-dismiss-key'?: string | number; 'storage-key'?: string | number; 'variant'?: string | number }> + 'tc-announcement-bar': TcProps<{ 'cta-href'?: string | number; 'cta-label'?: string | number; 'dismissible'?: boolean; 'icon'?: string | number; 'icon-name'?: string | number; 'persist-dismiss-key'?: string | number; 'storage-key'?: string | number; 'variant'?: string | number; onTcDismiss?: (e: CustomEvent) => void }> 'tc-api-reference-table': TcProps<{ 'title'?: string | number }> - 'tc-area-chart': TcProps<{ onTcPointHover?: (e: CustomEvent) => void }> + 'tc-area-chart': TcProps<{ 'height'?: string | number; 'loading'?: boolean; 'show-grid'?: string | number; 'show-legend'?: string | number; 'stacked'?: boolean; 'subtitle'?: string | number; 'title'?: string | number; onTcPointHover?: (e: CustomEvent) => void }> 'tc-artboard-backdrop': TcProps<{ 'kind'?: string | number; 'padding'?: string | number }> 'tc-aspect-ratio-box': TcProps<{ 'ratio'?: string | number }> 'tc-asset-bundle': TcProps<{ 'build-tag'?: string | number; 'category'?: string | number; 'default-build-tag'?: string | number; 'latest-build-ref'?: string | number; 'loading'?: boolean; 'name'?: string | number; 'target'?: string | number; 'target-icon'?: string | number; onTcAdvancedToggle?: (e: CustomEvent) => void; onTcBuildTagChange?: (e: CustomEvent) => void; onTcMenuClick?: (e: CustomEvent) => void }> @@ -45,11 +45,11 @@ export interface ToolcaseIntrinsicElements { 'tc-badge': TcProps<{ 'pill'?: boolean; 'text'?: string | number; 'variant'?: string | number }> 'tc-badge-row': TcProps<{ 'size'?: string | number }> 'tc-banner': TcProps<{ 'cta-href'?: string | number; 'cta-label'?: string | number; 'dismissible'?: boolean; 'icon'?: string | number; 'icon-name'?: string | number; 'persist-dismiss-key'?: string | number; 'storage-key'?: string | number; 'variant'?: string | number; onTcDismiss?: (e: CustomEvent) => void }> - 'tc-bar-chart': TcProps<{ onTcBarClick?: (e: CustomEvent) => void }> - 'tc-basic-card': TcProps + 'tc-bar-chart': TcProps<{ 'height'?: string | number; 'loading'?: boolean; 'orientation'?: string | number; 'show-values'?: boolean; 'subtitle'?: string | number; 'title'?: string | number; onTcBarClick?: (e: CustomEvent) => void }> + 'tc-basic-card': TcProps<{ 'color'?: string | number; 'icon'?: string | number; 'loading'?: boolean; 'text'?: string | number; 'text-a'?: string | number; 'text-b'?: string | number; 'value'?: string | number }> 'tc-basic-layout': TcProps<{ 'brand'?: string | number }> 'tc-battle-pass': TcProps<{ 'current-level'?: string | number; 'current-xp'?: string | number; 'has-premium'?: boolean; 'season-end'?: string | number; 'season-name'?: string | number; onTcClaim?: (e: CustomEvent) => void }> - 'tc-benchmark-chart': TcProps<{ onTcBarClick?: (e: CustomEvent) => void }> + 'tc-benchmark-chart': TcProps<{ 'interactive'?: boolean; 'lower-is-better'?: boolean; 'scale'?: string | number; 'title'?: string | number; onTcBarClick?: (e: CustomEvent) => void }> 'tc-bitmap-font-generator': TcProps<{ 'background'?: string | number; 'disabled'?: boolean; 'export-format'?: string | number; 'font-family'?: string | number; 'font-size'?: string | number; 'glyphs'?: string | number; 'glyphs-per-row'?: string | number; 'letter-spacing'?: string | number; 'line-height'?: string | number; 'padding'?: string | number; 'power-of-two'?: boolean; 'scale'?: string | number; 'text'?: string | number }> 'tc-blur-overlay': TcProps<{ 'background'?: string | number; 'blur-amount'?: string | number }> 'tc-boss-bar': TcProps<{ 'epithet'?: string | number; 'hp'?: string | number; 'hp-max'?: string | number; 'name'?: string | number; 'phase'?: string | number }> @@ -64,14 +64,14 @@ export interface ToolcaseIntrinsicElements { 'tc-bundle-bar': TcProps<{ 'filled-segments'?: string | number; 'meta'?: string | number; 'name'?: string | number; 'segments'?: string | number }> 'tc-button': TcProps<{ 'disabled'?: boolean; 'href'?: string | number; 'loading'?: boolean; 'outline'?: boolean; 'size'?: string | number; 'skin'?: string | number; 'type'?: string | number; 'variant'?: string | number }> 'tc-button-group': TcProps<{ 'aria-label'?: string | number; 'size'?: string | number; 'vertical'?: boolean }> - 'tc-callout-quote': TcProps<{ 'attribution'?: string | number; 'quote'?: string | number; 'source'?: string | number; 'source-href'?: string | number }> + 'tc-callout-quote': TcProps<{ 'attribution'?: string | number; 'quote'?: boolean; 'source'?: string | number; 'source-href'?: string | number }> 'tc-card': TcProps<{ 'img'?: string | number; 'img-position'?: string | number; 'subtitle'?: string | number; 'title'?: string | number; 'variant'?: string | number }> 'tc-card-options': TcProps<{ 'columns'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> - 'tc-carousel': TcProps<{ 'controls'?: boolean; 'fade'?: boolean; 'indicators'?: boolean; 'interval'?: string | number; 'pause'?: string | number; 'ride'?: string | number; onTcSlide?: (e: CustomEvent) => void; onTcSlid?: (e: CustomEvent) => void }> + 'tc-carousel': TcProps<{ 'controls'?: boolean; 'fade'?: boolean; 'indicators'?: boolean; 'interval'?: string | number; 'pause'?: string | number; 'ride'?: string | number; onTcSlid?: (e: CustomEvent) => void; onTcSlide?: (e: CustomEvent) => void }> 'tc-cdn-map': TcProps<{ 'height'?: string | number }> 'tc-changelog': TcProps<{ 'loading'?: boolean; 'max-visible'?: string | number; 'read-more-href'?: string | number; 'read-more-label'?: string | number }> - 'tc-character-create': TcProps<{ 'confirm-label'?: string | number; 'heading'?: string | number; 'name'?: string | number; 'name-placeholder'?: string | number; onTcName?: (e: CustomEvent) => void; onTcChange?: (e: CustomEvent) => void; onTcConfirm?: (e: CustomEvent) => void }> - 'tc-character-select': TcProps<{ 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void; onTcConfirm?: (e: CustomEvent) => void }> + 'tc-character-create': TcProps<{ 'confirm-label'?: string | number; 'heading'?: string | number; 'name'?: string | number; 'name-placeholder'?: string | number; onTcChange?: (e: CustomEvent) => void; onTcConfirm?: (e: CustomEvent) => void; onTcName?: (e: CustomEvent) => void }> + 'tc-character-select': TcProps<{ 'selected-id'?: string | number; onTcConfirm?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-chart-container': TcProps<{ 'empty'?: boolean; 'loading'?: boolean; 'subtitle'?: string | number; 'title'?: string | number }> 'tc-chat-window': TcProps<{ 'active-channel'?: string | number; 'height'?: string | number; 'placeholder'?: string | number; 'width'?: string | number; onTcChannelChange?: (e: CustomEvent) => void; onTcSend?: (e: CustomEvent) => void }> 'tc-check': TcProps<{ 'checked'?: boolean; 'disabled'?: boolean; 'indeterminate'?: boolean; 'inline'?: boolean; 'label'?: string | number; 'reverse'?: boolean; 'state'?: string | number; 'value'?: string | number }> @@ -84,21 +84,21 @@ export interface ToolcaseIntrinsicElements { 'tc-code-snippet': TcProps<{ 'code'?: string | number; 'language'?: string | number; 'loading'?: boolean; 'show-copy-button'?: string | number; 'title'?: string | number; onTcCopy?: (e: CustomEvent) => void }> 'tc-code-with-output': TcProps<{ 'code'?: string | number; 'language'?: string | number; 'layout'?: string | number; 'title'?: string | number }> 'tc-codex': TcProps<{ 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void }> - 'tc-col': TcProps<{ 'order'?: string | number; 'span'?: string | number }> - 'tc-collapse': TcProps<{ 'horizontal'?: boolean; 'open'?: boolean; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-col': TcProps<{ 'lg'?: string | number; 'md'?: string | number; 'order'?: string | number; 'sm'?: string | number; 'span'?: string | number; 'xl'?: string | number; 'xxl'?: string | number }> + 'tc-collapse': TcProps<{ 'horizontal'?: boolean; 'open'?: boolean; onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-color-picker': TcProps<{ 'columns'?: string | number; 'disabled'?: boolean; 'label'?: string | number; 'loading'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> - 'tc-colored-card': TcProps + 'tc-colored-card': TcProps<{ 'color'?: string | number; 'icon'?: string | number; 'loading'?: boolean; 'text'?: string | number; 'text-a'?: string | number; 'text-b'?: string | number; 'value'?: string | number }> 'tc-combo-box': TcProps<{ 'disabled'?: boolean; 'placeholder'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-combo-counter': TcProps<{ 'combo'?: string | number; 'font-size'?: string | number; 'label'?: string | number; 'timer'?: string | number }> 'tc-command-palette': TcProps<{ 'loading'?: boolean; 'open'?: boolean; 'placeholder'?: string | number; onTcClose?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-command-reference': TcProps<{ 'search-placeholder'?: string | number; 'searchable'?: string | number; 'title'?: string | number; onTcSearch?: (e: CustomEvent) => void }> - 'tc-community-links': TcProps<{ 'title'?: string | number }> + 'tc-community-links': TcProps<{ 'title'?: boolean }> 'tc-comparator': TcProps<{ 'description'?: string | number; 'loading'?: boolean; 'loading-count'?: string | number; 'show-summary'?: string | number; 'title'?: string | number }> 'tc-compass-bar': TcProps<{ 'fov'?: string | number; 'heading'?: string | number; 'height'?: string | number; 'show-cardinals'?: boolean; 'width'?: string | number }> 'tc-compass-rose': TcProps<{ 'heading'?: string | number; 'size'?: string | number }> 'tc-compatibility-matrix': TcProps<{ 'title'?: string | number }> 'tc-config-preview': TcProps<{ 'live-label'?: string | number }> - 'tc-confirm-dialog': TcProps<{ 'cancel-label'?: string | number; 'confirm-label'?: string | number; 'danger'?: boolean; 'dialog-title'?: string | number; 'eyebrow'?: string | number; 'message'?: string | number; 'open'?: boolean; onTcConfirm?: (e: CustomEvent) => void; onTcCancel?: (e: CustomEvent) => void }> + 'tc-confirm-dialog': TcProps<{ 'cancel-label'?: string | number; 'confirm-label'?: string | number; 'danger'?: boolean; 'dialog-title'?: string | number; 'eyebrow'?: string | number; 'message'?: string | number; 'open'?: boolean; onTcCancel?: (e: CustomEvent) => void; onTcConfirm?: (e: CustomEvent) => void }> 'tc-container': TcProps<{ 'breakpoint'?: string | number; 'fluid'?: boolean }> 'tc-context-menu': TcProps<{ onTcSelect?: (e: CustomEvent) => void }> 'tc-contributor-wall': TcProps<{ 'max-visible'?: string | number; 'title'?: string | number }> @@ -109,7 +109,7 @@ export interface ToolcaseIntrinsicElements { 'tc-cool-nav': TcProps<{ 'brand'?: string | number; 'expand-breakpoint'?: string | number; 'login-href'?: string | number; 'login-label'?: string | number; 'login-variant'?: string | number; 'scroll-offset'?: string | number; 'sticky'?: boolean; 'theme'?: string | number; onTcLogin?: (e: CustomEvent) => void; onTcNavToggle?: (e: CustomEvent) => void }> 'tc-cooldown-badge': TcProps<{ 'label'?: string | number; 'max'?: string | number; 'show-label'?: boolean; 'size'?: string | number; 'value'?: string | number }> 'tc-countdown-timer': TcProps<{ 'compact'?: boolean; 'label'?: string | number; 'sub-label'?: string | number; 'target'?: string | number; 'units'?: string | number; onTcExpire?: (e: CustomEvent) => void }> - 'tc-crafting-panel': TcProps<{ 'crafting'?: boolean; 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void; onTcCraft?: (e: CustomEvent) => void }> + 'tc-crafting-panel': TcProps<{ 'crafting'?: boolean; 'selected-id'?: string | number; onTcCraft?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-credits-scroll': TcProps<{ 'scroll-title'?: string | number; 'speed'?: string | number; onTcComplete?: (e: CustomEvent) => void }> 'tc-crosshair': TcProps<{ 'color'?: string | number; 'gap'?: string | number; 'size'?: string | number; 'spread'?: string | number; 'thickness'?: string | number; 'variant'?: string | number }> 'tc-currency-chip': TcProps<{ 'amount'?: string | number; 'color'?: string | number; 'glyph'?: string | number }> @@ -120,9 +120,9 @@ export interface ToolcaseIntrinsicElements { 'tc-dashboard-content': TcProps 'tc-dashboard-layout': TcProps<{ 'sidebar-open'?: boolean; onTcToggleSidebar?: (e: CustomEvent) => void }> 'tc-dashboard-sidebar': TcProps - 'tc-data-list': TcProps<{ 'empty-text'?: string | number; 'list-title'?: string | number; 'selectable'?: boolean; 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void; onTcAction?: (e: CustomEvent) => void }> + 'tc-data-list': TcProps<{ 'empty-text'?: string | number; 'list-title'?: string | number; 'selectable'?: boolean; 'selected-id'?: string | number; onTcAction?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-date-picker': TcProps<{ 'disabled'?: boolean; 'label'?: string | number; 'max'?: string | number; 'min'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> - 'tc-deadzone-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean }> + 'tc-deadzone-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean; onTcChange?: (e: CustomEvent) => void; onTcToggleMute?: (e: CustomEvent) => void }> 'tc-debug-overlay': TcProps<{ 'draw-calls'?: string | number; 'fps'?: string | number; 'mem-mb'?: string | number; 'triangles'?: string | number }> 'tc-dialogue-box': TcProps<{ 'speaker'?: string | number; 'text'?: string | number; 'typing-speed'?: string | number }> 'tc-diff-viewer': TcProps<{ 'after'?: string | number; 'before'?: string | number; 'filename'?: string | number; 'language'?: string | number; 'mode'?: string | number; onTcRender?: (e: CustomEvent) => void }> @@ -130,18 +130,18 @@ export interface ToolcaseIntrinsicElements { 'tc-divider': TcProps<{ 'label'?: string | number; 'vertical'?: boolean }> 'tc-download-stats': TcProps<{ 'monthly'?: string | number; 'package-name'?: string | number; 'registry'?: string | number; 'total'?: string | number; 'weekly'?: string | number }> 'tc-drawer': TcProps<{ 'open'?: boolean; 'pinned'?: boolean; 'side'?: string | number; 'size'?: string | number; 'title'?: string | number; onTcClose?: (e: CustomEvent) => void }> - 'tc-dropdown': TcProps<{ 'auto-close'?: string | number; 'direction'?: string | number; 'label'?: string | number; 'split'?: boolean; 'variant'?: string | number; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-dropdown': TcProps<{ 'auto-close'?: string | number; 'direction'?: string | number; 'label'?: string | number; 'split'?: boolean; 'variant'?: string | number; onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-dropdown-item': TcProps<{ 'active'?: boolean; 'disabled'?: boolean; 'divider'?: boolean; 'href'?: string | number }> 'tc-early-signup-form': TcProps<{ 'cta-label'?: string | number; 'eyebrow'?: string | number; 'field-label'?: string | number; 'helper-text'?: string | number; 'loading'?: boolean; 'placeholder'?: string | number; 'stat'?: string | number; 'subtitle'?: string | number; 'success-message'?: string | number; 'success-title'?: string | number; 'title'?: string | number; 'variant'?: string | number; onTcSubmit?: (e: CustomEvent) => void }> 'tc-ecosystem-map': TcProps<{ 'size'?: string | number; 'title'?: string | number; onTcSelect?: (e: CustomEvent) => void }> 'tc-editable-text': TcProps<{ 'aria-label'?: string | number; 'default-value'?: string | number; 'disabled'?: boolean; 'placeholder'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-empty-state': TcProps<{ 'icon'?: string | number }> 'tc-entity-cell': TcProps<{ 'clickable'?: boolean; 'color'?: string | number; 'initial'?: string | number; 'name'?: string | number; 'size'?: string | number; 'sub-label'?: string | number; onTcClick?: (e: CustomEvent) => void }> - 'tc-entity-profile-card': TcProps + 'tc-entity-profile-card': TcProps<{ 'loading'?: boolean; 'title'?: string | number }> 'tc-equipment-doll': TcProps<{ 'height'?: string | number; 'selected-id'?: string | number; 'silhouette'?: string | number; 'slot-size'?: string | number; 'width'?: string | number; onTcSelect?: (e: CustomEvent) => void }> 'tc-extended-select': TcProps<{ 'loading'?: boolean; 'name'?: string | number; 'no-results-text'?: string | number; 'placeholder'?: string | number; 'search-placeholder'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-eyebrow': TcProps - 'tc-faq-list': TcProps<{ onTcToggle?: (e: CustomEvent) => void }> + 'tc-faq-list': TcProps<{ 'schema'?: boolean; 'title'?: string | number; onTcToggle?: (e: CustomEvent) => void }> 'tc-feature-card': TcProps<{ 'description'?: string | number; 'eyebrow'?: string | number; 'icon'?: string | number; 'inline'?: boolean; 'size'?: string | number; 'title'?: string | number }> 'tc-feature-matrix': TcProps<{ 'title'?: string | number }> 'tc-file': TcProps<{ onTcMenuItemClick?: (e: CustomEvent) => void; onTcNameChange?: (e: CustomEvent) => void }> @@ -150,24 +150,24 @@ export interface ToolcaseIntrinsicElements { 'tc-floating-label': TcProps<{ 'for'?: string | number; 'label'?: string | number }> 'tc-form': TcProps<{ 'novalidate'?: boolean; 'validated'?: boolean }> 'tc-form-input': TcProps<{ 'disabled'?: boolean; 'error'?: string | number; 'help'?: string | number; 'helper'?: string | number; 'id'?: string | number; 'label'?: string | number; 'loading'?: boolean; 'max'?: string | number; 'min'?: string | number; 'name'?: string | number; 'placeholder'?: string | number; 'required'?: boolean; 'rows'?: string | number; 'step'?: string | number; 'type'?: string | number; onTcChange?: (e: CustomEvent) => void }> - 'tc-form-wizard': TcProps<{ 'complete-icon'?: string | number; 'complete-label'?: string | number; 'loading'?: boolean; onTcStepChange?: (e: CustomEvent) => void; onTcComplete?: (e: CustomEvent) => void }> - 'tc-fov-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean }> + 'tc-form-wizard': TcProps<{ 'complete-icon'?: string | number; 'complete-label'?: string | number; 'loading'?: boolean; onTcComplete?: (e: CustomEvent) => void; onTcStepChange?: (e: CustomEvent) => void }> + 'tc-fov-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean; onTcChange?: (e: CustomEvent) => void; onTcToggleMute?: (e: CustomEvent) => void }> 'tc-fps-cap-select': TcProps<{ 'disabled'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-fullscreen-toggle': TcProps<{ 'checked'?: boolean; 'disabled'?: boolean; onTcChange?: (e: CustomEvent) => void }> - 'tc-funnel-chart': TcProps<{ onTcSelect?: (e: CustomEvent) => void }> - 'tc-game-over-screen': TcProps<{ 'eyebrow'?: string | number; 'subtitle'?: string | number; 'title-color'?: string | number; 'title-text'?: string | number; 'variant'?: string | number }> - 'tc-game-showcase-card': TcProps<{ onTcClick?: (e: CustomEvent) => void }> + 'tc-funnel-chart': TcProps<{ 'height'?: string | number; 'loading'?: boolean; 'show-labels'?: string | number; 'subtitle'?: string | number; 'title'?: string | number; onTcSelect?: (e: CustomEvent) => void }> + 'tc-game-over-screen': TcProps<{ 'eyebrow'?: string | number; 'subtitle'?: string | number; 'title-color'?: string | number; 'title-text'?: string | number; 'variant'?: string | number; onTcAction?: (e: CustomEvent) => void }> + 'tc-game-showcase-card': TcProps<{ 'pitch'?: string | number; 'title'?: string | number; onTcClick?: (e: CustomEvent) => void }> 'tc-gamepad-button-prompt': TcProps<{ 'glyph'?: string | number; 'label'?: string | number; 'size'?: string | number }> - 'tc-gantt-chart': TcProps<{ onTcTaskClick?: (e: CustomEvent) => void }> + 'tc-gantt-chart': TcProps<{ 'end-date'?: string | number; 'loading'?: boolean; 'start-date'?: string | number; 'subtitle'?: string | number; 'title'?: string | number; onTcTaskClick?: (e: CustomEvent) => void }> 'tc-gilded-frame': TcProps<{ 'padding'?: string | number; 'tone'?: string | number }> - 'tc-github-stars-card': TcProps<{ 'cta-label'?: string | number; 'fetch-live'?: boolean; 'owner'?: string | number; 'repo'?: string | number; onTcStats?: (e: CustomEvent) => void; onTcCtaClick?: (e: CustomEvent) => void }> + 'tc-github-stars-card': TcProps<{ 'cta-label'?: string | number; 'fetch-live'?: boolean; 'owner'?: string | number; 'repo'?: string | number; onTcCtaClick?: (e: CustomEvent) => void; onTcStats?: (e: CustomEvent) => void }> 'tc-good-first-issues': TcProps<{ 'title'?: string | number; onTcIssueClick?: (e: CustomEvent) => void }> 'tc-graphics-preset-picker': TcProps<{ 'disabled'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-grid': TcProps<{ 'cell-size'?: string | number; 'columns'?: string | number; 'gap'?: string | number; 'rows'?: string | number }> - 'tc-group': TcProps<{ 'action-icon'?: string | number; 'action-label'?: string | number; 'badge'?: string | number; 'default-collapsed'?: boolean; 'label'?: string | number; onTcToggle?: (e: CustomEvent) => void; onTcActionClick?: (e: CustomEvent) => void }> + 'tc-group': TcProps<{ 'action-icon'?: string | number; 'action-label'?: string | number; 'badge'?: string | number; 'default-collapsed'?: boolean; 'label'?: string | number; onTcActionClick?: (e: CustomEvent) => void; onTcToggle?: (e: CustomEvent) => void }> 'tc-guild-panel': TcProps<{ 'guild-name'?: string | number; 'level'?: string | number; 'member-cap'?: string | number; 'motto'?: string | number; 'tag'?: string | number }> 'tc-heading': TcProps<{ 'as'?: string | number; 'gradient'?: boolean }> - 'tc-health-bar': TcProps<{ 'ghost'?: boolean; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> + 'tc-health-bar': TcProps<{ 'ghost'?: string | number; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> 'tc-heatmap': TcProps<{ 'cell-size'?: string | number; 'loading'?: boolean; 'subtitle'?: string | number; 'title'?: string | number; onTcCellHover?: (e: CustomEvent) => void }> 'tc-helper-text': TcProps<{ 'class-name'?: string | number; 'icon'?: string | number; 'id'?: string | number; 'text'?: string | number; 'variant'?: string | number }> 'tc-hero': TcProps<{ 'background-pattern-src'?: string | number; 'description'?: string | number; 'eyebrow'?: string | number; 'title'?: string | number; 'title-as'?: string | number; onTcAction?: (e: CustomEvent) => void }> @@ -178,10 +178,10 @@ export interface ToolcaseIntrinsicElements { 'tc-icon-badge': TcProps<{ 'bg'?: string | number; 'color'?: string | number; 'glyph'?: string | number; 'size'?: string | number }> 'tc-icon-button': TcProps<{ 'disabled'?: boolean; 'icon'?: string | number; 'label'?: string | number; 'outline'?: boolean; 'size'?: string | number; 'variant'?: string | number; onTcClick?: (e: CustomEvent) => void }> 'tc-icon-picker': TcProps<{ 'columns'?: string | number; 'label'?: string | number; 'loading'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> - 'tc-image': TcProps<{ onTcLoad?: (e: CustomEvent) => void; onTcError?: (e: CustomEvent) => void }> + 'tc-image': TcProps<{ onTcError?: (e: CustomEvent) => void; onTcLoad?: (e: CustomEvent) => void }> 'tc-image-crop': TcProps<{ 'aspect-ratio'?: string | number; 'circular'?: boolean; 'src'?: string | number; onTcCrop?: (e: CustomEvent) => void; onTcError?: (e: CustomEvent) => void }> 'tc-infinite-scroll': TcProps<{ 'has-more'?: boolean; 'loading'?: boolean; 'root-margin'?: string | number; 'threshold'?: string | number; onTcLoadMore?: (e: CustomEvent) => void }> - 'tc-input': TcProps<{ 'type'?: string | number }> + 'tc-input': TcProps<{ 'autocomplete'?: string | number; 'disabled'?: boolean; 'help'?: string | number; 'inputmode'?: string | number; 'label'?: string | number; 'max'?: string | number; 'maxlength'?: string | number; 'min'?: string | number; 'minlength'?: string | number; 'name'?: string | number; 'pattern'?: string | number; 'placeholder'?: string | number; 'readonly'?: boolean; 'required'?: boolean; 'size'?: string | number; 'state'?: string | number; 'step'?: string | number; 'type'?: string | number; 'value'?: string | number }> 'tc-input-group': TcProps<{ 'size'?: string | number }> 'tc-input-group-text': TcProps 'tc-install-tabs': TcProps<{ 'default-manager'?: string | number; 'dev'?: boolean; 'global'?: boolean; 'package'?: string | number; onTcChange?: (e: CustomEvent) => void; onTcCopy?: (e: CustomEvent) => void }> @@ -201,12 +201,12 @@ export interface ToolcaseIntrinsicElements { 'tc-label': TcProps<{ 'required'?: boolean; 'size'?: string | number; 'tooltip'?: string | number }> 'tc-leaderboard': TcProps<{ onTcSelect?: (e: CustomEvent) => void }> 'tc-leaderboard-trend': TcProps<{ 'direction'?: string | number; 'size'?: string | number; 'value'?: string | number }> - 'tc-legal-screen': TcProps<{ 'initial-section'?: string | number; 'screen-title'?: string | number; 'show-accept'?: boolean; onTcClose?: (e: CustomEvent) => void; onTcAccept?: (e: CustomEvent) => void }> + 'tc-legal-screen': TcProps<{ 'initial-section'?: string | number; 'screen-title'?: string | number; 'show-accept'?: boolean; onTcAccept?: (e: CustomEvent) => void; onTcClose?: (e: CustomEvent) => void }> 'tc-letterbox-bars': TcProps<{ 'bar-color'?: string | number; 'bar-height'?: string | number; 'duration'?: string | number; 'show'?: boolean }> 'tc-level-header': TcProps<{ 'level'?: string | number; 'next-label'?: string | number; 'title'?: string | number; 'xp'?: string | number; 'xp-max'?: string | number }> - 'tc-level-select': TcProps<{ 'height'?: string | number; 'selected-id'?: string | number; 'width'?: string | number; onTcSelect?: (e: CustomEvent) => void; onTcConfirm?: (e: CustomEvent) => void }> + 'tc-level-select': TcProps<{ 'height'?: string | number; 'selected-id'?: string | number; 'width'?: string | number; onTcConfirm?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-lightbox': TcProps<{ 'initial-index'?: string | number; 'open'?: boolean; onTcChange?: (e: CustomEvent) => void; onTcClose?: (e: CustomEvent) => void }> - 'tc-line-chart': TcProps<{ onTcPointHover?: (e: CustomEvent) => void }> + 'tc-line-chart': TcProps<{ 'height'?: string | number; 'loading'?: boolean; 'show-grid'?: string | number; 'show-legend'?: string | number; 'subtitle'?: string | number; 'title'?: string | number; onTcPointHover?: (e: CustomEvent) => void }> 'tc-link': TcProps<{ 'external'?: boolean; 'href'?: string | number; 'underline'?: string | number; 'variant'?: string | number }> 'tc-linked-providers-card': TcProps<{ 'empty-label'?: string | number; 'title'?: string | number; onTcToggle?: (e: CustomEvent) => void }> 'tc-list': TcProps<{ 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void }> @@ -221,22 +221,22 @@ export interface ToolcaseIntrinsicElements { 'tc-login': TcProps<{ 'background-pattern-src'?: string | number; 'description'?: string | number; 'loading'?: boolean; 'title'?: string | number; onTcConnect?: (e: CustomEvent) => void }> 'tc-logo-cloud': TcProps<{ 'columns'?: string | number; 'grayscale'?: boolean; 'title'?: string | number }> 'tc-loot-list': TcProps<{ 'list-title'?: string | number; onTcTake?: (e: CustomEvent) => void; onTcTakeAll?: (e: CustomEvent) => void }> - 'tc-loot-popup': TcProps<{ 'auto-fade-ms'?: string | number; 'discard-label'?: string | number; 'eyebrow'?: string | number; 'open'?: boolean; 'popup-title'?: string | number; onTcClose?: (e: CustomEvent) => void; onTcTake?: (e: CustomEvent) => void; onTcTakeAll?: (e: CustomEvent) => void; onTcDiscard?: (e: CustomEvent) => void }> + 'tc-loot-popup': TcProps<{ 'auto-fade-ms'?: string | number; 'discard-label'?: string | number; 'eyebrow'?: string | number; 'open'?: boolean; 'popup-title'?: string | number; onTcClose?: (e: CustomEvent) => void; onTcDiscard?: (e: CustomEvent) => void; onTcTake?: (e: CustomEvent) => void; onTcTakeAll?: (e: CustomEvent) => void }> 'tc-lore-text': TcProps 'tc-main-menu': TcProps<{ 'menu-title'?: string | number; 'selected-id'?: string | number; 'subtitle'?: string | number; onTcSelect?: (e: CustomEvent) => void }> 'tc-maintainer-card': TcProps<{ 'avatar-url'?: string | number; 'bio'?: string | number; 'location'?: string | number; 'name'?: string | number; 'role'?: string | number; 'sponsor-href'?: string | number; 'sponsor-label'?: string | number }> - 'tc-mana-bar': TcProps<{ 'ghost'?: boolean; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> - 'tc-markdown-editor': TcProps<{ 'disabled'?: boolean; 'height'?: string | number; 'label'?: string | number; 'placeholder'?: string | number; 'toolbar'?: string | number; 'value'?: string | number }> + 'tc-mana-bar': TcProps<{ 'ghost'?: string | number; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> + 'tc-markdown-editor': TcProps<{ 'disabled'?: boolean; 'height'?: string | number; 'label'?: string | number; 'placeholder'?: string | number; 'toolbar'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-marquee': TcProps<{ 'direction'?: string | number; 'pause-on-hover'?: boolean; 'separator'?: string | number; 'speed'?: string | number }> 'tc-matchmaking-screen': TcProps<{ 'elapsed'?: string | number; 'estimated'?: string | number; 'found-label'?: string | number; 'mode'?: string | number; 'region'?: string | number; 'state'?: string | number }> 'tc-menu-item': TcProps<{ 'disabled'?: boolean; 'hotkey'?: string | number; 'icon'?: string | number; 'label'?: string | number; 'selected'?: boolean; onTcSelect?: (e: CustomEvent) => void }> 'tc-metal-button': TcProps<{ 'disabled'?: boolean; 'href'?: string | number; 'loading'?: boolean; 'outline'?: boolean; 'size'?: string | number; 'skin'?: string | number; 'type'?: string | number; 'variant'?: string | number }> - 'tc-metric-card': TcProps + 'tc-metric-card': TcProps<{ 'icon'?: string | number; 'loading'?: boolean; 'subtitle'?: string | number; 'title'?: string | number; 'trend-color'?: string | number; 'value'?: string | number }> 'tc-metric-grid': TcProps<{ 'columns'?: string | number }> 'tc-metric-tile': TcProps<{ 'hint'?: string | number; 'icon'?: string | number; 'label'?: string | number; 'unit'?: string | number; 'value'?: string | number }> 'tc-migration-guide': TcProps<{ 'from'?: string | number; 'title'?: string | number; 'to'?: string | number }> 'tc-minimap': TcProps<{ 'background-image'?: string | number; 'rotation'?: string | number; 'size'?: string | number; 'world-height'?: string | number; 'world-width'?: string | number; 'world-x'?: string | number; 'world-y'?: string | number }> - 'tc-modal': TcProps<{ 'centered'?: boolean; 'fullscreen'?: string | number; 'open'?: boolean; 'scrollable'?: boolean; 'size'?: string | number; 'static-backdrop'?: boolean; 'title'?: string | number; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-modal': TcProps<{ 'centered'?: boolean; 'fullscreen'?: string | number; 'open'?: boolean; 'scrollable'?: boolean; 'size'?: string | number; 'static-backdrop'?: boolean; 'title'?: string | number; onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-mouse-sensitivity': TcProps<{ 'ads'?: string | number; 'disabled'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-multi-card-select': TcProps<{ 'columns'?: string | number; 'loading'?: boolean; 'loading-count'?: string | number; 'name'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-nav': TcProps<{ 'fill'?: boolean; 'justified'?: boolean; 'variant'?: string | number; 'vertical'?: boolean }> @@ -245,11 +245,11 @@ export interface ToolcaseIntrinsicElements { 'tc-navbar': TcProps<{ 'bg'?: string | number; 'brand'?: string | number; 'expand'?: string | number; 'fixed'?: string | number; 'sticky'?: string | number; 'variant'?: string | number }> 'tc-network-status-icon': TcProps<{ 'connected'?: boolean; 'loss'?: string | number; 'ping'?: string | number; 'show-label'?: boolean; 'size'?: string | number }> 'tc-newsletter-signup': TcProps<{ 'cta-label'?: string | number; 'description'?: string | number; 'placeholder'?: string | number; 'privacy-href'?: string | number; 'success-message'?: string | number; 'title'?: string | number; onTcSubmit?: (e: CustomEvent) => void }> - 'tc-node-editor': TcProps<{ 'disabled'?: boolean; 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void; onTcMoveNode?: (e: CustomEvent) => void; onTcConnect?: (e: CustomEvent) => void }> + 'tc-node-editor': TcProps<{ 'disabled'?: boolean; 'selected-id'?: string | number; onTcConnect?: (e: CustomEvent) => void; onTcMoveNode?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-normal-map-generator': TcProps<{ 'bevel-width'?: string | number; 'disabled'?: boolean; 'editable'?: boolean; 'emboss-height'?: string | number; 'preview-mode'?: string | number; 'source'?: string | number; 'strength'?: string | number; 'tool'?: string | number }> 'tc-number-input': TcProps<{ 'error'?: string | number; 'label'?: string | number; 'max'?: string | number; 'min'?: string | number; 'precision'?: string | number; 'prefix'?: string | number; 'step'?: string | number; 'suffix'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-objective-marker': TcProps<{ 'color'?: string | number; 'distance'?: string | number; 'label'?: string | number; 'pulse'?: boolean; 'size'?: string | number; 'x'?: string | number; 'y'?: string | number }> - 'tc-offcanvas': TcProps<{ 'backdrop'?: string | number; 'open'?: boolean; 'placement'?: string | number; 'scroll'?: boolean; 'title'?: string | number; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-offcanvas': TcProps<{ 'backdrop'?: string | number; 'open'?: boolean; 'placement'?: string | number; 'scroll'?: boolean; 'title'?: string | number; onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-option': TcProps<{ 'disabled'?: boolean; 'selected'?: boolean; 'value'?: string | number }> 'tc-otp-input': TcProps<{ 'error'?: string | number; 'label'?: string | number; 'length'?: string | number; 'masked'?: boolean; 'mode'?: string | number; 'name'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void; onTcComplete?: (e: CustomEvent) => void }> 'tc-page-footer': TcProps<{ 'brand'?: string | number; 'description'?: string | number; 'legal-text'?: string | number; 'tagline'?: string | number }> @@ -259,13 +259,13 @@ export interface ToolcaseIntrinsicElements { 'tc-panel-header': TcProps<{ 'heading'?: string | number; 'icon'?: string | number }> 'tc-particle-emitter': TcProps<{ 'burst'?: string | number; 'count'?: string | number; 'gravity'?: string | number; 'height'?: string | number; 'lifetime'?: string | number; 'particle-size'?: string | number; 'speed'?: string | number; 'width'?: string | number; onTcBurst?: (e: CustomEvent) => void }> 'tc-party-panel': TcProps<{ 'capacity'?: string | number }> - 'tc-pause-menu': TcProps<{ 'default-items'?: boolean; 'menu-title'?: string | number; 'open'?: boolean; 'resume-footer'?: boolean; 'screen-title'?: string | number; onTcResume?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void; onTcRestart?: (e: CustomEvent) => void; onTcQuit?: (e: CustomEvent) => void; onTcClose?: (e: CustomEvent) => void }> - 'tc-pause-screen': TcProps<{ 'default-items'?: boolean; 'menu-title'?: string | number; 'open'?: boolean; 'resume-footer'?: boolean; 'screen-title'?: string | number }> + 'tc-pause-menu': TcProps<{ 'default-items'?: boolean; 'menu-title'?: string | number; 'open'?: boolean; 'resume-footer'?: boolean; 'screen-title'?: string | number; onTcClose?: (e: CustomEvent) => void; onTcQuit?: (e: CustomEvent) => void; onTcRestart?: (e: CustomEvent) => void; onTcResume?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> + 'tc-pause-screen': TcProps<{ 'default-items'?: boolean; 'menu-title'?: string | number; 'open'?: boolean; 'resume-footer'?: boolean; 'screen-title'?: string | number; onTcClose?: (e: CustomEvent) => void; onTcQuit?: (e: CustomEvent) => void; onTcRestart?: (e: CustomEvent) => void; onTcResume?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-perk-picker': TcProps<{ 'columns'?: string | number; onTcSelect?: (e: CustomEvent) => void }> 'tc-phase-grid': TcProps<{ 'columns'?: string | number }> 'tc-phone-input': TcProps<{ 'default-country'?: string | number; 'error'?: string | number; 'label'?: string | number; 'name'?: string | number; 'placeholder'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-physics-editor': TcProps<{ 'alpha-threshold'?: string | number; 'disabled'?: boolean; 'source'?: string | number; 'tool'?: string | number; onTcChange?: (e: CustomEvent) => void }> - 'tc-pie-chart': TcProps<{ onTcSliceSelect?: (e: CustomEvent) => void }> + 'tc-pie-chart': TcProps<{ 'center-label'?: string | number; 'donut'?: boolean; 'height'?: string | number; 'loading'?: boolean; 'show-legend'?: string | number; 'subtitle'?: string | number; 'title'?: string | number; onTcSliceSelect?: (e: CustomEvent) => void }> 'tc-ping-display': TcProps<{ 'ping'?: string | number }> 'tc-pinned-feature-showcase': TcProps<{ 'description'?: string | number; 'eyebrow'?: string | number; 'image-alt'?: string | number; 'image-src'?: string | number; 'title'?: string | number }> 'tc-pipeline': TcProps @@ -274,7 +274,7 @@ export interface ToolcaseIntrinsicElements { 'tc-player-card': TcProps<{ 'card-title'?: string | number; 'level'?: string | number; 'online-status'?: string | number; 'player-name'?: string | number; 'rank'?: string | number; onTcAction?: (e: CustomEvent) => void }> 'tc-player-frame': TcProps<{ 'class-name'?: string | number; 'glyph'?: string | number; 'hp'?: string | number; 'hp-max'?: string | number; 'level'?: string | number; 'mp'?: string | number; 'mp-max'?: string | number; 'name'?: string | number; 'show-mp'?: boolean; 'show-stamina'?: boolean; 'stamina'?: string | number; 'stamina-max'?: string | number }> 'tc-plugin-grid': TcProps<{ 'columns'?: string | number; 'title-text'?: string | number; onTcCopy?: (e: CustomEvent) => void }> - 'tc-popover': TcProps<{ onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-popover': TcProps<{ onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-portrait': TcProps<{ 'circle'?: boolean; 'glyph'?: string | number; 'level'?: string | number; 'ring'?: string | number; 'size'?: string | number }> 'tc-press-any-key': TcProps<{ 'disabled'?: boolean; 'text'?: string | number }> 'tc-pricing-card': TcProps<{ 'badge-text'?: string | number; 'description'?: string | number; 'highlight'?: boolean; 'name'?: string | number; 'period'?: string | number; 'price'?: string | number; onTcAction?: (e: CustomEvent) => void }> @@ -285,7 +285,7 @@ export interface ToolcaseIntrinsicElements { 'tc-queued-file': TcProps<{ 'extension'?: string | number; 'format'?: string | number; 'name'?: string | number; 'size'?: string | number; onTcDismiss?: (e: CustomEvent) => void }> 'tc-quick-start': TcProps<{ 'title-text'?: string | number; onTcCopy?: (e: CustomEvent) => void }> 'tc-radial-wheel': TcProps<{ 'center-label'?: string | number; 'open'?: boolean; 'option-size'?: string | number; 'per-page'?: string | number; 'radius'?: string | number; onTcClose?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> - 'tc-radio': TcProps<{ 'checked'?: string | number; 'disabled'?: boolean; 'inline'?: boolean; 'label'?: string | number; 'name'?: string | number; 'reverse'?: boolean; 'value'?: string | number }> + 'tc-radio': TcProps<{ 'checked'?: boolean; 'disabled'?: boolean; 'inline'?: boolean; 'label'?: string | number; 'name'?: string | number; 'reverse'?: boolean; 'value'?: string | number }> 'tc-radio-group': TcProps<{ 'inline'?: boolean; 'label'?: string | number; 'name'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-range': TcProps<{ 'disabled'?: boolean; 'label'?: string | number; 'max'?: string | number; 'min'?: string | number; 'step'?: string | number; 'value'?: string | number }> 'tc-range-slider': TcProps<{ 'disabled'?: boolean; 'label'?: string | number; 'max'?: string | number; 'min'?: string | number; 'show-tooltip'?: boolean; 'step'?: string | number; 'ticks'?: boolean; onTcChange?: (e: CustomEvent) => void }> @@ -295,7 +295,7 @@ export interface ToolcaseIntrinsicElements { 'tc-report-dialog': TcProps<{ 'open'?: boolean; 'player-name'?: string | number; onTcCancel?: (e: CustomEvent) => void; onTcSubmit?: (e: CustomEvent) => void }> 'tc-reset-to-defaults': TcProps<{ 'disabled'?: boolean; onTcReset?: (e: CustomEvent) => void }> 'tc-resizable-panel': TcProps<{ 'direction'?: string | number; 'min-size'?: string | number; 'storage-key'?: string | number; onTcResize?: (e: CustomEvent) => void }> - 'tc-resource-bar': TcProps<{ 'ghost'?: boolean; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> + 'tc-resource-bar': TcProps<{ 'ghost'?: string | number; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> 'tc-result-screen': TcProps<{ 'eyebrow'?: string | number; 'subtitle'?: string | number; 'title-color'?: string | number; 'title-text'?: string | number; 'variant'?: string | number; onTcAction?: (e: CustomEvent) => void }> 'tc-rich-page-header': TcProps<{ 'description'?: string | number; 'icon-color'?: string | number; 'icon-name'?: string | number; 'sub'?: string | number; 'title-text'?: string | number }> 'tc-roadmap': TcProps<{ 'layout'?: string | number; 'title-text'?: string | number; onTcSelect?: (e: CustomEvent) => void }> @@ -310,7 +310,7 @@ export interface ToolcaseIntrinsicElements { 'tc-scrollspy': TcProps<{ 'offset'?: string | number; 'smooth-scroll'?: boolean; 'target'?: string | number; onTcActivate?: (e: CustomEvent) => void }> 'tc-section-card': TcProps<{ 'icon'?: string | number; 'title'?: string | number; 'variant'?: string | number }> 'tc-section-flag': TcProps<{ 'align'?: string | number; 'subtitle'?: string | number; 'title'?: string | number }> - 'tc-select': TcProps<{ 'disabled'?: boolean; 'label'?: string | number; 'multiple'?: boolean; 'size'?: string | number; 'state'?: string | number; 'value'?: string | number }> + 'tc-select': TcProps<{ 'disabled'?: boolean; 'label'?: string | number; 'multiple'?: boolean; 'name'?: string | number; 'size'?: string | number; 'state'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-select-row': TcProps<{ 'disabled'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-setting-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean; onTcChange?: (e: CustomEvent) => void; onTcToggleMute?: (e: CustomEvent) => void }> 'tc-settings-category-list': TcProps<{ 'selected-id'?: string | number; onTcSelect?: (e: CustomEvent) => void }> @@ -322,7 +322,7 @@ export interface ToolcaseIntrinsicElements { 'tc-skeleton': TcProps<{ 'count'?: string | number; 'height'?: string | number; 'variant'?: string | number; 'width'?: string | number }> 'tc-skill-bar': TcProps<{ 'gap'?: string | number; 'slot-size'?: string | number; onTcActivate?: (e: CustomEvent) => void }> 'tc-skill-tree': TcProps<{ 'height'?: string | number; 'points'?: string | number; 'selected-id'?: string | number; 'width'?: string | number; onTcSelect?: (e: CustomEvent) => void; onTcUnlock?: (e: CustomEvent) => void }> - 'tc-slices-card': TcProps + 'tc-slices-card': TcProps<{ 'loading'?: boolean; 'size'?: string | number; 'stroke-width'?: string | number; 'title'?: string | number }> 'tc-slider': TcProps<{ 'disabled'?: boolean; 'error'?: string | number; 'label'?: string | number; 'max'?: string | number; 'min'?: string | number; 'show-tooltip'?: boolean; 'step'?: string | number; 'ticks'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-social-links': TcProps<{ 'size'?: string | number; 'variant'?: string | number }> 'tc-spacer': TcProps<{ 'axis'?: string | number; 'size'?: string | number }> @@ -331,7 +331,7 @@ export interface ToolcaseIntrinsicElements { 'tc-sponsor-wall': TcProps<{ 'title'?: string | number }> 'tc-sprint-chain': TcProps<{ 'columns'?: string | number; 'current-id'?: string | number }> 'tc-stack': TcProps<{ 'align'?: string | number; 'direction'?: string | number; 'gap'?: string | number; 'inline'?: boolean; 'justify'?: string | number; 'wrap'?: boolean }> - 'tc-stamina-bar': TcProps<{ 'ghost'?: boolean; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> + 'tc-stamina-bar': TcProps<{ 'ghost'?: string | number; 'label'?: string | number; 'max'?: string | number; 'segments'?: string | number; 'show-text'?: boolean; 'value'?: string | number; 'variant'?: string | number }> 'tc-stamp': TcProps<{ 'angle'?: string | number; 'color'?: string | number; 'label'?: string | number; 'position'?: string | number }> 'tc-stat-card': TcProps<{ 'delta'?: string | number; 'delta-kind'?: string | number; 'footer'?: string | number; 'helper'?: string | number; 'icon'?: string | number; 'label'?: string | number; 'loading'?: boolean; 'unit'?: string | number; 'value'?: string | number }> 'tc-stat-row': TcProps<{ 'accent'?: string | number; 'label'?: string | number; 'trend'?: string | number; 'value'?: string | number }> @@ -341,41 +341,41 @@ export interface ToolcaseIntrinsicElements { 'tc-status-dot': TcProps<{ 'label'?: string | number; 'pulse'?: boolean; 'size'?: string | number; 'status'?: string | number }> 'tc-stepper': TcProps<{ 'active-step'?: string | number; 'clickable'?: boolean; 'orientation'?: string | number; onTcStepClick?: (e: CustomEvent) => void }> 'tc-subtitle': TcProps<{ 'align'?: string | number; 'boxed'?: boolean; 'font-size'?: string | number; 'max-width'?: string | number; 'speaker'?: string | number; 'text'?: string | number }> - 'tc-switch': TcProps<{ 'checked'?: boolean; 'disabled'?: boolean; 'label'?: string | number; 'reverse'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> + 'tc-switch': TcProps<{ 'checked'?: boolean; 'disabled'?: boolean; 'label'?: string | number; 'name'?: string | number; 'reverse'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-tab-bar': TcProps<{ 'active-id'?: string | number; 'size'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-tab-sections': TcProps<{ 'active-key'?: boolean; 'default-active-key'?: string | number; 'loading'?: boolean; onTcChange?: (e: CustomEvent) => void }> 'tc-table': TcProps<{ 'borderless'?: boolean; 'compact'?: boolean; 'empty-message'?: string | number; 'hoverable'?: boolean; 'loading'?: boolean; 'loading-rows'?: string | number; 'sticky-header'?: boolean; 'striped'?: boolean; onTcRowClick?: (e: CustomEvent) => void }> - 'tc-tag': TcProps<{ 'count'?: string | number; 'disabled'?: boolean; 'icon'?: string | number; 'removable'?: boolean; 'selected'?: boolean; 'static'?: boolean; 'variant'?: string | number }> + 'tc-tag': TcProps<{ 'count'?: string | number; 'disabled'?: boolean; 'icon'?: string | number; 'removable'?: boolean; 'selected'?: boolean; 'static'?: boolean; 'variant'?: string | number; onTcClick?: (e: CustomEvent) => void; onTcRemove?: (e: CustomEvent) => void }> 'tc-tag-input': TcProps<{ 'allow-create'?: boolean; 'disabled'?: boolean; 'label'?: string | number; 'loading'?: boolean; 'max-tags'?: string | number; 'placeholder'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-terminal-window': TcProps<{ 'animate-typing'?: boolean; 'prompt'?: string | number; 'speed'?: string | number; 'title'?: string | number }> 'tc-testimonial-carousel': TcProps<{ 'autoplay'?: boolean; 'interval'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-text': TcProps<{ 'as'?: string | number; 'size'?: string | number; 'variant'?: string | number }> - 'tc-textarea': TcProps<{ 'rows'?: string | number }> + 'tc-textarea': TcProps<{ 'autocomplete'?: string | number; 'disabled'?: boolean; 'help'?: string | number; 'inputmode'?: string | number; 'label'?: string | number; 'max'?: string | number; 'maxlength'?: string | number; 'min'?: string | number; 'minlength'?: string | number; 'name'?: string | number; 'pattern'?: string | number; 'placeholder'?: string | number; 'readonly'?: boolean; 'required'?: boolean; 'rows'?: string | number; 'size'?: string | number; 'state'?: string | number; 'step'?: string | number; 'value'?: string | number }> 'tc-theme': TcProps<{ 'name'?: string | number }> 'tc-tier-ladder': TcProps<{ 'current-tier-id'?: string | number; 'summary'?: string | number; 'title'?: string | number }> 'tc-time-picker': TcProps<{ 'clearable'?: boolean; 'disabled'?: boolean; 'error'?: string | number; 'format'?: string | number; 'label'?: string | number; 'minute-step'?: string | number; 'placeholder'?: string | number; 'show-seconds'?: boolean; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-timeline': TcProps<{ 'connector'?: string | number; 'loading'?: boolean; 'loading-count'?: string | number; 'overlap'?: string | number; 'variant'?: string | number }> 'tc-title': TcProps<{ 'align'?: string | number; 'size'?: string | number }> 'tc-title-screen': TcProps<{ 'eyebrow'?: string | number; 'subtitle'?: string | number; 'title-text'?: string | number }> - 'tc-toast': TcProps<{ 'autohide'?: string | number; 'delay'?: string | number; 'open'?: boolean; 'title'?: string | number; 'variant'?: string | number; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-toast': TcProps<{ 'autohide'?: string | number; 'delay'?: string | number; 'open'?: boolean; 'title'?: string | number; 'variant'?: string | number; onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-toggle': TcProps<{ 'disabled'?: boolean; 'label'?: string | number; 'on'?: boolean; onTcChange?: (e: CustomEvent) => void }> 'tc-toggle-card': TcProps<{ 'badge'?: string | number; 'checked'?: boolean; 'disabled'?: boolean; 'hint'?: string | number; 'icon'?: string | number; 'label'?: string | number; 'loading'?: boolean; 'name'?: string | number; 'value'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-toggle-row': TcProps<{ 'checked'?: boolean; 'disabled'?: boolean; onTcChange?: (e: CustomEvent) => void }> - 'tc-tooltip': TcProps<{ onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcHidden?: (e: CustomEvent) => void }> + 'tc-tooltip': TcProps<{ onTcHidden?: (e: CustomEvent) => void; onTcHide?: (e: CustomEvent) => void; onTcShow?: (e: CustomEvent) => void; onTcShown?: (e: CustomEvent) => void }> 'tc-transition-wipe': TcProps<{ 'direction'?: string | number; 'duration'?: string | number; 'show'?: boolean; 'wipe-color'?: string | number; onTcComplete?: (e: CustomEvent) => void }> 'tc-tree-view': TcProps<{ 'checkbox-mode'?: boolean; onTcExpandChange?: (e: CustomEvent) => void; onTcSelect?: (e: CustomEvent) => void }> 'tc-trend-indicator': TcProps<{ 'direction'?: string | number; 'size'?: string | number; 'value'?: string | number }> 'tc-usage-summary-panel': TcProps<{ 'loading'?: boolean; 'loading-count'?: string | number; 'title'?: string | number }> - 'tc-user-panel': TcProps<{ 'avatar-src'?: string | number; 'icon'?: string | number; 'icon-highlighted'?: boolean; 'initials'?: string | number; 'loading'?: boolean; 'plan'?: string | number; 'username'?: string | number; onTcMenuClick?: (e: CustomEvent) => void; onTcIconClick?: (e: CustomEvent) => void }> + 'tc-user-panel': TcProps<{ 'avatar-src'?: string | number; 'icon'?: string | number; 'icon-highlighted'?: boolean; 'initials'?: string | number; 'loading'?: boolean; 'plan'?: string | number; 'username'?: string | number; onTcIconClick?: (e: CustomEvent) => void; onTcMenuClick?: (e: CustomEvent) => void }> 'tc-version-label': TcProps<{ 'branch'?: string | number; 'build'?: string | number; 'version'?: string | number }> 'tc-version-picker': TcProps<{ 'name'?: string | number; 'value'?: string | number; 'variant'?: string | number; onTcChange?: (e: CustomEvent) => void }> 'tc-vertical-item-list': TcProps<{ 'active-key'?: boolean; 'default-active-key'?: string | number; 'disabled'?: boolean; 'loading'?: boolean; 'loading-count'?: string | number; onTcSelect?: (e: CustomEvent) => void }> - 'tc-victory-screen': TcProps<{ 'eyebrow'?: string | number; 'subtitle'?: string | number; 'title-color'?: string | number; 'title-text'?: string | number; 'variant'?: string | number }> - 'tc-video-embed': TcProps + 'tc-victory-screen': TcProps<{ 'eyebrow'?: string | number; 'subtitle'?: string | number; 'title-color'?: string | number; 'title-text'?: string | number; 'variant'?: string | number; onTcAction?: (e: CustomEvent) => void }> + 'tc-video-embed': TcProps<{ 'aspect-ratio'?: string | number; 'autoplay'?: boolean; 'controls'?: string | number; 'loop'?: boolean; 'muted'?: boolean; 'poster'?: string | number; 'src'?: string | number; 'title'?: string | number }> 'tc-vignette-overlay': TcProps<{ 'intensity'?: string | number; 'vignette-color'?: string | number }> 'tc-virtual-list': TcProps<{ 'end-reached-threshold'?: string | number; 'height'?: string | number; 'overscan'?: string | number; onTcEndReached?: (e: CustomEvent) => void }> 'tc-visually-hidden': TcProps<{ 'as'?: string | number }> - 'tc-volume-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean }> + 'tc-volume-slider': TcProps<{ 'disabled'?: boolean; 'format'?: string | number; 'max'?: string | number; 'min'?: string | number; 'muted'?: boolean; 'step'?: string | number; 'unit'?: string | number; 'value'?: string | number; 'with-mute'?: boolean; onTcChange?: (e: CustomEvent) => void; onTcToggleMute?: (e: CustomEvent) => void }> 'tc-waypoint-marker': TcProps<{ 'color'?: string | number; 'distance'?: string | number; 'icon'?: string | number; 'label'?: string | number; 'size'?: string | number; 'x'?: string | number; 'y'?: string | number }> 'tc-welcome-guide': TcProps<{ 'background-pattern-alt'?: string | number; 'background-pattern-src'?: string | number; 'loading'?: boolean; 'title'?: string | number }> } diff --git a/web-components/src/useTc.ts b/web-components/src/useTc.ts index fa84ad5d..74bee027 100644 --- a/web-components/src/useTc.ts +++ b/web-components/src/useTc.ts @@ -31,7 +31,7 @@ export type TcEventMap = Record export function useTc( instanceProps: Record, on: TcEventMap = {} -): RefObject { +): RefObject { const ref = useRef(null) // Keep a ref to the latest handlers so wrappers attached on mount always @@ -71,7 +71,7 @@ export function useTc( }) }) - return ref as RefObject + return ref as RefObject } /** @@ -84,7 +84,7 @@ export function useTc( */ export function useTcEvents( on: TcEventMap -): RefObject { +): RefObject { return useTc({}, on) }