@kumix/utils 0.1.0 → 0.1.2

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.
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
- import { a as NODE_ENV, c as isProduction, i as LOG_LEVEL, l as isStaging, n as API_PORT, o as PORT, r as DEBUG, s as isDevelopment, t as logger, u as getEnv } from "./logger-BOB35dQN.js";
1
+ import { a as NODE_ENV, c as isProduction, d as getEnv, i as LOG_LEVEL, l as isStaging, n as API_PORT, o as PORT, r as DEBUG, s as isDevelopment, t as logger, u as isTest } from "./logger-qDWnDXis.js";
2
2
  import slugify, { default as slugify$1 } from "@sindresorhus/slugify";
3
- import baseX from "base-x";
4
3
  import { createId, init } from "@paralleldrive/cuid2";
5
4
  import { customAlphabet } from "nanoid";
6
5
  import ms from "ms";
@@ -551,31 +550,30 @@ const EU_COUNTRY_CODES = [
551
550
  "AT",
552
551
  "BE",
553
552
  "BG",
553
+ "HR",
554
554
  "CY",
555
555
  "CZ",
556
- "DE",
557
556
  "DK",
558
557
  "EE",
559
- "ES",
560
558
  "FI",
561
559
  "FR",
562
- "GB",
560
+ "DE",
563
561
  "GR",
564
- "HR",
565
562
  "HU",
566
563
  "IE",
567
564
  "IT",
565
+ "LV",
568
566
  "LT",
569
567
  "LU",
570
- "LV",
571
568
  "MT",
572
569
  "NL",
573
570
  "PL",
574
571
  "PT",
575
572
  "RO",
576
- "SE",
573
+ "SK",
577
574
  "SI",
578
- "SK"
575
+ "ES",
576
+ "SE"
579
577
  ];
580
578
  //#endregion
581
579
  //#region src/constants/country/country-phone-codes.ts
