facebetter 2.0.0 → 2.0.1

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.
@@ -11,58 +11,33 @@
11
11
 
12
12
  /**
13
13
  * Engine configuration class
14
- * Analogous to EngineConfig in C++ / Java / Objective-C, with Web-only auth fields.
14
+ * Analogous to EngineConfig in C++ / Java / Objective-C. Web only accepts licenseToken.
15
15
  */
16
16
  class EngineConfig {
17
17
  /**
18
18
  * @param {Object} config
19
- * @param {string} [config.licenseToken] Compact JWS, or the raw `{success, token}`
19
+ * @param {string} config.licenseToken Compact JWS, or the raw `{success, token}`
20
20
  * HTTP response body from your auth server.
21
- * @param {string} [config.authProxyUrl] Production: forward the WASM auth
22
- * challenge to your server, which holds app_id/app_key and calls Cloudflare.
23
- * @param {Function} [config.fetchAuthResponse] Custom auth hook with signature
24
- * `async (challenge) => string|object`. For local debugging you can use
25
- * `createDirectAuthFetcher` to hit Cloudflare directly — never ship keys in
26
- * a production frontend bundle.
27
21
  * @param {boolean} [config.externalContext] Kept for cross-platform alignment;
28
- * unused on Web/WASM today.
22
+ * unused on Web today.
29
23
  */
30
24
  constructor(config = {}) {
31
25
  this.licenseToken = config.licenseToken || null;
32
- this.authProxyUrl = config.authProxyUrl || null;
33
- this.fetchAuthResponse = config.fetchAuthResponse || null;
34
26
  this.resourcePath = '/resource.fbd';
35
27
  /**
36
28
  * Whether to use an external GL context (native platforms only).
37
- * On Web/WASM this field is kept for config-shape alignment and has no effect.
29
+ * On Web this field is kept for config-shape alignment and has no effect.
38
30
  */
39
31
  this.externalContext = !!config.externalContext;
40
32
  }
41
33
 
42
34
  isValid() {
43
- if (this.licenseToken && typeof this.licenseToken === 'string' &&
44
- this.licenseToken.trim() !== '') {
45
- return true;
46
- }
47
- if (typeof this.fetchAuthResponse === 'function') {
48
- return true;
49
- }
50
- if (this.authProxyUrl && typeof this.authProxyUrl === 'string' &&
51
- this.authProxyUrl.trim() !== '') {
52
- return true;
53
- }
54
- return false;
35
+ return typeof this.licenseToken === 'string' &&
36
+ this.licenseToken.trim() !== '';
55
37
  }
56
38
 
57
39
  toString() {
58
- const mode = this.licenseToken
59
- ? 'licenseToken'
60
- : this.fetchAuthResponse
61
- ? 'fetchAuthResponse'
62
- : this.authProxyUrl
63
- ? 'authProxyUrl'
64
- : 'invalid';
65
- return `EngineConfig{mode='${mode}'}`;
40
+ return `EngineConfig{mode='${this.isValid() ? 'licenseToken' : 'invalid'}'}`;
66
41
  }
67
42
  }
68
43
 
@@ -312,40 +287,6 @@
312
287
  WhiteningStyle: WhiteningStyle$1
313
288
  });
314
289
 
