@mentra/crust 3.3.0-dev.944 → 3.4.0-beta.1237

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/android/build.gradle +16 -4
  2. package/android/src/main/java/com/mentra/crust/CrustModule.kt +6 -0
  3. package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +83 -30
  4. package/android/src/main/java/com/mentra/crust/jsc/MiniappNetworkPolicy.kt +25 -0
  5. package/android/src/main/java/com/mentra/crust/utils/CanonicalFilePath.kt +22 -0
  6. package/android/src/test/java/com/mentra/crust/jsc/FetchBridgeTest.kt +277 -0
  7. package/android/src/test/java/com/mentra/crust/jsc/MiniappNetworkPolicyTest.kt +45 -0
  8. package/android/src/test/java/com/mentra/crust/utils/CanonicalFilePathTest.kt +39 -0
  9. package/build/CrustModule.d.ts +6 -0
  10. package/build/CrustModule.d.ts.map +1 -1
  11. package/build/CrustModule.js.map +1 -1
  12. package/build/CrustModule.web.d.ts +2 -0
  13. package/build/CrustModule.web.d.ts.map +1 -1
  14. package/build/CrustModule.web.js +4 -0
  15. package/build/CrustModule.web.js.map +1 -1
  16. package/ios/Crust.podspec +3 -0
  17. package/ios/CrustModule.swift +5 -0
  18. package/ios/MiniappPolicyKit/Package.swift +13 -0
  19. package/ios/MiniappPolicyKit/Sources/MiniappPolicy/CanonicalFilePath.swift +13 -0
  20. package/ios/MiniappPolicyKit/Sources/MiniappPolicy/MiniappNetworkPolicy.swift +24 -0
  21. package/ios/MiniappPolicyKit/Tests/MiniappPolicyTests/MiniappPolicyTests.swift +117 -0
  22. package/ios/MiniappPolicyKit/Tests/MiniappPolicyTests/RedirectServer.swift +33 -0
  23. package/ios/PermissionStatusReceipt.swift +72 -1
  24. package/ios/Resources/startup.js +199 -87
  25. package/ios/Source/JSCPolyfillBridge.swift +56 -14
  26. package/package.json +4 -2
  27. package/src/CrustModule.ts +6 -0
  28. package/src/CrustModule.web.ts +4 -0
@@ -436,39 +436,183 @@
436
436
  }
437
437
  ;
438
438
  g.crypto = cryptoNs;
