@mentra/crust 3.4.0-dev.998 → 3.5.0-dev.1238
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 +7 -4
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +6 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +16 -2
- 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 +78 -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/Resources/startup.js +45 -19
- package/ios/Source/JSCPolyfillBridge.swift +25 -0
- package/package.json +4 -2
- package/src/CrustModule.ts +6 -0
- package/src/CrustModule.web.ts +4 -0
package/android/build.gradle
CHANGED
|
@@ -100,13 +100,16 @@ dependencies {
|
|
|
100
100
|
// APK weight). `:navigation` is the core engine; `:tripdata` carries the
|
|
101
101
|
// banner/maneuver formatting used to label turns.
|
|
102
102
|
// Version pinned to 3.25.0 — re-check https://docs.mapbox.com/android/navigation/releases/
|
|
103
|
+
// The -ndk27 artifacts are Mapbox's 16 KB page-aligned builds. The
|
|
104
|
+
// unsuffixed 3.25.0 artifacts still ship 4 KB-aligned native libraries,
|
|
105
|
+
// which Google Play rejects for 64-bit updates after Feb 1, 2027.
|
|
103
106
|
def isChinaBuild = System.getenv("EXPO_PUBLIC_DEPLOYMENT_REGION") == "china"
|
|
104
107
|
if (isChinaBuild) {
|
|
105
|
-
compileOnly 'com.mapbox.navigationcore:navigation:3.25.0'
|
|
106
|
-
compileOnly 'com.mapbox.navigationcore:tripdata:3.25.0'
|
|
108
|
+
compileOnly 'com.mapbox.navigationcore:navigation-ndk27:3.25.0'
|
|
109
|
+
compileOnly 'com.mapbox.navigationcore:tripdata-ndk27:3.25.0'
|
|
107
110
|
} else {
|
|
108
|
-
implementation 'com.mapbox.navigationcore:navigation:3.25.0'
|
|
109
|
-
implementation 'com.mapbox.navigationcore:tripdata:3.25.0'
|
|
111
|
+
implementation 'com.mapbox.navigationcore:navigation-ndk27:3.25.0'
|
|
112
|
+
implementation 'com.mapbox.navigationcore:tripdata-ndk27:3.25.0'
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
// Required for Java 8+ APIs on minSdk < 26 (was needed by Google Nav SDK;
|
|
@@ -24,6 +24,7 @@ import com.mentra.crust.jsc.InstalledMiniappManifest
|
|
|
24
24
|
import com.mentra.crust.jsc.JSCPolyfillBridge
|
|
25
25
|
import com.mentra.crust.http.NativeHttpRequests
|
|
26
26
|
import com.mentra.crust.http.nativeHttpFailureCode
|
|
27
|
+
import com.mentra.crust.utils.CanonicalFilePath
|
|
27
28
|
|
|
28
29
|
class CrustModule : Module() {
|
|
29
30
|
companion object {
|
|
@@ -185,6 +186,11 @@ class CrustModule : Module() {
|
|
|
185
186
|
sendEvent("onChange", mapOf("value" to value))
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// Where a path really is, symlinks resolved; the engine checks miniapp-named paths with it.
|
|
190
|
+
Function("canonicalFilePath") { path: String ->
|
|
191
|
+
CanonicalFilePath.of(path)
|
|
192
|
+
}
|
|
193
|
+
|
|
188
194
|
AsyncFunction("captureMiniappPreview") Coroutine { viewTag: Int ->
|
|
189
195
|
val activity = appContext.currentActivity ?: error("No activity available for screenshot")
|
|
190
196
|
PixelCopyPreview.capture(activity, viewTag)
|
|
@@ -138,8 +138,15 @@ object JSCPolyfillBridge {
|
|
|
138
138
|
?: return@register JSCDispatchOutcome.Error("INVALID_ARGS", "ws.open: missing sid")
|
|
139
139
|
val url = req["url"] as? String
|
|
140
140
|
?: return@register JSCDispatchOutcome.Error("INVALID_ARGS", "ws.open: missing url")
|
|
141
|
+
if (!MiniappNetworkPolicy.allowsWebSocket(url)) {
|
|
142
|
+
return@register JSCDispatchOutcome.Error("UNSUPPORTED_SCHEME", MiniappNetworkPolicy.WEB_SOCKET_REFUSAL)
|
|
143
|
+
}
|
|
141
144
|
val protocols = (req["protocols"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList()
|
|
142
|
-
val builder =
|
|
145
|
+
val builder = try {
|
|
146
|
+
Request.Builder().url(url)
|
|
147
|
+
} catch (e: IllegalArgumentException) {
|
|
148
|
+
return@register JSCDispatchOutcome.Error("INVALID_ARGS", "ws.open: ${e.message}")
|
|
149
|
+
}
|
|
143
150
|
if (protocols.isNotEmpty()) {
|
|
144
151
|
builder.header("Sec-WebSocket-Protocol", protocols.joinToString(", "))
|
|
145
152
|
}
|
|
@@ -247,6 +254,9 @@ object JSCPolyfillBridge {
|
|
|
247
254
|
if (reqId == null) {
|
|
248
255
|
return@register JSCDispatchOutcome.Error("INVALID_ARGS", "fetch.request requires reqId (use __mentraSendRequest)")
|
|
249
256
|
}
|
|
257
|
+
if (!MiniappNetworkPolicy.allowsFetch(url)) {
|
|
258
|
+
return@register JSCDispatchOutcome.Error("UNSUPPORTED_SCHEME", MiniappNetworkPolicy.FETCH_REFUSAL)
|
|
259
|
+
}
|
|
250
260
|
|
|
251
261
|
val method = (req["method"] as? String) ?: "GET"
|
|
252
262
|
val headers = (req["headers"] as? Map<*, *>)?.mapNotNull { (k, v) ->
|
|
@@ -260,7 +270,11 @@ object JSCPolyfillBridge {
|
|
|
260
270
|
?: return@register JSCDispatchOutcome.Error("INVALID_ARGS", "fetch.request: bodyBase64 is not base64")
|
|
261
271
|
}
|
|
262
272
|
|
|
263
|
-
val request =
|
|
273
|
+
val request = try {
|
|
274
|
+
buildHttpRequest(method, url, headers, bodyString, bodyBytes)
|
|
275
|
+
} catch (e: IllegalArgumentException) {
|
|
276
|
+
return@register JSCDispatchOutcome.Error("INVALID_ARGS", "fetch.request: ${e.message}")
|
|
277
|
+
}
|
|
264
278
|
|
|
265
279
|
httpClient.newCall(request).enqueue(object : Callback {
|
|
266
280
|
override fun onFailure(call: Call, e: IOException) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which URLs miniapp code may open through the polyfill's network bridge. Miniapp code is
|
|
5
|
+
* untrusted, so `fetch` and `WebSocket` reach only the network: never a `file:` URL (the
|
|
6
|
+
* Mentra App's own files), a `content:` URI or any other local scheme. Mirrors iOS
|
|
7
|
+
* `MiniappNetworkPolicy.swift` and the polyfill's own check, which miniapp code can skip by
|
|
8
|
+
* calling the bridge directly.
|
|
9
|
+
*/
|
|
10
|
+
object MiniappNetworkPolicy {
|
|
11
|
+
/** Leading C0 controls and spaces are skipped, as OkHttp's URL parser does. */
|
|
12
|
+
private val SCHEME = Regex("^[\\u0000-\\u0020]*([A-Za-z][A-Za-z0-9+.-]*):")
|
|
13
|
+
private val FETCH_SCHEMES = setOf("http", "https")
|
|
14
|
+
private val WEB_SOCKET_SCHEMES = setOf("ws", "wss", "http", "https")
|
|
15
|
+
|
|
16
|
+
const val FETCH_REFUSAL = "fetch: only http: and https: URLs are allowed"
|
|
17
|
+
const val WEB_SOCKET_REFUSAL = "ws.open: only ws:, wss:, http: and https: URLs are allowed"
|
|
18
|
+
|
|
19
|
+
/** The URL's scheme, lowercased, or null for a URL without one. */
|
|
20
|
+
fun scheme(url: String): String? = SCHEME.find(url)?.groupValues?.get(1)?.lowercase()
|
|
21
|
+
|
|
22
|
+
fun allowsFetch(url: String): Boolean = scheme(url) in FETCH_SCHEMES
|
|
23
|
+
|
|
24
|
+
fun allowsWebSocket(url: String): Boolean = scheme(url) in WEB_SOCKET_SCHEMES
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.mentra.crust.utils
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.IOException
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The real location of a path, for checking that a path a miniapp names stays inside the
|
|
8
|
+
* directory the host allows: absolute, with `.`, `..` and every symlink resolved.
|
|
9
|
+
*/
|
|
10
|
+
object CanonicalFilePath {
|
|
11
|
+
/** The canonical path of an existing file or directory, or null when there is none. */
|
|
12
|
+
fun of(path: String): String? {
|
|
13
|
+
if (!path.startsWith("/") || path.contains('\u0000')) return null
|
|
14
|
+
return try {
|
|
15
|
+
File(path).canonicalFile.takeIf { it.exists() }?.path
|
|
16
|
+
} catch (e: IOException) {
|
|
17
|
+
null
|
|
18
|
+
} catch (e: SecurityException) {
|
|
19
|
+
null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -39,6 +39,7 @@ class FetchBridgeTest {
|
|
|
39
39
|
private val results = LinkedBlockingQueue<Map<*, *>>()
|
|
40
40
|
private lateinit var server: MockWebServer
|
|
41
41
|
private lateinit var runtime: JSCRuntime
|
|
42
|
+
@Volatile private var redirectTarget = "file:///etc/hosts"
|
|
42
43
|
|
|
43
44
|
/** A 24 kHz 16-bit sine followed by every byte value: mostly not valid UTF-8. */
|
|
44
45
|
private val pcm = ByteArray(4800 * 2 + 256).also { out ->
|
|
@@ -54,6 +55,10 @@ class FetchBridgeTest {
|
|
|
54
55
|
private val utf8Bytes = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) +
|
|
55
56
|
"{\"a\":\"é😀\"}".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00, 0x7F)
|
|
56
57
|
|
|
58
|
+
/** Two MB of valid UTF-8 text: arrayBuffer() re-encodes it inside the 32 MB miniapp heap. */
|
|
59
|
+
private val largeUtf8Text: ByteArray =
|
|
60
|
+
"héllo 😀 中文 chunk of reply text ".repeat(52_000).toByteArray(Charsets.UTF_8)
|
|
61
|
+
|
|
57
62
|
// crust's build.gradle ships this file as the runtime's startup.js asset.
|
|
58
63
|
private val polyfill: String by lazy { File("../../jspolyfill/assets/startup.js").readText() }
|
|
59
64
|
|
|
@@ -63,8 +68,10 @@ class FetchBridgeTest {
|
|
|
63
68
|
override fun dispatch(request: RecordedRequest): MockResponse = when (request.path) {
|
|
64
69
|
"/reply.pcm" -> response("application/octet-stream").setBody(Buffer().write(pcm))
|
|
65
70
|
"/utf8.bin" -> response("application/octet-stream").setBody(Buffer().write(utf8Bytes))
|
|
71
|
+
"/large.txt" -> response("text/plain").setBody(Buffer().write(largeUtf8Text))
|
|
66
72
|
"/data.json" -> response("application/json").setBody("{\"message\":\"héllo 😀 中文\"}")
|
|
67
73
|
"/upload" -> response("text/plain").setBody("ok")
|
|
74
|
+
"/to-file" -> MockResponse().setResponseCode(302).setHeader("Location", redirectTarget)
|
|
68
75
|
else -> MockResponse().setResponseCode(404)
|
|
69
76
|
}
|
|
70
77
|
}
|
|
@@ -108,6 +115,21 @@ class FetchBridgeTest {
|
|
|
108
115
|
assertArrayEquals(utf8Bytes, bytes(result["bytes"]))
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
@Test fun `arrayBuffer re-encodes a large UTF-8 body within the miniapp heap`() {
|
|
119
|
+
val result = runMiniapp(
|
|
120
|
+
"""
|
|
121
|
+
const bytes = new Uint8Array(await (await fetch(BASE + "/large.txt")).arrayBuffer())
|
|
122
|
+
let sum = 0
|
|
123
|
+
for (let i = 0; i < bytes.length; i++) sum = (sum * 31 + bytes[i]) >>> 0
|
|
124
|
+
return {length: bytes.length, sum}
|
|
125
|
+
""",
|
|
126
|
+
)
|
|
127
|
+
var sum = 0L
|
|
128
|
+
for (byte in largeUtf8Text) sum = (sum * 31 + (byte.toInt() and 0xFF)) and 0xFFFFFFFFL
|
|
129
|
+
assertEquals(largeUtf8Text.size, (result["length"] as Number).toInt())
|
|
130
|
+
assertEquals(sum, (result["sum"] as Number).toLong())
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
@Test fun `responses are sent as text only when that text re-encodes to the same bytes`() {
|
|
112
134
|
val text = JSCPolyfillBridge.fetchResult(responseWith(utf8Bytes))
|
|
113
135
|
assertEquals(String(utf8Bytes, Charsets.UTF_8), text.getString("body"))
|
|
@@ -169,6 +191,62 @@ class FetchBridgeTest {
|
|
|
169
191
|
assertEquals("application/json; charset=utf-8", upload.getHeader("Content-Type"))
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Miniapp code is untrusted and can call the bridge without the polyfill's own check, so
|
|
196
|
+
* native must refuse a local URL itself. A file: URL would read the Mentra App's files.
|
|
197
|
+
*/
|
|
198
|
+
@Test fun `native fetch and WebSocket refuse local URLs even when the polyfill is skipped`() {
|
|
199
|
+
val secret = File.createTempFile("mentra-credentials", ".txt").apply {
|
|
200
|
+
writeText("refresh-token-that-must-not-leak")
|
|
201
|
+
deleteOnExit()
|
|
202
|
+
}
|
|
203
|
+
val urls = listOf(secret.toURI().toString(), "FILE://${secret.absolutePath}", "content://com.mentra.mentra.fileprovider/x", "data:text/plain,hi")
|
|
204
|
+
val result = runMiniapp(
|
|
205
|
+
"""
|
|
206
|
+
const outcomes = []
|
|
207
|
+
for (const url of ${org.json.JSONArray(urls)}) {
|
|
208
|
+
outcomes.push(await __mentraSendRequest("fetch", "request", [{url, method: "GET", headers: {}}]).then(
|
|
209
|
+
(r) => ({read: String(r && (r.body || r.bodyBase64))}),
|
|
210
|
+
(e) => ({error: String(e && (e.message || e.code))}),
|
|
211
|
+
))
|
|
212
|
+
let opened = false
|
|
213
|
+
try {
|
|
214
|
+
__dispatch("ws", "open", JSON.stringify([{sid: "s-" + outcomes.length, url, protocols: []}]))
|
|
215
|
+
opened = true
|
|
216
|
+
} catch (e) {
|
|
217
|
+
outcomes.push({error: String(e && e.message)})
|
|
218
|
+
}
|
|
219
|
+
if (opened) outcomes.push({read: "ws opened"})
|
|
220
|
+
}
|
|
221
|
+
return {outcomes}
|
|
222
|
+
""",
|
|
223
|
+
)
|
|
224
|
+
val outcomes = result["outcomes"] as List<*>
|
|
225
|
+
assertEquals(urls.size * 2, outcomes.size)
|
|
226
|
+
for (outcome in outcomes) {
|
|
227
|
+
val map = outcome as Map<*, *>
|
|
228
|
+
assertNull("a local URL was read: $map", map["read"])
|
|
229
|
+
assertTrue("unexpected refusal: $map", (map["error"] as String).contains("URLs are allowed"))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** OkHttp follows a redirect only to another http(s) URL, so a 302 to a file: URL reads nothing. */
|
|
234
|
+
@Test fun `a redirect to a file URL is not followed`() {
|
|
235
|
+
val secret = File.createTempFile("mentra-credentials", ".txt").apply {
|
|
236
|
+
writeText("refresh-token-that-must-not-leak")
|
|
237
|
+
deleteOnExit()
|
|
238
|
+
}
|
|
239
|
+
redirectTarget = secret.toURI().toString()
|
|
240
|
+
val result = runMiniapp(
|
|
241
|
+
"""
|
|
242
|
+
const response = await fetch(BASE + "/to-file")
|
|
243
|
+
return {status: response.status, text: await response.text()}
|
|
244
|
+
""",
|
|
245
|
+
)
|
|
246
|
+
assertEquals(302, result["status"])
|
|
247
|
+
assertFalse((result["text"] as String).contains("refresh-token"))
|
|
248
|
+
}
|
|
249
|
+
|
|
172
250
|
private fun runMiniapp(body: String): Map<*, *> {
|
|
173
251
|
val script =
|
|
174
252
|
"""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class MiniappNetworkPolicyTest {
|
|
10
|
+
@Test fun `fetch reaches only http and https`() {
|
|
11
|
+
for (url in listOf("http://example.com", "https://example.com/a?b", "HTTPS://EXAMPLE.COM", " https://example.com")) {
|
|
12
|
+
assertTrue(url, MiniappNetworkPolicy.allowsFetch(url))
|
|
13
|
+
}
|
|
14
|
+
for (url in listOf(
|
|
15
|
+
"file:///data/data/com.mentra.mentra/files/mmkv/mmkv.default",
|
|
16
|
+
"FILE:///etc/hosts",
|
|
17
|
+
"\tfile:///etc/hosts",
|
|
18
|
+
"content://com.mentra.mentra.fileprovider/cache/x",
|
|
19
|
+
"data:text/plain,hi",
|
|
20
|
+
"jar:file:///x!/y",
|
|
21
|
+
"ws://example.com",
|
|
22
|
+
"/data/data/com.mentra.mentra/files",
|
|
23
|
+
"relative/path",
|
|
24
|
+
"",
|
|
25
|
+
)) {
|
|
26
|
+
assertFalse(url, MiniappNetworkPolicy.allowsFetch(url))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test fun `WebSockets reach only ws, wss, http and https`() {
|
|
31
|
+
for (url in listOf("ws://10.0.0.2:9000", "WSS://example.com/s", "http://example.com", "https://example.com")) {
|
|
32
|
+
assertTrue(url, MiniappNetworkPolicy.allowsWebSocket(url))
|
|
33
|
+
}
|
|
34
|
+
for (url in listOf("file:///etc/hosts", "content://x/y", "data:,x", "/socket")) {
|
|
35
|
+
assertFalse(url, MiniappNetworkPolicy.allowsWebSocket(url))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test fun `scheme is lowercased and absent for a relative URL`() {
|
|
40
|
+
assertEquals("file", MiniappNetworkPolicy.scheme("FiLe:///x"))
|
|
41
|
+
assertEquals("https", MiniappNetworkPolicy.scheme("\u0000 https://x"))
|
|
42
|
+
assertNull(MiniappNetworkPolicy.scheme("/x"))
|
|
43
|
+
assertNull(MiniappNetworkPolicy.scheme("1http://x"))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.mentra.crust.utils
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.nio.file.Files
|
|
5
|
+
import org.junit.After
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertNull
|
|
8
|
+
import org.junit.Before
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
class CanonicalFilePathTest {
|
|
12
|
+
private lateinit var root: File
|
|
13
|
+
|
|
14
|
+
@Before fun setUp() {
|
|
15
|
+
root = Files.createTempDirectory("canonical-path-test").toFile().canonicalFile
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@After fun tearDown() {
|
|
19
|
+
root.deleteRecursively()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test fun `resolves dot segments and symlinks to the real location`() {
|
|
23
|
+
val sandbox = File(root, "sandbox").apply { mkdirs() }
|
|
24
|
+
val outside = File(root, "outside.txt").apply { writeText("secret") }
|
|
25
|
+
val link = File(sandbox, "link.txt")
|
|
26
|
+
Files.createSymbolicLink(link.toPath(), outside.toPath())
|
|
27
|
+
|
|
28
|
+
assertEquals(outside.path, CanonicalFilePath.of(link.path))
|
|
29
|
+
assertEquals(outside.path, CanonicalFilePath.of("${sandbox.path}/../outside.txt"))
|
|
30
|
+
assertEquals(sandbox.path, CanonicalFilePath.of("${sandbox.path}/./"))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test fun `is null for a missing, relative or malformed path`() {
|
|
34
|
+
assertNull(CanonicalFilePath.of("${root.path}/missing.txt"))
|
|
35
|
+
assertNull(CanonicalFilePath.of("relative/path"))
|
|
36
|
+
assertNull(CanonicalFilePath.of("${root.path}/a\u0000b"))
|
|
37
|
+
assertNull(CanonicalFilePath.of(""))
|
|
38
|
+
}
|
|
39
|
+
}
|
package/build/CrustModule.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
9
9
|
getPermissionStatus(requestId: string): Promise<AppPermissionReceipt>;
|
|
10
10
|
PI: number;
|
|
11
11
|
hello(): string;
|
|
12
|
+
/**
|
|
13
|
+
* The canonical path of an existing file or directory: absolute, with `.`,
|
|
14
|
+
* `..` and every symlink resolved. Null when nothing is there. The engine
|
|
15
|
+
* checks that a path a miniapp names stays inside the directory it may use.
|
|
16
|
+
*/
|
|
17
|
+
canonicalFilePath(path: string): string | null;
|
|
12
18
|
setValueAsync(value: string): Promise<void>;
|
|
13
19
|
/** Android: copy the displayed view's window pixels to a temporary JPEG URI. */
|
|
14
20
|
captureMiniappPreview(viewTag: number): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
|
package/build/CrustModule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AA8MtD,yDAAyD;AACzD,eAAe,mBAAmB,CAAc,OAAO,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport {AppPermissionReceipt, AppPermissionSnapshot, CrustModuleEvents, InstalledApp} from \"./Crust.types\"\n\ndeclare class CrustModule extends NativeModule<CrustModuleEvents> {\n /** True for the native iOS app running on an Apple Silicon Mac. */\n readonly isIOSAppOnMac?: boolean\n /** iOS: passive actual-app authorization status; never requests consent. */\n getPermissionStatus(requestId: null): Promise<AppPermissionSnapshot>\n /** iOS: a nonce publishes fresh status at the fixed managed-cache path. */\n getPermissionStatus(requestId: string): Promise<AppPermissionReceipt>\n PI: number\n hello(): string\n /**\n * The canonical path of an existing file or directory: absolute, with `.`,\n * `..` and every symlink resolved. Null when nothing is there. The engine\n * checks that a path a miniapp names stays inside the directory it may use.\n */\n canonicalFilePath(path: string): string | null\n setValueAsync(value: string): Promise<void>\n /** Android: copy the displayed view's window pixels to a temporary JPEG URI. */\n captureMiniappPreview(viewTag: number): Promise<string>\n nativeHttpRequest(\n requestId: string,\n method: string,\n url: string,\n headers: Record<string, string>,\n body?: string | null,\n ): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>\n cancelNativeHttpRequest(requestId: string): Promise<void>\n showAVRoutePicker(tintColor?: string | null): void\n\n /**\n * iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an\n * edge is deferred, the first swipe across that edge is consumed by the\n * app and the system gesture (Control Center, Notification Center, Home\n * indicator) only fires on a second swipe — i.e. a two-swipe-to-exit UX.\n *\n * Pass `[]` to restore default behavior. Android: no-op (Android has no\n * per-app equivalent; system gestures are configured at the OS level).\n */\n setDeferredSystemGestures(edges: Array<\"top\" | \"bottom\" | \"left\" | \"right\" | \"all\">): Promise<void>\n\n // MentraOS Notification Commands\n setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>\n getInstalledApps(): Promise<InstalledApp[]>\n getInstalledAppsForNotifications(): Promise<InstalledApp[]>\n hasNotificationListenerPermission(): Promise<boolean>\n refreshNotificationListener(): Promise<boolean>\n openNotificationListenerSettings(): Promise<boolean>\n isBetaBuild(): Promise<boolean>\n // location services commands\n showLocationServicesDialog(): Promise<boolean>\n isLocationServicesEnabled(): Promise<boolean>\n openLocationSettings(): Promise<boolean>\n openAppSettings(): Promise<boolean>\n openBluetoothSettings(): Promise<boolean>\n\n // Image Processing Commands\n processGalleryImage(\n inputPath: string,\n outputPath: string,\n options: {\n lensCorrection?: boolean\n colorCorrection?: boolean\n },\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n mergeHdrBrackets(\n underPath: string,\n normalPath: string,\n overPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n stabilizeVideo(\n inputPath: string,\n imuPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n // Media Library Commands\n saveToGalleryWithDate(\n filePath: string,\n captureTimeMillis?: number,\n displayName?: string,\n ): Promise<{\n success: boolean\n uri?: string\n identifier?: string\n existing?: boolean\n error?: string\n }>\n\n // Navigation (Android only — iOS stubs return error)\n startNavigation(\n lat: number,\n lng: number,\n options?: {\n simulate?: boolean\n speedMultiplier?: number\n /** Optional multi-stop list. When present takes precedence over lat/lng. Last entry is the final destination. */\n stops?: Array<{lat: number; lng: number}>\n /** \"walking\" | \"driving\" | \"cycling\" | \"two_wheeler\". Defaults driving. */\n mode?: string\n avoid?: {highways?: boolean; tolls?: boolean; ferries?: boolean}\n /** Force a reroute when the user is more than N meters past a pivot they didn't take. */\n missedTurnRerouteMeters?: number\n },\n ): Promise<{ok: boolean; error?: string}>\n stopNavigation(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Show the Google Nav SDK Terms & Conditions dialog if not already\n * accepted. Idempotent — resolves immediately with `{accepted: true}`\n * when the user has already accepted (cached in-process / on-disk /\n * inside the SDK).\n */\n requestNavigationPermission(): Promise<{ok: boolean; accepted: boolean; error?: string}>\n\n /**\n * Dev-only: clear cached T&C acceptance (SDK flag + on-disk pref +\n * in-process flag) so the next requestNavigationPermission() re-shows\n * the dialog. Android only; iOS returns {ok: false, error: \"not\n * supported on iOS\"}.\n */\n resetNavigationPermission(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev-only: nudge the simulated user position ~offsetMeters perpendicular\n * to the route so the Nav SDK reroutes. Default 20m. Android only.\n */\n simulateDeviation(offsetMeters?: number): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager shifts every\n * reported location ~8m perpendicular to the route bearing, simulating\n * a pedestrian walking on the wrong sidewalk. Only meaningful in\n * simulate mode; lets us verify the SDK's along-path pivot trigger\n * fires even when the user never crosses the 7m pivot point radius.\n * Android-only today; iOS is a no-op stub.\n */\n setWrongSidewalkOffset(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager takes over\n * from the Google simulator and walks the user along a modified\n * polyline that omits crossing micro-steps — reproducing the\n * wrong-sidewalk-then-missed-the-turn scenario. Android-only today;\n * iOS is a no-op stub.\n */\n setSkipCrossings(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n // Heading / compass (Android only)\n startHeading(): Promise<{ok: boolean; error?: string}>\n stopHeading(): Promise<{ok: boolean; error?: string}>\n\n // MentraJS Runtime — per-miniapp JSContext lifecycle.\n /**\n * Spawn a per-miniapp JS context. Re-spawn is allowed: a live context\n * with the same packageName is killed first. Returns true if the\n * polyfill bundle + miniapp source evaluated without throwing.\n *\n * The polyfill bundle is `mobile/modules/mentrajs-runtime/dist/startup.js`\n * (shipped inside the host binary). It installs window-style globals\n * (console, timers, fetch, localStorage, crypto) atop the JSC runtime.\n */\n mentraJsSpawn(packageName: string, polyfillBundle: string, miniappJs: string): Promise<boolean>\n mentraJsEvaluate(packageName: string, source: string): Promise<unknown>\n mentraJsKill(packageName: string): Promise<void>\n /**\n * Push a `{kind: \"event\"|\"response\", …}` envelope into the named\n * context's globalThis.__deliver. Returns when the underlying\n * evaluateScript completes (the JS handler runs synchronously on the\n * context's queue).\n */\n mentraJsDispatchToJs(packageName: string, envelope: Record<string, unknown>): Promise<void>\n mentraJsSetManifest(packageName: string, permissions: string[]): Promise<void>\n /** Diagnostic — returns the packageNames of every live JSContext. */\n mentraJsAlivePackages(): string[]\n /** Diagnostic — force a GC cycle on the named context. */\n mentraJsDebugForceGC(packageName: string): Promise<boolean>\n /**\n * Read the bundled MentraJS polyfill (startup.js) shipped inside the\n * host binary. Synchronous — host RN code calls this once on app boot\n * and caches the string, then passes it to every mentraJsSpawn so\n * every JSContext starts with the same polyfill ABI.\n */\n mentraJsLoadPolyfillBundle(): string\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<CrustModule>(\"Crust\")\n"]}
|
|
@@ -13,6 +13,8 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
13
13
|
body: string;
|
|
14
14
|
}>;
|
|
15
15
|
cancelNativeHttpRequest(requestId: string): Promise<void>;
|
|
16
|
+
/** The web has no local files to resolve. */
|
|
17
|
+
canonicalFilePath(_path: string): string | null;
|
|
16
18
|
hello(): string;
|
|
17
19
|
showAVRoutePicker(_tintColor?: string | null): void;
|
|
18
20
|
setDeferredSystemGestures(_edges: string[]): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACZ,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAC5D,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG3C,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;;;;IAevH,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D,KAAK;IAGL,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,qBAAqB,CAAC,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IACrF,gBAAgB;IAGhB,gCAAgC;IAGhC,iCAAiC;IAGjC,2BAA2B;IAG3B,gCAAgC;IAGhC,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAGjE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAG3C,YAAY,CAAC,IAAI,EAAE,MAAM;IAGzB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAGhE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAGxD,qBAAqB;IAGf,oBAAoB,CAAC,IAAI,EAAE,MAAM;IAGvC,0BAA0B;CAG3B;;AAED,wBAA4D"}
|
|
1
|
+
{"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACZ,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAC5D,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG3C,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;;;;IAevH,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D,6CAA6C;IAC7C,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAG/C,KAAK;IAGL,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,qBAAqB,CAAC,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IACrF,gBAAgB;IAGhB,gCAAgC;IAGhC,iCAAiC;IAGjC,2BAA2B;IAG3B,gCAAgC;IAGhC,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAGjE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAG3C,YAAY,CAAC,IAAI,EAAE,MAAM;IAGzB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAGhE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAGxD,qBAAqB;IAGf,oBAAoB,CAAC,IAAI,EAAE,MAAM;IAGvC,0BAA0B;CAG3B;;AAED,wBAA4D"}
|
package/build/CrustModule.web.js
CHANGED
|
@@ -25,6 +25,10 @@ class CrustModule extends NativeModule {
|
|
|
25
25
|
this.httpRequests.get(requestId)?.abort();
|
|
26
26
|
this.httpRequests.delete(requestId);
|
|
27
27
|
}
|
|
28
|
+
/** The web has no local files to resolve. */
|
|
29
|
+
canonicalFilePath(_path) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
28
32
|
hello() {
|
|
29
33
|
return "Hello world! 👋";
|
|
30
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import {registerWebModule, NativeModule} from \"expo\"\n\nimport {CrustModuleEvents} from \"./Crust.types\"\n\nclass CrustModule extends NativeModule<CrustModuleEvents> {\n PI = Math.PI\n private readonly httpRequests = new Map<string, AbortController>()\n async setValueAsync(value: string): Promise<void> {\n this.emit(\"onChange\", {value})\n }\n async nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null) {\n const controller = new AbortController()\n this.httpRequests.set(requestId, controller)\n try {\n const response = await fetch(url, {method, headers, body, signal: controller.signal})\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: await response.text(),\n }\n } finally {\n this.httpRequests.delete(requestId)\n }\n }\n async cancelNativeHttpRequest(requestId: string): Promise<void> {\n this.httpRequests.get(requestId)?.abort()\n this.httpRequests.delete(requestId)\n }\n hello() {\n return \"Hello world! 👋\"\n }\n showAVRoutePicker(_tintColor?: string | null) {}\n async setDeferredSystemGestures(_edges: string[]): Promise<void> {}\n async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}\n async getInstalledApps() {\n return []\n }\n async getInstalledAppsForNotifications() {\n return []\n }\n async hasNotificationListenerPermission() {\n return false\n }\n async refreshNotificationListener() {\n return false\n }\n async openNotificationListenerSettings() {\n return false\n }\n async isBetaBuild() {\n return false\n }\n async mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {\n return false\n }\n async mentraJsEvaluate(_pkg: string, _src: string) {\n return null\n }\n async mentraJsKill(_pkg: string) {\n return\n }\n async mentraJsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {\n return\n }\n async mentraJsSetManifest(_pkg: string, _perms: string[]) {\n return\n }\n mentraJsAlivePackages() {\n return []\n }\n async mentraJsDebugForceGC(_pkg: string) {\n return false\n }\n mentraJsLoadPolyfillBundle() {\n return \"\"\n }\n}\n\nexport default registerWebModule(CrustModule, \"CrustModule\")\n"]}
|
|
1
|
+
{"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,MAAM,CAAA;AAIpD,MAAM,WAAY,SAAQ,YAA+B;IACvD,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;IACK,YAAY,GAAG,IAAI,GAAG,EAA2B,CAAA;IAClE,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAC,KAAK,EAAC,CAAC,CAAA;IAChC,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,SAAiB,EAAE,MAAc,EAAE,GAAW,EAAE,OAA+B,EAAE,IAAoB;QAC3H,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAC5C,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAC,CAAC,CAAA;YACrF,OAAO;gBACL,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;gBAC/B,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACvD,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE;aAC5B,CAAA;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,uBAAuB,CAAC,SAAiB;QAC7C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAA;QACzC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrC,CAAC;IACD,6CAA6C;IAC7C,iBAAiB,CAAC,KAAa;QAC7B,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAA;IAC1B,CAAC;IACD,iBAAiB,CAAC,UAA0B,IAAG,CAAC;IAChD,KAAK,CAAC,yBAAyB,CAAC,MAAgB,IAAkB,CAAC;IACnE,KAAK,CAAC,qBAAqB,CAAC,gBAAyB,EAAE,UAAoB,IAAkB,CAAC;IAC9F,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,iCAAiC;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,2BAA2B;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,WAAW;QACf,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,IAAY,EAAE,SAAiB,EAAE,UAAkB;QACrE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gBAAgB,CAAC,IAAY,EAAE,IAAY;QAC/C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,OAAM;IACR,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY,EAAE,IAA6B;QACpE,OAAM;IACR,CAAC;IACD,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,MAAgB;QACtD,OAAM;IACR,CAAC;IACD,qBAAqB;QACnB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,0BAA0B;QACxB,OAAO,EAAE,CAAA;IACX,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA","sourcesContent":["import {registerWebModule, NativeModule} from \"expo\"\n\nimport {CrustModuleEvents} from \"./Crust.types\"\n\nclass CrustModule extends NativeModule<CrustModuleEvents> {\n PI = Math.PI\n private readonly httpRequests = new Map<string, AbortController>()\n async setValueAsync(value: string): Promise<void> {\n this.emit(\"onChange\", {value})\n }\n async nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null) {\n const controller = new AbortController()\n this.httpRequests.set(requestId, controller)\n try {\n const response = await fetch(url, {method, headers, body, signal: controller.signal})\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: await response.text(),\n }\n } finally {\n this.httpRequests.delete(requestId)\n }\n }\n async cancelNativeHttpRequest(requestId: string): Promise<void> {\n this.httpRequests.get(requestId)?.abort()\n this.httpRequests.delete(requestId)\n }\n /** The web has no local files to resolve. */\n canonicalFilePath(_path: string): string | null {\n return null\n }\n hello() {\n return \"Hello world! 👋\"\n }\n showAVRoutePicker(_tintColor?: string | null) {}\n async setDeferredSystemGestures(_edges: string[]): Promise<void> {}\n async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}\n async getInstalledApps() {\n return []\n }\n async getInstalledAppsForNotifications() {\n return []\n }\n async hasNotificationListenerPermission() {\n return false\n }\n async refreshNotificationListener() {\n return false\n }\n async openNotificationListenerSettings() {\n return false\n }\n async isBetaBuild() {\n return false\n }\n async mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {\n return false\n }\n async mentraJsEvaluate(_pkg: string, _src: string) {\n return null\n }\n async mentraJsKill(_pkg: string) {\n return\n }\n async mentraJsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {\n return\n }\n async mentraJsSetManifest(_pkg: string, _perms: string[]) {\n return\n }\n mentraJsAlivePackages() {\n return []\n }\n async mentraJsDebugForceGC(_pkg: string) {\n return false\n }\n mentraJsLoadPolyfillBundle() {\n return \"\"\n }\n}\n\nexport default registerWebModule(CrustModule, \"CrustModule\")\n"]}
|
package/ios/Crust.podspec
CHANGED
|
@@ -50,6 +50,9 @@ Pod::Spec.new do |s|
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
53
|
+
# MiniappPolicyKit's sources compile into this pod; its manifest, tests and
|
|
54
|
+
# local `swift test` output are only for testing them on a Mac.
|
|
55
|
+
s.exclude_files = "MiniappPolicyKit/Package.swift", "MiniappPolicyKit/Tests/**/*", "MiniappPolicyKit/.build/**/*"
|
|
53
56
|
|
|
54
57
|
# Ship the MentraJS polyfill bundle inside the pod's resource bundle so
|
|
55
58
|
# JSCRuntime can read it at runtime via Bundle.main. The source of
|
package/ios/CrustModule.swift
CHANGED
|
@@ -121,6 +121,11 @@ public class CrustModule: Module {
|
|
|
121
121
|
"Hello world! 👋"
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// Where a path really is, symlinks resolved; the engine checks miniapp-named paths with it.
|
|
125
|
+
Function("canonicalFilePath") { (path: String) -> String? in
|
|
126
|
+
CanonicalFilePath.of(path)
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
AsyncFunction("setValueAsync") { (value: String) in
|
|
125
130
|
self.sendEvent("onChange", [
|
|
126
131
|
"value": value,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
// The Crust pod compiles Sources/MiniappPolicy into its own module; this package
|
|
5
|
+
// exists so the rules can be unit tested on a Mac with `swift test`.
|
|
6
|
+
let package = Package(
|
|
7
|
+
name: "MiniappPolicyKit", platforms: [.macOS(.v13), .iOS(.v15)],
|
|
8
|
+
products: [.library(name: "MiniappPolicy", targets: ["MiniappPolicy"])],
|
|
9
|
+
targets: [
|
|
10
|
+
.target(name: "MiniappPolicy"),
|
|
11
|
+
.testTarget(name: "MiniappPolicyTests", dependencies: ["MiniappPolicy"]),
|
|
12
|
+
]
|
|
13
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The real location of a path, for checking that a path a miniapp names stays inside the
|
|
4
|
+
/// directory the host allows: absolute, with `.`, `..` and every symlink resolved.
|
|
5
|
+
public enum CanonicalFilePath {
|
|
6
|
+
/// The canonical path of an existing file or directory, or nil when there is none.
|
|
7
|
+
public static func of(_ path: String) -> String? {
|
|
8
|
+
guard path.hasPrefix("/"), !path.contains("\u{0}") else { return nil }
|
|
9
|
+
guard let resolved = realpath(path, nil) else { return nil }
|
|
10
|
+
defer { free(resolved) }
|
|
11
|
+
return String(cString: resolved)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Which URLs miniapp code may open through the polyfill's network bridge.
|
|
4
|
+
///
|
|
5
|
+
/// Miniapp code is untrusted, so `fetch` and `WebSocket` reach only the network.
|
|
6
|
+
/// URLSession also reads `file:` URLs, which would hand a miniapp the Mentra App's
|
|
7
|
+
/// own files, and `data:` URLs. Mirrors Android's `MiniappNetworkPolicy.kt` and the
|
|
8
|
+
/// polyfill's own check, which miniapp code can skip by calling the bridge directly.
|
|
9
|
+
public enum MiniappNetworkPolicy {
|
|
10
|
+
public static let fetchRefusal = "fetch: only http: and https: URLs are allowed"
|
|
11
|
+
public static let webSocketRefusal = "ws.open: only ws:, wss:, http: and https: URLs are allowed"
|
|
12
|
+
|
|
13
|
+
private static let fetchSchemes: Set<String> = ["http", "https"]
|
|
14
|
+
private static let webSocketSchemes: Set<String> = ["ws", "wss", "http", "https"]
|
|
15
|
+
|
|
16
|
+
/// `fetch`, and every redirect it follows.
|
|
17
|
+
public static func allowsFetch(_ url: URL) -> Bool {
|
|
18
|
+
fetchSchemes.contains(url.scheme?.lowercased() ?? "")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public static func allowsWebSocket(_ url: URL) -> Bool {
|
|
22
|
+
webSocketSchemes.contains(url.scheme?.lowercased() ?? "")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
@testable import MiniappPolicy
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class MiniappNetworkPolicyTests: XCTestCase {
|
|
6
|
+
func testFetchReachesOnlyHttpAndHttps() {
|
|
7
|
+
for url in ["http://example.com", "https://example.com/a?b", "HTTPS://EXAMPLE.COM", "http://192.168.1.20:8080/x"] {
|
|
8
|
+
XCTAssertTrue(MiniappNetworkPolicy.allowsFetch(URL(string: url)!), url)
|
|
9
|
+
}
|
|
10
|
+
// URLSession reads file: URLs, so this is what keeps miniapp code out of the app's files.
|
|
11
|
+
for url in [
|
|
12
|
+
"file:///var/mobile/Containers/Data/Application/X/Documents/mmkv/mmkv.default",
|
|
13
|
+
"FILE:///etc/hosts",
|
|
14
|
+
"file:/etc/hosts",
|
|
15
|
+
"data:text/plain,hi",
|
|
16
|
+
"ftp://example.com/x",
|
|
17
|
+
"ws://example.com",
|
|
18
|
+
"/etc/hosts",
|
|
19
|
+
"relative/path",
|
|
20
|
+
] {
|
|
21
|
+
XCTAssertFalse(MiniappNetworkPolicy.allowsFetch(URL(string: url)!), url)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Why the gate exists: the session the bridge uses hands back a local file's bytes.
|
|
26
|
+
func testUrlSessionWouldReadAFileUrl() throws {
|
|
27
|
+
let file = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).txt")
|
|
28
|
+
try Data("refresh-token".utf8).write(to: file)
|
|
29
|
+
defer { try? FileManager.default.removeItem(at: file) }
|
|
30
|
+
let done = expectation(description: "read")
|
|
31
|
+
var body: Data?
|
|
32
|
+
URLSession(configuration: .default).dataTask(with: file) { data, _, _ in
|
|
33
|
+
body = data
|
|
34
|
+
done.fulfill()
|
|
35
|
+
}.resume()
|
|
36
|
+
wait(for: [done], timeout: 5)
|
|
37
|
+
XCTAssertEqual(body, Data("refresh-token".utf8))
|
|
38
|
+
XCTAssertFalse(MiniappNetworkPolicy.allowsFetch(file))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The host's own downloads (blob.setFromUrl, system.download, meeting stills) check only
|
|
42
|
+
/// the first URL and rely on this: URLSession does not follow a redirect to a file: URL.
|
|
43
|
+
func testUrlSessionRefusesARedirectToAFileUrl() throws {
|
|
44
|
+
let file = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).txt")
|
|
45
|
+
try Data("refresh-token".utf8).write(to: file)
|
|
46
|
+
defer { try? FileManager.default.removeItem(at: file) }
|
|
47
|
+
let server = try RedirectServer(location: file.absoluteString)
|
|
48
|
+
defer { server.stop() }
|
|
49
|
+
|
|
50
|
+
for kind in ["data", "download"] {
|
|
51
|
+
let done = expectation(description: kind)
|
|
52
|
+
var read: String?
|
|
53
|
+
var failure: NSError?
|
|
54
|
+
let session = URLSession(configuration: .ephemeral)
|
|
55
|
+
let finish: (Data?, Error?) -> Void = { data, error in
|
|
56
|
+
read = data.map { String(decoding: $0, as: UTF8.self) }
|
|
57
|
+
failure = error as NSError?
|
|
58
|
+
done.fulfill()
|
|
59
|
+
}
|
|
60
|
+
if kind == "data" {
|
|
61
|
+
session.dataTask(with: server.url) { data, _, error in finish(data, error) }.resume()
|
|
62
|
+
} else {
|
|
63
|
+
session.downloadTask(with: server.url) { location, _, error in
|
|
64
|
+
finish(location.flatMap { try? Data(contentsOf: $0) }, error)
|
|
65
|
+
}.resume()
|
|
66
|
+
}
|
|
67
|
+
wait(for: [done], timeout: 10)
|
|
68
|
+
XCTAssertNotEqual(read, "refresh-token", kind)
|
|
69
|
+
XCTAssertEqual(failure?.code, NSURLErrorNoPermissionsToReadFile, kind)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func testWebSocketsReachOnlyNetworkSchemes() {
|
|
74
|
+
for url in ["ws://10.0.0.2:9000", "WSS://example.com/s", "http://example.com", "https://example.com"] {
|
|
75
|
+
XCTAssertTrue(MiniappNetworkPolicy.allowsWebSocket(URL(string: url)!), url)
|
|
76
|
+
}
|
|
77
|
+
for url in ["file:///etc/hosts", "data:,x", "/socket"] {
|
|
78
|
+
XCTAssertFalse(MiniappNetworkPolicy.allowsWebSocket(URL(string: url)!), url)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
final class CanonicalFilePathTests: XCTestCase {
|
|
84
|
+
private var root: URL!
|
|
85
|
+
|
|
86
|
+
override func setUpWithError() throws {
|
|
87
|
+
root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
88
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override func tearDownWithError() throws {
|
|
92
|
+
try? FileManager.default.removeItem(at: root)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func testResolvesDotSegmentsAndSymlinks() throws {
|
|
96
|
+
let sandbox = root.appendingPathComponent("sandbox")
|
|
97
|
+
try FileManager.default.createDirectory(at: sandbox, withIntermediateDirectories: true)
|
|
98
|
+
let outside = root.appendingPathComponent("outside.txt")
|
|
99
|
+
try Data("secret".utf8).write(to: outside)
|
|
100
|
+
let link = sandbox.appendingPathComponent("link.txt")
|
|
101
|
+
try FileManager.default.createSymbolicLink(at: link, withDestinationURL: outside)
|
|
102
|
+
|
|
103
|
+
let realOutside = try XCTUnwrap(CanonicalFilePath.of(outside.path))
|
|
104
|
+
XCTAssertEqual(CanonicalFilePath.of(link.path), realOutside)
|
|
105
|
+
XCTAssertEqual(CanonicalFilePath.of(sandbox.path + "/../outside.txt"), realOutside)
|
|
106
|
+
// The temporary directory sits under /var, itself a symlink to /private/var, like an
|
|
107
|
+
// iOS app's container: both sides of a containment check must be resolved.
|
|
108
|
+
XCTAssertTrue(realOutside.hasPrefix("/private/"), realOutside)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func testNilForMissingRelativeOrMalformedPaths() {
|
|
112
|
+
XCTAssertNil(CanonicalFilePath.of(root.appendingPathComponent("missing.txt").path))
|
|
113
|
+
XCTAssertNil(CanonicalFilePath.of("relative/path"))
|
|
114
|
+
XCTAssertNil(CanonicalFilePath.of(root.path + "/a\u{0}b"))
|
|
115
|
+
XCTAssertNil(CanonicalFilePath.of(""))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
|
|
4
|
+
/// A one-route HTTP server on 127.0.0.1 that answers every request with a 302 to `location`.
|
|
5
|
+
final class RedirectServer {
|
|
6
|
+
let url: URL
|
|
7
|
+
private let listener: NWListener
|
|
8
|
+
private let queue = DispatchQueue(label: "RedirectServer")
|
|
9
|
+
|
|
10
|
+
init(location: String) throws {
|
|
11
|
+
listener = try NWListener(using: .tcp, on: .any)
|
|
12
|
+
let response = "HTTP/1.1 302 Found\r\nLocation: \(location)\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
13
|
+
listener.newConnectionHandler = { [queue] connection in
|
|
14
|
+
connection.start(queue: queue)
|
|
15
|
+
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { _, _, _, _ in
|
|
16
|
+
connection.send(content: Data(response.utf8), completion: .contentProcessed { _ in connection.cancel() })
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let ready = DispatchSemaphore(value: 0)
|
|
20
|
+
listener.stateUpdateHandler = { state in
|
|
21
|
+
if case .ready = state { ready.signal() }
|
|
22
|
+
}
|
|
23
|
+
listener.start(queue: queue)
|
|
24
|
+
guard ready.wait(timeout: .now() + 5) == .success, let port = listener.port?.rawValue else {
|
|
25
|
+
throw URLError(.cannotConnectToHost)
|
|
26
|
+
}
|
|
27
|
+
url = URL(string: "http://127.0.0.1:\(port)/credentials")!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func stop() {
|
|
31
|
+
listener.cancel()
|
|
32
|
+
}
|
|
33
|
+
}
|
package/ios/Resources/startup.js
CHANGED
|
@@ -571,38 +571,48 @@
|
|
|
571
571
|
return parts.join("");
|
|
572
572
|
}
|
|
573
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
|
+
};
|
|
574
582
|
class TextEncoderPolyfill {
|
|
575
583
|
constructor() {
|
|
576
584
|
this.encoding = "utf-8";
|
|
577
585
|
}
|
|
578
586
|
encode(input) {
|
|
579
587
|
const str = input ?? "";
|
|
580
|
-
|
|
588
|
+
let length = 0;
|
|
581
589
|
for (let i = 0; i < str.length; i++) {
|
|
582
|
-
|
|
583
|
-
if (code
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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++;
|
|
590
599
|
if (code < 128) {
|
|
591
|
-
out
|
|
600
|
+
out[at++] = code;
|
|
592
601
|
} else if (code < 2048) {
|
|
593
|
-
out
|
|
602
|
+
out[at++] = 192 | code >> 6;
|
|
603
|
+
out[at++] = 128 | code & 63;
|
|
594
604
|
} else if (code < 65536) {
|
|
595
|
-
out
|
|
605
|
+
out[at++] = 224 | code >> 12;
|
|
606
|
+
out[at++] = 128 | code >> 6 & 63;
|
|
607
|
+
out[at++] = 128 | code & 63;
|
|
596
608
|
} else {
|
|
597
|
-
out
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
128 | code & 63
|
|
602
|
-
);
|
|
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;
|
|
603
613
|
}
|
|
604
614
|
}
|
|
605
|
-
return
|
|
615
|
+
return out;
|
|
606
616
|
}
|
|
607
617
|
}
|
|
608
618
|
;
|
|
@@ -658,10 +668,20 @@
|
|
|
658
668
|
return out;
|
|
659
669
|
};
|
|
660
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
|
+
};
|
|
661
675
|
if (typeof g.fetch !== "function") {
|
|
662
676
|
;
|
|
663
677
|
g.fetch = async (input, init) => {
|
|
664
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
|
+
}
|
|
665
685
|
const method = (init?.method ?? "GET").toUpperCase();
|
|
666
686
|
let bodyString = null;
|
|
667
687
|
let bodyBase64;
|
|
@@ -772,6 +792,12 @@
|
|
|
772
792
|
close: /* @__PURE__ */ new Set()
|
|
773
793
|
};
|
|
774
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
|
+
}
|
|
775
801
|
const protoList = Array.isArray(protocols) ? protocols : protocols ? [protocols] : [];
|
|
776
802
|
this.sid = `ws-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
|
|
777
803
|
sockets.set(this.sid, this);
|
|
@@ -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,6 +114,11 @@ 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"
|
|
@@ -273,6 +294,10 @@ public enum JSCPolyfillBridge {
|
|
|
273
294
|
let url = URL(string: urlString) else {
|
|
274
295
|
return .error(code: "INVALID_ARGS", message: "ws.open expects {sid, url, protocols?}")
|
|
275
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
|
+
}
|
|
276
301
|
var urlRequest = URLRequest(url: url)
|
|
277
302
|
if let protocols = req["protocols"] as? [String], !protocols.isEmpty {
|
|
278
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.5.0-dev.1238",
|
|
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.5.0-dev.1238"
|
|
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
|
}
|