315
- /**
316
- * Browser-side console lines aligned with C++ Logger (src/base/logging.cc) Release pattern:
317
- * [%Y-%m-%d %H:%M:%S.%e] [facebetter] [%l] [%t] - %v
318
- * spdlog level names: info, warning, error (see spdlog SPDLOG_LEVEL_NAMES).
319
- */
320
-
321
- function pad2(n) {
322
- return String(n).padStart(2, '0');
323
- }
324
-
325
- /**
326
- * @param {'info'|'warning'|'error'} levelTag — must match spdlog long level strings
327
- * @param {string} message
328
- * @returns {string}
329
- */
330
- function formatLogLine(levelTag, message) {
331
- const d = new Date();
332
- const ms = String(d.getMilliseconds()).padStart(3, '0');
333
- const ts = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(
334
- d.getHours()
335
- )}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${ms}`;
336
- return `[${ts}] [facebetter] [${levelTag}] [0] - ${message}`;
337
- }
338
-
339
- /** @param {string} message */
340
- function logInfo(message) {
341
- console.info(formatLogLine('info', message));
342
- }
343
-
344
- /** @param {string} message */
345
- function logWarn(message) {
346
- console.warn(formatLogLine('warning', message));
347
- }
348
-
349
290
  /**
350
291
  * Beauty Effect Engine Core
351
292
  * Platform-agnostic engine implementation with dependency injection
@@ -410,8 +351,6 @@
410
351
 
411
352
  this.config = engineConfig;
412
353
  this.licenseToken = engineConfig.licenseToken;
413
- this.authProxyUrl = engineConfig.authProxyUrl;
414
- this.fetchAuthResponse = engineConfig.fetchAuthResponse;
415
354
  this.resourcePath = '/resource.fbd';
416
355
  this.enginePtr = null;
417
356
  this.initialized = false;
@@ -431,7 +370,8 @@
431
370
  this._toImageData = platformAPI.toImageData;
432
371
  this._ensureFacebetterCanvas = platformAPI.ensureFacebetterCanvas;
433
372
  this._cleanupFacebetterCanvas = platformAPI.cleanupFacebetterCanvas;
434
-
373
+ this._pendingLogConfig = null;
374
+
435
375
  // Browser-specific state
436
376
  this._facebetterCanvas = null;
437
377
  this._createdFacebetterCanvas = false;
@@ -442,9 +382,8 @@
442
382
  if (this._ensureFacebetterCanvas) {
443
383
  this._ensureFacebetterCanvas();
444
384
  }
445
-
446
- // Start loading WASM module immediately in constructor
447
- this._wasmLoadPromise = this._loadWasmModule();
385
+
386
+ this._wasmLoadPromise = null;
448
387
  this._initPromise = null;
449
388
  }
450
389
 
@@ -501,8 +440,12 @@
501
440
  /**
502
441
  * Initializes the engine
503
442
  * @param {Object} [options] - Initialization options
504
- * @param {number} [options.timeout] - Timeout in milliseconds (default: 30000 for WASM, 10000 for auth)
505
- * @param {number} [options.authTimeout] - Timeout for online authentication in milliseconds (default: 10000)
443
+ * @param {number} [options.timeout] - Timeout in milliseconds (default: 120000)
444
+ * @param {number} [options.authTimeout] - Unused. Kept for compatibility.
445
+ * @param {function} [options.onProgress] - Download progress:
446
+ * `{ loaded, total, percent }` while fetching the runtime files.
447
+ * @param {string} [options.assetBaseUrl] - Directory that contains
448
+ * `facebetter-core.wasm` and `resource.fbd`. Optional when using npm.
506
449
  * @returns {Promise<void>} Promise that resolves when initialization is complete
507
450
  */
508
451
  async init(options = {}) {
@@ -519,9 +462,18 @@
519
462
  // Create initialization Promise
520
463
  this._initPromise = (async () => {
521
464
  try {
522
- const wasmTimeout = options.timeout || 30000;
465
+ const wasmTimeout = options.timeout || 120000;
466
+
467
+ if (!this._wasmLoadPromise) {
468
+ this._wasmLoadPromise = this._loadWasmModule({
469
+ onProgress: options.onProgress,
470
+ assetBaseUrl: options.assetBaseUrl,
471
+ wasmUrl: options.wasmUrl,
472
+ wasmAssetUrl: options.wasmAssetUrl,
473
+ resourceAssetUrl: options.resourceAssetUrl,
474
+ });
475
+ }
523
476
 
524
- // Wait for WASM module loading (with timeout)
525
477
  try {
526
478
  await Promise.race([
527
479
  this._wasmLoadPromise,
@@ -531,6 +483,11 @@
531
483
  throw this._enhanceError(error, 'Failed to load WASM module');
532
484
  }
533
485
 
486
+ if (this._pendingLogConfig) {
487
+ await this._applyLogConfig(this._pendingLogConfig);
488
+ this._pendingLogConfig = null;
489
+ }
490
+
534
491
  const Module = this._getWasmModule();
535
492
 
536
493
  // Setup usage report proxy for WASM
@@ -556,66 +513,12 @@
556
513
  };
557
514
  }
558
515
 
559
- // Web v2 auth: never accept appId/appKey in the browser.
560
- // - licenseToken: pre-fetched JWS or `{success, token}` body
561
- // - authProxyUrl: production — forward the challenge to your server
562
- // - fetchAuthResponse: custom hook; locally use createDirectAuthFetcher
516
+ // Web: fetch the token outside the engine, then pass licenseToken.
563
517
  if (!this.licenseToken) {
564
- if (typeof this.fetchAuthResponse === 'function') {
565
- Module.onOnlineAuth = async (payloadJson) => {
566
- try {
567
- const challenge = JSON.parse(payloadJson);
568
- const result = await this.fetchAuthResponse(challenge);
569
- if (result && typeof result === 'object' && typeof result.response === 'string') {
570
- return { response: result.response };
571
- }
572
- if (typeof result === 'string') {
573
- return result;
574
- }
575
- if (result && typeof result === 'object') {
576
- return JSON.stringify(result);
577
- }
578
- return '';
579
- } catch (err) {
580
- logWarn(
581
- `Online license: fetchAuthResponse error${err?.message ? `: ${err.message}` : ''}`
582
- );
583
- return JSON.stringify({
584
- success: false,
585
- error: 'auth_fetch_failed',
586
- message: err?.message || 'auth fetch failed',
587
- });
588
- }
589
- };
590
- } else if (this.authProxyUrl) {
591
- Module.onOnlineAuth = async (payloadJson) => {
592
- logInfo('Online license: proxying auth challenge');
593
- try {
594
- const response = await fetch(this.authProxyUrl, {
595
- method: 'POST',
596
- headers: { 'Content-Type': 'application/json' },
597
- body: payloadJson,
598
- });
599
- if (!response.ok) {
600
- logWarn(`Online license: proxy HTTP ${response.status}`);
601
- return '';
602
- }
603
- const text = await response.text();
604
- logInfo('Online license: proxy response ok');
605
- return text;
606
- } catch (err) {
607
- logWarn(
608
- `Online license: proxy error${err?.message ? `: ${err.message}` : ''}`
609
- );
610
- return '';
611
- }
612
- };
613
- } else {
614
- throw new FacebetterError(
615
- 'Web auth requires licenseToken, authProxyUrl, or fetchAuthResponse.',
616
- 'LICENSE_ERROR'
617
- );
618
- }
518
+ throw new FacebetterError(
519
+ 'Web auth requires licenseToken. Fetch it from your server before init().',
520
+ 'LICENSE_ERROR'
521
+ );
619
522
  }
620
523
 
621
524
  // C ABI still has app_id/app_key slots; leave them empty on Web.
@@ -659,7 +562,8 @@
659
562
  this.initialized = true;
660
563
  this._initPromise = null; // Clear Promise cache, allow re-initialization (if needed)
661
564
  } catch (error) {
662
- this._initPromise = null; // Clear Promise cache, allow retry
565
+ this._initPromise = null;
566
+ this._wasmLoadPromise = null;
663
567
  throw error;
664
568
  }
665
569
  })();
@@ -716,8 +620,9 @@
716
620
  }
717
621
 
718
622
  /**
719
- * Sets log configuration
720
- * Can be called before init() since fb_set_log_config is a global function
623
+ * Sets log configuration.
624
+ * If called before `init()`, the values are applied after the runtime downloads
625
+ * and before the engine is created.
721
626
  * @param {Object} config - Log configuration
722
627
  * @param {boolean} config.consoleEnabled - Enable console logging
723
628
  * @param {boolean} config.fileEnabled - Enable file logging
@@ -726,8 +631,15 @@
726
631
  * @returns {Promise<void>} Promise that resolves when log config is set
727
632
  */
728
633
  async setLogConfig(config) {
729
- // Wait for WASM module to be loaded (started in constructor)
634
+ if (!this._wasmLoadPromise) {
635
+ this._pendingLogConfig = config;
636
+ return;
637
+ }
730
638
  await this._wasmLoadPromise;
639
+ await this._applyLogConfig(config);
640
+ }
641
+
642
+ async _applyLogConfig(config) {
731
643
  const Module = this._getWasmModule();
732
644
 
733
645
  const fileNamePtr = allocUtf8(Module, config.fileName);
@@ -1632,100 +1544,204 @@
1632
1544
 
1633
1545
  }
1634
1546
 
1635
- /** Cloudflare v2 online auth URL. Proxy through your server in production; never ship keys in the frontend bundle. */
1636
- const FACEBETTER_AUTH_URL =
1637
- 'https://facebetter.pixpark.net/facebetter/v2/auth';
1547
+ const WASM_FILE = 'facebetter-core.wasm';
1548
+ const RESOURCE_FILE = 'resource.fbd';
1549
+ const MEMFS_RESOURCE_PATH = '/resource.fbd';
1638
1550
 
1639
- function toHex(buffer) {
1640
- return Array.from(new Uint8Array(buffer), (b) =>
1641
- b.toString(16).padStart(2, '0')
1642
- ).join('');
1551
+ function emitProgress(onProgress, loaded, total) {
1552
+ if (typeof onProgress !== 'function') {
1553
+ return;
1554
+ }
1555
+ const safeTotal = total > 0 ? total : loaded;
1556
+ const percent =
1557
+ safeTotal > 0 ? Math.min(100, Math.round((loaded / safeTotal) * 100)) : 0;
1558
+ onProgress({ loaded, total: safeTotal, percent });
1643
1559
  }
1644
1560
 
1645
- async function hmacSha256Hex(key, message) {
1646
- const encoder = new TextEncoder();
1647
- const cryptoKey = await crypto.subtle.importKey(
1648
- 'raw',
1649
- encoder.encode(key),
1650
- { name: 'HMAC', hash: 'SHA-256' },
1651
- false,
1652
- ['sign']
1653
- );
1654
- const signature = await crypto.subtle.sign(
1655
- 'HMAC',
1656
- cryptoKey,
1657
- encoder.encode(message)
1658
- );
1659
- return toHex(signature);
1561
+ async function readFileUrl(url, onChunk) {
1562
+ const { readFile } = await import('node:fs/promises');
1563
+ const { fileURLToPath } = await import('node:url');
1564
+ const buf = await readFile(fileURLToPath(url));
1565
+ const bytes = new Uint8Array(buf);
1566
+ onChunk(bytes.byteLength, bytes.byteLength);
1567
+ return bytes;
1660
1568
  }
1661
1569
 
1662
1570
  /**
1663
- * Call Cloudflare v2/auth directly with app_id / app_key.
1664
- * Intended for local debugging, or environments where key leakage is not a risk.
1665
- *
1666
- * @param {Object} options
1667
- * @param {string} options.appId
1668
- * @param {string} options.appKey
1669
- * @param {Object} options.challenge WASM challenge `{nonce, timestamp, platform, user_agent}`
1670
- * @param {string} [options.authUrl]
1671
- * @returns {Promise<string>} Raw HTTP body (`{success, token}` or an error envelope)
1571
+ * Fetch a binary with byte-level download progress.
1572
+ * @param {string} url
1573
+ * @param {function(number, number): void} onChunk loaded, total (0 if unknown)
1574
+ * @returns {Promise<Uint8Array>}
1672
1575
  */
1673
- async function requestFacebetterAuth({
1674
- appId,
1675
- appKey,
1676
- challenge,
1677
- authUrl = FACEBETTER_AUTH_URL,
1678
- }) {
1679
- const id = typeof appId === 'string' ? appId.trim() : '';
1680
- const key = typeof appKey === 'string' ? appKey.trim() : '';
1681
- if (!id || !key) {
1682
- throw new FacebetterError(
1683
- 'requestFacebetterAuth requires appId and appKey',
1684
- 'LICENSE_ERROR'
1685
- );
1576
+ async function fetchBinary(url, onChunk) {
1577
+ if (typeof url === 'string' && url.startsWith('file:')) {
1578
+ return readFileUrl(url, onChunk);
1686
1579
  }
1687
- if (!challenge || typeof challenge !== 'object') {
1580
+
1581
+ const response = await fetch(url);
1582
+ if (!response.ok) {
1688
1583
  throw new FacebetterError(
1689
- 'requestFacebetterAuth requires a WASM auth challenge',
1690
- 'LICENSE_ERROR'
1584
+ `Failed to download ${url} (${response.status})`,
1585
+ 'WASM_LOAD_ERROR'
1691
1586
  );
1692
1587
  }
1693
1588
 
1694
- const nonce = String(challenge.nonce || '');
1695
- const timestamp = Number(challenge.timestamp);
1696
- const platform = String(challenge.platform || 'web');
1697
- const userAgent = String(challenge.user_agent || '');
1698
- const payload = `v2|${id}|${timestamp}|${nonce}|${platform}`;
1699
- const hmac = await hmacSha256Hex(key, payload);
1700
-
1701
- const response = await fetch(authUrl, {
1702
- method: 'POST',
1703
- headers: { 'Content-Type': 'application/json' },
1704
- body: JSON.stringify({
1705
- app_id: id,
1706
- hmac_signature: hmac,
1707
- timestamp,
1708
- nonce,
1709
- platform,
1710
- user_agent: userAgent,
1589
+ const totalHeader = Number(response.headers.get('content-length'));
1590
+ const total = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : 0;
1591
+
1592
+ if (!response.body || typeof response.body.getReader !== 'function') {
1593
+ const buffer = await response.arrayBuffer();
1594
+ const bytes = new Uint8Array(buffer);
1595
+ onChunk(bytes.byteLength, total || bytes.byteLength);
1596
+ return bytes;
1597
+ }
1598
+
1599
+ const reader = response.body.getReader();
1600
+ const chunks = [];
1601
+ let loaded = 0;
1602
+ while (true) {
1603
+ const { done, value } = await reader.read();
1604
+ if (done) {
1605
+ break;
1606
+ }
1607
+ chunks.push(value);
1608
+ loaded += value.byteLength;
1609
+ onChunk(loaded, total);
1610
+ }
1611
+
1612
+ const bytes = new Uint8Array(loaded);
1613
+ let offset = 0;
1614
+ for (const chunk of chunks) {
1615
+ bytes.set(chunk, offset);
1616
+ offset += chunk.byteLength;
1617
+ }
1618
+ onChunk(loaded, total || loaded);
1619
+ return bytes;
1620
+ }
1621
+
1622
+ async function fetchRuntimeAssets({ wasmUrl, resourceUrl, onProgress }) {
1623
+ let wasmLoaded = 0;
1624
+ let wasmTotal = 0;
1625
+ let resourceLoaded = 0;
1626
+ let resourceTotal = 0;
1627
+
1628
+ const emit = () => {
1629
+ emitProgress(
1630
+ onProgress,
1631
+ wasmLoaded + resourceLoaded,
1632
+ wasmTotal + resourceTotal
1633
+ );
1634
+ };
1635
+
1636
+ const [wasmBytes, resourceBytes] = await Promise.all([
1637
+ fetchBinary(wasmUrl, (loaded, total) => {
1638
+ wasmLoaded = loaded;
1639
+ wasmTotal = total || loaded;
1640
+ emit();
1711
1641
  }),
1712
- });
1713
- return response.text();
1642
+ fetchBinary(resourceUrl, (loaded, total) => {
1643
+ resourceLoaded = loaded;
1644
+ resourceTotal = total || loaded;
1645
+ emit();
1646
+ }),
1647
+ ]);
1648
+
1649
+ emitProgress(
1650
+ onProgress,
1651
+ wasmBytes.byteLength + resourceBytes.byteLength,
1652
+ wasmBytes.byteLength + resourceBytes.byteLength
1653
+ );
1654
+ return { wasmBytes, resourceBytes };
1655
+ }
1656
+
1657
+ function scriptDirectory() {
1658
+ if (typeof document === 'undefined') {
1659
+ return '';
1660
+ }
1661
+ if (document.currentScript?.src) {
1662
+ return document.currentScript.src.slice(
1663
+ 0,
1664
+ document.currentScript.src.lastIndexOf('/') + 1
1665
+ );
1666
+ }
1667
+ const scripts = document.getElementsByTagName('script');
1668
+ for (let i = scripts.length - 1; i >= 0; i--) {
1669
+ const src = scripts[i].src;
1670
+ if (src && (src.includes('facebetter.js') || src.includes('facebetter'))) {
1671
+ return src.slice(0, src.lastIndexOf('/') + 1);
1672
+ }
1673
+ }
1674
+ if (typeof location !== 'undefined') {
1675
+ return new URL('.', location.href).href;
1676
+ }
1677
+ return './';
1714
1678
  }
1715
1679
 
1716
1680
  /**
1717
- * Build an `EngineConfig.fetchAuthResponse` that talks to Cloudflare directly
1718
- * (no customer proxy).
1719
- *
1720
- * @param {Object} options
1721
- * @param {string} options.appId
1722
- * @param {string} options.appKey
1723
- * @param {string} [options.authUrl]
1724
- * @returns {(challenge: Object) => Promise<string>}
1681
+ * Resolve sidecar wasm / resource URLs.
1682
+ * Prefer `assetBaseUrl`, then `facebetter-core/assets`, then the script directory.
1725
1683
  */
1726
- function createDirectAuthFetcher({ appId, appKey, authUrl } = {}) {
1727
- return (challenge) =>
1728
- requestFacebetterAuth({ appId, appKey, challenge, authUrl });
1684
+ async function resolveRuntimeAssetUrls(options = {}) {
1685
+ const joinBase = (base) => {
1686
+ const prefix = base.endsWith('/') ? base : `${base}/`;
1687
+ return {
1688
+ wasmUrl: options.wasmAssetUrl || `${prefix}${WASM_FILE}`,
1689
+ resourceUrl: options.resourceAssetUrl || `${prefix}${RESOURCE_FILE}`,
1690
+ };
1691
+ };
1692
+
1693
+ if (options.wasmAssetUrl && options.resourceAssetUrl) {
1694
+ return {
1695
+ wasmUrl: options.wasmAssetUrl,
1696
+ resourceUrl: options.resourceAssetUrl,
1697
+ };
1698
+ }
1699
+ if (options.assetBaseUrl) {
1700
+ return joinBase(options.assetBaseUrl);
1701
+ }
1702
+ if (typeof options.wasmUrl === 'string' && /^(https?:|file:|\/)/.test(options.wasmUrl)) {
1703
+ return joinBase(options.wasmUrl.slice(0, options.wasmUrl.lastIndexOf('/') + 1));
1704
+ }
1705
+
1706
+ try {
1707
+ const assets = await import('facebetter-core/assets');
1708
+ return {
1709
+ wasmUrl: options.wasmAssetUrl || assets.wasmUrl,
1710
+ resourceUrl: options.resourceAssetUrl || assets.resourceUrl,
1711
+ };
1712
+ } catch {
1713
+ return joinBase(scriptDirectory());
1714
+ }
1715
+ }
1716
+
1717
+ /**
1718
+ * Instantiate the Emscripten factory using already-downloaded bytes.
1719
+ */
1720
+ async function instantiateRuntime(factory, wasmBytes, resourceBytes) {
1721
+ if (!factory) {
1722
+ throw new FacebetterError(
1723
+ 'WASM module does not export createFaceBetterModule',
1724
+ 'WASM_LOAD_ERROR'
1725
+ );
1726
+ }
1727
+
1728
+ const module = await factory({
1729
+ instantiateWasm(imports, receiveInstance) {
1730
+ return WebAssembly.instantiate(wasmBytes, imports).then((result) => {
1731
+ receiveInstance(result.instance, result.module);
1732
+ });
1733
+ },
1734
+ });
1735
+
1736
+ if (!module) {
1737
+ throw new FacebetterError('Failed to initialize WASM module', 'WASM_LOAD_ERROR');
1738
+ }
1739
+ if (!module.FS) {
1740
+ throw new FacebetterError('Runtime filesystem is not available', 'WASM_LOAD_ERROR');
1741
+ }
1742
+ module.FS.writeFile(MEMFS_RESOURCE_PATH, resourceBytes);
1743
+ module.ready = true;
1744
+ return module;
1729
1745
  }
1730
1746
 
1731
1747
  /**
@@ -1734,6 +1750,7 @@
1734
1750
  * Uses document.currentScript for automatic path detection in UMD builds
1735
1751
  */
1736
1752
 
1753
+
1737
1754
  /**
1738
1755
  * Gets the base path of the current script
1739
1756
  * Similar to Node.js __dirname
@@ -1839,6 +1856,7 @@
1839
1856
  */
1840
1857
  async function loadWasmModule(options = {}) {
1841
1858
  if (wasmModuleInstance) {
1859
+ options.onProgress?.({ loaded: 1, total: 1, percent: 100 });
1842
1860
  return wasmModuleInstance;
1843
1861
  }
1844
1862
 
@@ -1848,57 +1866,44 @@
1848
1866
 
1849
1867
  wasmModulePromise = (async () => {
1850
1868
  try {
1851
- let FaceBetterModuleFactory;
1852
-
1869
+ let factory;
1870
+
1853
1871
  if (options.wasmUrl) {
1854
- // If custom URL is provided, use it (for backward compatibility or custom builds)
1855
- const wasmModule = await import(options.wasmUrl);
1856
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1872
+ const wasmModule = await import(/* @vite-ignore */ options.wasmUrl);
1873
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1857
1874
  } else {
1858
- // Try to import from facebetter-core npm package
1859
- // Note: In UMD builds, this may not work if facebetter-core is not available
1860
- // In that case, fallback to relative path detection
1861
1875
  try {
1862
1876
  const wasmModule = await import('facebetter-core');
1863
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1864
- } catch (e) {
1865
- // Fallback: try to find facebetter-core.js relative to current script
1866
- // This handles UMD builds where npm packages may not be available
1877
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1878
+ } catch {
1867
1879
  const basePath = getScriptBasePath();
1868
- const wasmPath = resolvePath('facebetter-core.js', basePath);
1869
- const wasmModule = await import(wasmPath);
1870
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1880
+ const gluePath = resolvePath('facebetter-core.js', basePath);
1881
+ const wasmModule = await import(gluePath);
1882
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1883
+ if (!options.assetBaseUrl) {
1884
+ options = { ...options, assetBaseUrl: basePath };
1885
+ }
1871
1886
  }
1872
1887
  }
1873
-
1874
- if (!FaceBetterModuleFactory) {
1875
- throw new Error('WASM module does not export createFaceBetterModule function');
1876
- }
1877
-
1878
- // Configure module options
1879
- // With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
1880
- // No need to handle .wasm and .data file paths separately
1881
- const moduleOptions = {
1882
- locateFile: options.locateFile || function(path, prefix) {
1883
- // With SINGLE_FILE=1, Emscripten handles embedded files automatically
1884
- // Custom locateFile is only needed if user provides one
1885
- return prefix + path;
1886
- },
1887
- ...options
1888
- };
1889
1888
 
1890
- // Initialize the module (MODULARIZE returns a Promise)
1891
- wasmModuleInstance = await FaceBetterModuleFactory(moduleOptions);
1892
-
1893
- if (!wasmModuleInstance) {
1894
- throw new Error('Failed to initialize WASM module');
1889
+ if (!factory) {
1890
+ throw new Error('WASM module does not export createFaceBetterModule function');
1895
1891
  }
1896
1892
 
1897
- wasmModuleInstance.ready = true;
1893
+ const urls = await resolveRuntimeAssetUrls(options);
1894
+ const { wasmBytes, resourceBytes } = await fetchRuntimeAssets({
1895
+ ...urls,
1896
+ onProgress: options.onProgress,
1897
+ });
1898
+ wasmModuleInstance = await instantiateRuntime(
1899
+ factory,
1900
+ wasmBytes,
1901
+ resourceBytes
1902
+ );
1898
1903
  return wasmModuleInstance;
1899
1904
  } catch (e) {
1900
1905
  wasmModulePromise = null;
1901
- throw new Error(`Failed to load WASM module: ${e.message}`);
1906
+ throw new FacebetterError(`Failed to load WASM module: ${e.message}`, 'WASM_LOAD_ERROR');
1902
1907
  }
1903
1908
  })();
1904
1909
 
@@ -2016,7 +2021,7 @@
2016
2021
 
2017
2022
  // Create platform API
2018
2023
  const platformAPI = {
2019
- loadWasmModule: () => loadWasmModule(),
2024
+ loadWasmModule: (options) => loadWasmModule(options),
2020
2025
  getWasmModule: getWasmModule,
2021
2026
  getWasmBuffer: getWasmBuffer,
2022
2027
  toImageData: toImageData,
@@ -2065,9 +2070,6 @@
2065
2070
  BeautyEffectEngine: BrowserBeautyEffectEngine,
2066
2071
  EngineConfig,
2067
2072
  FacebetterError,
2068
- FACEBETTER_AUTH_URL,
2069
- createDirectAuthFetcher,
2070
- requestFacebetterAuth,
2071
2073
  ...constants,
2072
2074
  loadWasmModule: loadWasmModule
2073
2075
  };
@@ -2087,7 +2089,6 @@
2087
2089
  exports.EyebrowStyle = EyebrowStyle;
2088
2090
  exports.EyelashColor = EyelashColor;
2089
2091
  exports.EyelashStyle = EyelashStyle;
2090
- exports.FACEBETTER_AUTH_URL = FACEBETTER_AUTH_URL;
2091
2092
  exports.FacebetterError = FacebetterError;
2092
2093
  exports.FrameType = FrameType;
2093
2094
  exports.LipstickColor = LipstickColor;
@@ -2097,10 +2098,8 @@
2097
2098
  exports.SmoothingStyle = SmoothingStyle;
2098
2099
  exports.WhiteningStyle = WhiteningStyle;
2099
2100
  exports.createBeautyEffectEngine = createBeautyEffectEngine;
2100
- exports.createDirectAuthFetcher = createDirectAuthFetcher;
2101
2101
  exports.default = index;
2102
2102
  exports.loadWasmModule = loadWasmModule;
2103
- exports.requestFacebetterAuth = requestFacebetterAuth;
2104
2103
 
2105
2104
  Object.defineProperty(exports, '__esModule', { value: true });
2106
2105