pakstr 0.19.3 → 0.21.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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/android-template/app/build.gradle.kts +4 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
  4. package/android-template/app/src/main/assets/www/index.html +0 -6
  5. package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
  6. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
  7. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
  10. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
  12. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
  13. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
  14. package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
  15. package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
  16. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
  17. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
  18. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
  19. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
  20. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
  21. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
  22. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
  23. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
  24. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
  25. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
  26. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
  27. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
  28. package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
  29. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
  30. package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
  31. package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
  32. package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
  33. package/android-template/app/src/main/res/values/strings.xml +27 -0
  34. package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
  35. package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
  36. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
  37. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
  38. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
  39. package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
  40. package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
  41. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
  42. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
  43. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
  44. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
  45. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
  46. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
  47. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
  48. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
  49. package/android-template/gradle/libs.versions.toml +8 -0
  50. package/dist/cli.js +8 -0
  51. package/dist/commands/init.js +16 -0
  52. package/dist/commands/nsite.js +38 -0
  53. package/dist/commands/publish.js +50 -2
  54. package/dist/commands/run.js +5 -5
  55. package/dist/core/blossom.js +2 -2
  56. package/dist/core/nsite.js +176 -0
  57. package/dist/core/pakstrConfig.js +85 -6
  58. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ import org.junit.Assert.assertArrayEquals
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertThrows
7
+ import org.junit.Test
8
+
9
+ class Nip46ClientKeyProtocolTest {
10
+
11
+ @Test
12
+ fun client_keys_encrypt_and_decrypt_without_mutating_peer_input() {
13
+ val first =
14
+ Nip46ClientKey.fromPrivateKey(
15
+ privateKey(1)
16
+ )
17
+ val second =
18
+ Nip46ClientKey.fromPrivateKey(
19
+ privateKey(2)
20
+ )
21
+
22
+ first.use {
23
+ second.use {
24
+ val peerPublicKey = second.publicKey
25
+ val original = peerPublicKey.copyOf()
26
+ val payload =
27
+ first.encryptNip44(
28
+ "sensitive message",
29
+ second.publicKeyHex
30
+ )
31
+
32
+ assertEquals(
33
+ "sensitive message",
34
+ second.decryptNip44(
35
+ payload,
36
+ first.publicKeyHex
37
+ )
38
+ )
39
+ assertArrayEquals(original, peerPublicKey)
40
+ }
41
+ }
42
+ }
43
+
44
+ @Test
45
+ fun closed_and_invalid_keys_fail_with_safe_errors() {
46
+ val key = Nip46ClientKey.generate()
47
+ key.close()
48
+
49
+ val closedError =
50
+ assertThrows(IllegalStateException::class.java) {
51
+ key.encryptNip44(
52
+ "private plaintext",
53
+ "f".repeat(64)
54
+ )
55
+ }
56
+ assertFalse(
57
+ closedError.message.orEmpty()
58
+ .contains("private plaintext")
59
+ )
60
+
61
+ val active = Nip46ClientKey.generate()
62
+ active.use {
63
+ val invalidError =
64
+ assertThrows(IllegalArgumentException::class.java) {
65
+ active.encryptNip44(
66
+ "private plaintext",
67
+ "not-a-pubkey"
68
+ )
69
+ }
70
+ assertFalse(
71
+ invalidError.message.orEmpty()
72
+ .contains("private plaintext")
73
+ )
74
+ }
75
+ }
76
+
77
+ private fun privateKey(value: Int): ByteArray {
78
+ return ByteArray(32).also {
79
+ it[31] = value.toByte()
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,52 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertThrows
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ class Nip46ClientKeyTest {
10
+
11
+ @Test
12
+ fun generated_key_signs_with_its_public_identity() {
13
+ Nip46ClientKey.generate().use { key ->
14
+ val message = Bip340Hash.sha256("message".toByteArray())
15
+ val signature = key.sign(message)
16
+
17
+ assertEquals(32, key.publicKey.size)
18
+ assertTrue(key.publicKeyHex.matches(Regex("[0-9a-f]{64}")))
19
+ assertTrue(
20
+ Bip340Verifier.verify(
21
+ key.publicKeyHex,
22
+ Bip340Hash.bytesToHex(message),
23
+ Bip340Hash.bytesToHex(signature)
24
+ )
25
+ )
26
+ }
27
+ }
28
+
29
+ @Test
30
+ fun closed_key_rejects_signing() {
31
+ val key = Nip46ClientKey.generate()
32
+ key.close()
33
+
34
+ assertThrows(IllegalStateException::class.java) {
35
+ key.sign(ByteArray(32))
36
+ }
37
+ }
38
+
39
+ @Test
40
+ fun diagnostics_do_not_expose_private_key() {
41
+ val privateKey =
42
+ Bip340Hash.hexToBytes(
43
+ "0000000000000000000000000000000000000000000000000000000000000003"
44
+ )
45
+ val privateKeyHex = Bip340Hash.bytesToHex(privateKey)
46
+ val key = Nip46ClientKey.fromPrivateKey(privateKey)
47
+
48
+ key.use {
49
+ assertFalse(key.toString().contains(privateKeyHex))
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,42 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Test
7
+
8
+ class NostrEventSerializerTest {
9
+
10
+ @Test
11
+ fun serializes_nip01_event_with_exact_required_escaping() {
12
+ val content =
13
+ "quote\" slash\\ backspace\b formfeed " +
14
+ " newline\n return\r tab\t </ " +
15
+ "

 café"
16
+ val tagValue = "tag\"\\</
"
17
+ val event =
18
+ JSONObject()
19
+ .put("pubkey", "a".repeat(64))
20
+ .put("created_at", 1L)
21
+ .put("kind", 24133)
22
+ .put(
23
+ "tags",
24
+ JSONArray()
25
+ .put(
26
+ JSONArray()
27
+ .put("p")
28
+ .put(tagValue)
29
+ )
30
+ )
31
+ .put("content", content)
32
+
33
+ assertEquals(
34
+ "[0,\"${"a".repeat(64)}\",1,24133," +
35
+ "[[\"p\",\"tag\\\"\\\\</
\"]]," +
36
+ "\"quote\\\" slash\\\\ backspace\\b " +
37
+ "formfeed\\f newline\\n return\\r tab\\t " +
38
+ "</ 

 café\"]",
39
+ NostrEventSerializer.serialize(event)
40
+ )
41
+ }
42
+ }
@@ -0,0 +1,32 @@
1
+ package com.pakstr.app.signer
2
+
3
+ import com.pakstr.app.signer.exceptions.SignerUnavailableException
4
+ import org.json.JSONObject
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Test
7
+
8
+ class NostrBridgeErrorTest {
9
+ @Test
10
+ fun signer_unavailable_uses_availability_error_code() {
11
+ val response =
12
+ NostrBridge.signerErrorResponse(
13
+ SignerUnavailableException("not connected")
14
+ )
15
+
16
+ val json = JSONObject(response)
17
+ assertEquals("SIGNER_UNAVAILABLE", json.getString("error"))
18
+ assertEquals("not connected", json.getString("message"))
19
+ }
20
+
21
+ @Test
22
+ fun other_failures_use_generic_signer_error_code() {
23
+ val response =
24
+ NostrBridge.signerErrorResponse(
25
+ IllegalStateException("failed")
26
+ )
27
+
28
+ val json = JSONObject(response)
29
+ assertEquals("SIGNER_ERROR", json.getString("error"))
30
+ assertEquals("failed", json.getString("message"))
31
+ }
32
+ }
@@ -0,0 +1,120 @@
1
+ package com.pakstr.app.signer.bunker
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertNull
6
+ import org.junit.Assert.assertThrows
7
+ import org.junit.Test
8
+
9
+ class BunkerConfigTest {
10
+
11
+ @Test
12
+ fun parses_valid_bunker_connection() {
13
+ val config =
14
+ BunkerConfig.parse(
15
+ "bunker://$VALID_PUBKEY" +
16
+ "?relay=wss://relay.one" +
17
+ "&relay=wss%3A%2F%2Frelay.two%2Fnostr" +
18
+ "&secret=pairing%20secret"
19
+ )
20
+
21
+ assertEquals(VALID_PUBKEY, config.remoteSignerPubkey)
22
+ assertEquals(
23
+ listOf(
24
+ "wss://relay.one",
25
+ "wss://relay.two/nostr"
26
+ ),
27
+ config.relayUrls
28
+ )
29
+ assertEquals("pairing secret", config.pairingSecret)
30
+ }
31
+
32
+ @Test
33
+ fun accepts_connection_without_pairing_secret() {
34
+ val config =
35
+ BunkerConfig.parse(
36
+ "bunker://$VALID_PUBKEY?relay=wss://relay.example"
37
+ )
38
+
39
+ assertNull(config.pairingSecret)
40
+ }
41
+
42
+ @Test
43
+ fun rejects_non_secure_relay() {
44
+ assertThrows(IllegalArgumentException::class.java) {
45
+ BunkerConfig.parse(
46
+ "bunker://$VALID_PUBKEY?relay=ws://relay.example"
47
+ )
48
+ }
49
+ }
50
+
51
+ @Test
52
+ fun rejects_duplicate_secrets_and_unsafe_uri_components() {
53
+ listOf(
54
+ "bunker://$VALID_PUBKEY?relay=wss://relay.example&secret=one&secret=two",
55
+ "bunker://user@$VALID_PUBKEY?relay=wss://relay.example",
56
+ "bunker://$VALID_PUBKEY/path?relay=wss://relay.example",
57
+ "bunker://$VALID_PUBKEY?relay=wss://relay.example#fragment"
58
+ ).forEach { value ->
59
+ assertThrows(IllegalArgumentException::class.java) {
60
+ BunkerConfig.parse(value)
61
+ }
62
+ }
63
+ }
64
+
65
+ @Test
66
+ fun rejects_missing_relay() {
67
+ assertThrows(IllegalArgumentException::class.java) {
68
+ BunkerConfig.parse(
69
+ "bunker://$VALID_PUBKEY"
70
+ )
71
+ }
72
+ }
73
+
74
+ @Test
75
+ fun rejects_malformed_public_key() {
76
+ assertThrows(IllegalArgumentException::class.java) {
77
+ BunkerConfig.parse(
78
+ "bunker://not-a-pubkey?relay=wss://relay.example"
79
+ )
80
+ }
81
+ }
82
+
83
+ @Test
84
+ fun rejects_public_key_outside_curve_field() {
85
+ assertThrows(IllegalArgumentException::class.java) {
86
+ BunkerConfig.parse(
87
+ "bunker://${"f".repeat(64)}?relay=wss://relay.example"
88
+ )
89
+ }
90
+ }
91
+
92
+ @Test
93
+ fun pairing_secret_is_not_exposed_by_diagnostics_or_errors() {
94
+ val secret = "unique-pairing-secret"
95
+ val config =
96
+ BunkerConfig.parse(
97
+ "bunker://$VALID_PUBKEY" +
98
+ "?relay=wss://relay.example" +
99
+ "&secret=$secret"
100
+ )
101
+
102
+ assertFalse(config.toString().contains(secret))
103
+
104
+ val error =
105
+ assertThrows(IllegalArgumentException::class.java) {
106
+ BunkerConfig.parse(
107
+ "bunker://$VALID_PUBKEY" +
108
+ "?relay=ws://relay.example" +
109
+ "&secret=$secret"
110
+ )
111
+ }
112
+
113
+ assertFalse(error.message.orEmpty().contains(secret))
114
+ }
115
+
116
+ companion object {
117
+ private const val VALID_PUBKEY =
118
+ "cb227f60cb2a6d982ecbfc51b9199d340afc00b1ef81b67d8e3778e097e70d2c"
119
+ }
120
+ }
@@ -0,0 +1,111 @@
1
+ package com.pakstr.app.signer.bunker
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertThrows
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Test
10
+
11
+ class Nip46ModelsTest {
12
+
13
+ @Test
14
+ fun request_round_trip_preserves_method_and_string_params() {
15
+ val request =
16
+ Nip46Request(
17
+ REQUEST_ID,
18
+ Nip46Method.NIP44_ENCRYPT,
19
+ listOf(PUBKEY, "private plaintext")
20
+ )
21
+
22
+ val parsed = Nip46Request.parse(request.toJson())
23
+
24
+ assertEquals(REQUEST_ID, parsed.id)
25
+ assertEquals(Nip46Method.NIP44_ENCRYPT, parsed.method)
26
+ assertEquals(2, parsed.params.size)
27
+ assertFalse(request.toString().contains("private plaintext"))
28
+ }
29
+
30
+ @Test
31
+ fun empty_params_are_serialized_as_array() {
32
+ val encoded =
33
+ JSONObject(
34
+ Nip46Request(
35
+ REQUEST_ID,
36
+ Nip46Method.GET_PUBLIC_KEY,
37
+ emptyList()
38
+ ).toJson()
39
+ )
40
+
41
+ assertEquals(0, encoded.getJSONArray("params").length())
42
+ }
43
+
44
+ @Test
45
+ fun response_parsing_is_strict_and_safe() {
46
+ val secretResult = "sensitive result"
47
+ val response =
48
+ Nip46Response.parse(
49
+ JSONObject()
50
+ .put("id", REQUEST_ID)
51
+ .put("result", secretResult)
52
+ .toString()
53
+ )
54
+
55
+ assertEquals(secretResult, response.result)
56
+ assertFalse(response.toString().contains(secretResult))
57
+
58
+ assertThrows(Nip46ProtocolException::class.java) {
59
+ Nip46Response.parse(
60
+ JSONObject()
61
+ .put("id", REQUEST_ID)
62
+ .put("result", 42)
63
+ .toString()
64
+ )
65
+ }
66
+ }
67
+
68
+ @Test
69
+ fun malformed_json_unknown_methods_and_invalid_ids_are_rejected() {
70
+ assertThrows(Nip46ProtocolException::class.java) {
71
+ Nip46Request.parse("not-json")
72
+ }
73
+ assertThrows(Nip46ProtocolException::class.java) {
74
+ Nip46Request.parse(
75
+ JSONObject()
76
+ .put("id", REQUEST_ID)
77
+ .put("method", "unknown")
78
+ .put("params", JSONArray())
79
+ .toString()
80
+ )
81
+ }
82
+ assertThrows(Nip46ProtocolException::class.java) {
83
+ Nip46Request(
84
+ "predictable",
85
+ Nip46Method.CONNECT,
86
+ emptyList()
87
+ ).toJson()
88
+ }
89
+ }
90
+
91
+ @Test
92
+ fun secure_request_ids_are_canonical_and_unique() {
93
+ val ids =
94
+ (0 until 256)
95
+ .map {
96
+ SecureNip46RequestIdGenerator.nextId()
97
+ }
98
+
99
+ assertTrue(
100
+ ids.all { it.matches(Regex("[0-9a-f]{64}")) }
101
+ )
102
+ assertEquals(ids.size, ids.toSet().size)
103
+ }
104
+
105
+ companion object {
106
+ private const val REQUEST_ID =
107
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
108
+ private const val PUBKEY =
109
+ "cb227f60cb2a6d982ecbfc51b9199d340afc00b1ef81b67d8e3778e097e70d2c"
110
+ }
111
+ }