pakstr 0.28.0 → 0.29.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 (27) hide show
  1. package/README.md +10 -0
  2. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +65 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/local/LocalAccountStoreTest.kt +78 -0
  4. package/android-template/app/src/androidTest/java/com/pakstr/app/utils/SignerPickerBottomSheetTest.kt +40 -6
  5. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +124 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip19Codec.kt +181 -0
  7. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +25 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountImportCandidate.kt +42 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountStore.kt +17 -2
  10. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +80 -4
  11. package/android-template/app/src/main/res/layout/activity_developer_tools.xml +15 -0
  12. package/android-template/app/src/main/res/layout/dialog_picker.xml +62 -0
  13. package/android-template/app/src/main/res/values/strings.xml +15 -2
  14. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip19CodecTest.kt +178 -0
  15. package/dist/cli.js +13 -6
  16. package/dist/commands/artifactPublisher.js +125 -0
  17. package/dist/commands/build.js +16 -30
  18. package/dist/commands/publish.js +37 -3
  19. package/dist/commands/run.js +60 -29
  20. package/dist/core/apkSignature.js +197 -0
  21. package/dist/core/nsite.js +49 -0
  22. package/dist/runners/DockerGradleRunner.js +45 -24
  23. package/dist/runners/DockerSignRunner.js +58 -19
  24. package/dist/runners/LocalGradleRunner.js +7 -0
  25. package/dist/runners/process.js +51 -0
  26. package/dist/runners/timeouts.js +66 -0
  27. package/package.json +4 -3
@@ -0,0 +1,42 @@
1
+ package com.pakstr.app.signer.local
2
+
3
+ import com.pakstr.app.crypto.Nip19Codec
4
+ import com.pakstr.app.crypto.Nip46ClientKey
5
+
6
+ internal class LocalAccountImportCandidate private constructor(
7
+ private val key: Nip46ClientKey,
8
+ val npub: String
9
+ ) : AutoCloseable {
10
+
11
+ internal fun <T> withPrivateKey(
12
+ block: (ByteArray) -> T
13
+ ): T = key.withPrivateKey(block)
14
+
15
+ override fun close() {
16
+ key.close()
17
+ }
18
+
19
+ companion object {
20
+ fun parse(nsec: String): LocalAccountImportCandidate {
21
+ val privateKey = Nip19Codec.decodeNsec(nsec)
22
+ return try {
23
+ val key = Nip46ClientKey.fromPrivateKey(privateKey)
24
+ try {
25
+ val publicKey = key.publicKey
26
+ val npub =
27
+ try {
28
+ Nip19Codec.encodeNpub(publicKey)
29
+ } finally {
30
+ publicKey.fill(0)
31
+ }
32
+ LocalAccountImportCandidate(key, npub)
33
+ } catch (error: Exception) {
34
+ key.close()
35
+ throw error
36
+ }
37
+ } finally {
38
+ privateKey.fill(0)
39
+ }
40
+ }
41
+ }
42
+ }
@@ -16,6 +16,9 @@ internal class LocalAccountStorageException(
16
16
  cause: Throwable? = null
17
17
  ) : Exception(message, cause)
18
18
 
19
+ internal class LocalAccountAlreadyExistsException :
20
+ IllegalStateException("Local account already exists")
21
+
19
22
  internal class LocalAccountStore(
20
23
  context: Context,
21
24
  private val preferencesName: String = PREFERENCES_NAME,
@@ -47,6 +50,18 @@ internal class LocalAccountStore(
47
50
  }
48
51
  }
49
52
 
53
+ fun importAccount(privateKey: ByteArray): String =
54
+ synchronized(STORAGE_LOCK) {
55
+ if (hasAccountLocked()) {
56
+ throw LocalAccountAlreadyExistsException()
57
+ }
58
+
59
+ Nip46ClientKey.fromPrivateKey(privateKey).use { key ->
60
+ key.withPrivateKey(::saveNewKey)
61
+ key.publicKeyHex
62
+ }
63
+ }
64
+
50
65
  fun hasAccount(): Boolean =
