@character-foundry/character-foundry 0.4.3 → 0.5.0

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 (63) hide show
  1. package/dist/app-framework.cjs +12 -3
  2. package/dist/app-framework.cjs.map +1 -1
  3. package/dist/app-framework.d.cts +9 -1
  4. package/dist/app-framework.d.ts +9 -1
  5. package/dist/app-framework.js +12 -3
  6. package/dist/app-framework.js.map +1 -1
  7. package/dist/charx.cjs +85 -52
  8. package/dist/charx.cjs.map +1 -1
  9. package/dist/charx.d.cts +22 -22
  10. package/dist/charx.d.ts +22 -22
  11. package/dist/charx.js +85 -52
  12. package/dist/charx.js.map +1 -1
  13. package/dist/exporter.cjs +104 -54
  14. package/dist/exporter.cjs.map +1 -1
  15. package/dist/exporter.d.cts +19 -19
  16. package/dist/exporter.d.ts +19 -19
  17. package/dist/exporter.js +104 -54
  18. package/dist/exporter.js.map +1 -1
  19. package/dist/federation.cjs +104 -36
  20. package/dist/federation.cjs.map +1 -1
  21. package/dist/federation.d.cts +54 -19
  22. package/dist/federation.d.ts +54 -19
  23. package/dist/federation.js +104 -36
  24. package/dist/federation.js.map +1 -1
  25. package/dist/index.cjs +104 -54
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +29 -29
  28. package/dist/index.d.ts +29 -29
  29. package/dist/index.js +104 -54
  30. package/dist/index.js.map +1 -1
  31. package/dist/loader.cjs +171 -31
  32. package/dist/loader.cjs.map +1 -1
  33. package/dist/loader.d.cts +37 -23
  34. package/dist/loader.d.ts +37 -23
  35. package/dist/loader.js +171 -31
  36. package/dist/loader.js.map +1 -1
  37. package/dist/lorebook.d.cts +23 -23
  38. package/dist/lorebook.d.ts +23 -23
  39. package/dist/normalizer.cjs +72 -18
  40. package/dist/normalizer.cjs.map +1 -1
  41. package/dist/normalizer.d.cts +37 -37
  42. package/dist/normalizer.d.ts +37 -37
  43. package/dist/normalizer.js +72 -18
  44. package/dist/normalizer.js.map +1 -1
  45. package/dist/png.cjs +72 -18
  46. package/dist/png.cjs.map +1 -1
  47. package/dist/png.d.cts +25 -25
  48. package/dist/png.d.ts +25 -25
  49. package/dist/png.js +72 -18
  50. package/dist/png.js.map +1 -1
  51. package/dist/schemas.cjs +80 -23
  52. package/dist/schemas.cjs.map +1 -1
  53. package/dist/schemas.d.cts +85 -67
  54. package/dist/schemas.d.ts +85 -67
  55. package/dist/schemas.js +80 -23
  56. package/dist/schemas.js.map +1 -1
  57. package/dist/voxta.cjs +91 -20
  58. package/dist/voxta.cjs.map +1 -1
  59. package/dist/voxta.d.cts +23 -23
  60. package/dist/voxta.d.ts +23 -23
  61. package/dist/voxta.js +91 -20
  62. package/dist/voxta.js.map +1 -1
  63. package/package.json +24 -24
