pakstr 0.22.0 → 0.24.0
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/README.md +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +12 -28
- package/android-template/app/src/main/java/com/pakstr/app/PakstrBridge.kt +13 -1
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseSettings.kt +98 -0
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeApiBaseValidator.kt +30 -0
- package/android-template/app/src/main/java/com/pakstr/app/RuntimeConfig.kt +7 -12
- package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +1 -1
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +70 -4
- package/android-template/app/src/main/res/layout/activity_developer_tools.xml +257 -66
- package/android-template/app/src/main/res/values/strings.xml +24 -5
- package/android-template/app/src/test/java/com/pakstr/app/PakstrBridgeRuntimeApiBaseTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseProxyTest.kt +49 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseSettingsTest.kt +107 -0
- package/android-template/app/src/test/java/com/pakstr/app/RuntimeApiBaseValidatorTest.kt +50 -0
- package/dist/commands/publish.js +22 -0
- package/dist/core/zapStore.js +2 -0
- package/dist/core/zapstoreConfig.js +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ build:
|
|
|
49
49
|
# apiBase: https://api.example.com # Optional packaged /api/* proxy target.
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`runtime.apiBase` must be an absolute HTTPS URL. It is public APK metadata, not a secret.
|
|
52
|
+
`runtime.apiBase` must be an absolute HTTPS URL. It is the immutable packaged/default proxy target and public APK metadata, not a secret. The effective target precedence is persistent runtime override, then packaged default, then disabled. A web app may call `window.PakstrBridge.setApiBaseUrl(url)` to validate and synchronously persist an HTTPS override, and `window.PakstrBridge.getApiBaseUrl()` to read the effective URL. Developer Tools uses the same setting. Reset removes the override and restores the packaged default; without a packaged default it disables `/api/*`. Changes affect the next proxy request without an app restart, LocalServer restart, or WebView reload.
|
|
53
53
|
|
|
54
54
|
### 3. Build, sign, and publish
|
|
55
55
|
```bash
|
|
@@ -80,6 +80,11 @@ internal fun localRequestUrl(
|
|
|
80
80
|
if (query.isNullOrEmpty()) "" else "?$query"
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
+
internal fun proxyApiUrl(
|
|
84
|
+
apiBase: String,
|
|
85
|
+
uri: String
|
|
86
|
+
): URL = URL(apiBase + uri)
|
|
87
|
+
|
|
83
88
|
internal data class ProxyHttpResponse(
|
|
84
89
|
val code: Int,
|
|
85
90
|
val contentType: String?,
|
|
@@ -258,7 +263,8 @@ class LocalServer(
|
|
|
258
263
|
)
|
|
259
264
|
if (uri.startsWith("/api/")) {
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
val apiBase = RuntimeConfig.apiBase(context)
|
|
267
|
+
if (apiBase == null) {
|
|
262
268
|
|
|
263
269
|
AppDebugLogger.network(
|
|
264
270
|
|
|
@@ -283,7 +289,7 @@ class LocalServer(
|
|
|
283
289
|
)
|
|
284
290
|
}
|
|
285
291
|
|
|
286
|
-
return proxyApi(session, uri)
|
|
292
|
+
return proxyApi(session, uri, apiBase)
|
|
287
293
|
|
|
288
294
|
}
|
|
289
295
|
|
|
@@ -370,7 +376,7 @@ class LocalServer(
|
|
|
370
376
|
}
|
|
371
377
|
|
|
372
378
|
private fun proxyApi(
|
|
373
|
-
session: IHTTPSession?, uri: String
|
|
379
|
+
session: IHTTPSession?, uri: String, apiBase: String
|
|
374
380
|
): Response {
|
|
375
381
|
|
|
376
382
|
if (AppDebugLogger.isNetworkLoggingEnabled(context)) {
|
|
@@ -386,29 +392,7 @@ class LocalServer(
|
|
|
386
392
|
|
|
387
393
|
return try {
|
|
388
394
|
|
|
389
|
-
val
|
|
390
|
-
|
|
391
|
-
val apiBase = config.optString("apiBase")
|
|
392
|
-
|
|
393
|
-
if (apiBase.isBlank()) {
|
|
394
|
-
AppDebugLogger.error(
|
|
395
|
-
|
|
396
|
-
context,
|
|
397
|
-
|
|
398
|
-
"API",
|
|
399
|
-
|
|
400
|
-
"apiBase missing"
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
return newFixedLengthResponse(
|
|
404
|
-
Response.Status.INTERNAL_ERROR,
|
|
405
|
-
"application/json",
|
|
406
|
-
"""{"error":"apiBase missing"}"""
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
val finalUrl = apiBase.removeSuffix("/") + uri
|
|
411
|
-
val url = URL(finalUrl)
|
|
395
|
+
val url = proxyApiUrl(apiBase, uri)
|
|
412
396
|
val method = session?.method?.name ?: "GET"
|
|
413
397
|
val requestHeaders = session?.headers ?: emptyMap()
|
|
414
398
|
val localRequestUrl = localRequestUrl(
|
|
@@ -438,7 +422,7 @@ class LocalServer(
|
|
|
438
422
|
if (AppDebugLogger.isNetworkLoggingEnabled(context)) {
|
|
439
423
|
AppDebugLogger.network(
|
|
440
424
|
context,
|
|
441
|
-
"
|
|
425
|
+
"Proxy response: ${response.code}"
|
|
442
426
|
)
|
|
443
427
|
}
|
|
444
428
|
return newFixedLengthResponse(
|
|
@@ -498,7 +482,7 @@ class LocalServer(
|
|
|
498
482
|
AppDebugLogger.network(
|
|
499
483
|
|
|
500
484
|
context,
|
|
501
|
-
"
|
|
485
|
+
"Proxy response: $code"
|
|
502
486
|
|
|
503
487
|
)
|
|
504
488
|
|
|
@@ -2,10 +2,22 @@ package com.pakstr.app
|
|
|
2
2
|
|
|
3
3
|
import android.webkit.JavascriptInterface
|
|
4
4
|
|
|
5
|
-
class PakstrBridge
|
|
5
|
+
class PakstrBridge(
|
|
6
|
+
private val runtimeApiBaseSettings: RuntimeApiBaseSettings
|
|
7
|
+
) {
|
|
6
8
|
|
|
7
9
|
@JavascriptInterface
|
|
8
10
|
fun getNip55CallbackUrl(): String {
|
|
9
11
|
return Nip55Callback.callbackPrefix
|
|
10
12
|
}
|
|
13
|
+
|
|
14
|
+
@JavascriptInterface
|
|
15
|
+
fun setApiBaseUrl(url: String): Boolean {
|
|
16
|
+
return runtimeApiBaseSettings.setOverride(url)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@JavascriptInterface
|
|
20
|
+
fun getApiBaseUrl(): String? {
|
|
21
|
+
return runtimeApiBaseSettings.current().url
|
|
22
|
+
}
|
|
11
23
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
|
|
6
|
+
class RuntimeApiBaseSettings private constructor(
|
|
7
|
+
private val preferences: Preferences,
|
|
8
|
+
private val packagedValueProvider: () -> String?
|
|
9
|
+
) {
|
|
10
|
+
enum class Source {
|
|
11
|
+
RUNTIME_OVERRIDE,
|
|
12
|
+
PACKAGED_DEFAULT,
|
|
13
|
+
DISABLED
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
data class Value(
|
|
17
|
+
val url: String?,
|
|
18
|
+
val source: Source
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
fun current(): Value = resolveEffectiveValue(
|
|
22
|
+
persistedOverride = preferences.getString(API_BASE_OVERRIDE_KEY),
|
|
23
|
+
packagedDefault = packagedValueProvider()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
fun setOverride(value: String): Boolean {
|
|
27
|
+
val canonical = RuntimeApiBaseValidator.validateAndCanonicalize(value)
|
|
28
|
+
?: return false
|
|
29
|
+
return preferences.putString(API_BASE_OVERRIDE_KEY, canonical)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun reset(): Boolean = preferences.remove(API_BASE_OVERRIDE_KEY)
|
|
33
|
+
|
|
34
|
+
internal interface Preferences {
|
|
35
|
+
fun getString(key: String): String?
|
|
36
|
+
fun putString(key: String, value: String): Boolean
|
|
37
|
+
fun remove(key: String): Boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private class SharedPreferencesAdapter(
|
|
41
|
+
private val preferences: SharedPreferences
|
|
42
|
+
) : Preferences {
|
|
43
|
+
override fun getString(key: String): String? = preferences.getString(key, null)
|
|
44
|
+
|
|
45
|
+
override fun putString(key: String, value: String): Boolean =
|
|
46
|
+
preferences.edit().putString(key, value).commit()
|
|
47
|
+
|
|
48
|
+
override fun remove(key: String): Boolean =
|
|
49
|
+
preferences.edit().remove(key).commit()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
companion object {
|
|
53
|
+
internal const val PREFERENCES_FILE = "pakstr_runtime_api_base"
|
|
54
|
+
internal const val API_BASE_OVERRIDE_KEY = "api_base_override"
|
|
55
|
+
|
|
56
|
+
fun from(context: Context): RuntimeApiBaseSettings {
|
|
57
|
+
val applicationContext = context.applicationContext
|
|
58
|
+
val preferences = applicationContext.getSharedPreferences(
|
|
59
|
+
PREFERENCES_FILE,
|
|
60
|
+
Context.MODE_PRIVATE
|
|
61
|
+
)
|
|
62
|
+
return RuntimeApiBaseSettings(
|
|
63
|
+
SharedPreferencesAdapter(preferences)
|
|
64
|
+
) {
|
|
65
|
+
RuntimeConfig.packagedApiBase(applicationContext)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
internal fun resolveEffectiveValue(
|
|
70
|
+
persistedOverride: String?,
|
|
71
|
+
packagedDefault: String?
|
|
72
|
+
): Value {
|
|
73
|
+
val override = RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
74
|
+
persistedOverride
|
|
75
|
+
)
|
|
76
|
+
if (override != null) {
|
|
77
|
+
return Value(override, Source.RUNTIME_OVERRIDE)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val packaged = RuntimeApiBaseValidator.validateAndCanonicalize(
|
|
81
|
+
packagedDefault
|
|
82
|
+
)
|
|
83
|
+
return if (packaged != null) {
|
|
84
|
+
Value(packaged, Source.PACKAGED_DEFAULT)
|
|
85
|
+
} else {
|
|
86
|
+
Value(null, Source.DISABLED)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
internal fun createForTesting(
|
|
91
|
+
preferences: Preferences,
|
|
92
|
+
packagedValueProvider: () -> String?
|
|
93
|
+
): RuntimeApiBaseSettings = RuntimeApiBaseSettings(
|
|
94
|
+
preferences,
|
|
95
|
+
packagedValueProvider
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import java.net.URI
|
|
4
|
+
|
|
5
|
+
object RuntimeApiBaseValidator {
|
|
6
|
+
const val MAX_LENGTH = 2048
|
|
7
|
+
|
|
8
|
+
fun validateAndCanonicalize(value: String?): String? {
|
|
9
|
+
if (
|
|
10
|
+
value == null ||
|
|
11
|
+
value.isEmpty() ||
|
|
12
|
+
value.length > MAX_LENGTH ||
|
|
13
|
+
value != value.trim()
|
|
14
|
+
) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
val uri = runCatching { URI(value) }.getOrNull() ?: return null
|
|
19
|
+
if (!uri.isAbsolute || !uri.scheme.equals("https", ignoreCase = true)) return null
|
|
20
|
+
if (uri.host.isNullOrBlank()) return null
|
|
21
|
+
if (uri.rawUserInfo != null || uri.rawQuery != null || uri.rawFragment != null) return null
|
|
22
|
+
|
|
23
|
+
val host = uri.host.lowercase().let {
|
|
24
|
+
if (it.contains(':') && !it.startsWith("[")) "[$it]" else it
|
|
25
|
+
}
|
|
26
|
+
val port = if (uri.port == -1) "" else ":${uri.port}"
|
|
27
|
+
val path = uri.rawPath.orEmpty().trimEnd('/')
|
|
28
|
+
return "https://$host$port$path"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -58,19 +58,14 @@ object RuntimeConfig {
|
|
|
58
58
|
*/
|
|
59
59
|
fun apiBase(
|
|
60
60
|
context: Context
|
|
61
|
-
): String?
|
|
61
|
+
): String? = RuntimeApiBaseSettings.from(context).current().url
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return value.takeIf {
|
|
72
|
-
it.isNotBlank()
|
|
73
|
-
}
|
|
63
|
+
internal fun packagedApiBase(
|
|
64
|
+
context: Context
|
|
65
|
+
): String? = load(context).optString(
|
|
66
|
+
"apiBase", ""
|
|
67
|
+
).takeIf {
|
|
68
|
+
it.isNotBlank()
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
/**
|
|
@@ -15,13 +15,18 @@ import android.widget.TextView
|
|
|
15
15
|
import android.widget.Toast
|
|
16
16
|
import androidx.activity.enableEdgeToEdge
|
|
17
17
|
import androidx.appcompat.app.AppCompatActivity
|
|
18
|
+
import androidx.core.view.ViewCompat
|
|
19
|
+
import androidx.core.view.WindowInsetsCompat
|
|
20
|
+
import androidx.core.view.updatePadding
|
|
18
21
|
import com.google.android.material.appbar.MaterialToolbar
|
|
19
22
|
import com.google.android.material.button.MaterialButton
|
|
20
23
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
21
24
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
|
25
|
+
import com.google.android.material.textfield.TextInputEditText
|
|
26
|
+
import com.google.android.material.textfield.TextInputLayout
|
|
22
27
|
import com.pakstr.app.BuildConfig
|
|
23
28
|
import com.pakstr.app.R
|
|
24
|
-
import com.pakstr.app.
|
|
29
|
+
import com.pakstr.app.RuntimeApiBaseSettings
|
|
25
30
|
|
|
26
31
|
class DeveloperToolsActivity : AppCompatActivity() {
|
|
27
32
|
|
|
@@ -30,8 +35,12 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
30
35
|
private lateinit var uptimeValue: TextView
|
|
31
36
|
private lateinit var debugModeSwitch: SwitchMaterial
|
|
32
37
|
private lateinit var apiSwitch: SwitchMaterial
|
|
38
|
+
private lateinit var apiBaseInputLayout: TextInputLayout
|
|
39
|
+
private lateinit var apiBaseInput: TextInputEditText
|
|
40
|
+
private lateinit var apiBaseSource: TextView
|
|
33
41
|
private lateinit var debugWebViewStatus: TextView
|
|
34
42
|
private lateinit var webViewStatus: TextView
|
|
43
|
+
private lateinit var runtimeApiBaseSettings: RuntimeApiBaseSettings
|
|
35
44
|
|
|
36
45
|
private val uptimeUpdater = object : Runnable {
|
|
37
46
|
override fun run() {
|
|
@@ -50,11 +59,14 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
setContentView(R.layout.activity_developer_tools)
|
|
62
|
+
runtimeApiBaseSettings = RuntimeApiBaseSettings.from(this)
|
|
53
63
|
|
|
64
|
+
setupWindowInsets()
|
|
54
65
|
setupToolbar()
|
|
55
66
|
bindViews()
|
|
56
67
|
populateRuntimeDetails()
|
|
57
68
|
setupDebugControls()
|
|
69
|
+
setupApiBaseControls()
|
|
58
70
|
setupLogActions()
|
|
59
71
|
setupCrashTesting()
|
|
60
72
|
setupWebViewActions()
|
|
@@ -71,6 +83,18 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
71
83
|
super.onStop()
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
private fun setupWindowInsets() {
|
|
87
|
+
ViewCompat.setOnApplyWindowInsetsListener(
|
|
88
|
+
findViewById(R.id.developerToolsRoot)
|
|
89
|
+
) { view, insets ->
|
|
90
|
+
val bars = insets.getInsets(
|
|
91
|
+
WindowInsetsCompat.Type.systemBars()
|
|
92
|
+
)
|
|
93
|
+
view.updatePadding(top = bars.top)
|
|
94
|
+
insets
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
private fun setupToolbar() {
|
|
75
99
|
findViewById<MaterialToolbar>(R.id.developerToolsToolbar).setNavigationOnClickListener {
|
|
76
100
|
finish()
|
|
@@ -81,6 +105,9 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
81
105
|
uptimeValue = findViewById(R.id.runtimeUptimeValue)
|
|
82
106
|
debugModeSwitch = findViewById(R.id.debugModeSwitch)
|
|
83
107
|
apiSwitch = findViewById(R.id.apiEnabledSwitch)
|
|
108
|
+
apiBaseInputLayout = findViewById(R.id.apiBaseInputLayout)
|
|
109
|
+
apiBaseInput = findViewById(R.id.apiBaseInput)
|
|
110
|
+
apiBaseSource = findViewById(R.id.apiBaseSource)
|
|
84
111
|
debugWebViewStatus = findViewById(R.id.debugWebViewStatus)
|
|
85
112
|
webViewStatus = findViewById(R.id.webViewStatus)
|
|
86
113
|
}
|
|
@@ -101,8 +128,6 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
private fun setupDebugControls() {
|
|
104
|
-
apiSwitch.isChecked = RuntimeConfig.apiEnabled(this)
|
|
105
|
-
|
|
106
131
|
debugModeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
|
107
132
|
if (isChecked) {
|
|
108
133
|
DeveloperToolsManager.enableDebug(this)
|
|
@@ -115,9 +140,50 @@ class DeveloperToolsActivity : AppCompatActivity() {
|
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
|
143
|
+
private fun setupApiBaseControls() {
|
|
144
|
+
findViewById<MaterialButton>(R.id.saveApiBaseButton).setOnClickListener {
|
|
145
|
+
val value = apiBaseInput.text?.toString().orEmpty()
|
|
146
|
+
if (runtimeApiBaseSettings.setOverride(value)) {
|
|
147
|
+
apiBaseInputLayout.error = null
|
|
148
|
+
refreshApiBase()
|
|
149
|
+
showMessage(R.string.developer_tools_api_base_saved)
|
|
150
|
+
} else {
|
|
151
|
+
apiBaseInputLayout.error = getString(
|
|
152
|
+
R.string.developer_tools_api_base_invalid
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
findViewById<MaterialButton>(R.id.resetApiBaseButton).setOnClickListener {
|
|
157
|
+
if (runtimeApiBaseSettings.reset()) {
|
|
158
|
+
apiBaseInputLayout.error = null
|
|
159
|
+
refreshApiBase()
|
|
160
|
+
showMessage(R.string.developer_tools_api_base_reset)
|
|
161
|
+
} else {
|
|
162
|
+
showMessage(R.string.developer_tools_api_base_save_failed)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
refreshApiBase()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun refreshApiBase() {
|
|
169
|
+
val current = runtimeApiBaseSettings.current()
|
|
170
|
+
apiSwitch.isChecked = current.url != null
|
|
171
|
+
apiBaseInput.setText(current.url.orEmpty())
|
|
172
|
+
apiBaseSource.setText(
|
|
173
|
+
when (current.source) {
|
|
174
|
+
RuntimeApiBaseSettings.Source.RUNTIME_OVERRIDE ->
|
|
175
|
+
R.string.developer_tools_api_base_runtime_override
|
|
176
|
+
RuntimeApiBaseSettings.Source.PACKAGED_DEFAULT ->
|
|
177
|
+
R.string.developer_tools_api_base_packaged_default
|
|
178
|
+
RuntimeApiBaseSettings.Source.DISABLED ->
|
|
179
|
+
R.string.developer_tools_api_base_not_configured
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
118
184
|
private fun refreshStatuses() {
|
|
119
185
|
debugModeSwitch.isChecked = DeveloperToolsManager.isEnabled(this)
|
|
120
|
-
|
|
186
|
+
refreshApiBase()
|
|
121
187
|
|
|
122
188
|
val status = if (isWebViewDebugEnabled()) {
|
|
123
189
|
R.string.developer_tools_enabled
|