@character-foundry/character-foundry 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/app-framework.cjs +143 -26
  2. package/dist/app-framework.cjs.map +1 -1
  3. package/dist/app-framework.d.cts +16 -1
  4. package/dist/app-framework.d.ts +16 -1
  5. package/dist/app-framework.js +151 -34
  6. package/dist/app-framework.js.map +1 -1
  7. package/dist/charx.cjs +70 -8
  8. package/dist/charx.cjs.map +1 -1
  9. package/dist/charx.js +70 -8
  10. package/dist/charx.js.map +1 -1
  11. package/dist/core.cjs +93 -6
  12. package/dist/core.cjs.map +1 -1
  13. package/dist/core.d.cts +42 -1
  14. package/dist/core.d.ts +42 -1
  15. package/dist/core.js +93 -6
  16. package/dist/core.js.map +1 -1
  17. package/dist/exporter.cjs +88 -8
  18. package/dist/exporter.cjs.map +1 -1
  19. package/dist/exporter.js +89 -9
  20. package/dist/exporter.js.map +1 -1
  21. package/dist/federation.cjs +1 -0
  22. package/dist/federation.cjs.map +1 -1
  23. package/dist/federation.js +1 -0
  24. package/dist/federation.js.map +1 -1
  25. package/dist/index.cjs +122 -18
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +123 -19
  28. package/dist/index.js.map +1 -1
  29. package/dist/loader.cjs +42 -14
  30. package/dist/loader.cjs.map +1 -1
  31. package/dist/loader.js +43 -15
  32. package/dist/loader.js.map +1 -1
  33. package/dist/lorebook.cjs +1 -0
  34. package/dist/lorebook.cjs.map +1 -1
  35. package/dist/lorebook.js +1 -0
  36. package/dist/lorebook.js.map +1 -1
  37. package/dist/media.cjs +1 -0
  38. package/dist/media.cjs.map +1 -1
  39. package/dist/media.js +1 -0
  40. package/dist/media.js.map +1 -1
  41. package/dist/normalizer.cjs +1 -0
  42. package/dist/normalizer.cjs.map +1 -1
  43. package/dist/normalizer.d.cts +1 -0
  44. package/dist/normalizer.d.ts +1 -0
  45. package/dist/normalizer.js +1 -0
  46. package/dist/normalizer.js.map +1 -1
  47. package/dist/png.cjs +19 -0
  48. package/dist/png.cjs.map +1 -1
  49. package/dist/png.js +19 -0
  50. package/dist/png.js.map +1 -1
  51. package/dist/schemas.cjs +80 -0
  52. package/dist/schemas.cjs.map +1 -1
  53. package/dist/schemas.d.cts +30 -0
  54. package/dist/schemas.d.ts +30 -0
  55. package/dist/schemas.js +80 -0
  56. package/dist/schemas.js.map +1 -1
  57. package/dist/voxta.cjs +14 -102
  58. package/dist/voxta.cjs.map +1 -1
  59. package/dist/voxta.js +15 -103
  60. package/dist/voxta.js.map +1 -1
  61. package/package.json +4 -4
