pakstr 0.31.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/README.md +1 -1
  2. package/android-template/app/build.gradle.kts +1 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/PakstrWebBridgeTest.kt +153 -0
  4. package/android-template/app/src/main/assets/nip07.js +124 -74
  5. package/android-template/app/src/main/java/com/pakstr/app/AppOrigin.kt +77 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/PakstrBridge.kt +5 -5
  7. package/android-template/app/src/main/java/com/pakstr/app/PakstrWebBridge.kt +89 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/WebMessageRouter.kt +186 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +65 -49
  10. package/android-template/app/src/main/java/com/pakstr/app/signer/AmberRequestChannel.kt +156 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/signer/AmberSigner.kt +142 -390
  12. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +69 -489
  13. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +13 -2
  14. package/android-template/app/src/test/java/com/pakstr/app/AppOriginTest.kt +114 -0
  15. package/android-template/app/src/test/java/com/pakstr/app/WebMessageRouterTest.kt +279 -0
  16. package/android-template/app/src/test/java/com/pakstr/app/signer/AmberRequestChannelTest.kt +294 -0
  17. package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeDispatchTest.kt +113 -0
  18. package/android-template/gradle/libs.versions.toml +2 -0
  19. package/package.json +1 -1
@@ -1,514 +1,90 @@
1
1
  package com.pakstr.app.signer
2
2
 
3
- import android.os.Handler
4
- import android.os.Looper
5
- import android.webkit.JavascriptInterface
6
- import android.webkit.WebView
7
-
8
- import com.pakstr.app.debug.AppDebugLogger
9
3
  import com.pakstr.app.signer.exceptions.SignerUnavailableException
10
- import kotlinx.coroutines.CoroutineScope
11
- import kotlinx.coroutines.Dispatchers
12
- import kotlinx.coroutines.SupervisorJob
13
- import kotlinx.coroutines.launch
4
+ import com.pakstr.app.signer.interfaces.NostrSigner
14
5
  import org.json.JSONObject
15
6
 