439
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
440
+ const BASE64_VALUES = new Uint8Array(128).fill(255);
441
+ for (let i = 0; i < BASE64_ALPHABET.length; i++) BASE64_VALUES[BASE64_ALPHABET.charCodeAt(i)] = i;
442
+ const CHARS_PER_CHUNK = 8192;
443
+ function bytesOf(data) {
444
+ return data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
445
+ }
446
+ function bytesToBase64(bytes) {
447
+ const parts = [];
448
+ const chars = [];
449
+ for (let i = 0; i < bytes.length; i += 3) {
450
+ const b1 = bytes[i];
451
+ const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
452
+ const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
453
+ chars.push(
454
+ BASE64_ALPHABET.charCodeAt(b1 >> 2),
455
+ BASE64_ALPHABET.charCodeAt((b1 & 3) << 4 | b2 >> 4),
456
+ i + 1 < bytes.length ? BASE64_ALPHABET.charCodeAt((b2 & 15) << 2 | b3 >> 6) : 61,
457
+ i + 2 < bytes.length ? BASE64_ALPHABET.charCodeAt(b3 & 63) : 61
458
+ );
459
+ if (chars.length >= CHARS_PER_CHUNK) {
460
+ parts.push(String.fromCharCode.apply(null, chars));
461
+ chars.length = 0;
462
+ }
463
+ }
464
+ parts.push(String.fromCharCode.apply(null, chars));
465
+ return parts.join("");
466
+ }
467
+ function base64ToBytes(base64) {
468
+ const length = base64.length;
469
+ if (length % 4 !== 0) throw new TypeError("Invalid base64 from the native bridge");
470
+ const padding = length === 0 || base64.charCodeAt(length - 1) !== 61 ? 0 : base64.charCodeAt(length - 2) === 61 ? 2 : 1;
471
+ const out = new Uint8Array(length / 4 * 3 - padding);
472
+ const unpadded = padding === 0 ? length : length - 4;
473
+ let codes = 0;
474
+ let values = 0;
475
+ let o = 0;
476
+ for (let i = 0; i < unpadded; i += 4) {
477
+ const c1 = base64.charCodeAt(i);
478
+ const c2 = base64.charCodeAt(i + 1);
479
+ const c3 = base64.charCodeAt(i + 2);
480
+ const c4 = base64.charCodeAt(i + 3);
481
+ const v1 = BASE64_VALUES[c1 & 127];
482
+ const v2 = BASE64_VALUES[c2 & 127];
483
+ const v3 = BASE64_VALUES[c3 & 127];
484
+ const v4 = BASE64_VALUES[c4 & 127];
485
+ codes |= c1 | c2 | c3 | c4;
486
+ values |= v1 | v2 | v3 | v4;
487
+ out[o++] = v1 << 2 | v2 >> 4;
488
+ out[o++] = (v2 & 15) << 4 | v3 >> 2;
489
+ out[o++] = (v3 & 3) << 6 | v4;
490
+ }
491
+ if (padding > 0) {
492
+ const c1 = base64.charCodeAt(unpadded);
493
+ const c2 = base64.charCodeAt(unpadded + 1);
494
+ const c3 = padding === 1 ? base64.charCodeAt(unpadded + 2) : 65;
495
+ const v1 = BASE64_VALUES[c1 & 127];
496
+ const v2 = BASE64_VALUES[c2 & 127];
497
+ const v3 = BASE64_VALUES[c3 & 127];
498
+ codes |= c1 | c2 | c3;
499
+ values |= v1 | v2 | v3;
500
+ out[o++] = v1 << 2 | v2 >> 4;
501
+ if (padding === 1) out[o++] = (v2 & 15) << 4 | v3 >> 2;
502
+ }
503
+ if (codes > 127 || values > 63) throw new TypeError("Invalid base64 from the native bridge");
504
+ return out;
505
+ }
506
+ function decodeUtf8(bytes, fatal, ignoreBOM) {
507
+ const length = bytes.length;
508
+ const parts = [];
509
+ const units = [];
510
+ let i = !ignoreBOM && length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
511
+ while (i < length) {
512
+ const asciiStart = i;
513
+ while (i < length && bytes[i] < 128) i++;
514
+ if (i > asciiStart) {
515
+ if (units.length > 0) {
516
+ parts.push(String.fromCharCode.apply(null, units));
517
+ units.length = 0;
518
+ }
519
+ for (let start = asciiStart; start < i; start += CHARS_PER_CHUNK) {
520
+ const chunk = bytes.subarray(start, Math.min(i, start + CHARS_PER_CHUNK));
521
+ parts.push(String.fromCharCode.apply(null, chunk));
522
+ }
523
+ if (i >= length) break;
524
+ }
525
+ const lead = bytes[i++];
526
+ let codePoint;
527
+ let needed;
528
+ let lower = 128;
529
+ let upper = 191;
530
+ if (lead >= 194 && lead <= 223) {
531
+ needed = 1;
532
+ codePoint = lead & 31;
533
+ } else if (lead >= 224 && lead <= 239) {
534
+ if (lead === 224) lower = 160;
535
+ if (lead === 237) upper = 159;
536
+ needed = 2;
537
+ codePoint = lead & 15;
538
+ } else if (lead >= 240 && lead <= 244) {
539
+ if (lead === 240) lower = 144;
540
+ if (lead === 244) upper = 143;
541
+ needed = 3;
542
+ codePoint = lead & 7;
543
+ } else {
544
+ needed = -1;
545
+ codePoint = 0;
546
+ }
547
+ while (needed > 0) {
548
+ const next = i < length ? bytes[i] : -1;
549
+ if (next < lower || next > upper) break;
550
+ codePoint = codePoint << 6 | next & 63;
551
+ lower = 128;
552
+ upper = 191;
553
+ needed--;
554
+ i++;
555
+ }
556
+ if (needed !== 0) {
557
+ if (fatal) throw new TypeError("The encoded data was not valid utf-8");
558
+ units.push(65533);
559
+ } else if (codePoint > 65535) {
560
+ codePoint -= 65536;
561
+ units.push(55296 + (codePoint >> 10), 56320 + (codePoint & 1023));
562
+ } else {
563
+ units.push(codePoint);
564
+ }
565
+ if (units.length >= CHARS_PER_CHUNK) {
566
+ parts.push(String.fromCharCode.apply(null, units));
567
+ units.length = 0;
568
+ }
569
+ }
570
+ if (units.length > 0) parts.push(String.fromCharCode.apply(null, units));
571
+ return parts.join("");
572
+ }
439
573
  if (typeof g.TextEncoder !== "function") {
574
+ const codePointAt = (str, i) => {
575
+ const code = str.charCodeAt(i);
576
+ if (code >= 55296 && code <= 56319 && i + 1 < str.length) {
577
+ const next = str.charCodeAt(i + 1);
578
+ if (next >= 56320 && next <= 57343) return (code - 55296 << 10) + (next - 56320) + 65536;
579
+ }
580
+ return code;
581
+ };
440
582
  class TextEncoderPolyfill {
441
583
  constructor() {
442
584
  this.encoding = "utf-8";
443
585
  }
444
586
  encode(input) {
445
587
  const str = input ?? "";
446
- const out = [];
588
+ let length = 0;
447
589
  for (let i = 0; i < str.length; i++) {
448
- let code = str.charCodeAt(i);
449
- if (code >= 55296 && code <= 56319 && i + 1 < str.length) {
450
- const next = str.charCodeAt(i + 1);
451
- if (next >= 56320 && next <= 57343) {
452
- code = (code - 55296 << 10) + (next - 56320) + 65536;
453
- i++;
454
- }
455
- }
590
+ const code = codePointAt(str, i);
591
+ if (code > 65535) i++;
592
+ length += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
593
+ }
594
+ const out = new Uint8Array(length);
595
+ let at = 0;
596
+ for (let i = 0; i < str.length; i++) {
597
+ const code = codePointAt(str, i);
598
+ if (code > 65535) i++;
456
599
  if (code < 128) {
457
- out.push(code);
600
+ out[at++] = code;
458
601
  } else if (code < 2048) {
459
- out.push(192 | code >> 6, 128 | code & 63);
602
+ out[at++] = 192 | code >> 6;
603
+ out[at++] = 128 | code & 63;
460
604
  } else if (code < 65536) {
461
- out.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
605
+ out[at++] = 224 | code >> 12;
606
+ out[at++] = 128 | code >> 6 & 63;
607
+ out[at++] = 128 | code & 63;
462
608
  } else {
463
- out.push(
464
- 240 | code >> 18,
465
- 128 | code >> 12 & 63,
466
- 128 | code >> 6 & 63,
467
- 128 | code & 63
468
- );
609
+ out[at++] = 240 | code >> 18;
610
+ out[at++] = 128 | code >> 12 & 63;
611
+ out[at++] = 128 | code >> 6 & 63;
612
+ out[at++] = 128 | code & 63;
469
613
  }
470
614
  }
471
- return new Uint8Array(out);
615
+ return out;
472
616
  }
473
617
  }
474
618
  ;
@@ -476,36 +620,14 @@
476
620
  }