@@ -681,6 +681,7 @@ export declare function flattenSchema<T extends z.ZodRawShape>(schema: z.ZodObje
681
681
  export declare function analyzeField(name: string, zodType: z.ZodTypeAny): FieldInfo;
682
682
  /**
683
683
  * Get the value at a dot-notation path from an object.
684
+ * SECURITY: Rejects dangerous keys (__proto__, constructor, prototype) to prevent prototype pollution.
684
685
  *
685
686
  * @example
686
687
  * ```ts
@@ -690,6 +691,7 @@ export declare function analyzeField(name: string, zodType: z.ZodTypeAny): Field
690
691
  export declare function getValueAtPath(obj: Record<string, unknown>, path: string): unknown;
691
692
  /**
692
693
  * Set a value at a dot-notation path in an object (immutably).
694
+ * SECURITY: Rejects dangerous keys (__proto__, constructor, prototype) to prevent prototype pollution.
693
695
  *
694
696
  * @example
695
697
  * ```ts
@@ -809,11 +811,24 @@ export declare function TextInput({ value, onChange, name, label, error, disable
809
811
  * ```
810
812
  */
811
813
  export declare function Textarea({ value, onChange, name, label, error, disabled, required, hint, }: FieldWidgetProps<string>): react_jsx_runtime.JSX.Element;
814
+ /**
815
+ * Props for NumberInput - allows number | undefined since empty inputs are undefined
816
+ */
817
+ export type NumberInputProps = Omit<FieldWidgetProps<number | undefined>, "onChange"> & {
818
+ /** Value can be number or undefined (empty) */
819
+ value: number | undefined;
820
+ /** onChange receives number or undefined (empty) */
821
+ onChange: (value: number | undefined) => void;
822
+ };
812
823
  /**
813
824
  * Headless number input widget.
814
825
  * Renders a number input with min/max/step support.
826
+ *
827
+ * Note: This widget properly handles empty inputs as `undefined`,
828
+ * not as a type-cast lie. The form resolver handles validation
829
+ * for required fields.
815
830
  */
816
- export declare function NumberInput({ value, onChange, name, label, error, disabled, required, hint, }: FieldWidgetProps<number>): react_jsx_runtime.JSX.Element;
831
+ export declare function NumberInput({ value, onChange, name, label, error, disabled, required, hint, }: NumberInputProps): react_jsx_runtime.JSX.Element;
817
832
  /**
818
833
  * Headless switch/toggle widget.
819
834
  * Renders a checkbox with switch semantics (role="switch").
@@ -681,6 +681,7 @@ export declare function flattenSchema<T extends z.ZodRawShape>(schema: z.ZodObje
681
681
  export declare function analyzeField(name: string, zodType: z.ZodTypeAny): FieldInfo;
682
682
  /**
683
683
  * Get the value at a dot-notation path from an object.
684
+ * SECURITY: Rejects dangerous keys (__proto__, constructor, prototype) to prevent prototype pollution.
684
685
  *
685
686
  * @example
686
687
  * ```ts
@@ -690,6 +691,7 @@ export declare function analyzeField(name: string, zodType: z.ZodTypeAny): Field
690
691
  export declare function getValueAtPath(obj: Record<string, unknown>, path: string): unknown;
691
692
  /**
692
693
  * Set a value at a dot-notation path in an object (immutably).
694
+ * SECURITY: Rejects dangerous keys (__proto__, constructor, prototype) to prevent prototype pollution.
693
695
  *
694
696
  * @example
695
697
  * ```ts
@@ -809,11 +811,24 @@ export declare function TextInput({ value, onChange, name, label, error, disable
809
811
  * ```
810
812
  */
811
813
  export declare function Textarea({ value, onChange, name, label, error, disabled, required, hint, }: FieldWidgetProps<string>): react_jsx_runtime.JSX.Element;
814
+ /**
815
+ * Props for NumberInput - allows number | undefined since empty inputs are undefined
816
+ */
817
+ export type NumberInputProps = Omit<FieldWidgetProps<number | undefined>, "onChange"> & {
818
+ /** Value can be number or undefined (empty) */
819
+ value: number | undefined;
820
+ /** onChange receives number or undefined (empty) */
821
+ onChange: (value: number | undefined) => void;
822
+ };
812
823
  /**
813
824
  * Headless number input widget.
814
825
  * Renders a number input with min/max/step support.
826
+ *
827
+ * Note: This widget properly handles empty inputs as `undefined`,
828
+ * not as a type-cast lie. The form resolver handles validation
829
+ * for required fields.
815
830
  */
816
- export declare function NumberInput({ value, onChange, name, label, error, disabled, required, hint, }: FieldWidgetProps<number>): react_jsx_runtime.JSX.Element;
831
+ export declare function NumberInput({ value, onChange, name, label, error, disabled, required, hint, }: NumberInputProps): react_jsx_runtime.JSX.Element;
817
832
  /**
818
833
  * Headless switch/toggle widget.
819
834
  * Renders a checkbox with switch semantics (role="switch").
@@ -1,5 +1,5 @@
1
1
  // ../app-framework/dist/index.js
2
- import { useMemo, useEffect as useEffect2, useCallback as useCallback5, useRef as useRef3 } from "react";
2
+ import { useMemo as useMemo2, useEffect as useEffect2, useCallback as useCallback5, useRef as useRef3, useState as useState5 } from "react";
3
3
  import "zod";
4
4
  import { useForm, Controller, FormProvider } from "react-hook-form";
5
5
  import { zodResolver } from "@hookform/resolvers/zod";
@@ -10,7 +10,7 @@ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
10
10
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
11
11
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
12
12
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
13
- import { useState, useCallback, useRef, useEffect } from "react";
13
+ import { useState, useCallback, useRef, useEffect, useMemo } from "react";
14
14
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
15
15
  import { useState as useState2, useCallback as useCallback2 } from "react";
16
16
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -20,7 +20,7 @@ import { useState as useState4, useCallback as useCallback4, useRef as useRef2 }
20
20
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
21
21
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
22
22
  import { Fragment, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
23
- import { useState as useState5, useCallback as useCallback6 } from "react";
23
+ import { useState as useState6, useCallback as useCallback6 } from "react";
24
24
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
25
25
  var noopServices = {
26
26
  toast: {
@@ -352,13 +352,23 @@ function analyzeField(name, zodType) {
352
352
  constraints: extractConstraints(currentType)
353
353
  };
354
354
  }
355
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
356
+ function isSafeKey(key) {
357
+ return !DANGEROUS_KEYS.has(key);
358
+ }
355
359
  function getValueAtPath(obj, path) {
356
360
  const parts = path.split(".");
357
361
  let current = obj;
358
362
  for (const part of parts) {
363
+ if (!isSafeKey(part)) {
364
+ return void 0;
365
+ }
359
366
  if (current == null || typeof current !== "object") {
360
367
  return void 0;
361
368
  }
369
+ if (!Object.hasOwn(current, part)) {
370
+ return void 0;
371
+ }
362
372
  current = current[part];
363
373
  }
364
374
  return current;
@@ -368,12 +378,16 @@ function setValueAtPath(obj, path, value) {
368
378
  if (parts.length === 0 || parts[0] === void 0) {
369
379
  return obj;
370
380
  }
381
+ const first = parts[0];
382
+ if (!isSafeKey(first)) {
383
+ console.warn(`Rejected dangerous property key in path: ${first}`);
384
+ return obj;
385
+ }
371
386
  if (parts.length === 1) {
372
- return { ...obj, [parts[0]]: value };
387
+ return { ...obj, [first]: value };
373
388
  }
374
- const first = parts[0];
375
389
  const rest = parts.slice(1);
376
- const nested = obj[first] ?? {};
390
+ const nested = (Object.hasOwn(obj, first) ? obj[first] : {}) ?? {};
377
391
  return {
378
392
  ...obj,
379
393
  [first]: setValueAtPath(nested, rest.join("."), value)
@@ -765,11 +779,31 @@ function SearchableSelect({
765
779
  const [isOpen, setIsOpen] = useState(false);
766
780
  const [searchTerm, setSearchTerm] = useState("");
767
781
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
768
- const options = hint?.options ?? [];
769
- const filteredOptions = searchTerm ? options.filter(
770
- (opt) => opt.label.toLowerCase().includes(searchTerm.toLowerCase()) || opt.value.toLowerCase().includes(searchTerm.toLowerCase())
771
- ) : options;
772
- const selectedOption = options.find((opt) => opt.value === value);
782
+ const rawOptions = hint?.options ?? [];
783
+ const indexedOptions = useMemo(() => {
784
+ return rawOptions.map((opt) => ({
785
+ option: opt,
786
+ labelLower: opt.label.toLowerCase(),
787
+ valueLower: opt.value.toLowerCase()
788
+ }));
789
+ }, [rawOptions]);
790
+ const optionsByValue = useMemo(() => {
791
+ const map = /* @__PURE__ */ new Map();
792
+ for (const opt of rawOptions) {
793
+ map.set(opt.value, opt);
794
+ }
795
+ return map;
796
+ }, [rawOptions]);
797
+ const filteredOptions = useMemo(() => {
798
+ if (!searchTerm) {
799
+ return rawOptions;
800
+ }
801
+ const searchLower = searchTerm.toLowerCase();
802
+ return indexedOptions.filter(
803
+ (indexed) => indexed.labelLower.includes(searchLower) || indexed.valueLower.includes(searchLower)
804
+ ).map((indexed) => indexed.option);
805
+ }, [searchTerm, indexedOptions, rawOptions]);
806
+ const selectedOption = optionsByValue.get(value ?? "");
773
807
  const displayValue = selectedOption?.label ?? value ?? "";