@@ -1149,7 +1147,7 @@ const REGIONS = {
1149
1147
  "BY-VI": "Vitsyebskaya voblasts'",
1150
1148
  "BE-BRU": "Brussels Hoofdstedelijk Gewest",
1151
1149
  "BE-VLG": "Vlaams Gewest",
1152
- "BE-WAL": "Waals Gewest[note 2]",
1150
+ "BE-WAL": "Waals Gewest",
1153
1151
  "BZ-BZ": "Belize",
1154
1152
  "BZ-CY": "Cayo",
1155
1153
  "BZ-CZL": "Corozal",
@@ -1981,7 +1979,7 @@ const REGIONS = {
1981
1979
  "IN-BR": "Bihār",
1982
1980
  "IN-CH": "Chandīgarh",
1983
1981
  "IN-CG": "Chhattīsgarh",
1984
- "IN-DH": "Dādra and Nagar Haveli and Damān and Diu[1]",
1982
+ "IN-DH": "Dādra and Nagar Haveli and Damān and Diu",
1985
1983
  "IN-DL": "Delhi",
1986
1984
  "IN-GA": "Goa",
1987
1985
  "IN-GJ": "Gujarāt",
@@ -2005,7 +2003,7 @@ const REGIONS = {
2005
2003
  "IN-RJ": "Rājasthān",
2006
2004
  "IN-SK": "Sikkim",
2007
2005
  "IN-TN": "Tamil Nādu",
2008
- "IN-TS": "Telangāna[2]",
2006
+ "IN-TS": "Telangāna",
2009
2007
  "IN-TR": "Tripura",
2010
2008
  "IN-UP": "Uttar Pradesh",
2011
2009
  "IN-UK": "Uttarākhand",
@@ -2070,7 +2068,7 @@ const REGIONS = {
2070
2068
  "IE-C": "Connaught",
2071
2069
  "IE-L": "Leinster",
2072
2070
  "IE-M": "Munster",
2073
- "IE-U": "Ulster[a]",
2071
+ "IE-U": "Ulster",
2074
2072
  "IL-D": "HaDarom",
2075
2073
  "IL-M": "HaMerkaz",
2076
2074
  "IL-Z": "HaTsafon",
@@ -3084,7 +3082,7 @@ const REGIONS = {
3084
3082
  "PE-TAC": "Tacna",
3085
3083
  "PE-TUM": "Tumbes",
3086
3084
  "PE-UCA": "Ucayali",
3087
- "PH-14": "Autonomous Region in Muslim Mindanao[b]",
3085
+ "PH-14": "Autonomous Region in Muslim Mindanao",
3088
3086
  "PH-05": "Bicol",
3089
3087
  "PH-02": "Cagayan Valley",
3090
3088
  "PH-40": "Calabarzon",
@@ -4366,7 +4364,7 @@ const REGIONS = {
4366
4364
  "ME-20": "Ulcinj",
4367
4365
  "ME-21": "Žabljak",
4368
4366
  "ME-25": "Zeta",
4369
- "RS-KM": "Kosovo-Metohija[1]",
4367
+ "RS-KM": "Kosovo-Metohija",
4370
4368
  "RS-VO": "Vojvodina",
4371
4369
  "SS-EC": "Central Equatoria",
4372
4370
  "SS-EE": "Eastern Equatoria",
@@ -4387,8 +4385,12 @@ const REGION_CODES = Object.keys(REGIONS);
4387
4385
  * Provides a comprehensive set of all country-specific domain extensions
4388
4386
  */
4389
4387
  /**
4390
- * A Set containing all country code top-level domains (ccTLDs)
4391
- * Used for domain validation, parsing, and identification
4388
+ * A Set containing ISO 3166-1 alpha-2 country code top-level domains (ccTLDs),
4389
+ * plus a small set of commonly-recognized regional TLDs (`eu`, `eus`, `gal`,
4390
+ * `cat`, `asia`). De-duplicated and pruned of retired/withdrawn codes:
4391
+ * - `an` (Netherlands Antilles, retired 2010)
4392
+ * - `tp` (Timor-Leste, retired 2015 — use `tl`)
4393
+ * `gb` is included alongside `uk` per IANA reservation.
4392
4394
  *
4393
4395
  * @example
4394
4396
  * ```ts
@@ -4401,286 +4403,261 @@ const REGION_CODES = Object.keys(REGIONS);
4401
4403
  * const domain = 'example.co.uk';
4402
4404
  * const tld = domain.split('.').pop();
4403
4405
  * const isCountryDomain = tld ? ccTLDs.has(tld) : false; // true
4404
- *
4405
- * // Filter domains by ccTLD
4406
- * const domains = ['example.com', 'example.co.uk', 'example.org'];
4407
- * const countryDomains = domains.filter(domain => {
4408
- * const tld = domain.split('.').pop();
4409
- * return tld ? ccTLDs.has(tld) : false;
4410
- * }); // ['example.co.uk']
4411
4406
  * ```
4412
4407
  */
4413
4408
  const ccTLDs = /* @__PURE__ */ new Set([
4409
+ "ac",
4410
+ "ad",
4411
+ "ae",
4414
4412
  "af",
4415
- "ax",
4413
+ "ag",
4414
+ "ai",
4416
4415
  "al",
4417
- "dz",
4418
- "as",
4419
- "ad",
4416
+ "am",
4420
4417
  "ao",
4421
- "ai",
4422
4418
  "aq",
4423
- "ag",
4424
4419
  "ar",
4425
- "am",
4426
- "aw",
4427
- "ac",
4428
- "au",
4420
+ "as",
4429
4421
  "at",
4422
+ "au",
4423
+ "aw",
4424
+ "ax",
4430
4425
  "az",
4431
- "bs",
4432
- "bh",
4433
- "bd",
4426
+ "ba",
4434
4427
  "bb",
4435
- "eus",
4436
- "by",
4428
+ "bd",
4437
4429
  "be",
4438
- "bz",
4430
+ "bf",
4431
+ "bg",
4432
+ "bh",
4433
+ "bi",
4439
4434
  "bj",
4435
+ "bl",
4440
4436
  "bm",
4441
- "bt",
4437
+ "bn",
4442
4438
  "bo",
4443
4439
  "bq",
4444
- "an",
4445
- "nl",
4446
- "ba",
4447
- "bw",
4448
- "bv",
4449
4440
  "br",
4450
- "io",
4451
- "vg",
4452
- "bn",
4453
- "bg",
4454
- "bf",
4455
- "mm",
4456
- "bi",
4457
- "kh",
4458
- "cm",
4441
+ "bs",
4442
+ "bt",
4443
+ "bv",
4444
+ "bw",
4445
+ "by",
4446
+ "bz",
4459
4447
  "ca",
4460
- "cv",
4461
4448
  "cat",
4462
- "ky",
4463
- "cf",
4464
- "td",
4465
- "cl",
4466
- "cn",
4467
- "cx",
4468
4449
  "cc",
4469
- "co",
4470
- "km",
4471
4450
  "cd",
4451
+ "cf",
4472
4452
  "cg",
4453
+ "ch",
4454
+ "ci",
4473
4455
  "ck",
4456
+ "cl",
4457
+ "cm",
4458
+ "cn",
4459
+ "co",
4474
4460
  "cr",
4475
- "ci",
4476
- "hr",
4477
4461
  "cu",
4462
+ "cv",
4478
4463
  "cw",
4464
+ "cx",
4479
4465
  "cy",
4480
4466
  "cz",
4481
- "dk",
4467
+ "de",
4482
4468
  "dj",
4469
+ "dk",
4483
4470
  "dm",
4484
4471
  "do",
4485
- "tl",
4486
- "tp",
4472
+ "dz",
4487
4473
  "ec",
4474
+ "ee",
4488
4475
  "eg",
4489
- "sv",
4490
- "gq",
4476
+ "eh",
4491
4477
  "er",
4492
- "ee",
4478
+ "es",
4493
4479
  "et",
4494
4480
  "eu",
4481
+ "eus",
4482
+ "fi",
4483
+ "fj",
4495
4484
  "fk",
4496
- "fo",
4497
4485
  "fm",
4498
- "fj",
4499
- "fi",
4486
+ "fo",
4500
4487
  "fr",
4501
- "gf",
4502
- "pf",
4503
- "tf",
4504
4488
  "ga",
4505
4489
  "gal",
4506
- "gm",
4507
- "ps",
4490
+ "gb",
4491
+ "gd",
4508
4492
  "ge",
4509
- "de",
4493
+ "gf",
4494
+ "gg",
4510
4495
  "gh",
4511
4496
  "gi",
4512
- "gr",
4513
4497
  "gl",
4514
- "gd",
4498
+ "gm",
4499
+ "gn",
4515
4500
  "gp",
4516
- "gu",
4501
+ "gq",
4502
+ "gr",
4503
+ "gs",
4517
4504
  "gt",
4518
- "gg",
4519
- "gn",
4505
+ "gu",
4520
4506
  "gw",
4521
4507
  "gy",
4522
- "ht",
4508
+ "hk",
4523
4509
  "hm",
4524
4510
  "hn",
4525
- "hk",
4511
+ "hr",
4512
+ "ht",
4526
4513
  "hu",
4527
- "is",
4528
- "in",
4529
4514
  "id",
4530
- "ir",
4531
- "iq",
4532
4515
  "ie",
4533
- "im",
4534
4516
  "il",
4517
+ "im",
4518
+ "in",
4519
+ "io",
4520
+ "iq",
4521
+ "ir",
4522
+ "is",
4535
4523
  "it",
4536
- "jm",
4537
- "jp",
4538
4524
  "je",
4525
+ "jm",
4539
4526
  "jo",
4540
- "kz",
4527
+ "jp",
4541
4528
  "ke",
4529
+ "kg",
4530
+ "kh",
4542
4531
  "ki",
4532
+ "km",
4533
+ "kn",
4534
+ "kp",
4535
+ "kr",
4543
4536
  "kw",
4544
- "kg",
4537
+ "ky",
4538
+ "kz",
4545
4539
  "la",
4546
- "lv",
4547
4540
  "lb",
4548
- "ls",
4549
- "lr",
4550
- "ly",
4541
+ "lc",
4551
4542
  "li",
4543
+ "lk",
4544
+ "lr",
4545
+ "ls",
4552
4546
  "lt",
4553
4547
  "lu",
4554
- "mo",
4555
- "mk",
4548
+ "lv",
4549
+ "ly",
4550
+ "ma",
4551
+ "mc",
4552
+ "md",
4553
+ "me",
4554
+ "mf",
4556
4555
  "mg",
4557
- "mw",
4558
- "my",
4559
- "mv",
4560
- "ml",
4561
- "mt",
4562
4556
  "mh",
4557
+ "mk",
4558
+ "ml",
4559
+ "mm",
4560
+ "mn",
4561
+ "mo",
4562
+ "mp",
4563
4563
  "mq",
4564
4564
  "mr",
4565
+ "ms",
4566
+ "mt",
4565
4567
  "mu",
4566
- "yt",
4568
+ "mv",
4569
+ "mw",
4567
4570
  "mx",
4568
- "md",
4569
- "mc",
4570
- "mn",
4571
- "me",
4572
- "ms",
4573
- "ma",
4571
+ "my",
4574
4572
  "mz",
4575
- "mm",
4576
4573
  "na",
4577
- "nr",
4578
- "np",
4579
- "nl",
4580
4574
  "nc",
4581
- "nz",
4582
- "ni",
4583
4575
  "ne",
4584
- "ng",
4585
- "nu",
4586
4576
  "nf",
4587
- "nc",
4588
- "tr",
4589
- "kp",
4590
- "mp",
4577
+ "ng",
4578
+ "ni",
4579
+ "nl",
4591
4580
  "no",
4581
+ "np",
4582
+ "nr",
4583
+ "nu",
4584
+ "nz",
4592
4585
  "om",
4593
- "pk",
4594
- "pw",
4595
- "ps",
4596
4586
  "pa",
4597
- "pg",
4598
- "py",
4599
4587
  "pe",
4588
+ "pf",
4589
+ "pg",
4600
4590
  "ph",
4601
- "pn",
4591
+ "pk",
4602
4592
  "pl",
4603
- "pt",
4593
+ "pm",
4594
+ "pn",
4604
4595
  "pr",
4596
+ "ps",
4597
+ "pt",
4598
+ "pw",
4599
+ "py",
4605
4600
  "qa",
4601
+ "re",
4606
4602
  "ro",
4603
+ "rs",
4607
4604
  "ru",
4608
4605
  "rw",
4609
- "re",
4610
- "bq",
4611
- "an",
4612
- "bl",
4613
- "gp",
4614
- "fr",
4615
- "sh",
4616
- "kn",
4617
- "lc",
4618
- "mf",
4619
- "gp",
4620
- "fr",
4621
- "pm",
4622
- "vc",
4623
- "ws",
4624
- "sm",
4625
- "st",
4626
4606
  "sa",
4627
- "sn",
4628
- "rs",
4607
+ "sb",
4629
4608
  "sc",
4630
- "sl",
4609
+ "sd",
4610
+ "se",
4631
4611
  "sg",
4632
- "bq",
4633
- "an",
4634
- "nl",
4635
- "sx",
4636
- "an",
4637
- "sk",
4612
+ "sh",
4638
4613
  "si",
4639
- "sb",
4640
- "so",
4614
+ "sj",
4615
+ "sk",
4616
+ "sl",
4617
+ "sm",
4618
+ "sn",
4641
4619
  "so",
4642
- "za",
4643
- "gs",
4644
- "kr",
4645
- "ss",
4646
- "es",
4647
- "lk",
4648
- "sd",
4649
4620
  "sr",
4650
- "sj",
4651
- "sz",
4652
- "se",
4653
- "ch",
4621
+ "ss",
4622
+ "st",
4623
+ "sv",
4624
+ "sx",
4654
4625
  "sy",
4655
- "tw",
4656
- "tj",
4657
- "tz",
4658
- "th",
4626
+ "sz",
4627
+ "tc",
4628
+ "td",
4629
+ "tf",
4659
4630
  "tg",
4631
+ "th",
4632
+ "tj",
4660
4633
  "tk",
4661
- "to",
4662
- "tt",
4634
+ "tl",
4635
+ "tm",
4663
4636
  "tn",
4637
+ "to",
4664
4638
  "tr",
4665
- "tm",
4666
- "tc",
4639
+ "tt",
4667
4640
  "tv",
4668
- "ug",
4641
+ "tw",
4642
+ "tz",
4669
4643
  "ua",
4670
- "ae",
4644
+ "ug",
4671
4645
  "uk",
4646
+ "um",
4672
4647
  "us",
4673
- "vi",
4674
4648
  "uy",
4675
4649
  "uz",
4676
- "vu",
4677
4650
  "va",
4651
+ "vc",
4678
4652
  "ve",
4653
+ "vg",
4654
+ "vi",
4679
4655
  "vn",
4656
+ "vu",
4680
4657
  "wf",
4681
- "eh",
4682
- "ma",
4658
+ "ws",
4683
4659
  "ye",
4660
+ "za",
4684
4661
  "zm",
4685
4662
  "zw"
4686
4663
  ]);
@@ -4827,6 +4804,15 @@ const DEFAULT_THEME_MODE = THEME_MODES.SYSTEM;
4827
4804
  */
4828
4805
  const NEXT_PUBLIC_RECAPTCHA_SITE_KEY = getEnv("NEXT_PUBLIC_RECAPTCHA_SITE_KEY");
4829
4806
  /**
4807
+ * Recaptcha secret key for server-side token verification.
4808
+ * Used by verifyRecaptchaToken to call Google's siteverify endpoint.
4809
+ * Must NOT be exposed to the client (no NEXT_PUBLIC prefix).
4810
+ *
4811
+ * @example
4812
+ * const isValid = await verifyRecaptchaToken(token);
4813
+ */
4814
+ const RECAPTCHA_SECRET_KEY = getEnv("RECAPTCHA_SECRET_KEY");
4815
+ /**
4830
4816
  * Slack webhook URLs for different log types.
4831
4817
  * Used for sending log messages to specific Slack channels based on log category.
4832
4818
  *
@@ -4853,7 +4839,7 @@ const SLACK_WEBHOOKS = {
4853
4839
  * @example
4854
4840
  * <img src={OG_AVATAR_URL + userId} />
4855
4841
  */
4856
- const OG_AVATAR_URL = "https://api.kumix.com/og/avatar/";
4842
+ const OG_AVATAR_URL = "https://api.kumix.io/og/avatar/";
4857
4843
  /**
4858
4844
  * Default pagination limit for API responses and UI lists.
4859
4845
  *
@@ -5063,7 +5049,7 @@ const LOCALHOST_GEO_DATA = {
5063
5049
  * }
5064
5050
  * ```
5065
5051
  */
5066
- const LOCALHOST_IP = "63.141.57.109";
5052
+ const LOCALHOST_IP = "198.51.100.1";
5067
5053
  //#endregion
5068
5054
  //#region src/constants/reserved-slugs.ts
5069
5055
  /**
@@ -5239,15 +5225,18 @@ const googleTrackEvent = ({ name, properties }) => {
5239
5225
  }
5240
5226
  };
5241
5227
  /**
5242
- * Helper function to safely get component name from path.
5228
+ * Helper function to safely get the first path segment (component category)
5229
+ * from a component path. For "ui/buttons/Button" this returns "ui".
5243
5230
  *
5244
5231
  * @param componentPath - Component path string
5245
- * @returns Component name or original path if invalid
5232
+ * @returns First segment of the path, or "unknown" if invalid
5246
5233
  */
5247
5234
  const getComponentName = (componentPath) => {
5248
5235
  if (!componentPath || typeof componentPath !== "string") return "unknown";
5249
- const parts = componentPath.trim().split("/");
5250
- return parts.length > 1 ? parts[0] : componentPath.trim();
5236
+ const trimmed = componentPath.trim();
5237
+ if (!trimmed) return "unknown";
5238
+ const firstSlash = trimmed.indexOf("/");
5239
+ return firstSlash === -1 ? trimmed : trimmed.slice(0, firstSlash);
5251
5240
  };
5252
5241
  /**
5253
5242
  * Tracks code copy events with validation.
@@ -5544,6 +5533,7 @@ const googleTrackItemRestore = (module, itemType, itemId) => {
5544
5533
  * ```
5545
5534
  */
5546
5535
  const chunk = (array, chunk_size) => {
5536
+ if (chunk_size <= 0) return [];
5547
5537
  return array.reduce((resultArray, item, index) => {
5548
5538
  const chunkIndex = Math.floor(index / chunk_size);
5549
5539
  if (!resultArray[chunkIndex]) resultArray[chunkIndex] = [];
@@ -5648,15 +5638,17 @@ const stableSort = (arr, compare) => arr.map((item, index) => ({
5648
5638
  * Asset URL builder utilities
5649
5639
  * Provides helpers to construct absolute asset URLs against a configurable asset host.
5650
5640
  */
5651
- const DEFAULT_ASSET_HOST = getEnv("NEXT_PUBLIC_ASSETS_URL") || getEnv("VITE_ASSETS_URL") || "https://assets.kumix.com";
5641
+ const DEFAULT_ASSET_HOST = getEnv("NEXT_PUBLIC_ASSETS_URL") || getEnv("VITE_ASSETS_URL") || "https://assets.kumix.io";
5652
5642
  /**
5653
5643
  * Builds an absolute URL for a given asset path using the provided asset host (CDN) base.
5654
5644
  * Accepts relative paths (e.g., `logo/logo.png`, `/images/icon.svg`) and returns a fully-qualified URL.
5655
5645
  * If an absolute URL is provided as `path`, it will be returned as-is (host is ignored).
5656
5646
  *
5657
5647
  * @param path - Asset path or absolute URL to resolve
5658
- * @param host - Optional asset host (base URL). Defaults to `https://assets.kumix.com`
5659
- * @returns A fully-qualified asset URL string, or `null` if inputs are invalid
5648
+ * @param host - Optional asset host (base URL). Defaults to `https://assets.kumix.io`
5649
+ * @returns A fully-qualified asset URL string. On invalid input (empty path or
5650
+ * malformed base), falls back to the configured asset host rather than
5651
+ * throwing — call sites that need strict failure should validate inputs first.
5660
5652
  *
5661
5653
  * @example
5662
5654
  * ```ts
@@ -5664,7 +5656,7 @@ const DEFAULT_ASSET_HOST = getEnv("NEXT_PUBLIC_ASSETS_URL") || getEnv("VITE_ASSE
5664
5656
  *
5665
5657
  * // Default host
5666
5658
  * const u1 = assetsUrl('logo/logo.png');
5667
- * // 'https://assets.kumix.com/logo/logo.png'
5659
+ * // 'https://assets.kumix.io/logo/logo.png'
5668
5660
  *
5669
5661
  * // Custom host (full URL)
5670
5662
  * const u2 = assetsUrl('/images/icon.svg', 'https://cdn.example.com');
@@ -5678,16 +5670,16 @@ const DEFAULT_ASSET_HOST = getEnv("NEXT_PUBLIC_ASSETS_URL") || getEnv("VITE_ASSE
5678
5670
  * const u4 = assetsUrl('https://other.com/a.png', 'https://cdn.example.com');
5679
5671
  * // 'https://other.com/a.png'
5680
5672
  *
5681
- * // Edge cases
5673
+ * // Edge cases — falsy path or malformed base returns the configured asset host
5682
5674
  * const u5 = assetsUrl('');
5683
- * // null
5675
+ * // 'https://assets.kumix.io' (the configured host)
5684
5676
  *
5685
5677
  * const u6 = assetsUrl('logo.png', '://bad-host');
5686
- * // null (invalid base)
5678
+ * // '://bad-host' (the supplied host — invalid, returned as-is)
5687
5679
  * ```
5688
5680
  */
5689
5681
  function assetsUrl(path, host = DEFAULT_ASSET_HOST) {
5690
- const BASE_HOST = (getEnv("NEXT_PUBLIC_ASSETS_URL") || getEnv("VITE_ASSETS_URL")) ?? host;
5682
+ const BASE_HOST = host;
5691
5683
  if (!path || typeof path !== "string") return BASE_HOST;
5692
5684
  try {
5693
5685
  const base = /^https?:\/\//i.test(BASE_HOST) ? BASE_HOST : `https://${BASE_HOST}`;
@@ -5711,17 +5703,18 @@ function assetsUrl(path, host = DEFAULT_ASSET_HOST) {
5711
5703
  * import { getFlagUrl } from "@kumix/ui/utils/flag-url";
5712
5704
  *
5713
5705
  * const urlUs = getFlagUrl("US");
5714
- * // "https://assets.kumix.com/media/flags/us.svg"
5706
+ * // "https://assets.kumix.io/media/flags/us.svg"
5715
5707
  *
5716
5708
  * const urlId = getFlagUrl("id");
5717
- * // "https://assets.kumix.com/media/flags/id.svg"
5709
+ * // "https://assets.kumix.io/media/flags/id.svg"
5718
5710
  *
5719
5711
  * // Can be used directly in an <img /> or <Image /> component
5720
5712
  * // <img src={getFlagUrl("GB")} alt="United Kingdom flag" />
5721
5713
  * ```
5722
5714
  */
5723
5715
  function getFlagUrl(flag) {
5724
- const flagCode = flag.toLowerCase();
5716
+ if (typeof flag !== "string" || !flag.trim()) return `${assetsUrl("media/flags")}/unknown.svg`;
5717
+ const flagCode = flag.trim().toLowerCase();
5725
5718
  return `${assetsUrl("media/flags")}/${flagCode}.svg`;
5726
5719
  }
5727
5720
  //#endregion
@@ -5791,7 +5784,7 @@ function mapLocale(locale) {
5791
5784
  * Supports Open Graph, Twitter cards, icons, canonical URLs, robots, manifest, and multi-language alternates.
5792
5785
  *
5793
5786
  * @param params - Metadata construction options
5794
- * @param params.baseUrl - The base URL/domain for metadataBase (optional, e.g., 'https://kumix.com')
5787
+ * @param params.baseUrl - The base URL/domain for metadataBase (optional, e.g., 'https://kumix.io')
5795
5788
  * @param params.title - The base title for the page (used in Open Graph and Twitter)
5796
5789
  * @param params.fullTitle - The full title for the page (overrides title in the <title> tag)
5797
5790
  * @param params.description - Description of the page (used in meta, Open Graph, Twitter)
@@ -5835,8 +5828,8 @@ function mapLocale(locale) {
5835
5828
  * creator: '@lukmanaviccena',
5836
5829
  * },
5837
5830
  * alternateLanguages: {
5838
- * en: 'https://kumix.com/en',
5839
- * id: 'https://kumix.com/id',
5831
+ * en: 'https://kumix.io/en',
5832
+ * id: 'https://kumix.io/id',
5840
5833
  * },
5841
5834
  * icons: [
5842
5835
  * { rel: 'icon', url: '/favicon.ico' },
@@ -5890,8 +5883,14 @@ function constructMetadata({ baseUrl, title, fullTitle, description, icons = [
5890
5883
  type: "website",
5891
5884
  locale: "en_US"
5892
5885
  }, twitter = {}, canonicalUrl, alternateLanguages, noIndex = false, noArchive = false, noSnippet = false, manifest }) {
5886
+ let metadataBase;
5887
+ if (baseUrl) try {
5888
+ metadataBase = new URL(baseUrl);
5889
+ } catch {
5890
+ metadataBase = void 0;
5891
+ }
5893
5892
  return {
5894
- metadataBase: baseUrl ? new URL(baseUrl) : void 0,
5893
+ metadataBase,
5895
5894
  title: fullTitle || title,
5896
5895
  description,
5897
5896
  icons,
@@ -6507,7 +6506,6 @@ const resizeImage = (file, opts = {
6507
6506
  const reader = new FileReader();
6508
6507
  reader.onload = (e) => {
6509
6508
  const img = new Image();
6510
- img.src = e.target?.result;
6511
6509
  img.onload = () => {
6512
6510
  const targetWidth = opts.width;
6513
6511
  const targetHeight = opts.height;
@@ -6537,6 +6535,7 @@ const resizeImage = (file, opts = {
6537
6535
  resolve(canvas.toDataURL("image/jpeg", opts.quality));
6538
6536
  };
6539
6537
  img.onerror = (error) => reject(/* @__PURE__ */ new Error(`Image loading error: ${error}`));
6538
+ img.src = e.target?.result;
6540
6539
  };
6541
6540
  reader.onerror = (error) => reject(/* @__PURE__ */ new Error(`FileReader error: ${error}`));
6542
6541
  reader.readAsDataURL(file);
@@ -6630,11 +6629,11 @@ async function loadImage(file) {
6630
6629
  return new Promise((resolve, reject) => {
6631
6630
  const image = new Image();
6632
6631
  const reader = new FileReader();
6632
+ image.onload = () => resolve(image);
6633
+ image.onerror = (err) => reject(err);
6633
6634
  reader.onload = (e) => {
6634
6635
  image.src = e.target?.result;
6635
6636
  };
6636
- image.onload = () => resolve(image);
6637
- image.onerror = (err) => reject(err);
6638
6637
  reader.readAsDataURL(file);
6639
6638
  });
6640
6639
  }
@@ -6804,11 +6803,28 @@ const hasStorageItem = (key, storageType = "localStorage") => {
6804
6803
  };
6805
6804
  //#endregion
6806
6805
  //#region src/functions/crypto/base-id.ts
6807
- /**
6808
- * Utility functions for generating unique, time-sortable IDs
6809
- * Provides ULID-compatible ID generation with custom prefixes for entity identification
6810
- */
6811
- const base32 = baseX("0123456789ABCDEFGHJKMNPQRSTVWXYZ");
6806
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
6807
+ /**
6808
+ * Encodes a 16-byte (128-bit) buffer into a fixed-length 26-character
6809
+ * Crockford base32 ULID string. Unlike big-number base encoders, this pads
6810
+ * the output to a constant length so IDs remain lexicographically sortable
6811
+ * regardless of leading zero bytes.
6812
+ */
6813
+ function encodeULID(buf) {
6814
+ let bits = 0;
6815
+ let value = 0;
6816
+ let output = "";
6817
+ for (let i = 0; i < buf.length; i++) {
6818
+ value = value << 8 | buf[i];
6819
+ bits += 8;
6820
+ while (bits >= 5) {
6821
+ bits -= 5;
6822
+ output += CROCKFORD[value >>> bits & 31];
6823
+ }
6824
+ }
6825
+ if (bits > 0) output += CROCKFORD[value << 5 - bits & 31];
6826
+ return output;
6827
+ }
6812
6828
  /**
6813
6829
  * Creates a ULID-compatible buffer (48 bits timestamp + 80 bits randomness)
6814
6830
  *
@@ -6848,8 +6864,7 @@ function createULIDBuffer() {
6848
6864
  * ```
6849
6865
  */
6850
6866
  const baseIdCustom = ({ prefix }) => {
6851
- const buf = createULIDBuffer();
6852
- return `${prefix}${base32.encode(buf)}`;
6867
+ return `${prefix}${encodeULID(createULIDBuffer())}`;
6853
6868
  };
6854
6869
  /**
6855
6870
  * Creates a unique, time-sortable ID with a known prefix from the predefined set
@@ -6885,6 +6900,15 @@ const baseId = ({ prefix }) => baseIdCustom({ prefix });
6885
6900
  * CUID (Collision-resistant Unique IDentifier) generation utilities
6886
6901
  * Provides functions for generating unique identifiers using @paralleldrive/cuid2
6887
6902
  */
6903
+ const generatorCache = /* @__PURE__ */ new Map();
6904
+ function getGenerator(length) {
6905
+ let generator = generatorCache.get(length);
6906
+ if (!generator) {
6907
+ generator = init({ length });
6908
+ generatorCache.set(length, generator);
6909
+ }
6910
+ return generator;
6911
+ }
6888
6912
  /**
6889
6913
  * Generates a CUID (Collision-resistant Unique IDentifier)
6890
6914
  * CUIDs are designed to be globally unique, even in distributed systems.
@@ -6921,10 +6945,8 @@ function cuid() {
6921
6945
  return createId();
6922
6946
  }
6923
6947
  /**
6924
- * Generates a custom ID with an optional prefix and configurable length
6925
- * This function uses `@paralleldrive/cuid2` to create unique identifiers
6926
- * with an optional prefix. Useful for creating IDs with specific patterns
6927
- * like `user_abc123` or `order_987xyz`.
6948
+ * Generates a custom ID with an optional prefix and configurable length.
6949
+ * Uses `@paralleldrive/cuid2`'s cryptographically-secure RNG.
6928
6950
  *
6929
6951
  * @param prefix - Optional prefix to prepend to the ID
6930
6952
  * @param length - The length of the generated ID (default is 12)
@@ -6932,40 +6954,25 @@ function cuid() {
6932
6954
  *
6933
6955
  * @example
6934
6956
  * ```ts
6935
- * import { customeId } from '@/utils/functions';
6957
+ * import { customId } from '@/utils/functions';
6936
6958
  *
6937
6959
  * // Generate ID with prefix
6938
- * const userId = customeId('user_');
6960
+ * const userId = customId('user_');
6939
6961
  * // Returns: 'user_l5r8xn2gzqnb'
6940
6962
  *
6941
6963
  * // Generate ID with custom length
6942
- * const orderId = customeId('order_', 16);
6943
- * // Returns: 'order_l5r8xn2gzqnbf4r2'
6944
- *
6945
- * // Generate ID without prefix
6946
- * const randomId = customeId();
6947
- * // Returns: 'l5r8xn2gzqnb'
6948
- *
6949
- * // Use for different entity types
6950
- * const productId = customeId('prod_', 8);
6951
- * const invoiceId = customeId('inv_', 10);
6952
- * const sessionId = customeId('sess_', 20);
6953
- *
6954
- * // Use in API responses
6955
- * const apiResponse = {
6956
- * id: customeId('api_'),
6957
- * timestamp: Date.now(),
6958
- * data: {...}
6959
- * };
6964
+ * const orderId = customId('order_', 16);
6960
6965
  * ```
6961
6966
  */
6962
- function customeId(prefix, length = 12) {
6963
- const generateId = init({
6964
- random: Math.random,
6965
- length
6966
- });
6967
+ function customId(prefix, length = 12) {
6968
+ const generateId = getGenerator(length);
6967
6969
  return `${prefix || ""}${generateId()}`;
6968
6970
  }
6971
+ /**
6972
+ * @deprecated Use {@link customId} instead. This alias has a typo and is kept
6973
+ * only for backward compatibility; it will be removed in a future major.
6974
+ */
6975
+ const customeId = customId;
6969
6976
  //#endregion
6970
6977
  //#region src/functions/crypto/hash-string.ts
6971
6978
  /**
@@ -7011,6 +7018,8 @@ async function hashStringSHA256(str) {
7011
7018
  * Utility function for generating unique IDs
7012
7019
  * Provides a wrapper around nanoid for consistent ID generation
7013
7020
  */
7021
+ const DEFAULT_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
7022
+ const DEFAULT_SIZE = 7;
7014
7023
  /**
7015
7024
  * Generates a random alphanumeric ID of specified length
7016
7025
  * Uses a custom alphabet of numbers and letters (both cases)
@@ -7036,7 +7045,7 @@ async function hashStringSHA256(str) {
7036
7045
  * ```
7037
7046
  */
7038
7047
  const nanoid = (chars) => {
7039
- return customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", chars || 7)();
7048
+ return customAlphabet(DEFAULT_ALPHABET, chars || DEFAULT_SIZE)();
7040
7049
  };
7041
7050
  /**
7042
7051
  * Generates a unique identifier using the current timestamp and a random number
@@ -7067,7 +7076,8 @@ const nanoid = (chars) => {
7067
7076
  * ```
7068
7077
  */
7069
7078
  function uid() {
7070
- return (Date.now() + Math.floor(Math.random() * 1e3)).toString();
7079
+ const random = Math.floor(Math.random() * 1e3).toString().padStart(3, "0");
7080
+ return `${Date.now()}${random}`;
7071
7081
  }
7072
7082
  //#endregion
7073
7083
  //#region src/functions/datetime/billing-utils.ts
@@ -7216,8 +7226,9 @@ const getBillingStartDate = (billingCycleStart, referenceDate = /* @__PURE__ */
7216
7226
  * ```
7217
7227
  */
7218
7228
  const formatDate = (datetime, options, locale = "en-US") => {
7219
- if (datetime.toString() === "Invalid Date") return "";
7220
- return new Date(datetime).toLocaleDateString(locale, {
7229
+ const date = new Date(datetime);
7230
+ if (Number.isNaN(date.getTime())) return "";
7231
+ return date.toLocaleDateString(locale, {
7221
7232
  day: "numeric",
7222
7233
  month: "long",
7223
7234
  year: "numeric",
@@ -7265,11 +7276,12 @@ const formatDate = (datetime, options, locale = "en-US") => {
7265
7276
  */
7266
7277
  const formatDateTimeSmart = (datetime, options, locale = "en-US") => {
7267
7278
  const date = new Date(datetime);
7279
+ if (Number.isNaN(date.getTime())) return "Invalid Date";
7268
7280
  const now = /* @__PURE__ */ new Date();
7269
7281
  return new Date(datetime).toLocaleDateString(locale, {
7270
7282
  month: "short",
7271
7283
  day: "numeric",
7272
- ...date.getUTCFullYear() !== now.getUTCFullYear() ? { year: "numeric" } : {
7284
+ ...date.getFullYear() !== now.getFullYear() ? { year: "numeric" } : {
7273
7285
  hour: "numeric",
7274
7286
  minute: "numeric",
7275
7287
  hour12: true
@@ -7324,8 +7336,9 @@ const formatDateTimeSmart = (datetime, options, locale = "en-US") => {
7324
7336
  * ```
7325
7337
  */
7326
7338
  const formatDateTime = (datetime, options, locale = "en-US") => {
7327
- if (datetime.toString() === "Invalid Date") return "";
7328
- return new Date(datetime).toLocaleTimeString(locale, {
7339
+ const date = new Date(datetime);
7340
+ if (Number.isNaN(date.getTime())) return "";
7341
+ return date.toLocaleString(locale, {
7329
7342
  month: "short",
7330
7343
  day: "numeric",
7331
7344
  year: "numeric",
@@ -7561,18 +7574,26 @@ const getFirstAndLastDay = (day, referenceDate = /* @__PURE__ */ new Date()) =>
7561
7574
  const currentDay = today.getDate();
7562
7575
  const currentMonth = today.getMonth();
7563
7576
  const currentYear = today.getFullYear();
7564
- if (currentDay >= day) return {
7565
- firstDay: new Date(currentYear, currentMonth, day),
7566
- lastDay: new Date(currentYear, currentMonth + 1, day - 1)
7567
- };
7568
- else {
7569
- const lastYear = currentMonth === 0 ? currentYear - 1 : currentYear;
7570
- const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
7577
+ const lastDayOfMonth = (year, month) => new Date(year, month + 1, 0).getDate();
7578
+ const clamp = (year, month, requested) => Math.min(requested, lastDayOfMonth(year, month));
7579
+ if (currentDay >= day) {
7580
+ const firstDayClamped = clamp(currentYear, currentMonth, day);
7581
+ const lastMonth = currentMonth + 1;
7582
+ const lastYear = currentYear + (currentMonth === 11 ? 1 : 0);
7583
+ const lastDayClamped = clamp(lastMonth === 12 ? lastYear : currentYear, lastMonth % 12, Math.max(1, day - 1));
7571
7584
  return {
7572
- firstDay: new Date(lastYear, lastMonth, day),
7573
- lastDay: new Date(currentYear, currentMonth, day - 1)
7585
+ firstDay: new Date(currentYear, currentMonth, firstDayClamped),
7586
+ lastDay: new Date(lastMonth === 12 ? lastYear : currentYear, lastMonth % 12, lastDayClamped)
7574
7587
  };
7575
7588
  }
7589
+ const lastYear = currentMonth === 0 ? currentYear - 1 : currentYear;
7590
+ const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
7591
+ const firstDayClamped = clamp(lastYear, lastMonth, day);
7592
+ const lastDayClamped = clamp(currentYear, currentMonth, Math.max(1, day - 1));
7593
+ return {
7594
+ firstDay: new Date(lastYear, lastMonth, firstDayClamped),
7595
+ lastDay: new Date(currentYear, currentMonth, lastDayClamped)
7596
+ };
7576
7597
  };
7577
7598
  //#endregion
7578
7599
  //#region src/functions/datetime/get-year.ts
@@ -7746,7 +7767,29 @@ const timeAgo = (timestamp, { withAgo, neverText = "Never", justNowText = "Just
7746
7767
  * ```
7747
7768
  */
7748
7769
  const getTimeZones = () => {
7749
- return Intl.supportedValuesOf("timeZone").map((timezone) => {
7770
+ const safeSupported = () => {
7771
+ if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") try {
7772
+ return Intl.supportedValuesOf("timeZone");
7773
+ } catch {}
7774
+ return [
7775
+ "UTC",
7776
+ "America/New_York",
7777
+ "America/Los_Angeles",
7778
+ "Europe/London",
7779
+ "Europe/Paris",
7780
+ "Asia/Tokyo"
7781
+ ];
7782
+ };
7783
+ const timezones = safeSupported();
7784
+ const parseOffsetMinutes = (offset) => {
7785
+ const match = offset.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/);
7786
+ if (!match) return 0;
7787
+ const sign = match[1] === "-" ? -1 : 1;
7788
+ const hours = Number.parseInt(match[2] ?? "0", 10);
7789
+ const minutes = Number.parseInt(match[3] ?? "0", 10);
7790
+ return sign * (hours * 60 + minutes);
7791
+ };
7792
+ return timezones.map((timezone) => {
7750
7793
  const offset = new Intl.DateTimeFormat("en", {
7751
7794
  timeZone: timezone,
7752
7795
  timeZoneName: "shortOffset"
@@ -7755,7 +7798,7 @@ const getTimeZones = () => {
7755
7798
  return {
7756
7799
  value: timezone,
7757
7800
  label: `(${formattedOffset}) ${timezone.replace(/_/g, " ")}`,
7758
- numericOffset: parseInt(formattedOffset.replace("GMT", "").replace("+", "") || "0")
7801
+ numericOffset: parseOffsetMinutes(formattedOffset)
7759
7802
  };
7760
7803
  }).sort((a, b) => a.numericOffset - b.numericOffset);
7761
7804
  };
@@ -7806,6 +7849,7 @@ const getTimeZones = () => {
7806
7849
  */
7807
7850
  function formatFileSize(bytes, decimals = 2, base = 1024) {
7808
7851
  if (bytes === 0) return "0 Bytes";
7852
+ if (bytes < 0) return `-${formatFileSize(-bytes, decimals, base)}`;
7809
7853
  const k = base;
7810
7854
  const sizes = [
7811
7855
  "B",
@@ -7818,7 +7862,7 @@ function formatFileSize(bytes, decimals = 2, base = 1024) {
7818
7862
  "ZB",
7819
7863
  "YB"
7820
7864
  ];
7821
- const i = Math.floor(Math.log(bytes) / Math.log(k));
7865
+ const i = Math.min(Math.max(Math.floor(Math.log(bytes) / Math.log(k)), 0), sizes.length - 1);
7822
7866
  return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
7823
7867
  }
7824
7868
  //#endregion
@@ -7874,6 +7918,8 @@ function formatFileSize(bytes, decimals = 2, base = 1024) {
7874
7918
  async function fetchWithRetry(input, init, options = {}) {
7875
7919
  const { timeout = 5e3, maxRetries = 10, retryDelay = 1e3 } = options;
7876
7920
  let lastError = null;
7921
+ let httpError = null;
7922
+ let lastStatus = 0;
7877
7923
  for (let i = 0; i < maxRetries; i++) {
7878
7924
  const controller = new AbortController();
7879
7925
  const timeoutId = setTimeout(() => {
@@ -7886,21 +7932,25 @@ async function fetchWithRetry(input, init, options = {}) {
7886
7932
  });
7887
7933
  clearTimeout(timeoutId);
7888
7934
  if (response.ok) return response;
7935
+ lastStatus = response.status;
7889
7936
  if (response.status === 429 || response.status >= 500) {
7937
+ httpError = /* @__PURE__ */ new Error(`HTTP error ${response.status}`);
7890
7938
  const delay = retryDelay + i ** 2 * 50;
7891
7939
  await new Promise((resolve) => setTimeout(resolve, delay));
7892
7940
  continue;
7893
7941
  }
7894
- if (response.status === 403) throw new Error("Unauthorized");
7895
- if (!response.ok) {
7896
- let errorMessage;
7897
- try {
7898
- errorMessage = (await response.json()).error || `HTTP error ${response.status}`;
7899
- } catch {
7900
- errorMessage = `HTTP error ${response.status}`;
7901
- }
7902
- throw new Error(errorMessage);
7942
+ if (response.status === 401 || response.status === 403) {
7943
+ httpError = /* @__PURE__ */ new Error(response.status === 401 ? "Unauthorized" : "Forbidden");
7944
+ break;
7903
7945
  }
7946
+ let errorMessage;
7947
+ try {
7948
+ errorMessage = (await response.json())?.error || `HTTP error ${response.status}`;
7949
+ } catch {
7950
+ errorMessage = `HTTP error ${response.status}`;
7951
+ }
7952
+ httpError = new Error(errorMessage);
7953
+ break;
7904
7954
  } catch (error) {
7905
7955
  clearTimeout(timeoutId);
7906
7956
  lastError = error instanceof Error ? error : new Error(String(error));
@@ -7909,7 +7959,8 @@ async function fetchWithRetry(input, init, options = {}) {
7909
7959
  await new Promise((resolve) => setTimeout(resolve, delay));
7910
7960
  }
7911
7961
  }
7912
- throw new Error(`Failed after ${maxRetries} retries`);
7962
+ if (httpError) throw httpError;
7963
+ throw new Error(`Failed after ${maxRetries} retries${lastStatus ? ` (last status: ${lastStatus})` : ""}`);
7913
7964
  }
7914
7965
  //#endregion
7915
7966
  //#region src/functions/http/fetch-with-timeout.ts
@@ -8069,13 +8120,25 @@ async function fetcher(endpoint, method = "GET", body, options) {
8069
8120
  if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
8070
8121
  payload = JSON.stringify(body);
8071
8122
  }
8072
- const data = await (await fetch(endpoint, {
8123
+ const res = await fetch(endpoint, {
8073
8124
  method,
8074
8125
  headers,
8075
8126
  body: payload
8076
- })).json();
8077
- if (options?.log && isDevelopment) console.log(`[request] ${endpoint}`, data);
8078
- return data?.data;
8127
+ });
8128
+ let data;
8129
+ try {
8130
+ data = await res.json();
8131
+ } catch {
8132
+ data = void 0;
8133
+ }
8134
+ if (options?.log && isDevelopment) logger.info(`[request] ${endpoint}`, data);
8135
+ if (!res.ok) {
8136
+ const message = (typeof data === "object" && data !== null && "error" in data ? String(data.error) : `HTTP error ${res.status}`) || `HTTP error ${res.status}`;
8137
+ const error = new Error(message);
8138
+ error.status = res.status;
8139
+ throw error;
8140
+ }
8141
+ return typeof data === "object" && data !== null && "data" in data ? data.data : data;
8079
8142
  }
8080
8143
  //#endregion
8081
8144
  //#region src/functions/http/ip.ts
@@ -8177,7 +8240,7 @@ function getClientIP(request) {
8177
8240
  */
8178
8241
  function isValidIP(ip) {
8179
8242
  if (!ip || ip === "unknown") return false;
8180
- return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip) || /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/.test(ip);
8243
+ return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip) || /^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(?:ffff(?::0{1,4})?:)?(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/.test(ip);
8181
8244
  }
8182
8245
  /**
8183
8246
  * Gets user agent information from the request
@@ -8329,7 +8392,7 @@ async function getIPAddress() {
8329
8392
  if (ip && typeof ip === "string") return ip;
8330
8393
  }
8331
8394
  } catch (serviceError) {
8332
- console.warn(`IP service ${service} failed:`, serviceError);
8395
+ logger.warn(`IP service ${service} failed:`, serviceError);
8333
8396
  }
8334
8397
  return "unknown";
8335
8398
  } catch (error) {
@@ -8386,7 +8449,7 @@ function normalizePath(input) {
8386
8449
  * export function middleware(req: NextRequest) {
8387
8450
  * const { domain, path, fullPath, key } = parse(req);
8388
8451
  *
8389
- * console.log(domain); // e.g., "api.kumix.com"
8452
+ * console.log(domain); // e.g., "api.kumix.io"
8390
8453
  * console.log(path); // e.g., "/v1/users"
8391
8454
  * console.log(fullPath); // e.g., "/v1/users?page=1"
8392
8455
  * console.log(key); // e.g., "v1"
@@ -8410,22 +8473,26 @@ function normalizePath(input) {
8410
8473
  * ```
8411
8474
  */
8412
8475
  const parse = (req) => {
8413
- let domain = req.headers.get("host");
8414
- domain = domain.replace(/^www\./, "").toLowerCase();
8476
+ const domain = (req.headers.get("host") || "").replace(/^www\./, "").toLowerCase();
8415
8477
  const path = req.nextUrl.pathname;
8416
8478
  const searchParams = req.nextUrl.searchParams.toString();
8417
8479
  const searchParamsObj = Object.fromEntries(req.nextUrl.searchParams);
8418
8480
  const searchParamsString = searchParams.length > 0 ? `?${searchParams}` : "";
8419
8481
  const fullPath = `${path}${searchParamsString}`;
8482
+ const safeDecode = (value) => {
8483
+ try {
8484
+ return decodeURIComponent(value);
8485
+ } catch {
8486
+ return value;
8487
+ }
8488
+ };
8420
8489
  const pathSegments = path.split("/").filter(Boolean);
8421
- const key = pathSegments.length > 0 ? decodeURIComponent(pathSegments[0]) : "";
8422
- const fullKey = path.length > 1 ? decodeURIComponent(path.slice(1)) : "";
8423
8490
  return {
8424
8491
  domain,
8425
8492
  path,
8426
8493
  fullPath,
8427
- key,
8428
- fullKey,
8494
+ key: pathSegments.length > 0 ? safeDecode(pathSegments[0]) : "",
8495
+ fullKey: path.length > 1 ? safeDecode(path.slice(1)) : "",
8429
8496
  searchParamsObj,
8430
8497
  searchParamsString
8431
8498
  };
@@ -8458,9 +8525,9 @@ const parse = (req) => {
8458
8525
  */
8459
8526
  async function verifyRecaptchaToken(token) {
8460
8527
  try {
8461
- const secretKey = NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
8528
+ const secretKey = RECAPTCHA_SECRET_KEY;
8462
8529
  if (!secretKey) {
8463
- logger.error("NEXT_PUBLIC_RECAPTCHA_SITE_KEY is not set");
8530
+ logger.error("RECAPTCHA_SECRET_KEY is not set");
8464
8531
  return false;
8465
8532
  }
8466
8533
  const data = await (await fetch("https://www.google.com/recaptcha/api/siteverify", {
@@ -8531,10 +8598,13 @@ const logTypeToEnv = {
8531
8598
  * // In development, logs to console instead of Slack
8532
8599
  * ```
8533
8600
  */
8534
- const SlackLog = async ({ message, type, mention = false }) => {
8535
- if (isDevelopment || !SLACK_WEBHOOKS.ALERTS || !SLACK_WEBHOOKS.CRON || !SLACK_WEBHOOKS.LINKS || !SLACK_WEBHOOKS.SUBSCRIBERS || !SLACK_WEBHOOKS.ERRORS) logger.log(message);
8601
+ const SlackLog = async ({ message, type, mention = false, mentionUserId }) => {
8536
8602
  const HOOK = logTypeToEnv[type];
8537
- if (!HOOK) return;
8603
+ if (isDevelopment || !HOOK) {
8604
+ logger.log(message);
8605
+ return;
8606
+ }
8607
+ const resolvedMentionId = mentionUserId || (typeof process !== "undefined" ? process.env.SLACK_MENTION_USER_ID : void 0);
8538
8608
  try {
8539
8609
  return await fetch(HOOK, {
8540
8610
  method: "POST",
@@ -8543,7 +8613,7 @@ const SlackLog = async ({ message, type, mention = false }) => {
8543
8613
  type: "section",
8544
8614
  text: {
8545
8615
  type: "mrkdwn",
8546
- text: `${mention ? "<@U0404G6J3NJ> " : ""}${type === "alerts" || type === "errors" ? ":alert: " : ""}${message}`
8616
+ text: `${mention && resolvedMentionId ? `<@${resolvedMentionId}> ` : ""}${type === "alerts" || type === "errors" ? ":alert: " : ""}${message}`
8547
8617
  }
8548
8618
  }] })
8549
8619
  });
@@ -8696,37 +8766,30 @@ function getUserName(user) {
8696
8766
  //#region src/functions/string/camel-case.ts
8697
8767
  /**
8698
8768
  * Utility function for string case conversion
8699
- * Provides conversion from snake_case to camelCase format
8769
+ * Provides conversion from various cases (snake, kebab, space, Pascal) to camelCase
8700
8770
  */
8701
8771
  /**
8702
- * Converts a string from snake_case to camelCase format
8703
- * If the string is already in camelCase, it will be returned unchanged
8772
+ * Converts a string to camelCase.
8773
+ * Handles snake_case, kebab-case, CONST_CASE, PascalCase, and space-separated words.
8704
8774
  *
8705
8775
  * @param str - The string to convert to camelCase
8706
8776
  * @returns The camelCase version of the input string
8707
8777
  *
8708
8778
  * @example
8709
8779
  * ```ts
8710
- * // Convert snake_case to camelCase
8711
- * toCamelCase('user_name')
8712
- * // Returns "userName"
8713
- *
8714
- * // Already camelCase strings remain unchanged
8715
- * toCamelCase('userName')
8716
- * // Returns "userName"
8717
- *
8718
- * // Multi-word snake_case strings
8719
- * toCamelCase('first_name_last_name')
8720
- * // Returns "firstNameLastName"
8721
- *
8722
- * // Handles single-word strings
8723
- * toCamelCase('user')
8724
- * // Returns "user"
8780
+ * toCamelCase('user_name') // "userName"
8781
+ * toCamelCase('user-name') // "userName"
8782
+ * toCamelCase('first name') // "firstName"
8783
+ * toCamelCase('PascalCase') // "pascalCase"
8784
+ * toCamelCase('first_name_last') // "firstNameLastName"
8725
8785
  * ```
8726
8786
  */
8727
8787
  const toCamelCase = (str) => {
8728
- if (/^[a-z][a-zA-Z0-9]*$/.test(str)) return str;
8729
- return str.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
8788
+ if (!str) return "";
8789
+ const words = str.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).filter(Boolean);
8790
+ if (words.length === 0) return "";
8791
+ const [first, ...rest] = words;
8792
+ return `${first.toLowerCase()}${rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("")}`;
8730
8793
  };
8731
8794
  //#endregion
8732
8795
  //#region src/functions/string/capitalize.ts
@@ -8868,29 +8931,28 @@ const combineWords = (words) => {
8868
8931
  * ```ts
8869
8932
  * // Basic usage with defaults (USD)
8870
8933
  * currencyFormatter(1000)
8871
- * // Returns "$1,000"
8934
+ * // Returns "$1,000.00"
8872
8935
  *
8873
8936
  * // With custom currency
8874
8937
  * currencyFormatter(1000, {}, 'en-US', 'EUR')
8875
- * // Returns "€1,000"
8938
+ * // Returns "€1,000.00"
8876
8939
  *
8877
8940
  * // With custom locale
8878
8941
  * currencyFormatter(1000, {}, 'de-DE', 'EUR')
8879
- * // Returns "1.000 €"
8942
+ * // Returns "1.000,00 €"
8880
8943
  *
8881
- * // With custom options
8882
- * currencyFormatter(1234.56, { maximumFractionDigits: 2 })
8883
- * // Returns "$1,234.56"
8944
+ * // With custom options (no decimals)
8945
+ * currencyFormatter(1234.56, { maximumFractionDigits: 0 })
8946
+ * // Returns "$1,235"
8884
8947
  *
8885
8948
  * // Combining custom options, locale and currency
8886
- * currencyFormatter(1234.56, { maximumFractionDigits: 2 }, 'ja-JP', 'JPY')
8887
- * // Returns "¥1,235" (rounded to 0 decimal placeas by default for JPY)
8949
+ * currencyFormatter(1234.56, { maximumFractionDigits: 0 }, 'ja-JP', 'JPY')
8950
+ * // Returns "¥1,235"
8888
8951
  * ```
8889
8952
  */
8890
8953
  const currencyFormatter = (value, options, locale = "en-US", currencyCode = "USD") => Intl.NumberFormat(locale, {
8891
8954
  style: "currency",
8892
8955
  currency: currencyCode,
8893
- maximumFractionDigits: 0,
8894
8956
  ...options
8895
8957
  }).format(value);
8896
8958
  //#endregion
@@ -9002,7 +9064,10 @@ function nFormatter(num, opts = { digits: 1 }, locale = "en-US") {
9002
9064
  if (!num) return "0";
9003
9065
  if (opts.full) return Intl.NumberFormat(locale).format(num);
9004
9066
  const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
9005
- if (num < 1) return num.toFixed(opts.digits).replace(rx, "$1");
9067
+ const digits = opts.digits ?? 1;
9068
+ const sign = num < 0 ? "-" : "";
9069
+ const abs = Math.abs(num);
9070
+ if (abs < 1) return `${sign}${abs.toFixed(digits).replace(rx, "$1")}`;
9006
9071
  const item = [
9007
9072
  {
9008
9073
  value: 1,
@@ -9032,8 +9097,8 @@ function nFormatter(num, opts = { digits: 1 }, locale = "en-US") {
9032
9097
  value: 0xde0b6b3a7640000,
9033
9098
  symbol: "E"
9034
9099
  }
9035
- ].slice().reverse().find((item) => num >= item.value);
9036
- return item ? (num / item.value).toFixed(opts.digits).replace(rx, "$1") + item.symbol : "0";
9100
+ ].slice().reverse().find((item) => abs >= item.value);
9101
+ return item ? `${sign}${(abs / item.value).toFixed(digits).replace(rx, "$1")}${item.symbol}` : "0";
9037
9102
  }
9038
9103
  //#endregion
9039
9104
  //#region src/functions/string/normalize-string.ts
@@ -9076,14 +9141,11 @@ function nFormatter(num, opts = { digits: 1 }, locale = "en-US") {
9076
9141
  const normalizeString = (key) => {
9077
9142
  if (!key) return "";
9078
9143
  const original = key;
9079
- const normalized = key.replace(/^\uFEFF/, "").replace(/^\uFFFE/, "").replace(/^\uEFBBBF/, "").replace(/^[\u0000\uFEFF]{2}/, "").replace(/^[\uFFFE\u0000]{2}/, "").replace(/^\u2028/, "").replace(/^\u2029/, "").replace(/./g, (char) => {
9144
+ const normalized = key.replace(/^\uFEFF/, "").replace(/^\uFFFE/, "").replace(/^[\u00EF\u00BB\u00BF]+/, "").replace(/^[\u0000\uFEFF]{2}/, "").replace(/^[\uFFFE\u0000]{2}/, "").replace(/^\u2028/, "").replace(/^\u2029/, "").replace(/./g, (char) => {
9080
9145
  const code = char.charCodeAt(0);
9081
9146
  return code <= 31 || code >= 127 && code <= 159 ? "" : char;
9082
9147
  }).replace(/\s+/g, " ").trim().toLowerCase();
9083
- if (isDevelopment && original !== normalized) {
9084
- logger.log(`Normalized key: "${original}" -> "${normalized}"`);
9085
- logger.log("Original char codes:", Array.from(original).map((c) => c.charCodeAt(0)));
9086
- }
9148
+ if (isDevelopment && typeof process !== "undefined" && process.env?.KUMIX_DEBUG_NORMALIZE && original !== normalized) logger.log(`normalizeString: input changed (length ${original.length} -> ${normalized.length})`);
9087
9149
  return normalized;
9088
9150
  };
9089
9151
  //#endregion
@@ -9212,8 +9274,11 @@ function regexEscape(str) {
9212
9274
  * ```
9213
9275
  */
9214
9276
  const truncate = (str, length) => {
9215
- if (!str || str.length <= length) return str ?? null;
9216
- return `${str.slice(0, length - 3)}...`;
9277
+ if (!str) return null;
9278
+ if (str.length <= length) return str;
9279
+ const ELLIPSIS = "...";
9280
+ if (length <= 3) return str.slice(0, length);
9281
+ return `${str.slice(0, length - 3)}${ELLIPSIS}`;
9217
9282
  };
9218
9283
  //#endregion
9219
9284
  //#region src/functions/string/smart-truncate.ts
@@ -9270,9 +9335,16 @@ const truncateDomain = (domain, maxLength) => {
9270
9335
  * ```
9271
9336
  */
9272
9337
  const smartTruncate = (link, maxLength) => {
9338
+ if (!link) return "";
9339
+ if (maxLength <= 0) return "";
9273
9340
  if (link.length <= maxLength) return link;
9274
9341
  const [domain, ...pathParts] = link.split("/");
9275
- const truncatedPath = truncate(pathParts.join("/"), maxLength - 8);
9342
+ const path = pathParts.join("/");
9343
+ const minDomainLength = 8;
9344
+ if (maxLength < minDomainLength) return link.slice(0, maxLength);
9345
+ const maxPathLength = maxLength - minDomainLength;
9346
+ const truncatedPath = path ? truncate(path, maxPathLength) : null;
9347
+ if (!truncatedPath) return truncateDomain(domain, maxLength);
9276
9348
  return `${truncateDomain(domain, maxLength - truncatedPath.length)}/${truncatedPath}`;
9277
9349
  };
9278
9350
  //#endregion
@@ -9319,10 +9391,17 @@ const trim = (u) => typeof u === "string" ? u.trim() : u;
9319
9391
  * Provides functions for validating URL strings and checking URL validity
9320
9392
  */
9321
9393
  /**
9322
- * Checks if a string is a valid URL
9323
- * Uses the URL constructor for validation
9394
+ * Checks if a string is a valid URL.
9395
+ *
9396
+ * By default rejects non-fetchable schemes (`javascript:`, `data:`, `file:`,
9397
+ * `vbscript:`) to avoid XSS via redirect/href validation. Set `allowAnyScheme`
9398
+ * to accept any scheme the URL parser accepts.
9399
+ *
9400
+ * Uses the URL constructor for validation.
9324
9401
  *
9325
9402
  * @param url - The string to check
9403
+ * @param options - Validation options
9404
+ * @param options.allowAnyScheme - When true, accept any scheme (defaults to false)
9326
9405
  * @returns True if the string is a valid URL, false otherwise
9327
9406
  *
9328
9407
  * @example
@@ -9334,11 +9413,9 @@ const trim = (u) => typeof u === "string" ? u.trim() : u;
9334
9413
  * isValidUrl('http://localhost:3000'); // true
9335
9414
  * isValidUrl('ftp://files.example.com'); // true
9336
9415
  *
9337
- * // Invalid URLs
9416
+ * // Invalid / unsafe URLs (rejected by default)
9417
+ * isValidUrl('javascript:alert(1)'); // false
9338
9418
  * isValidUrl('example.com'); // false (missing protocol)
9339
- * isValidUrl('not a url'); // false
9340
- * isValidUrl(''); // false
9341
- * isValidUrl('https://'); // false (incomplete)
9342
9419
  *
9343
9420
  * // Use in form validation
9344
9421
  * function validateUrlInput(input: string) {
@@ -9347,14 +9424,21 @@ const trim = (u) => typeof u === "string" ? u.trim() : u;
9347
9424
  * }
9348
9425
  * return null;
9349
9426
  * }
9350
- *
9351
- * // Use in data processing
9352
- * const validUrls = urls.filter(isValidUrl);
9353
9427
  * ```
9354
9428
  */
9355
- const isValidUrl = (url) => {
9429
+ const isValidUrl = (url, options = {}) => {
9430
+ if (!url || typeof url !== "string") return false;
9356
9431
  try {
9357
- new URL(url);
9432
+ const parsed = new URL(url);
9433
+ if (options.allowAnyScheme) return true;
9434
+ const blocked = [
9435
+ "javascript:",
9436
+ "data:",
9437
+ "vbscript:",
9438
+ "file:"
9439
+ ];
9440
+ const scheme = parsed.protocol.toLowerCase();
9441
+ if (blocked.some((b) => scheme === b)) return false;
9358
9442
  return true;
9359
9443
  } catch (_) {
9360
9444
  return false;
@@ -9386,9 +9470,11 @@ const isValidUrl = (url) => {
9386
9470
  * generateDomainFromName('My Cool Project', 'com')
9387
9471
  * // Returns "mycoolproject.com"
9388
9472
  *
9389
- * // With country code TLD if possible
9473
+ * // ccTLD fallback only applies when the trailing 2 chars form a known ccTLD;
9474
+ * // for "Canada" the trailing 2 chars are "da" (not a ccTLD), so the default
9475
+ * // extension is used.
9390
9476
  * generateDomainFromName('Canada')
9391
- * // Returns "canad.ca"
9477
+ * // Returns "canada.link"
9392
9478
  *
9393
9479
  * // With vowels removed if needed
9394
9480
  * generateDomainFromName('Digital', 'io')
@@ -9676,9 +9762,11 @@ function linkConstructorSimple({ domain, key }) {
9676
9762
  */
9677
9763
  const getSearchParams = (url) => {
9678
9764
  const params = {};
9679
- new URL(url).searchParams.forEach((val, key) => {
9680
- params[key] = val;
9681
- });
9765
+ try {
9766
+ new URL(url).searchParams.forEach((val, key) => {
9767
+ params[key] = val;
9768
+ });
9769
+ } catch {}
9682
9770
  return params;
9683
9771
  };
9684
9772
  /**
@@ -9720,13 +9808,15 @@ const getSearchParams = (url) => {
9720
9808
  */
9721
9809
  const getSearchParamsWithArray = (url) => {
9722
9810
  const params = {};
9723
- new URL(url).searchParams.forEach((val, key) => {
9724
- if (key in params) {
9725
- const param = params[key];
9726
- if (Array.isArray(param)) param.push(val);
9727
- else params[key] = [param, val];
9728
- } else params[key] = val;
9729
- });
9811
+ try {
9812
+ new URL(url).searchParams.forEach((val, key) => {
9813
+ if (key in params) {
9814
+ const param = params[key];
9815
+ if (Array.isArray(param)) param.push(val);
9816
+ else params[key] = [param, val];
9817
+ } else params[key] = val;
9818
+ });
9819
+ } catch {}
9730
9820
  return params;
9731
9821
  };
9732
9822
  /**
@@ -9964,7 +10054,7 @@ const getUrlFromStringIfValid = (str) => {
9964
10054
  */
9965
10055
  const getPrettyUrl = (url) => {
9966
10056
  if (!url) return "";
9967
- return url.replace(/(^\w+:|^)\/\//, "").replace("www.", "").replace(/\/$/, "");
10057
+ return url.replace(/(^\w+:|^)\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
9968
10058
  };
9969
10059
  /**
9970
10060
  * Generates a URL-friendly slug from a given title
@@ -10166,7 +10256,7 @@ const constructURLFromUTMParams = (url, utmParams) => {
10166
10256
  try {
10167
10257
  const newURL = new URL(url);
10168
10258
  for (const [key, value] of Object.entries(utmParams)) if (value === "") newURL.searchParams.delete(key);
10169
- else newURL.searchParams.set(key, value.replace("+", " "));
10259
+ else newURL.searchParams.set(key, value.replace(/\+/g, " "));
10170
10260
  return newURL.toString();
10171
10261
  } catch (_) {
10172
10262
  return "";
@@ -10211,9 +10301,7 @@ const constructURLFromUTMParams = (url, utmParams) => {
10211
10301
  const getUrlWithoutUTMParams = (url) => {
10212
10302
  try {
10213
10303
  const newURL = new URL(url);
10214
- paramsMetadata.forEach((param) => {
10215
- newURL.searchParams.delete(param.key);
10216
- });
10304
+ for (const key of UTMTags) newURL.searchParams.delete(key);
10217
10305
  return newURL.toString();
10218
10306
  } catch (_) {
10219
10307
  return url;
@@ -10321,6 +10409,34 @@ const createHref = (href, domain, utmParams) => {
10321
10409
  const deepEqual = (obj1, obj2) => {
10322
10410
  if (obj1 === obj2) return true;
10323
10411
  if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null) return false;
10412
+ if (Object.prototype.toString.call(obj1) !== Object.prototype.toString.call(obj2)) return false;
10413
+ if (obj1 instanceof Date && obj2 instanceof Date) return obj1.getTime() === obj2.getTime();
10414
+ if (obj1 instanceof RegExp && obj2 instanceof RegExp) return obj1.source === obj2.source && obj1.flags === obj2.flags;
10415
+ if (Array.isArray(obj1) && Array.isArray(obj2)) {
10416
+ if (obj1.length !== obj2.length) return false;
10417
+ for (let i = 0; i < obj1.length; i++) if (!deepEqual(obj1[i], obj2[i])) return false;
10418
+ return true;
10419
+ }
10420
+ if (obj1 instanceof Map && obj2 instanceof Map) {
10421
+ if (obj1.size !== obj2.size) return false;
10422
+ for (const [key, value] of obj1) {
10423
+ if (!obj2.has(key)) return false;
10424
+ if (!deepEqual(value, obj2.get(key))) return false;
10425
+ }
10426
+ return true;
10427
+ }
10428
+ if (obj1 instanceof Set && obj2 instanceof Set) {
10429
+ if (obj1.size !== obj2.size) return false;
10430
+ for (const value of obj1) {
10431
+ let found = false;
10432
+ for (const other of obj2) if (deepEqual(value, other)) {
10433
+ found = true;
10434
+ break;
10435
+ }
10436
+ if (!found) return false;
10437
+ }
10438
+ return true;
10439
+ }
10324
10440
  const keys1 = Object.keys(obj1);
10325
10441
  const keys2 = Object.keys(obj2);
10326
10442
  if (keys1.length !== keys2.length) return false;
@@ -10617,64 +10733,64 @@ function getEmailDomain(email) {
10617
10733
  * Checks if a URL can be embedded in an iframe based on security headers
10618
10734
  */
10619
10735
  /**
10620
- * Determines if a URL can be displayed in an iframe by checking security headers
10621
- * Examines Content-Security-Policy and X-Frame-Options headers to verify embedding permissions
10736
+ * Determines if a URL can be displayed in an iframe by checking security headers.
10737
+ * Examines Content-Security-Policy `frame-ancestors` and `X-Frame-Options` headers.
10738
+ *
10739
+ * A URL is considered embeddable only when:
10740
+ * - `frame-ancestors` is absent OR explicitly allows `*` / `self` (same origin) /
10741
+ * a wildcard/origin that matches `requestDomain`, AND
10742
+ * - `X-Frame-Options` is not `DENY` or `SAMEORIGIN` (when origin differs).
10622
10743
  *
10623
10744
  * @param options - The options for the check
10624
10745
  * @param options.url - The URL to check for iframe embedding capability
10625
- * @param options.requestDomain - The domain that will be embedding the iframe
10626
- * @returns A Promise that resolves to true if the URL can be embedded, false otherwise
10746
+ * @param options.requestDomain - The origin that will be embedding the iframe
10747
+ * (e.g. `https://myapp.com`). Compared against CSP source expressions.
10748
+ * @returns A Promise that resolves to true if the URL can be embedded, false otherwise.
10627
10749
  *
10628
10750
  * @example
10629
10751
  * ```ts
10630
10752
  * import { isIframeable } from '@/utils/functions';
10631
10753
  *
10632
- * // Check if a URL can be embedded in an iframe
10633
10754
  * const canEmbed = await isIframeable({
10634
10755
  * url: 'https://example.com',
10635
10756
  * requestDomain: 'https://myapp.com'
10636
10757
  * });
10637
- *
10638
- * if (canEmbed) {
10639
- * console.log('URL can be embedded in an iframe');
10640
- * } else {
10641
- * console.log('URL cannot be embedded due to security restrictions');
10642
- * }
10643
- *
10644
- * // In a React component
10645
- * function EmbedPreview({ url }) {
10646
- * const [canEmbed, setCanEmbed] = useState<boolean | null>(null);
10647
- *
10648
- * useEffect(() => {
10649
- * async function checkEmbedding() {
10650
- * const result = await isIframeable({
10651
- * url,
10652
- * requestDomain: window.location.origin
10653
- * });
10654
- * setCanEmbed(result);
10655
- * }
10656
- * checkEmbedding();
10657
- * }, [url]);
10658
- *
10659
- * if (canEmbed === null) return <div>Checking...</div>;
10660
- * if (canEmbed === false) return <div>Cannot embed this URL</div>;
10661
- *
10662
- * return <iframe src={url} />;
10663
- * }
10664
10758
  * ```
10665
10759
  */
10666
10760
  const isIframeable = async ({ url, requestDomain }) => {
10667
10761
  const res = await fetch(url);
10762
+ const normalizeOrigin = (value) => value.trim().replace(/\/$/, "").toLowerCase();
10763
+ const requestedOrigin = normalizeOrigin(requestDomain);
10668
10764
  const cspHeader = res.headers.get("content-security-policy");
10669
10765
  if (cspHeader) {
10670
10766
  const frameAncestorsMatch = cspHeader.match(/frame-ancestors\s+([\s\S]+?)(?=;|$)/i);
10671
- if (frameAncestorsMatch) {
10672
- if (frameAncestorsMatch[1] === "*") return true;
10673
- if (frameAncestorsMatch[1].split(/\s+/).includes(requestDomain)) return true;
10674
- }
10767
+ if (frameAncestorsMatch) return frameAncestorsMatch[1].trim().split(/\s+/).filter(Boolean).some((source) => {
10768
+ const s = source.toLowerCase();
10769
+ if (s === "'none'") return false;
10770
+ if (s === "*" || s === "https:*" || s === "http:*") return true;
10771
+ if (s === "'self'") try {
10772
+ return normalizeOrigin(new URL(res.url).origin) === requestedOrigin;
10773
+ } catch {
10774
+ return false;
10775
+ }
10776
+ if (s.includes("*")) {
10777
+ const pattern = s.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
10778
+ return new RegExp(`^${pattern}$`).test(requestedOrigin);
10779
+ }
10780
+ return normalizeOrigin(s) === requestedOrigin;
10781
+ });
10675
10782
  }
10676
10783
  const xFrameOptions = res.headers.get("X-Frame-Options");
10677
- if (xFrameOptions === "DENY" || xFrameOptions === "SAMEORIGIN") return false;
10784
+ if (xFrameOptions) {
10785
+ const value = xFrameOptions.trim().toUpperCase();
10786
+ if (value === "DENY") return false;
10787
+ if (value === "SAMEORIGIN") try {
10788
+ return normalizeOrigin(new URL(res.url).origin) === requestedOrigin;
10789
+ } catch {
10790
+ return false;
10791
+ }
10792
+ if (value.startsWith("ALLOW-FROM")) return false;
10793
+ }
10678
10794
  return true;
10679
10795
  };
10680
10796
  //#endregion
@@ -10758,6 +10874,6 @@ const isReservedKeyGlobal = (key) => {
10758
10874
  ].includes(key);
10759
10875
  };
10760
10876
  //#endregion
10761
- export { API_PORT, CONNECT_SUPPORTED_COUNTRIES, CONTINENTS, CONTINENT_CODES, COUNTRIES, COUNTRY_CODES, COUNTRY_PHONE_CODES, DEBUG, DEFAULT_THEME_MODE, EU_COUNTRY_CODES, FRAMER_MOTION_LIST_ITEM_VARIANTS, INFINITY_NUMBER, LAST_VISITED_WORKSPACE, LOCALHOST_GEO_DATA, LOCALHOST_IP, LOG_LEVEL, META_THEME_COLORS, NEXT_PUBLIC_RECAPTCHA_SITE_KEY, NODE_ENV, OG_AVATAR_URL, PAGINATION_LIMIT, PORT, REDIRECT_QUERY_PARAM, REGIONS, REGION_CODES, RESERVED_SLUGS, SECOND_LEVEL_DOMAINS, SLACK_WEBHOOKS, SPECIAL_APEX_DOMAINS, STAGGER_CHILD_VARIANTS, SlackLog, TAB_ITEM_ANIMATION_SETTINGS, THEME_MODES, THE_BEGINNING_OF_TIME, TWO_WEEKS_IN_SECONDS, UTMTags, assetsUrl, baseId, baseIdCustom, capitalize, ccTLDs, chunk, clearAllCookies, clearStorage, cn, combineWords, constructMetadata, constructURLFromUTMParams, createHref, createId, cuid, currencyFormatter, customeId, debounce, deepEqual, fetchWithRetry, fetchWithTimeout, fetcher, fetcherSWR, fileToBase64, formatDate, formatDateTime, formatDateTimeSmart, formatFileSize, formatPeriod, formatTime, generateDomainFromName, getAdjustedBillingCycleStart, getAllCookies, getApexDomain, getBillingStartDate, getClientIP, getClientIPFromHeaders, getCookie, getCurrentYear, getDateTimeLocal, getDaysDifference, getDomainWithoutWWW, getEmailDomain, getEnv, getFirstAndLastDay, getFlagUrl, getHeight, getIPAddress, getInitials, getLastDayOfMonth, getParamsFromURL, getPrettyUrl, getRequestOrigin, getSearchParams, getSearchParamsWithArray, getSizeAvatar, getSlug, getStorageItem, getSubdomain, getTimeZones, getUrlFromString, getUrlFromStringIfValid, getUrlObjFromString, getUrlWithoutUTMParams, getUserAgent, getUserAgentFromHeaders, getUserName, googleTrackCodeCopy, googleTrackDirectionChange, googleTrackEvent, googleTrackItemCreate, googleTrackItemDelete, googleTrackItemRestore, googleTrackItemUpdate, googleTrackItemView, googleTrackThemeChange, googleTrackViewChange, hasCookie, hasStorageItem, hashStringSHA256, init as initCuid, isBusinessEmail, isDevelopment, isDisposableEmail, isIframeable, isProduction, isReservedKeyGlobal, isStaging, isUnsupportedKey, isValidIP, isValidUrl, linkConstructor, linkConstructorSimple, loadImage, logger, mapLocale, nFormatter, nanoid, normalizeEmail, normalizePath, normalizeString, paramsMetadata, parse, pluralize, randomValue, regexEscape, removeCookie, removeStorageItem, resizeAndCropImage, resizeImage, setCookie, setStorageItem, slugify, smartTruncate, stableSort, throttle, timeAgo, toCamelCase, trim, truncate, uid, validDomainRegex, validKeyRegex, validSlugRegex, validateEmail, verifyRecaptchaToken };
10877
+ export { API_PORT, CONNECT_SUPPORTED_COUNTRIES, CONTINENTS, CONTINENT_CODES, COUNTRIES, COUNTRY_CODES, COUNTRY_PHONE_CODES, DEBUG, DEFAULT_THEME_MODE, EU_COUNTRY_CODES, FRAMER_MOTION_LIST_ITEM_VARIANTS, INFINITY_NUMBER, LAST_VISITED_WORKSPACE, LOCALHOST_GEO_DATA, LOCALHOST_IP, LOG_LEVEL, META_THEME_COLORS, NEXT_PUBLIC_RECAPTCHA_SITE_KEY, NODE_ENV, OG_AVATAR_URL, PAGINATION_LIMIT, PORT, RECAPTCHA_SECRET_KEY, REDIRECT_QUERY_PARAM, REGIONS, REGION_CODES, RESERVED_SLUGS, SECOND_LEVEL_DOMAINS, SLACK_WEBHOOKS, SPECIAL_APEX_DOMAINS, STAGGER_CHILD_VARIANTS, SlackLog, TAB_ITEM_ANIMATION_SETTINGS, THEME_MODES, THE_BEGINNING_OF_TIME, TWO_WEEKS_IN_SECONDS, UTMTags, assetsUrl, baseId, baseIdCustom, capitalize, ccTLDs, chunk, clearAllCookies, clearStorage, cn, combineWords, constructMetadata, constructURLFromUTMParams, createHref, createId, cuid, currencyFormatter, customId, customeId, debounce, deepEqual, fetchWithRetry, fetchWithTimeout, fetcher, fetcherSWR, fileToBase64, formatDate, formatDateTime, formatDateTimeSmart, formatFileSize, formatPeriod, formatTime, generateDomainFromName, getAdjustedBillingCycleStart, getAllCookies, getApexDomain, getBillingStartDate, getClientIP, getClientIPFromHeaders, getCookie, getCurrentYear, getDateTimeLocal, getDaysDifference, getDomainWithoutWWW, getEmailDomain, getEnv, getFirstAndLastDay, getFlagUrl, getHeight, getIPAddress, getInitials, getLastDayOfMonth, getParamsFromURL, getPrettyUrl, getRequestOrigin, getSearchParams, getSearchParamsWithArray, getSizeAvatar, getSlug, getStorageItem, getSubdomain, getTimeZones, getUrlFromString, getUrlFromStringIfValid, getUrlObjFromString, getUrlWithoutUTMParams, getUserAgent, getUserAgentFromHeaders, getUserName, googleTrackCodeCopy, googleTrackDirectionChange, googleTrackEvent, googleTrackItemCreate, googleTrackItemDelete, googleTrackItemRestore, googleTrackItemUpdate, googleTrackItemView, googleTrackThemeChange, googleTrackViewChange, hasCookie, hasStorageItem, hashStringSHA256, init as initCuid, isBusinessEmail, isDevelopment, isDisposableEmail, isIframeable, isProduction, isReservedKeyGlobal, isStaging, isTest, isUnsupportedKey, isValidIP, isValidUrl, linkConstructor, linkConstructorSimple, loadImage, logger, mapLocale, nFormatter, nanoid, normalizeEmail, normalizePath, normalizeString, paramsMetadata, parse, pluralize, randomValue, regexEscape, removeCookie, removeStorageItem, resizeAndCropImage, resizeImage, setCookie, setStorageItem, slugify, smartTruncate, stableSort, throttle, timeAgo, toCamelCase, trim, truncate, uid, validDomainRegex, validKeyRegex, validSlugRegex, validateEmail, verifyRecaptchaToken };
10762
10878
 
10763
10879
  //# sourceMappingURL=index.js.map