@@ -31,8 +31,8 @@ declare const CCv3DataSchema: z.ZodObject<{
31
31
  character_book: z.ZodNullable<z.ZodOptional<z.ZodObject<{
32
32
  name: z.ZodOptional<z.ZodString>;
33
33
  description: z.ZodOptional<z.ZodString>;
34
- scan_depth: z.ZodOptional<z.ZodNumber>;
35
- token_budget: z.ZodOptional<z.ZodNumber>;
34
+ scan_depth: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
35
+ token_budget: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
36
36
  recursive_scanning: z.ZodOptional<z.ZodBoolean>;
37
37
  extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
38
38
  entries: z.ZodArray<z.ZodObject<{
@@ -268,14 +268,14 @@ declare const CCv3DataSchema: z.ZodObject<{
268
268
  }, z.ZodTypeAny, "passthrough">[];
269
269
  name?: string | undefined;
270
270
  description?: string | undefined;
271
- scan_depth?: number | undefined;
272
- token_budget?: number | undefined;
271
+ scan_depth?: unknown;
272
+ token_budget?: unknown;
273
273
  recursive_scanning?: boolean | undefined;
274
274
  extensions?: Record<string, unknown> | undefined;
275
275
  }>>>;
276
276
  extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
277
277
  assets: z.ZodOptional<z.ZodArray<z.ZodObject<{
278
- type: z.ZodEnum<[
278
+ type: z.ZodEffects<z.ZodEnum<[
279
279
  "icon",
280
280
  "background",
281
281
  "emotion",
@@ -284,7 +284,7 @@ declare const CCv3DataSchema: z.ZodObject<{
284
284
  "video",
285
285
  "custom",
286
286
  "x-risu-asset"
287
- ]>;
287
+ ]>, "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset", unknown>;
288
288
  uri: z.ZodString;
289
289
  name: z.ZodString;
290
290
  ext: z.ZodString;
@@ -295,15 +295,15 @@ declare const CCv3DataSchema: z.ZodObject<{
295
295
  ext: string;
296
296
  }, {
297
297
  name: string;
298
- type: "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset";
299
298
  uri: string;
300
299
  ext: string;
300
+ type?: unknown;
301
301
  }>, "many">>;
302
302
  nickname: z.ZodOptional<z.ZodString>;
303
303
  creator_notes_multilingual: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
304
304
  source: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
305
- creation_date: z.ZodOptional<z.ZodNumber>;
306
- modification_date: z.ZodOptional<z.ZodNumber>;
305
+ creation_date: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
306
+ modification_date: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
307
307
  }, "strip", z.ZodTypeAny, {
308
308
  name: string;
309
309
  description: string;
@@ -444,8 +444,8 @@ declare const CCv3DataSchema: z.ZodObject<{
444
444
  }, z.ZodTypeAny, "passthrough">[];
445
445
  name?: string | undefined;
446
446
  description?: string | undefined;
447
- scan_depth?: number | undefined;
448
- token_budget?: number | undefined;
447
+ scan_depth?: unknown;
448
+ token_budget?: unknown;
449
449
  recursive_scanning?: boolean | undefined;
450
450
  extensions?: Record<string, unknown> | undefined;
451
451
  } | null | undefined;
@@ -455,15 +455,15 @@ declare const CCv3DataSchema: z.ZodObject<{
455
455
  group_only_greetings?: string[] | undefined;
456
456
  assets?: {
457
457
  name: string;
458
- type: "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset";
459
458
  uri: string;
460
459
  ext: string;
460
+ type?: unknown;
461
461
  }[] | undefined;
462
462
  nickname?: string | undefined;
463
463
  creator_notes_multilingual?: Record<string, string> | undefined;
464
464
  source?: string[] | undefined;
465
- creation_date?: number | undefined;
466
- modification_date?: number | undefined;
465
+ creation_date?: unknown;
466
+ modification_date?: unknown;
467
467
  }>;
468
468
  }, "strip", z.ZodTypeAny, {
469
469
  data: {
@@ -610,8 +610,8 @@ declare const CCv3DataSchema: z.ZodObject<{
610
610
  }, z.ZodTypeAny, "passthrough">[];
611
611
  name?: string | undefined;
612
612
  description?: string | undefined;
613
- scan_depth?: number | undefined;
614
- token_budget?: number | undefined;
613
+ scan_depth?: unknown;
614
+ token_budget?: unknown;
615
615
  recursive_scanning?: boolean | undefined;
616
616
  extensions?: Record<string, unknown> | undefined;
617
617
  } | null | undefined;
@@ -621,15 +621,15 @@ declare const CCv3DataSchema: z.ZodObject<{
621
621
  group_only_greetings?: string[] | undefined;
622
622
  assets?: {
623
623
  name: string;
624
- type: "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset";
625
624
  uri: string;
626
625
  ext: string;
626
+ type?: unknown;
627
627
  }[] | undefined;
628
628
  nickname?: string | undefined;
629
629
  creator_notes_multilingual?: Record<string, string> | undefined;
630
630
  source?: string[] | undefined;
631
- creation_date?: number | undefined;
632
- modification_date?: number | undefined;
631
+ creation_date?: unknown;
632
+ modification_date?: unknown;
633
633
  };
634
634
  spec: "chara_card_v3";
635
635
  spec_version: "3.0";
@@ -31,8 +31,8 @@ declare const CCv3DataSchema: z.ZodObject<{
31
31
  character_book: z.ZodNullable<z.ZodOptional<z.ZodObject<{
32
32
  name: z.ZodOptional<z.ZodString>;
33
33
  description: z.ZodOptional<z.ZodString>;
34
- scan_depth: z.ZodOptional<z.ZodNumber>;
35
- token_budget: z.ZodOptional<z.ZodNumber>;
34
+ scan_depth: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
35
+ token_budget: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
36
36
  recursive_scanning: z.ZodOptional<z.ZodBoolean>;
37
37
  extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
38
38
  entries: z.ZodArray<z.ZodObject<{
@@ -268,14 +268,14 @@ declare const CCv3DataSchema: z.ZodObject<{
268
268
  }, z.ZodTypeAny, "passthrough">[];
269
269
  name?: string | undefined;
270
270
  description?: string | undefined;
271
- scan_depth?: number | undefined;
272
- token_budget?: number | undefined;
271
+ scan_depth?: unknown;
272
+ token_budget?: unknown;
273
273
  recursive_scanning?: boolean | undefined;
274
274
  extensions?: Record<string, unknown> | undefined;
275
275
  }>>>;
276
276
  extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
277
277
  assets: z.ZodOptional<z.ZodArray<z.ZodObject<{
278
- type: z.ZodEnum<[
278
+ type: z.ZodEffects<z.ZodEnum<[
279
279
  "icon",
280
280
  "background",
281
281
  "emotion",
@@ -284,7 +284,7 @@ declare const CCv3DataSchema: z.ZodObject<{
284
284
  "video",
285
285
  "custom",
286
286
  "x-risu-asset"
287
- ]>;
287
+ ]>, "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset", unknown>;
288
288
  uri: z.ZodString;
289
289
  name: z.ZodString;
290
290
  ext: z.ZodString;
@@ -295,15 +295,15 @@ declare const CCv3DataSchema: z.ZodObject<{
295
295
  ext: string;
296
296
  }, {
297
297
  name: string;
298
- type: "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset";
299
298
  uri: string;
300
299
  ext: string;
300
+ type?: unknown;
301
301
  }>, "many">>;
302
302
  nickname: z.ZodOptional<z.ZodString>;
303
303
  creator_notes_multilingual: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
304
304
  source: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
305
- creation_date: z.ZodOptional<z.ZodNumber>;
306
- modification_date: z.ZodOptional<z.ZodNumber>;
305
+ creation_date: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
306
+ modification_date: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
307
307
  }, "strip", z.ZodTypeAny, {
308
308
  name: string;
309
309
  description: string;
@@ -444,8 +444,8 @@ declare const CCv3DataSchema: z.ZodObject<{
444
444
  }, z.ZodTypeAny, "passthrough">[];
445
445
  name?: string | undefined;
446
446
  description?: string | undefined;
447
- scan_depth?: number | undefined;
448
- token_budget?: number | undefined;
447
+ scan_depth?: unknown;
448
+ token_budget?: unknown;
449
449
  recursive_scanning?: boolean | undefined;
450
450
  extensions?: Record<string, unknown> | undefined;
451
451
  } | null | undefined;
@@ -455,15 +455,15 @@ declare const CCv3DataSchema: z.ZodObject<{
455
455
  group_only_greetings?: string[] | undefined;
456
456
  assets?: {
457
457
  name: string;
458
- type: "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset";
459
458
  uri: string;
460
459
  ext: string;
460
+ type?: unknown;
461
461
  }[] | undefined;
462
462
  nickname?: string | undefined;
463
463
  creator_notes_multilingual?: Record<string, string> | undefined;
464
464
  source?: string[] | undefined;
465
- creation_date?: number | undefined;
466
- modification_date?: number | undefined;
465
+ creation_date?: unknown;
466
+ modification_date?: unknown;
467
467
  }>;
468
468
  }, "strip", z.ZodTypeAny, {
469
469
  data: {
@@ -610,8 +610,8 @@ declare const CCv3DataSchema: z.ZodObject<{
610
610
  }, z.ZodTypeAny, "passthrough">[];
611
611
  name?: string | undefined;
612
612
  description?: string | undefined;
613
- scan_depth?: number | undefined;
614
- token_budget?: number | undefined;
613
+ scan_depth?: unknown;
614
+ token_budget?: unknown;
615
615
  recursive_scanning?: boolean | undefined;
616
616
  extensions?: Record<string, unknown> | undefined;
617
617
  } | null | undefined;
@@ -621,15 +621,15 @@ declare const CCv3DataSchema: z.ZodObject<{
621
621
  group_only_greetings?: string[] | undefined;
622
622
  assets?: {
623
623
  name: string;
624
- type: "custom" | "icon" | "background" | "emotion" | "user_icon" | "sound" | "video" | "x-risu-asset";
625
624
  uri: string;
626
625
  ext: string;
626
+ type?: unknown;
627
627
  }[] | undefined;
628
628
  nickname?: string | undefined;
629
629
  creator_notes_multilingual?: Record<string, string> | undefined;
630
630
  source?: string[] | undefined;
631
- creation_date?: number | undefined;
632
- modification_date?: number | undefined;
631
+ creation_date?: unknown;
632
+ modification_date?: unknown;
633
633
  };
634
634
  spec: "chara_card_v3";
635
635
  spec_version: "3.0";
package/dist/exporter.js CHANGED
@@ -184,6 +184,58 @@ import { z } from "zod";
184
184
  import { z as z2 } from "zod";
185
185
  import { z as z3 } from "zod";
186
186
  import "zod";
187
+ function preprocessTimestamp(val) {
188
+ if (val === null || val === void 0) return void 0;
189
+ let num;
190
+ if (typeof val === "number") {
191
+ num = val;
192
+ } else if (typeof val === "string") {
193
+ const trimmed = val.trim();
194
+ if (!trimmed) return void 0;
195
+ const parsed = Number(trimmed);
196
+ if (!isNaN(parsed)) {
197
+ num = parsed;
198
+ } else {
199
+ const date = new Date(trimmed);
200
+ if (isNaN(date.getTime())) return void 0;
201
+ num = Math.floor(date.getTime() / 1e3);
202
+ }
203
+ } else {
204
+ return void 0;
205
+ }
206
+ if (num > 1e10) {
207
+ num = Math.floor(num / 1e3);
208
+ }
209
+ if (num < 0) return void 0;
210
+ return num;
211
+ }
212
+ function preprocessNumeric(val) {
213
+ if (val === null || val === void 0) return void 0;
214
+ if (typeof val === "number") {
215
+ return isNaN(val) ? void 0 : val;
216
+ }
217
+ if (typeof val === "string") {
218
+ const trimmed = val.trim();
219
+ if (!trimmed) return void 0;
220
+ const parsed = Number(trimmed);
221
+ return isNaN(parsed) ? void 0 : parsed;
222
+ }
223
+ return void 0;
224
+ }
225
+ var KNOWN_ASSET_TYPES = /* @__PURE__ */ new Set([
226
+ "icon",
227
+ "background",
228
+ "emotion",
229
+ "user_icon",
230
+ "sound",
231
+ "video",
232
+ "custom",
233
+ "x-risu-asset"
234
+ ]);
235
+ function preprocessAssetType(val) {
236
+ if (typeof val !== "string") return "custom";
237
+ return KNOWN_ASSET_TYPES.has(val) ? val : "custom";
238
+ }
187
239
  var ISO8601Schema = z.string().datetime();
188
240
  var UUIDSchema = z.string().uuid();
189
241
  var SpecSchema = z.enum(["v2", "v3"]);
@@ -206,16 +258,19 @@ var SourceFormatSchema = z.enum([
206
258
  // VoxPkg format
207
259
  ]);
208
260
  var OriginalShapeSchema = z.enum(["wrapped", "unwrapped", "legacy"]);
209
- var AssetTypeSchema = z.enum([
210
- "icon",
211
- "background",
212
- "emotion",
213
- "user_icon",
214
- "sound",
215
- "video",
216
- "custom",
217
- "x-risu-asset"
218
- ]);
261
+ var AssetTypeSchema = z.preprocess(
262
+ preprocessAssetType,
263
+ z.enum([
264
+ "icon",
265
+ "background",
266
+ "emotion",
267
+ "user_icon",
268
+ "sound",
269
+ "video",
270
+ "custom",
271
+ "x-risu-asset"
272
+ ])
273
+ );
219
274
  var AssetDescriptorSchema = z.object({
220
275
  type: AssetTypeSchema,
221
276
  uri: z.string(),
@@ -249,8 +304,8 @@ var CCv2LorebookEntrySchema = z2.object({
249
304
  var CCv2CharacterBookSchema = z2.object({
250
305
  name: z2.string().optional(),
251
306
  description: z2.string().optional(),
252
- scan_depth: z2.number().int().nonnegative().optional(),
253
- token_budget: z2.number().int().nonnegative().optional(),
307
+ scan_depth: z2.preprocess(preprocessNumeric, z2.number().int().nonnegative().optional()),
308
+ token_budget: z2.preprocess(preprocessNumeric, z2.number().int().nonnegative().optional()),
254
309
  recursive_scanning: z2.boolean().optional(),
255
310
  extensions: z2.record(z2.unknown()).optional(),
256
311
  entries: z2.array(CCv2LorebookEntrySchema)
@@ -313,8 +368,8 @@ var CCv3LorebookEntrySchema = z3.object({
313
368
  var CCv3CharacterBookSchema = z3.object({
314
369
  name: z3.string().optional(),
315
370
  description: z3.string().optional(),
316
- scan_depth: z3.number().int().nonnegative().optional(),
317
- token_budget: z3.number().int().nonnegative().optional(),
371
+ scan_depth: z3.preprocess(preprocessNumeric, z3.number().int().nonnegative().optional()),
372
+ token_budget: z3.preprocess(preprocessNumeric, z3.number().int().nonnegative().optional()),
318
373
  recursive_scanning: z3.boolean().optional(),
319
374
  extensions: z3.record(z3.unknown()).optional(),
320
375
  entries: z3.array(CCv3LorebookEntrySchema)
@@ -346,10 +401,9 @@ var CCv3DataInnerSchema = z3.object({
346
401
  nickname: z3.string().optional(),
347
402
  creator_notes_multilingual: z3.record(z3.string()).optional(),
348
403
  source: z3.array(z3.string()).optional(),
349
- creation_date: z3.number().int().nonnegative().optional(),
350
- // Unix timestamp in seconds
351
- modification_date: z3.number().int().nonnegative().optional()
352
- // Unix timestamp in seconds
404
+ // Unix timestamps - preprocess to handle ISO strings, numeric strings, milliseconds
405
+ creation_date: z3.preprocess(preprocessTimestamp, z3.number().int().nonnegative().optional()),
406
+ modification_date: z3.preprocess(preprocessTimestamp, z3.number().int().nonnegative().optional())
353
407
  });
354
408
  var CCv3DataSchema = z3.object({
355
409
  spec: z3.literal("chara_card_v3"),
@@ -514,36 +568,6 @@ var SAFE_ASSET_TYPES = /* @__PURE__ */ new Set([
514
568
  "data",
515
569
  "unknown"
516
570
  ]);
517
- var SAFE_EXTENSIONS = /* @__PURE__ */ new Set([
518
- // Images
519
- "png",
520
- "jpg",
521
- "jpeg",
522
- "webp",
523
- "gif",
524
- "avif",
525
- "svg",
526
- "bmp",
527
- "ico",
528
- // Audio
529
- "mp3",
530
- "wav",
531
- "ogg",
532
- "flac",
533
- "m4a",
534
- "aac",
535
- "opus",
536
- // Video
537
- "mp4",
538
- "webm",
539
- "avi",
540
- "mov",
541
- "mkv",
542
- // Data
543
- "json",
544
- "txt",
545
- "bin"
546
- ]);
547
571
  function getCharxCategory(mimetype) {
548
572
  if (mimetype.startsWith("image/")) return "images";
549
573
  if (mimetype.startsWith("audio/")) return "audio";
@@ -559,11 +583,20 @@ function sanitizeAssetType(type) {
559
583
  return sanitized || "custom";
560
584
  }
561
585
  function sanitizeExtension(ext) {
562
- const normalized = ext.replace(/^\./, "").toLowerCase().replace(/[^a-z0-9]/g, "");
563
- if (SAFE_EXTENSIONS.has(normalized)) {
564
- return normalized;
586
+ const normalized = ext.trim().replace(/^\./, "").toLowerCase();
587
+ if (!normalized) {
588
+ throw new Error("Invalid asset extension: empty extension");
589
+ }
590
+ if (normalized.length > 64) {
591
+ throw new Error(`Invalid asset extension: too long (${normalized.length} chars)`);
592
+ }
593
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
594
+ throw new Error("Invalid asset extension: path separators are not allowed");
565
595
  }
566
- return "bin";
596
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
597
+ throw new Error(`Invalid asset extension: "${ext}"`);
598
+ }
599
+ return normalized;
567
600
  }
568
601
  function sanitizeName(name, ext) {
569
602
  let safeName = name;
@@ -7014,6 +7047,22 @@ function sanitizeName2(name, ext) {
7014
7047
  if (!safeName) safeName = "asset";
7015
7048
  return safeName;
7016
7049
  }
7050
+ function sanitizeExtension2(ext) {
7051
+ const normalized = ext.trim().replace(/^\./, "").toLowerCase();
7052
+ if (!normalized) {
7053
+ throw new Error("Invalid asset extension: empty extension");
7054
+ }
7055
+ if (normalized.length > 64) {
7056
+ throw new Error(`Invalid asset extension: too long (${normalized.length} chars)`);
7057
+ }
7058
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
7059
+ throw new Error("Invalid asset extension: path separators are not allowed");
7060
+ }
7061
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
7062
+ throw new Error(`Invalid asset extension: "${ext}"`);
7063
+ }
7064
+ return normalized;
7065
+ }
7017
7066
  function writeVoxta(card, assets, options = {}) {
7018
7067
  const { compressionLevel = 6, includePackageJson = false } = options;
7019
7068
  const cardData = card.data;
@@ -7102,8 +7151,9 @@ function writeVoxta(card, assets, options = {}) {
7102
7151
  let assetCount = 0;
7103
7152
  let mainThumbnail;
7104
7153
  for (const asset of assets) {
7105
- const safeName = sanitizeName2(asset.name, asset.ext);
7106
- const finalFilename = `${safeName}.${asset.ext}`;
7154
+ const safeExt = sanitizeExtension2(asset.ext);
7155
+ const safeName = sanitizeName2(asset.name, safeExt);
7156
+ const finalFilename = `${safeName}.${safeExt}`;
7107
7157
  let voxtaPath = "";
7108
7158
  const tags = asset.tags || [];
7109
7159
  const isMainIcon = asset.type === "icon" && (tags.includes("portrait-override") || asset.name === "main" || asset.isMain);