7
+ /**
8
+ * Native side of `window.nostr`. WebMessageRouter calls [handle] for
9
+ * `nostr.*` messages that already passed its origin and frame checks.
10
+ *
11
+ * Results are returned as the signer produced them (signEvent returns the
12
+ * signed event JSON, which nip07.js parses). Failures are thrown and mapped
13
+ * to `{ error, message }` by [signerErrorResponse].
14
+ */
16
15
  class NostrBridge(
17
- private val webView: WebView,
18
- private val signerManager: SignerManager
16
+ private val signerProvider: suspend () -> NostrSigner,
17
+ private val log: (String) -> Unit = {},
18
+ private val logError: (String, Throwable) -> Unit = { _, _ -> }
19
19
  ) {
20
20
 
21
- private val mainHandler = Handler(Looper.getMainLooper())
22
-
23
- private val scope = CoroutineScope(
24
- SupervisorJob() + Dispatchers.IO
25
- )
26
-
27
- @JavascriptInterface
28
- fun call(
21
+ suspend fun handle(
29
22
  method: String,
30
- callbackId: String,
31
- payload: String?
32
- ) {
33
- AppDebugLogger.log(
34
-
35
- webView.context,
36
-
37
- "SIGNER",
38
-
39
- "Request: $method"
40
-
41
- )
42
- when (method) {
43
-
44
- "getPublicKey" -> {
45
-
46
- scope.launch {
47
-
48
- try {
49
- val pk =
50
- signerManager
51
- .getConfiguredSigner()
52
- .getPublicKey()
53
- resolve(
54
- callbackId,
55
- pk
56
- )
57
- } catch (e: SignerUnavailableException) {
58
- AppDebugLogger.error(
59
-
60
- webView.context,
61
-
62
- "SIGNER",
63
-
64
- "getPublicKey failed",
65
-
66
- e
67
-
68
- )
69
- resolve(
70
- callbackId,
71
- errorResponse(
72
- "SIGNER_UNAVAILABLE",
73
- e.message ?: "Signer unavailable"
74
- )
75
- )
76
-
77
- } catch (e: Exception) {
78
- AppDebugLogger.error(
79
-
80
- webView.context,
81
-
82
- "SIGNER",
83
-
84
- "getPublicKey failed",
85
-
86
- e
87
-
88
- )
89
- resolve(
90
- callbackId,
91
- errorResponse(
92
- "SIGNER_ERROR",
93
- e.message ?: "Unknown signer error"
94
- )
95
- )
23
+ params: JSONObject?
24
+ ): String {
25
+ if (method !in METHODS) {
26
+ log("Request: unsupported method")
27
+ throw UnsupportedBridgeMethodException()
28
+ }
29
+ log("Request: $method")
30
+
31
+ return try {
32
+ val result = when (method) {
33
+ "getPublicKey" -> signerProvider().getPublicKey()
34
+ "signEvent" -> {
35
+ val event = requireNotNull(params) {
36
+ "Event payload is empty"
96
37
  }
38
+ signerProvider().signEvent(event.toString())
97
39
  }
98
- }
99
-
100
- "signEvent" -> {
101
- scope.launch {
102
-
103
- try {
104
- require(!payload.isNullOrBlank()) {
105
- "Event payload is empty"
106
- }
107
-
108
- val result =
109
- signerManager
110
- .getConfiguredSigner()
111
- .signEvent(
112
- payload
113
- )
114
-
115
- AppDebugLogger.log(
116
-
117
- webView.context,
118
-
119
- "SIGNER",
120
-
121
- "signEvent success"
122
-
123
- )
124
-
125
- resolve(
126
- callbackId,
127
- result
128
- )
129
-
130
- } catch (e: SignerUnavailableException) {
131
- resolve(
132
- callbackId,
133
- errorResponse(
134
- "SIGNER_UNAVAILABLE",
135
- e.message ?: "Signer unavailable"
136
- )
137
- )
138
-
139
- } catch (e: Exception) {
140
- AppDebugLogger.error(
141
-
142
- webView.context,
143
-
144
- "SIGNER",
145
-
146
- "signEvent failed",
147
-
148
- e
149
-
150
- )
151
- resolve(
152
- callbackId,
153
- errorResponse(
154
- "SIGNER_ERROR",
155
- e.message ?: "Unknown signer error"
156
- )
157
- )
40
+ "nip04_encrypt" -> {
41
+ val json = requireNotNull(params) {
42
+ "NIP04 encrypt payload is empty"
158
43
  }
44
+ val pubkey = json.getString("pubkey")
45
+ val plaintext = json.getString("plaintext")
46
+ signerProvider().nip04Encrypt(pubkey, plaintext)
159
47
  }
160
- }
161
-
162
- "nip04_encrypt" -> {
163
-
164
- scope.launch {
165
-
166
- try {
167
-
168
- require(!payload.isNullOrBlank()) {
169
- "NIP04 encrypt payload is empty"
170
- }
171
-
172
- val json =
173
- JSONObject(payload)
174
-
175
- val pubkey =
176
- json.getString("pubkey")
177
-
178
- val plaintext =
179
- json.getString("plaintext")
180
-
181
- val result =
182
- signerManager
183
- .getConfiguredSigner()
184
- .nip04Encrypt(
185
- pubkey,
186
- plaintext
187
- )
188
-
189
- AppDebugLogger.log(
190
-
191
- webView.context,
192
-
193
- "SIGNER",
194
-
195
- "nip04_encrypt success"
196
-
197
- )
198
- resolve(
199
- callbackId,
200
- result
201
- )
202
-
203
- } catch (e: SignerUnavailableException) {
204
-
205
- resolve(
206
- callbackId,
207
- errorResponse(
208
- "SIGNER_UNAVAILABLE",
209
- e.message ?: "Signer unavailable"
210
- )
211
- )
212
-
213
- } catch (e: Exception) {
214
- AppDebugLogger.error(
215
-
216
- webView.context,
217
-
218
- "SIGNER",
219
-
220
- "nip04_encrypt failed",
221
-
222
- e
223
-
224
- )
225
- resolve(
226
- callbackId,
227
- errorResponse(
228
- "SIGNER_ERROR",
229
- e.message ?: "Unknown signer error"
230
- )
231
- )
48
+ "nip04_decrypt" -> {
49
+ val json = requireNotNull(params) {
50
+ "NIP04 decrypt payload is empty"
232
51
  }
52
+ val pubkey = json.getString("pubkey")
53
+ val ciphertext = json.getString("ciphertext")
54
+ signerProvider().nip04Decrypt(pubkey, ciphertext)
233
55
  }
234
- }
235
-
236
- "nip04_decrypt" -> {
237
-
238
- scope.launch {
239
-
240
- try {
241
-
242
- require(!payload.isNullOrBlank()) {
243
- "NIP04 decrypt payload is empty"
244
- }
245
-
246
- val json =
247
- JSONObject(payload)
248
-
249
- val pubkey =
250
- json.getString("pubkey")
251
-
252
- val ciphertext =
253
- json.getString("ciphertext")
254
-
255
- val result =
256
- signerManager
257
- .getConfiguredSigner()
258
- .nip04Decrypt(
259
- pubkey,
260
- ciphertext
261
- )
262
-
263
- AppDebugLogger.log(
264
-
265
- webView.context,
266
-
267
- "SIGNER",
268
-
269
- "nip04_decrypt success"
270
-
271
- )
272
- resolve(
273
- callbackId,
274
- result
275
- )
276
-
277
- } catch (e: SignerUnavailableException) {
278
-
279
- resolve(
280
- callbackId,
281
- errorResponse(
282
- "SIGNER_UNAVAILABLE",
283
- e.message ?: "Signer unavailable"
284
- )
285
- )
286
-
287
- } catch (e: Exception) {
288
- AppDebugLogger.error(
289
-
290
- webView.context,
291
-
292
- "SIGNER",
293
-
294
- "nip04_decrypt failed",
295
-
296
- e
297
-
298
- )
299
- resolve(
300
- callbackId,
301
- errorResponse(
302
- "SIGNER_ERROR",
303
- e.message ?: "Unknown signer error"
304
- )
305
- )
306
- }
56
+ "nip44_encrypt" -> {
57
+ val json = params ?: JSONObject()
58
+ val pubkey = json.getString("pubkey")
59
+ val plaintext = json.getString("plaintext")
60
+ signerProvider().nip44Encrypt(pubkey, plaintext)
307
61
  }
308
- }
309
-
310
- "nip44_encrypt" -> {
311
-
312
- scope.launch {
313
-
314
- try {
315
-
316
- val json =
317
- JSONObject(payload ?: "")
318
-
319
- val result =
320
- signerManager
321
- .getConfiguredSigner()
322
- .nip44Encrypt(
323
- json.getString("pubkey"),
324
- json.getString("plaintext")
325
- )
326
- AppDebugLogger.log(
327
-
328
- webView.context,
329
-
330
- "SIGNER",
331
-
332
- "nip44_encrypt success"
333
-
334
- )
335
- resolve(
336
- callbackId,
337
- result
338
- )
339
-
340
- } catch (e: SignerUnavailableException) {
341
- resolve(
342
- callbackId,
343
- signerErrorResponse(e)
344
- )
345
- } catch (e: Exception) {
346
- AppDebugLogger.error(
347
-
348
- webView.context,
349
-
350
- "SIGNER",
351
-
352
- "nip44_encrypt failed",
353
-
354
- e
355
-
356
- )
357
- resolve(
358
- callbackId,
359
- signerErrorResponse(e)
360
- )
361
- }
62
+ "nip44_decrypt" -> {
63
+ val json = params ?: JSONObject()
64
+ val pubkey = json.getString("pubkey")
65
+ val ciphertext = json.getString("ciphertext")
66
+ signerProvider().nip44Decrypt(pubkey, ciphertext)
362
67
  }
68
+ else -> throw UnsupportedBridgeMethodException()
363
69
  }
364
-
365
- "nip44_decrypt" -> {
366
-
367
- scope.launch {
368
-
369
- try {
370
-
371
- val json =
372
- JSONObject(payload ?: "")
373
-
374
- val result =
375
- signerManager
376
- .getConfiguredSigner()
377
- .nip44Decrypt(
378
- json.getString("pubkey"),
379
- json.getString("ciphertext")
380
- )
381
- AppDebugLogger.log(
382
-
383
- webView.context,
384
-
385
- "SIGNER",
386
-
387
- "nip44_decrypt success"
388
-
389
- )
390
- resolve(
391
- callbackId,
392
- result
393
- )
394
-
395
- } catch (e: SignerUnavailableException) {
396
- resolve(
397
- callbackId,
398
- signerErrorResponse(e)
399
- )
400
- } catch (e: Exception) {
401
- AppDebugLogger.error(
402
-
403
- webView.context,
404
-
405
- "SIGNER",
406
-
407
- "nip44_decrypt failed",
408
-
409
- e
410
-
411
- )
412
- resolve(
413
- callbackId,
414
- signerErrorResponse(e)
415
- )
416
- }
417
- }
418
- }
419
-
420
- else -> {
421
-
422
- resolve(
423
- callbackId,
424
- ""
425
- )
426
- }
70
+ log("$method success")
71
+ result
72
+ } catch (e: Exception) {
73
+ logError("$method failed", e)
74
+ throw e
427
75
  }
428
76
  }
429
77
 
430
- @JavascriptInterface
431
- fun debugLog(
432
- level: String,
433
- message: String
434
- ) {
435
-
436
- when (level.uppercase()) {
437
-
438
- "ERROR" -> {
439
-
440
- AppDebugLogger.error(
441
- webView.context,
442
- "WEB-JS",
443
- message
444
- )
445
-
446
- }
447
-
448
- "WARN" -> {
449
-
450
- AppDebugLogger.log(
451
- webView.context,
452
- "WEB-WARN",
453
- message
454
- )
455
-
456
- }
457
-
458
- else -> {
459
-
460
- AppDebugLogger.log(
461
- webView.context,
462
- "WEB",
463
- message
464
- )
465
-
466
- }
467
- }
468
- }
469
-
470
- private fun resolve(
471
- callbackId: String,
472
- result: String
473
- ) {
474
-
475
- mainHandler.post {
476
-
477
- val js =
478
- """
479
- window.__nostrResolve(
480
- ${JSONObject.quote(callbackId)},
481
- ${JSONObject.quote(result)}
482
- );
483
- """.trimIndent()
484
-
485
- webView.evaluateJavascript(js, null)
486
- }
487
- }
488
-
489
- private fun errorResponse(
490
- code: String,
491
- message: String
492
- ): String {
493
-
494
- return JSONObject()
495
- .apply {
496
-
497
- put(
498
- "error",
499
- code
500
- )
501
-
502
- put(
503
- "message",
504
- message
505
- )
506
-
507
- }
508
- .toString()
509
- }
510
-
511
78
  companion object {
79
+ private val METHODS = setOf(
80
+ "getPublicKey",
81
+ "signEvent",
82
+ "nip04_encrypt",
83
+ "nip04_decrypt",
84
+ "nip44_encrypt",
85
+ "nip44_decrypt"
86
+ )
87
+
512
88
  internal fun signerErrorResponse(error: Exception): String {
513
89
  return JSONObject()
514
90
  .put(
@@ -531,3 +107,7 @@ class NostrBridge(
531
107
  }
532
108
  }
533
109
  }
110
+
111
+ class UnsupportedBridgeMethodException : IllegalArgumentException(
112
+ "Unsupported method"
113
+ )
@@ -39,6 +39,10 @@ class SignerManager(
39
39
  LocalAccountStore(this.context)
40
40
  }
41
41
 
42
+ // One channel per launcher, shared by every AmberSigner instance, so a
43
+ // replaced signer's Amber activity still holds the slot until it returns.
44
+ private val amberChannel = AmberRequestChannel()
45
+
42
46
  private var activeSigner: NostrSigner? = null
43
47
  private var currentUserPubkey: String? = null
44
48
  private var pendingPairing: PendingPairing? = null
@@ -317,7 +321,13 @@ class SignerManager(
317
321
  "AMBER",
318
322
  "Activity result received: ${result.resultCode}"
319
323
  )
320
- (activeSigner as? AmberSigner)?.onResult(result)
324
+ if (!amberChannel.deliver(result)) {
325
+ AppDebugLogger.log(
326
+ context,
327
+ "AMBER",
328
+ "Activity result ignored: no matching request"
329
+ )
330
+ }
321
331
  }
322
332
 
323
333
  private fun createAmberSigner(): AmberSigner {
@@ -336,7 +346,7 @@ class SignerManager(
336
346
  "Amber signer application is not installed"
337
347
  )
338
348
  }
339
- return AmberSigner(amberLauncher)
349
+ return AmberSigner(amberLauncher, amberChannel)
340
350
  }
341
351
 
342
352
  private fun createBunkerSigner(): BunkerSigner {
@@ -427,6 +437,7 @@ class SignerManager(
427
437
 
428
438
  private fun closeActiveSigner() {
429
439
  (activeSigner as? BunkerSigner)?.close()
440
+ (activeSigner as? AmberSigner)?.close()
430
441
  activeSigner = null
431
442
  currentUserPubkey = null
432
443
  }