@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.
- package/android/build.gradle +16 -4
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +6 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +83 -30
- package/android/src/main/java/com/mentra/crust/jsc/MiniappNetworkPolicy.kt +25 -0
- package/android/src/main/java/com/mentra/crust/utils/CanonicalFilePath.kt +22 -0
- package/android/src/test/java/com/mentra/crust/jsc/FetchBridgeTest.kt +277 -0
- package/android/src/test/java/com/mentra/crust/jsc/MiniappNetworkPolicyTest.kt +45 -0
- package/android/src/test/java/com/mentra/crust/utils/CanonicalFilePathTest.kt +39 -0
- package/build/CrustModule.d.ts +6 -0
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +2 -0
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +4 -0
- package/build/CrustModule.web.js.map +1 -1
- package/ios/Crust.podspec +3 -0
- package/ios/CrustModule.swift +5 -0
- package/ios/MiniappPolicyKit/Package.swift +13 -0
- package/ios/MiniappPolicyKit/Sources/MiniappPolicy/CanonicalFilePath.swift +13 -0
- package/ios/MiniappPolicyKit/Sources/MiniappPolicy/MiniappNetworkPolicy.swift +24 -0
- package/ios/MiniappPolicyKit/Tests/MiniappPolicyTests/MiniappPolicyTests.swift +117 -0
- package/ios/MiniappPolicyKit/Tests/MiniappPolicyTests/RedirectServer.swift +33 -0
- package/ios/PermissionStatusReceipt.swift +72 -1
- package/ios/Resources/startup.js +199 -87
- package/ios/Source/JSCPolyfillBridge.swift +56 -14
- package/package.json +4 -2
- package/src/CrustModule.ts +6 -0
- package/src/CrustModule.web.ts +4 -0
package/ios/Resources/startup.js
CHANGED
|
@@ -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
|
-
|
|
588
|
+
let length = 0;
|
|
447
589
|
for (let i = 0; i < str.length; i++) {
|
|
448
|
-
|
|
449
|
-
if (code
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
600
|
+
out[at++] = code;
|
|
458
601
|
} else if (code < 2048) {
|
|
459
|
-
out
|
|
602
|
+
out[at++] = 192 | code >> 6;
|
|
603
|
+
out[at++] = 128 | code & 63;
|
|
460
604
|
} else if (code < 65536) {
|
|
461
|
-
out
|
|
605
|
+
out[at++] = 224 | code >> 12;
|
|
606
|
+
out[at++] = 128 | code >> 6 & 63;
|
|
607
|
+
out[at++] = 128 | code & 63;
|
|
462
608
|
} else {
|
|
463
|
-
out
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
"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.
|
|
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",
|
package/src/CrustModule.ts
CHANGED
|
@@ -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>
|
package/src/CrustModule.web.ts
CHANGED
|
@@ -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
|
}
|