51
66
  synchronized(STORAGE_LOCK) {
52
67
  hasAccountLocked()
@@ -105,8 +120,8 @@ internal class LocalAccountStore(
105
120
  }
106
121
 
107
122
  private fun saveNewKey(privateKey: ByteArray) {
108
- check(!hasAccountLocked()) {
109
- "Local account already exists"
123
+ if (hasAccountLocked()) {
124
+ throw LocalAccountAlreadyExistsException()
110
125
  }
111
126
 
112
127
  val wrappingKey = getOrCreateWrappingKey()
@@ -5,6 +5,7 @@ import android.os.Bundle
5
5
  import android.view.LayoutInflater
6
6
  import android.view.View
7
7
  import android.view.ViewGroup
8
+ import android.view.WindowManager
8
9
  import android.widget.Button
9
10
  import android.widget.RadioGroup
10
11
  import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@@ -14,9 +15,11 @@ import com.pakstr.app.R
14
15
 
15
16
  class SignerPickerBottomSheet(
16
17
  private val hasBunkerSession: Boolean,
18
+ private val hasLocalAccount: Boolean,
17
19
  private val onAmberSelected: () -> Unit,
18
20
  private val onBunkerSelected: (String) -> Boolean,
19
21
  private val onLocalSelected: () -> Unit,
22
+ private val onLocalImportSelected: (String) -> Boolean,
20
23
  private val onSharePairing: (List<String>) -> Unit,
21
24
  private val onBunkerLogout: () -> Unit,
22
25
  private val onDismissed: () -> Unit = {}
@@ -34,6 +37,11 @@ class SignerPickerBottomSheet(
34
37
  )
35
38
  }
36
39
 
40
+ override fun onStart() {
41
+ super.onStart()
42
+ dialog?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
43
+ }
44
+
37
45
  override fun onViewCreated(
38
46
  view: View,
39
47
  savedInstanceState: Bundle?
@@ -42,6 +50,11 @@ class SignerPickerBottomSheet(
42
50
  val radioGroup = view.findViewById<RadioGroup>(R.id.options_radio_group)
43
51
  val uriContainer = view.findViewById<TextInputLayout>(R.id.bunker_uri_container)
44
52
  val uriInput = view.findViewById<TextInputEditText>(R.id.bunker_uri)
53
+ val localActions = view.findViewById<View>(R.id.local_account_actions)
54
+ val localExisting = view.findViewById<View>(R.id.local_account_existing)
55
+ val localActionGroup = view.findViewById<RadioGroup>(R.id.local_action_radio_group)
56
+ val nsecContainer = view.findViewById<TextInputLayout>(R.id.local_nsec_container)
57
+ val nsecInput = view.findViewById<TextInputEditText>(R.id.local_nsec)
45
58
  val confirm = view.findViewById<Button>(R.id.button_confirm)
46
59
  val sharePairing = view.findViewById<Button>(R.id.button_share_pairing)
47
60
  val logout = view.findViewById<Button>(R.id.button_logout_bunker)
@@ -49,19 +62,58 @@ class SignerPickerBottomSheet(
49
62
  logout.visibility =
50
63
  if (hasBunkerSession) View.VISIBLE else View.GONE
51
64
 
52
- fun updateBunkerControls() {
65
+ fun updateControls() {
53
66
  val bunkerSelected =
54
67
  radioGroup.checkedRadioButtonId == R.id.radio_option_two
68
+ val localSelected =
69
+ radioGroup.checkedRadioButtonId == R.id.radio_option_three
55
70
  uriContainer.visibility =
56
71
  if (bunkerSelected) View.VISIBLE else View.GONE
57
72
  sharePairing.visibility =
58
73
  if (bunkerSelected) View.VISIBLE else View.GONE
74
+ localActions.visibility =
75
+ if (localSelected && !hasLocalAccount) {
76
+ View.VISIBLE
77
+ } else {
78
+ View.GONE
79
+ }
80
+ localExisting.visibility =
81
+ if (localSelected && hasLocalAccount) {
82
+ View.VISIBLE
83
+ } else {
84
+ View.GONE
85
+ }
86
+ nsecContainer.visibility =
87
+ if (
88
+ localSelected &&
89
+ !hasLocalAccount &&
90
+ localActionGroup.checkedRadioButtonId ==
91
+ R.id.radio_local_import
92
+ ) {
93
+ View.VISIBLE
94
+ } else {
95
+ View.GONE
96
+ }
59
97
  }
60
98
  radioGroup.setOnCheckedChangeListener { _, _ ->
61
99
  uriContainer.error = null
62
- updateBunkerControls()
100
+ nsecContainer.error = null
101
+ if (radioGroup.checkedRadioButtonId != R.id.radio_option_three) {
102
+ nsecInput.text?.clear()
103
+ }
104
+ updateControls()
63
105
  }
64
- updateBunkerControls()
106
+ localActionGroup.setOnCheckedChangeListener { _, _ ->
107
+ nsecContainer.error = null
108
+ if (
109
+ localActionGroup.checkedRadioButtonId !=
110
+ R.id.radio_local_import
111
+ ) {
112
+ nsecInput.text?.clear()
113
+ }
114
+ updateControls()
115
+ }
116
+ updateControls()
65
117
 
66
118
  confirm.setOnClickListener {
67
119
  when (radioGroup.checkedRadioButtonId) {
@@ -73,7 +125,24 @@ class SignerPickerBottomSheet(
73
125
  return@setOnClickListener
74
126
  }
75
127
  }
76
- R.id.radio_option_three -> onLocalSelected()
128
+ R.id.radio_option_three -> {
129
+ val importing =
130
+ !hasLocalAccount &&
131
+ localActionGroup.checkedRadioButtonId ==
132
+ R.id.radio_local_import
133
+ if (importing) {
134
+ val value =
135
+ nsecInput.text?.toString().orEmpty().trim()
136
+ if (!onLocalImportSelected(value)) {
137
+ nsecContainer.error =
138
+ getString(R.string.signer_picker_local_invalid)
139
+ return@setOnClickListener
140
+ }
141
+ nsecInput.text?.clear()
142
+ } else {
143
+ onLocalSelected()
144
+ }
145
+ }
77
146
  else -> onAmberSelected()
78
147
  }
79
148
  dismiss()
@@ -104,6 +173,13 @@ class SignerPickerBottomSheet(
104
173
  }
105
174
  }
106
175
 
176
+ override fun onDestroyView() {
177
+ view?.findViewById<TextInputEditText>(R.id.local_nsec)
178
+ ?.text
179
+ ?.clear()
180
+ super.onDestroyView()
181
+ }
182
+
107
183
  override fun onDismiss(dialog: DialogInterface) {
108
184
  super.onDismiss(dialog)
109
185
  onDismissed()
@@ -208,6 +208,21 @@
208
208
  android:layout_height="wrap_content"
209
209
  android:textColor="?android:attr/textColorSecondary" />
210
210
  </LinearLayout>
211
+
212
+ <TextView
213
+ style="@style/TextAppearance.MaterialComponents.Subtitle2"
214
+ android:layout_width="match_parent"
215
+ android:layout_height="wrap_content"
216
+ android:layout_marginTop="12dp"
217
+ android:text="@string/developer_tools_chrome_webview_inspection" />
218
+
219
+ <TextView
220
+ style="@style/TextAppearance.MaterialComponents.Caption"
221
+ android:layout_width="match_parent"
222
+ android:layout_height="wrap_content"
223
+ android:layout_marginTop="4dp"
224
+ android:text="@string/developer_tools_chrome_webview_inspection_note"
225
+ android:textColor="?android:attr/textColorSecondary" />
211
226
  </LinearLayout>
212
227
  </com.google.android.material.card.MaterialCardView>
213
228
 
@@ -97,6 +97,68 @@
97
97
  android:textSize="16sp" />
98
98
  </RadioGroup>
99
99
 
100
+ <LinearLayout
101
+ android:id="@+id/local_account_actions"
102
+ android:layout_width="match_parent"
103
+ android:layout_height="wrap_content"
104
+ android:layout_marginBottom="12dp"
105
+ android:orientation="vertical"
106
+ android:paddingStart="16dp"
107
+ android:paddingEnd="16dp"
108
+ android:visibility="gone">
109
+
110
+ <RadioGroup
111
+ android:id="@+id/local_action_radio_group"
112
+ android:layout_width="match_parent"
113
+ android:layout_height="wrap_content"
114
+ android:orientation="vertical">
115
+
116
+ <RadioButton
117
+ android:id="@+id/radio_local_create"
118
+ android:layout_width="match_parent"
119
+ android:layout_height="wrap_content"
120
+ android:checked="true"
121
+ android:text="@string/signer_picker_local_create" />
122
+
123
+ <RadioButton
124
+ android:id="@+id/radio_local_import"
125
+ android:layout_width="match_parent"
126
+ android:layout_height="wrap_content"
127
+ android:text="@string/signer_picker_local_import" />
128
+ </RadioGroup>
129
+
130
+ <com.google.android.material.textfield.TextInputLayout
131
+ android:id="@+id/local_nsec_container"
132
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
133
+ android:layout_width="match_parent"
134
+ android:layout_height="wrap_content"
135
+ android:layout_marginTop="8dp"
136
+ android:hint="@string/signer_picker_local_nsec_hint"
137
+ android:visibility="gone"
138
+ app:endIconMode="password_toggle">
139
+
140
+ <com.google.android.material.textfield.TextInputEditText
141
+ android:id="@+id/local_nsec"
142
+ android:layout_width="match_parent"
143
+ android:layout_height="wrap_content"
144
+ android:importantForAutofill="no"
145
+ android:inputType="textPassword|textNoSuggestions"
146
+ android:maxLines="1"
147
+ android:saveEnabled="false" />
148
+ </com.google.android.material.textfield.TextInputLayout>
149
+ </LinearLayout>
150
+
151
+ <TextView
152
+ android:id="@+id/local_account_existing"
153
+ android:layout_width="match_parent"
154
+ android:layout_height="wrap_content"
155
+ android:layout_marginBottom="12dp"
156
+ android:paddingStart="16dp"
157
+ android:paddingEnd="16dp"
158
+ android:text="@string/signer_picker_local_existing"
159
+ android:textColor="#4B5563"
160
+ android:visibility="gone" />
161
+
100
162
  <com.google.android.material.textfield.TextInputLayout
101
163
  android:id="@+id/bunker_uri_container"
102
164
  style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
@@ -32,6 +32,8 @@
32
32
  <string name="developer_tools_api_base_save_failed">Unable to persist the API Base URL setting.</string>
33
33
  <string name="developer_tools_api_base_invalid">Enter an absolute HTTPS URL without credentials, query, or fragment.</string>
34
34
  <string name="developer_tools_webview_debug">WebView Debug</string>
35
+ <string name="developer_tools_chrome_webview_inspection">Chrome WebView Inspection</string>
36
+ <string name="developer_tools_chrome_webview_inspection_note">To inspect this app in Chrome DevTools, enable Developer options → USB debugging on your Android device. This is required even when connecting over Wi-Fi.</string>
35
37
  <string name="developer_tools_enabled">Enabled</string>
36
38
  <string name="developer_tools_disabled">Disabled</string>
37
39
 
@@ -66,11 +68,22 @@
66
68
 
67
69
  <string name="signer_settings">Signer settings</string>
68
70
  <string name="signer_picker_title">Choose a signer</string>
69
- <string name="signer_picker_subtitle">Use Amber, connect to a remote signer, or create an app-local account.</string>
71
+ <string name="signer_picker_subtitle">Use Amber, connect to a remote signer, or use an app-local account.</string>
70
72
  <string name="signer_picker_amber">Continue with Amber</string>
71
73
  <string name="signer_picker_bunker">Connect to remote signer (Bunker)</string>
72
- <string name="signer_picker_local">Create app-local account</string>
74
+ <string name="signer_picker_local">App-local account</string>
75
+ <string name="signer_picker_local_create">Create new account</string>
76
+ <string name="signer_picker_local_import">Import existing account</string>
77
+ <string name="signer_picker_local_nsec_hint">Enter your nsec</string>
78
+ <string name="signer_picker_local_existing">Use the existing app-local account on this device.</string>
79
+ <string name="signer_picker_local_invalid">Enter a valid lowercase nsec.</string>
80
+ <string name="signer_picker_local_confirm_title">Confirm account import</string>
81
+ <string name="signer_picker_local_confirm_message">Import this account?\n\n%1$s</string>
82
+ <string name="signer_picker_local_confirm">Import</string>
83
+ <string name="signer_picker_local_cancel">Cancel</string>
73
84
  <string name="signer_picker_local_failed">Unable to create the app-local account.</string>
85
+ <string name="signer_picker_local_status_failed">Unable to check the app-local account.</string>
86
+ <string name="signer_picker_local_import_failed">Unable to import the app-local account.</string>
74
87
  <string name="signer_picker_bunker_hint">Paste a bunker:// connection</string>
75
88
  <string name="signer_picker_share_pairing">Share a nostrconnect:// pairing</string>
76
89
  <string name="signer_picker_logout_bunker">Disconnect remote signer</string>
@@ -0,0 +1,178 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ import com.pakstr.app.signer.local.LocalAccountImportCandidate
4
+ import org.junit.Assert.assertArrayEquals
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertThrows
8
+ import org.junit.Test
9
+
10
+ class Nip19CodecTest {
11
+
12
+ @Test
13
+ fun decodes_known_nsec_vector() {
14
+ val decoded = Nip19Codec.decodeNsec(NSEC_ONE)
15
+
16
+ assertArrayEquals(
17
+ ByteArray(32).also { it[31] = 1 },
18
+ decoded
19
+ )
20
+ decoded.fill(0)
21
+ }
22
+
23
+ @Test
24
+ fun encodes_known_npub_vector() {
25
+ val publicKey = Bip340Hash.hexToBytes(PUBLIC_KEY_ONE)
26
+
27
+ try {
28
+ assertEquals(NPUB_ONE, Nip19Codec.encodeNpub(publicKey))
29
+ } finally {
30
+ publicKey.fill(0)
31
+ }
32
+ }
33
+
34
+ @Test
35
+ fun import_candidate_derives_expected_npub_and_closes_key() {
36
+ val candidate = LocalAccountImportCandidate.parse(NSEC_ONE)
37
+
38
+ assertEquals(NPUB_ONE, candidate.npub)
39
+ assertFalse(candidate.toString().contains(NSEC_ONE))
40
+ candidate.close()
41
+ assertThrows(IllegalStateException::class.java) {
42
+ candidate.withPrivateKey { }
43
+ }
44
+ }
45
+
46
+ @Test
47
+ fun rejects_noncanonical_or_corrupt_nsec() {
48
+ val invalid = listOf(
49
+ NSEC_ONE.uppercase(),
50
+ "Nsec" + NSEC_ONE.substring(4),
51
+ "xsec" + NSEC_ONE.substring(4),
52
+ NSEC_ONE.dropLast(1) + "q",
53
+ NSEC_ONE.replaceRange(8, 9, "i"),
54
+ NSEC_ONE.dropLast(1),
55
+ NSEC_ONE + "q"
56
+ )
57
+
58
+ invalid.forEach { value ->
59
+ assertThrows(InvalidNsecException::class.java) {
60
+ Nip19Codec.decodeNsec(value)
61
+ }
62
+ }
63
+ }
64
+
65
+ @Test
66
+ fun rejects_invalid_private_scalars() {
67
+ val zero = encodeNsecForTest(ByteArray(32))
68
+ val order = Bip340Hash.hexToBytes(SECP256K1_ORDER)
69
+ val aboveOrder = order.copyOf().also {
70
+ it[it.lastIndex] = (it.last().toInt() + 1).toByte()
71
+ }
72
+
73
+ try {
74
+ assertThrows(IllegalArgumentException::class.java) {
75
+ LocalAccountImportCandidate.parse(zero)
76
+ }
77
+ assertThrows(IllegalArgumentException::class.java) {
78
+ LocalAccountImportCandidate.parse(
79
+ encodeNsecForTest(order)
80
+ )
81
+ }
82
+ assertThrows(IllegalArgumentException::class.java) {
83
+ LocalAccountImportCandidate.parse(
84
+ encodeNsecForTest(aboveOrder)
85
+ )
86
+ }
87
+ } finally {
88
+ order.fill(0)
89
+ aboveOrder.fill(0)
90
+ }
91
+ }
92
+
93
+ @Test
94
+ fun rejects_wrong_payload_lengths_and_accepts_leading_zero() {
95
+ assertThrows(InvalidNsecException::class.java) {
96
+ Nip19Codec.decodeNsec(encodeNsecForTest(ByteArray(31)))
97
+ }
98
+ assertThrows(InvalidNsecException::class.java) {
99
+ Nip19Codec.decodeNsec(encodeNsecForTest(ByteArray(33)))
100
+ }
101
+
102
+ LocalAccountImportCandidate.parse(NSEC_ONE).use { candidate ->
103
+ assertEquals(NPUB_ONE, candidate.npub)
104
+ }
105
+ }
106
+
107
+ private fun encodeNsecForTest(bytes: ByteArray): String {
108
+ val data = convertBits(bytes)
109
+ val values = hrpExpand("nsec") + data + IntArray(6)
110
+ val checksum = polymod(values) xor 1
111
+ val check = IntArray(6) { index ->
112
+ (checksum shr (5 * (5 - index))) and 31
113
+ }
114
+ return "nsec1" + (data + check).joinToString("") {
115
+ CHARSET[it].toString()
116
+ }
117
+ }
118
+
119
+ private fun convertBits(bytes: ByteArray): IntArray {
120
+ var accumulator = 0
121
+ var bits = 0
122
+ val output = ArrayList<Int>()
123
+ bytes.forEach { byte ->
124
+ accumulator = (accumulator shl 8) or (byte.toInt() and 0xff)
125
+ bits += 8
126
+ while (bits >= 5) {
127
+ bits -= 5
128
+ output += (accumulator shr bits) and 31
129
+ }
130
+ }
131
+ if (bits > 0) {
132
+ output += (accumulator shl (5 - bits)) and 31
133
+ }
134
+ return output.toIntArray()
135
+ }
136
+
137
+ private fun hrpExpand(hrp: String): IntArray =
138
+ IntArray(hrp.length * 2 + 1).also { result ->
139
+ hrp.forEachIndexed { index, character ->
140
+ result[index] = character.code shr 5
141
+ result[hrp.length + 1 + index] = character.code and 31
142
+ }
143
+ }
144
+
145
+ private fun polymod(values: IntArray): Int {
146
+ var checksum = 1
147
+ val generators = intArrayOf(
148
+ 0x3b6a57b2,
149
+ 0x26508e6d,
150
+ 0x1ea119fa,
151
+ 0x3d4233dd,
152
+ 0x2a1462b3
153
+ )
154
+ values.forEach { value ->
155
+ val top = checksum ushr 25
156
+ checksum = (checksum and 0x1ffffff) shl 5 xor value
157
+ generators.forEachIndexed { index, generator ->
158
+ if ((top shr index) and 1 != 0) {
159
+ checksum = checksum xor generator
160
+ }
161
+ }
162
+ }
163
+ return checksum
164
+ }
165
+
166
+ private companion object {
167
+ const val CHARSET =
168
+ "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
169
+ const val NSEC_ONE =
170
+ "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsmhltgl"
171
+ const val PUBLIC_KEY_ONE =
172
+ "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
173
+ const val NPUB_ONE =
174
+ "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d"
175
+ const val SECP256K1_ORDER =
176
+ "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
177
+ }
178
+ }
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runCLI = runCLI;
7
+ const path_1 = __importDefault(require("path"));
7
8
  const build_1 = require("./commands/build");
8
9
  const sign_1 = require("./commands/sign");
9
10
  const publish_1 = require("./commands/publish");
@@ -15,9 +16,14 @@ const deletePublisher_1 = require("./commands/deletePublisher");
15
16
  const dotenv_1 = require("./core/dotenv");
16
17
  const package_json_1 = __importDefault(require("../package.json"));
17
18
  async function runCLI(argv) {
18
- // Auto-load a local `.env` (gitignored) so `pakstr run` works out of the box
19
- // after `pakstr init`. Existing env vars (e.g. CI secrets) always win.
20
- (0, dotenv_1.loadDotEnv)();
19
+ const command = argv[0];
20
+ const args = argv.slice(1);
21
+ const configFlag = extractFlag(args, "--config");
22
+ // Auto-load the `.env` (gitignored) next to the config file, where
23
+ // `pakstr init` writes it, so `pakstr run` works out of the box. Without
24
+ // --config that is the current directory. Existing env vars (e.g. CI
25
+ // secrets) always win.
26
+ (0, dotenv_1.loadDotEnv)(configDir(configFlag));
21
27
  if (argv.includes("--help") || argv.includes("-h")) {
22
28
  printHelp();
23
29
  return;
@@ -26,13 +32,10 @@ async function runCLI(argv) {
26
32
  console.log(package_json_1.default.version);
27
33
  return;
28
34
  }
29
- const command = argv[0];
30
- const args = argv.slice(1);
31
35
  if (!command) {
32
36
  printHelp();
33
37
  return;
34
38
  }
35
- const configFlag = extractFlag(args, "--config");
36
39
  switch (command) {
37
40
  case "init":
38
41
  await (0, init_1.initCommand)({ force: args.includes("--force"), out: configFlag });
@@ -99,6 +102,10 @@ function extractFlag(args, flag) {
99
102
  return undefined;
100
103
  return args[i + 1];
101
104
  }
105
+ /** Directory of the config file, resolved the same way as loadPakstrConfig. */
106
+ function configDir(configFlag) {
107
+ return configFlag ? path_1.default.dirname(path_1.default.resolve(configFlag)) : process.cwd();
108
+ }
102
109
  function printHelp() {
103
110
  console.log(`
104
111
  Pakstr — one-step nsec -> build -> sign -> publish.