774
808
  useEffect(() => {
775
809
  function handleClickOutside(e) {
@@ -1392,9 +1426,9 @@ function AutoForm({
1392
1426
  widgetRegistry: widgetRegistry2,
1393
1427
  children
1394
1428
  }) {
1395
- const fieldInfoMap = useMemo(() => analyzeSchema(schema), [schema]);
1396
- const flatFieldInfoMap = useMemo(() => flattenSchema(schema), [schema]);
1397
- const schemaDefaults = useMemo(() => {
1429
+ const fieldInfoMap = useMemo2(() => analyzeSchema(schema), [schema]);
1430
+ const flatFieldInfoMap = useMemo2(() => flattenSchema(schema), [schema]);
1431
+ const schemaDefaults = useMemo2(() => {
1398
1432
  const defaults = {};
1399
1433
  function extractDefaults(fields, prefix = "") {
1400
1434
  fields.forEach((info, key) => {
@@ -1410,33 +1444,84 @@ function AutoForm({
1410
1444
  extractDefaults(fieldInfoMap);
1411
1445
  return defaults;
1412
1446
  }, [fieldInfoMap]);
1447
+ const mergedDefaults = useMemo2(() => {
1448
+ return deepMerge(
1449
+ deepMerge(schemaDefaults, defaultValues ?? {}),
1450
+ values ?? {}
1451
+ );
1452
+ }, [schemaDefaults, defaultValues, values]);
1413
1453
  const methods = useForm({
1414
1454
  resolver: zodResolver(schema),
1415
- defaultValues: {
1416
- ...schemaDefaults,
1417
- ...defaultValues,
1418
- ...values
1419
- },
1455
+ defaultValues: mergedDefaults,
1420
1456
  mode: "onChange",
1421
1457
  shouldUnregister: true
1422
1458
  });
1423
- const { control, handleSubmit, watch, formState, reset } = methods;
1424
- const watchedValues = watch();
1459
+ const { control, handleSubmit, watch, formState, reset, getValues } = methods;
1425
1460
  const prevValuesRef = useRef3(values);
1461
+ const conditionFields = useMemo2(() => {
1462
+ const fields = /* @__PURE__ */ new Set();
1463
+ const extractConditionFields = (hints, prefix = "") => {
1464
+ for (const [key, value] of Object.entries(hints)) {
1465
+ if (!value || typeof value !== "object") continue;
1466
+ const hint = value;
1467
+ if (hint.condition && typeof hint.condition === "object") {
1468
+ const condition = hint.condition;
1469
+ if (condition.field) {
1470
+ fields.add(condition.field);
1471
+ }
1472
+ }
1473
+ if (!("widget" in hint) && !("label" in hint) && !("condition" in hint)) {
1474
+ extractConditionFields(hint, prefix ? `${prefix}.${key}` : key);
1475
+ }
1476
+ }
1477
+ };
1478
+ extractConditionFields(uiHints);
1479
+ return Array.from(fields);
1480
+ }, [uiHints]);
1481
+ const [conditionValues, setConditionValues] = useState5({});
1482
+ useEffect2(() => {
1483
+ if (conditionFields.length === 0) return;
1484
+ const initial = {};
1485
+ const currentValues = getValues();
1486
+ for (const field of conditionFields) {
1487
+ initial[field] = getValueAtPath(currentValues, field);
1488
+ }
1489
+ setConditionValues(initial);
1490
+ const subscription = watch((formValues, { name }) => {
1491
+ if (name && conditionFields.includes(name)) {
1492
+ setConditionValues((prev) => ({
1493
+ ...prev,
1494
+ [name]: getValueAtPath(formValues, name)
1495
+ }));
1496
+ }
1497
+ });
1498
+ return () => subscription.unsubscribe();
1499
+ }, [conditionFields, watch, getValues]);
1426
1500
  useEffect2(() => {
1427
1501
  if (values && !shallowEqual(values, prevValuesRef.current)) {
1428
1502
  prevValuesRef.current = values;
1429
- reset({ ...schemaDefaults, ...defaultValues, ...values });
1503
+ const resetValues = deepMerge(
1504
+ deepMerge(schemaDefaults, defaultValues ?? {}),
1505
+ values ?? {}
1506
+ );
1507
+ reset(resetValues);
1430
1508
  }
1431
1509
  }, [values, reset, schemaDefaults, defaultValues]);
1510
+ const onChangeRef = useRef3(onChange);
1511
+ onChangeRef.current = onChange;
1512
+ const schemaRef = useRef3(schema);
1513
+ schemaRef.current = schema;
1432
1514
  useEffect2(() => {
1433
- if (onChange) {
1434
- const result = schema.safeParse(watchedValues);
1435
- if (result.success) {
1436
- onChange(result.data);
1515
+ if (!onChangeRef.current) return;
1516
+ const subscription = watch((formValues, { type }) => {
1517
+ if (type !== "change") return;
1518
+ const result = schemaRef.current.safeParse(formValues);
1519
+ if (result.success && onChangeRef.current) {
1520
+ onChangeRef.current(result.data);
1437
1521
  }
1438
- }
1439
- }, [watchedValues, onChange, schema]);
1522
+ });
1523
+ return () => subscription.unsubscribe();
1524
+ }, [watch]);
1440
1525
  const HINT_KEYS = /* @__PURE__ */ new Set([
1441
1526
  "widget",
1442
1527
  "label",
@@ -1482,9 +1567,9 @@ function AutoForm({
1482
1567
  const isConditionMet = useCallback5(
1483
1568
  (condition) => {
1484
1569
  if (!condition) return true;
1485
- const fieldValue = getValueAtPath(watchedValues, condition.field);
1570
+ const fieldValue = conditionFields.length > 0 ? conditionValues[condition.field] : getValueAtPath(getValues(), condition.field);
1486
1571
  if (condition.when) {
1487
- return condition.when(fieldValue, watchedValues);
1572
+ return condition.when(fieldValue, getValues());
1488
1573
  }
1489
1574
  if ("equals" in condition && condition.equals !== void 0) {
1490
1575
  return fieldValue === condition.equals;
@@ -1500,9 +1585,9 @@ function AutoForm({
1500
1585
  }
1501
1586
  return true;
1502
1587
  },
1503
- [watchedValues]
1588
+ [conditionFields, conditionValues, getValues]
1504
1589
  );
1505
- const orderedFields = useMemo(() => {
1590
+ const orderedFields = useMemo2(() => {
1506
1591
  if (fieldOrder) {
1507
1592
  return fieldOrder.map((f) => String(f));
1508
1593
  }
@@ -1594,19 +1679,51 @@ function AutoForm({
1594
1679
  }
1595
1680
  return formContent;
1596
1681
  }
1682
+ function deepMerge(base, override) {
1683
+ const result = { ...base };
1684
+ for (const key of Object.keys(override)) {
1685
+ if (!isSafeKey2(key)) continue;
1686
+ const baseVal = base[key];
1687
+ const overrideVal = override[key];
1688
+ if (isPlainObject(baseVal) && isPlainObject(overrideVal)) {
1689
+ result[key] = deepMerge(
1690
+ baseVal,
1691
+ overrideVal
1692
+ );
1693
+ } else if (overrideVal !== void 0) {
1694
+ result[key] = overrideVal;
1695
+ }
1696
+ }
1697
+ return result;
1698
+ }
1699
+ function isPlainObject(val) {
1700
+ return val !== null && typeof val === "object" && !Array.isArray(val) && Object.getPrototypeOf(val) === Object.prototype;
1701
+ }
1702
+ var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1703
+ function isSafeKey2(key) {
1704
+ return !DANGEROUS_KEYS2.has(key);
1705
+ }
1597
1706
  function setNestedValue(obj, path, value) {
1598
1707
  const parts = path.split(".");
1599
1708
  let current = obj;
1600
1709
  for (let i = 0; i < parts.length - 1; i++) {
1601
1710
  const part = parts[i];
1602
1711
  if (part === void 0) continue;
1603
- if (!(part in current) || typeof current[part] !== "object") {
1712
+ if (!isSafeKey2(part)) {
1713
+ console.warn(`Rejected dangerous property key in path: ${part}`);
1714
+ return;
1715
+ }
1716
+ if (!Object.hasOwn(current, part) || typeof current[part] !== "object") {
1604
1717
  current[part] = {};
1605
1718
  }
1606
1719
  current = current[part];
1607
1720
  }
1608
1721
  const lastPart = parts[parts.length - 1];
1609
1722
  if (lastPart !== void 0) {
1723
+ if (!isSafeKey2(lastPart)) {
1724
+ console.warn(`Rejected dangerous property key in path: ${lastPart}`);
1725
+ return;
1726
+ }
1610
1727
  current[lastPart] = value;
1611
1728
  }
1612
1729
  }
@@ -1636,7 +1753,7 @@ function FieldGroup({
1636
1753
  className,
1637
1754
  children
1638
1755
  }) {
1639
- const [isCollapsed, setIsCollapsed] = useState5(defaultCollapsed);
1756
+ const [isCollapsed, setIsCollapsed] = useState6(defaultCollapsed);
1640
1757
  const toggleCollapsed = useCallback6(() => {
1641
1758
  if (collapsible) {
1642
1759
  setIsCollapsed((prev) => !prev);