477
621
  if (typeof g.TextDecoder !== "function") {
478
622
  class TextDecoderPolyfill {
479
- constructor(encoding = "utf-8") {
623
+ constructor(encoding = "utf-8", options) {
480
624
  this.encoding = encoding;
625
+ this.fatal = options?.fatal === true;
626
+ this.ignoreBOM = options?.ignoreBOM === true;
481
627
  }
482
628
  decode(buffer) {
483
629
  if (!buffer) return "";
484
- const bytes = buffer instanceof Uint8Array ? buffer : buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
485
- let out = "";
486
- let i = 0;
487
- while (i < bytes.length) {
488
- const b1 = bytes[i++];
489
- let code;
490
- if (b1 < 128) {
491
- code = b1;
492
- } else if (b1 < 192) {
493
- code = 65533;
494
- } else if (b1 < 224) {
495
- code = (b1 & 31) << 6 | bytes[i++] & 63;
496
- } else if (b1 < 240) {
497
- code = (b1 & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63;
498
- } else {
499
- code = (b1 & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63;
500
- }
501
- if (code > 65535) {
502
- code -= 65536;
503
- out += String.fromCharCode(55296 + (code >> 10), 56320 + (code & 1023));
504
- } else {
505
- out += String.fromCharCode(code);
506
- }
507
- }
508
- return out;
630
+ return decodeUtf8(bytesOf(buffer), this.fatal, this.ignoreBOM);
509
631
  }
510
632
  }
511
633
  ;
@@ -546,15 +668,28 @@
546
668
  return out;
547
669
  };
548
670
  }
671
+ const urlScheme = (url) => {
672
+ const match = /^[\u0000- ]*([A-Za-z][A-Za-z0-9+.-]*):/.exec(url);
673
+ return match ? match[1].toLowerCase() : null;
674
+ };
549
675
  if (typeof g.fetch !== "function") {
550
676
  ;
551
677
  g.fetch = async (input, init) => {
552
678
  const url = typeof input === "string" ? input : input.toString();
679
+ const scheme = urlScheme(url);
680
+ if (scheme !== "http" && scheme !== "https") {
681
+ throw new TypeError(
682
+ `fetch: only http: and https: URLs can be fetched, not ${scheme ? `${scheme}:` : "a relative URL"}`
683
+ );
684
+ }
553
685
  const method = (init?.method ?? "GET").toUpperCase();
554
686
  let bodyString = null;
687
+ let bodyBase64;
555
688
  const reqBody = init?.body;
556
689
  if (typeof reqBody === "string") {
557
690
  bodyString = reqBody;
691
+ } else if (reqBody instanceof ArrayBuffer || ArrayBuffer.isView(reqBody)) {
692
+ bodyBase64 = bytesToBase64(bytesOf(reqBody));
558
693
  } else if (reqBody && typeof reqBody.toString === "function") {
559
694
  bodyString = reqBody.toString();
560
695
  }
@@ -578,9 +713,10 @@
578
713
  }
579
714
  const sendRequest = g.__mentraSendRequest;
580
715
  const result = await sendRequest("fetch", "request", [
581
- { url, method, headers, body: bodyString }
716
+ bodyBase64 === void 0 ? { url, method, headers, body: bodyString } : { url, method, headers, bodyBase64 }
582
717
  ]);
583
- const bodyText = typeof result.body === "string" ? result.body : "";
718
+ const responseText = typeof result.body === "string" ? result.body : null;
719
+ const responseBase64 = typeof result.bodyBase64 === "string" ? result.bodyBase64 : "";
584
720
  const responseHeaders = new Map(Object.entries(result.headers ?? {}));
585
721
  class ResponseLike {
586
722
  constructor() {
@@ -597,11 +733,13 @@
597
733
  };
598
734
  }
599
735
  async text() {
600
- return bodyText;
736
+ if (responseText === null) return decodeUtf8(base64ToBytes(responseBase64), false, false);
737
+ return responseText.charCodeAt(0) === 65279 ? responseText.slice(1) : responseText;
601
738
  }
602
739
  async json() {
740
+ const text = await this.text();
603
741
  try {
604
- return JSON.parse(bodyText);
742
+ return JSON.parse(text);
605
743
  } catch (e) {
606
744
  const err = new Error(
607
745
  `Failed to parse JSON response from ${url}: ${e instanceof Error ? e.message : String(e)}`
@@ -611,52 +749,21 @@
611
749
  }
612
750
  }
613
751
  async arrayBuffer() {
752
+ if (responseText === null) return base64ToBytes(responseBase64).buffer;
614
753
  const enc = new g.TextEncoder();
615
- return enc.encode(bodyText).buffer;
754
+ return enc.encode(responseText).buffer;
616
755
  }
617
756
  }
618
757
  return new ResponseLike();
619
758
  };
620
759
  }
621
760
  if (typeof g.WebSocket !== "function") {
622
- let bytesToBase642 = function(bytes) {
623
- const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
624
- let out = "";
625
- let i = 0;
626
- while (i < bytes.length) {
627
- const c1 = bytes[i++];
628
- const c2 = i < bytes.length ? bytes[i++] : NaN;
629
- const c3 = i < bytes.length ? bytes[i++] : NaN;
630
- const e1 = c1 >> 2;
631
- const e2 = (c1 & 3) << 4 | (Number.isNaN(c2) ? 0 : c2 >> 4);
632
- const e3 = Number.isNaN(c2) ? 64 : (c2 & 15) << 2 | (Number.isNaN(c3) ? 0 : c3 >> 6);
633
- const e4 = Number.isNaN(c3) ? 64 : c3 & 63;
634
- out += b64[e1] + b64[e2] + (e3 === 64 ? "=" : b64[e3]) + (e4 === 64 ? "=" : b64[e4]);
635
- }
636
- return out;
637
- }, base64ToBytes2 = function(s) {
638
- const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
639
- const clean = s.replace(/=+$/, "");
640
- const out = [];
641
- let buf = 0;
642
- let bits = 0;
643
- for (let i = 0; i < clean.length; i++) {
644
- const idx = b64.indexOf(clean[i]);
645
- if (idx < 0) continue;
646
- buf = buf << 6 | idx;
647
- bits += 6;
648
- if (bits >= 8) {
649
- bits -= 8;
650
- out.push(buf >> bits & 255);
651
- }
652
- }
653
- return new Uint8Array(out);
654
- }, queueMicrotaskSafe2 = function(fn) {
761
+ let queueMicrotaskSafe2 = function(fn) {
655
762
  const q = g.queueMicrotask;
656
763
  if (typeof q === "function") q(fn);
657
764
  else g.Promise.resolve().then(fn);
658
765
  };
659
- var bytesToBase64 = bytesToBase642, base64ToBytes = base64ToBytes2, queueMicrotaskSafe = queueMicrotaskSafe2;
766
+ var queueMicrotaskSafe = queueMicrotaskSafe2;
660
767
  const sockets = /* @__PURE__ */ new Map();
661
768
  g.__mentraDeliverWebSocketEvent = (sid, type, payload) => {
662
769
  const sock = sockets.get(sid);
@@ -685,6 +792,12 @@
685
792
  close: /* @__PURE__ */ new Set()
686
793
  };
687
794
  this.url = String(url);
795
+ const scheme = urlScheme(this.url);
796
+ if (scheme !== "ws" && scheme !== "wss" && scheme !== "http" && scheme !== "https") {
797
+ throw new SyntaxError(
798
+ `WebSocket: the URL's scheme must be ws:, wss:, http: or https:, not ${scheme ? `${scheme}:` : "none"}`
799
+ );
800
+ }
688
801
  const protoList = Array.isArray(protocols) ? protocols : protocols ? [protocols] : [];
689
802
  this.sid = `ws-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
690
803
  sockets.set(this.sid, this);
@@ -718,8 +831,7 @@
718
831
  payload = data;
719
832
  } else {
720
833
  kind = "binary";
721
- const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
722
- payload = bytesToBase642(bytes);
834
+ payload = bytesToBase64(bytesOf(data));
723
835
  }
724
836
  try {
725
837
  __dispatch("ws", "send", JSON.stringify([{ sid: this.sid, kind, payload }]));
@@ -752,7 +864,7 @@
752
864
  }
753
865
  const synth = { type, target: this, ...ev };
754
866
  if (type === "message" && ev.kind === "binary" && typeof ev.data === "string") {
755
- synth.data = base64ToBytes2(ev.data).buffer;
867
+ synth.data = base64ToBytes(ev.data).buffer;
756
868
  } else if (type === "message" && ev.kind === "text") {
757
869
  synth.data = ev.data;
758
870
  }
@@ -37,6 +37,22 @@ public enum JSCPolyfillBridge {
37
37
  fileprivate final class WebSocketSessionDelegate: NSObject, URLSessionWebSocketDelegate {
38
38
  static let shared = WebSocketSessionDelegate()
39
39
 
40
+ /// A redirect is followed only to another network URL, never to a local scheme.
41
+ /// Completion-handler data tasks still ask the delegate about redirects.
42
+ func urlSession(
43
+ _ session: URLSession,
44
+ task: URLSessionTask,
45
+ willPerformHTTPRedirection response: HTTPURLResponse,
46
+ newRequest request: URLRequest,
47
+ completionHandler: @escaping (URLRequest?) -> Void,
48
+ ) {
49
+ guard let url = request.url, MiniappNetworkPolicy.allowsFetch(url) else {
50
+ completionHandler(nil)
51
+ return
52
+ }
53
+ completionHandler(request)
54
+ }
55
+
40
56
  func urlSession(
41
57
  _ session: URLSession,
42
58
  webSocketTask: URLSessionWebSocketTask,
@@ -98,16 +114,32 @@ public enum JSCPolyfillBridge {
98
114
  guard let reqId else {
99
115
  return .error(code: "INVALID_ARGS", message: "fetch.request requires reqId (use __mentraSendRequest)")
100
116
  }
117
+ // URLSession reads file: URLs. Miniapp code can call this route without the
118
+ // polyfill's own check, so the scheme is checked here (MiniappNetworkPolicy).
119
+ guard MiniappNetworkPolicy.allowsFetch(url) else {
120
+ return .error(code: "UNSUPPORTED_SCHEME", message: MiniappNetworkPolicy.fetchRefusal)
121
+ }
101
122
 
102
123
  var urlRequest = URLRequest(url: url)
103
- urlRequest.httpMethod = (req["method"] as? String) ?? "GET"
124
+ let method = (req["method"] as? String) ?? "GET"
125
+ urlRequest.httpMethod = method
104
126
  if let headers = req["headers"] as? [String: Any] {
105
127
  for (k, v) in headers {
106
128
  urlRequest.setValue(String(describing: v), forHTTPHeaderField: k)
107
129
  }
108
130
  }
109
- if let body = req["body"] as? String, !body.isEmpty {
110
- urlRequest.httpBody = body.data(using: .utf8)
131
+ // ArrayBuffer and typed-array bodies arrive as base64 of their bytes.
132
+ // As on Android, an empty body is still sent, and POST, PUT and PATCH
133
+ // always carry one, so the server sees Content-Length: 0.
134
+ if let encoded = req["bodyBase64"] as? String {
135
+ guard let data = Data(base64Encoded: encoded) else {
136
+ return .error(code: "INVALID_ARGS", message: "fetch.request: bodyBase64 is not base64")
137
+ }
138
+ urlRequest.httpBody = data
139
+ } else if let body = req["body"] as? String {
140
+ urlRequest.httpBody = Data(body.utf8)
141
+ } else if ["POST", "PUT", "PATCH"].contains(method.uppercased()) {
142
+ urlRequest.httpBody = Data()
111
143
  }
112
144
 
113
145
  // Reasonable defaults — caller can override later by passing
@@ -141,21 +173,27 @@ public enum JSCPolyfillBridge {
141
173
  }
142
174
  return out
143
175
  }()
144
- let bodyStr: String = {
145
- guard let data else { return "" }
146
- return String(data: data, encoding: .utf8) ?? ""
147
- }()
176
+ var result: [String: Any] = [
177
+ "status": status,
178
+ "statusText": HTTPURLResponse.localizedString(forStatusCode: status),
179
+ "headers": headersDict,
180
+ "ok": (200..<300).contains(status),
181
+ ]
182
+ // The string `body` only when the bytes are exactly its UTF-8
183
+ // encoding; otherwise every byte as base64. The decode fails
184
+ // on invalid UTF-8, and the round trip also catches a dropped
185
+ // byte order mark.
186
+ let body = data ?? Data()
187
+ if let text = String(data: body, encoding: .utf8), Data(text.utf8) == body {
188
+ result["body"] = text
189
+ } else {
190
+ result["bodyBase64"] = body.base64EncodedString()
191
+ }
148
192
  let payload: [String: Any] = [
149
193
  "kind": "response",
150
194
  "reqId": reqId,
151
195
  "ok": true,
152
- "result": [
153
- "status": status,
154
- "statusText": HTTPURLResponse.localizedString(forStatusCode: status),
155
- "headers": headersDict,
156
- "body": bodyStr,
157
- "ok": (200..<300).contains(status),
158
- ] as [String: Any],
196
+ "result": result,
159
197
  ]
160
198
  JSCRuntime.shared.dispatchToJs(packageName: packageName, envelope: payload)
161
199
  }
@@ -256,6 +294,10 @@ public enum JSCPolyfillBridge {
256
294
  let url = URL(string: urlString) else {
257
295
  return .error(code: "INVALID_ARGS", message: "ws.open expects {sid, url, protocols?}")
258
296
  }
297
+ // A WebSocket task also needs a network scheme; any other raises an exception.
298
+ guard MiniappNetworkPolicy.allowsWebSocket(url) else {
299
+ return .error(code: "UNSUPPORTED_SCHEME", message: MiniappNetworkPolicy.webSocketRefusal)
300
+ }
259
301
  var urlRequest = URLRequest(url: url)
260
302
  if let protocols = req["protocols"] as? [String], !protocols.isEmpty {
261
303
  urlRequest.setValue(protocols.joined(separator: ", "), forHTTPHeaderField: "Sec-WebSocket-Protocol")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/crust",
3
- "version": "3.3.0-dev.944",
3
+ "version": "3.4.0-beta.1237",
4
4
  "description": "Mentra Native Module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  "license": "Apache-2.0",
36
36
  "homepage": "https://github.com/Mentra-Community/MentraOS/tree/dev/mobile/modules/crust#readme",
37
37
  "dependencies": {
38
- "@mentra/jspolyfill": "3.3.0-dev.944"
38
+ "@mentra/jspolyfill": "3.4.0-beta.1237"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@expo/config-plugins": ">=8.0.0",
@@ -63,6 +63,8 @@
63
63
  "!ios/build/**",
64
64
  "!ios/Pods",
65
65
  "!ios/Pods/**",
66
+ "!ios/MiniappPolicyKit/.build",
67
+ "!ios/MiniappPolicyKit/.build/**",
66
68
  "app.plugin.js",
67
69
  "build",
68
70
  "plugin/build",
@@ -11,6 +11,12 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
11
11
  getPermissionStatus(requestId: string): Promise<AppPermissionReceipt>
12
12
  PI: number
13
13
  hello(): string
14
+ /**
15
+ * The canonical path of an existing file or directory: absolute, with `.`,
16
+ * `..` and every symlink resolved. Null when nothing is there. The engine
17
+ * checks that a path a miniapp names stays inside the directory it may use.
18
+ */
19
+ canonicalFilePath(path: string): string | null
14
20
  setValueAsync(value: string): Promise<void>
15
21
  /** Android: copy the displayed view's window pixels to a temporary JPEG URI. */
16
22
  captureMiniappPreview(viewTag: number): Promise<string>
@@ -27,6 +27,10 @@ class CrustModule extends NativeModule<CrustModuleEvents> {
27
27
  this.httpRequests.get(requestId)?.abort()
28
28
  this.httpRequests.delete(requestId)
29
29
  }
30
+ /** The web has no local files to resolve. */
31
+ canonicalFilePath(_path: string): string | null {
32
+ return null
33
+ }
30
34
  hello() {
31
35
  return "Hello world! 👋"
32
36
  }