From f5219cfbee2b554d4d4bdc2439f1ced7241b0cca Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 4 Aug 2026 18:22:21 +0200 Subject: [PATCH 1/5] fix: require private Paykit auth scope --- Bitkit/Info.plist | 2 ++ Bitkit/Models/PubkyAuthRequest.swift | 12 +++++++++--- BitkitTests/PubkyAuthRequestTests.swift | 21 +++++++++++++++++++++ changelog.d/next/pending.fixed.md | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 changelog.d/next/pending.fixed.md diff --git a/Bitkit/Info.plist b/Bitkit/Info.plist index 5ae0d535c..e7804ec2b 100644 --- a/Bitkit/Info.plist +++ b/Bitkit/Info.plist @@ -21,6 +21,8 @@ E2E_BACKEND $(E2E_BACKEND) + E2E_HOMEGATE_URL + $(E2E_HOMEGATE_URL) E2E_NETWORK $(E2E_NETWORK) TREZOR_BRIDGE diff --git a/Bitkit/Models/PubkyAuthRequest.swift b/Bitkit/Models/PubkyAuthRequest.swift index aaca814e5..23efee576 100644 --- a/Bitkit/Models/PubkyAuthRequest.swift +++ b/Bitkit/Models/PubkyAuthRequest.swift @@ -5,7 +5,13 @@ enum PubkyAuthClaim: String, Equatable { case watchOnlyAccountV1 = "watch-only-account-v1" static let queryParameter = "x-bitkit-claim" - static let watchOnlyAccountCapabilities = "/pub/paykit/v0/bitkit/server/:rw" + static let watchOnlyAccountCapabilities = "/pub/paykit/v0/bitkit/server/:rw,/pub/paykit/v0/private/bitkit/server/:rw" + private static let watchOnlyAccountCapabilitySet = Set(watchOnlyAccountCapabilities.split(separator: ",").map(String.init)) + + static func matchesWatchOnlyAccountCapabilities(_ capabilities: String) -> Bool { + let requestedCapabilitySet = Set(capabilities.split(separator: ",").map(String.init)) + return requestedCapabilitySet == watchOnlyAccountCapabilitySet + } } enum PubkyAuthRequestError: Error, Equatable { @@ -79,7 +85,7 @@ struct PubkyAuthRequest { throw PubkyAuthRequestError.duplicateBitkitClaim } guard let claimValue = claimValues.first else { - if capabilities == PubkyAuthClaim.watchOnlyAccountCapabilities { + if PubkyAuthClaim.matchesWatchOnlyAccountCapabilities(capabilities) { throw PubkyAuthRequestError.missingBitkitClaim } return nil @@ -87,7 +93,7 @@ struct PubkyAuthRequest { guard let claim = PubkyAuthClaim(rawValue: claimValue) else { throw PubkyAuthRequestError.unsupportedBitkitClaim(claimValue) } - guard capabilities == PubkyAuthClaim.watchOnlyAccountCapabilities else { + guard PubkyAuthClaim.matchesWatchOnlyAccountCapabilities(capabilities) else { throw PubkyAuthRequestError.invalidBitkitClaimCapabilities } diff --git a/BitkitTests/PubkyAuthRequestTests.swift b/BitkitTests/PubkyAuthRequestTests.swift index 0539982f5..a45b423ff 100644 --- a/BitkitTests/PubkyAuthRequestTests.swift +++ b/BitkitTests/PubkyAuthRequestTests.swift @@ -33,6 +33,18 @@ final class PubkyAuthRequestTests: XCTestCase { XCTAssertEqual(request.bitkitClaim, .watchOnlyAccountV1) } + func testParseUrlRecognizesWatchOnlyAccountClaimWithReorderedCapabilities() throws { + let capabilities = PubkyAuthClaim.watchOnlyAccountCapabilities + .split(separator: ",") + .reversed() + .joined(separator: ",") + let url = authUrl(capabilities: capabilities, claimValues: [PubkyAuthClaim.watchOnlyAccountV1.rawValue]) + + let request = try PubkyAuthRequest.parse(url: url) + + XCTAssertEqual(request.bitkitClaim, .watchOnlyAccountV1) + } + func testParseUrlWithoutBitkitClaimPreservesNormalAuth() throws { let request = try PubkyAuthRequest.parse(url: authUrl(capabilities: "/pub/bitkit.to/:rw")) @@ -74,6 +86,15 @@ final class PubkyAuthRequestTests: XCTestCase { } } + func testParseUrlRejectsWatchOnlyClaimWithoutPrivateCapability() { + let capabilities = "/pub/paykit/v0/bitkit/server/:rw" + let url = authUrl(capabilities: capabilities, claimValues: [PubkyAuthClaim.watchOnlyAccountV1.rawValue]) + + XCTAssertThrowsError(try PubkyAuthRequest.parse(url: url)) { + XCTAssertEqual($0 as? PubkyAuthRequestError, .invalidBitkitClaimCapabilities) + } + } + // MARK: - parseCapabilities func testParseCapabilitiesSingleEntry() { diff --git a/changelog.d/next/pending.fixed.md b/changelog.d/next/pending.fixed.md new file mode 100644 index 000000000..5e47caa71 --- /dev/null +++ b/changelog.d/next/pending.fixed.md @@ -0,0 +1 @@ +Fixed Paykit Server authorization to require both public and private payment capabilities. From b0dee53384f704d01f2e74ed7e8f0076153ba2bb Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 4 Aug 2026 19:10:58 +0200 Subject: [PATCH 2/5] chore: name changelog fragment --- changelog.d/next/{pending.fixed.md => 653.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{pending.fixed.md => 653.fixed.md} (100%) diff --git a/changelog.d/next/pending.fixed.md b/changelog.d/next/653.fixed.md similarity index 100% rename from changelog.d/next/pending.fixed.md rename to changelog.d/next/653.fixed.md From 8fc8700431d4731330a9b8bc4dca405aa624257a Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 4 Aug 2026 20:46:14 +0200 Subject: [PATCH 3/5] feat: use Paykit local testnet in E2E builds --- Bitkit.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Bitkit/Constants/Env.swift | 2 +- Bitkit/Services/PubkyService.swift | 15 ++++++++++++--- BitkitTests/PaykitSdkClientConfigTests.swift | 19 +++++++++++++++++++ 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 BitkitTests/PaykitSdkClientConfigTests.swift diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index f5f46c539..3b9f89be1 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1175,7 +1175,7 @@ repositoryURL = "https://github.com/pubky/paykit-rs"; requirement = { kind = exactVersion; - version = "0.1.0-rc39"; + version = "0.1.0-rc41"; }; }; 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ab1ee9a73..60ea04688 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pubky/paykit-rs", "state" : { - "revision" : "2fa056570bd5f93c166e43cf16c5356f6407cbbd", - "version" : "0.1.0-rc39" + "revision" : "f6d43c33ef479051a9e4b54c0d1c48e3b170cbaa", + "version" : "0.1.0-rc41" } }, { diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index c0dfbc4b6..a12ea7278 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -71,7 +71,7 @@ enum Env { (infoPlistValue("E2E_BACKEND") ?? "local").lowercased() } - private static var isLocalE2EBackend: Bool { + static var isLocalE2EBackend: Bool { isE2E && e2eBackend == "local" } diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift index 3ad14422c..2957c325f 100644 --- a/Bitkit/Services/PubkyService.swift +++ b/Bitkit/Services/PubkyService.swift @@ -789,11 +789,12 @@ actor PaykitSdkService { return sdk } - let created = try PaykitSdk.withPaymentAdapter( + let created = try PaykitSdk.withPaymentAdapterAndPubkyClientConfig( stateStore: stateStore, sessionProvider: sessionProvider, paymentAdapter: paymentAdapter, - config: Self.config() + config: Self.config(), + pubkyClient: Self.pubkyClientConfig() ) sdk = created return created @@ -925,7 +926,15 @@ actor PaykitSdkService { } private func bootstrap() throws -> PubkySessionBootstrap { - try PubkySessionBootstrap() + try PubkySessionBootstrap.withPubkyClientConfig(pubkyClient: Self.pubkyClientConfig()) + } + + nonisolated static func pubkyClientConfig(isLocalE2EBackend: Bool = Env.isLocalE2EBackend) -> PubkyClientConfig { + var config = Paykit.defaultPubkyClientConfig() + if isLocalE2EBackend { + config.environment = .localTestnet + } + return config } private nonisolated static func config() throws -> PaykitSdkConfig { diff --git a/BitkitTests/PaykitSdkClientConfigTests.swift b/BitkitTests/PaykitSdkClientConfigTests.swift new file mode 100644 index 000000000..9d1453b29 --- /dev/null +++ b/BitkitTests/PaykitSdkClientConfigTests.swift @@ -0,0 +1,19 @@ +@testable import Bitkit +import Paykit +import XCTest + +final class PaykitSdkClientConfigTests: XCTestCase { + func testProductionUsesDefaultPubkyClient() { + let config = PaykitSdkService.pubkyClientConfig(isLocalE2EBackend: false) + + XCTAssertEqual(config.environment, .production) + XCTAssertNil(config.testnetHost) + } + + func testLocalE2EUsesLocalPubkyTestnet() { + let config = PaykitSdkService.pubkyClientConfig(isLocalE2EBackend: true) + + XCTAssertEqual(config.environment, .localTestnet) + XCTAssertNil(config.testnetHost) + } +} From 0f45cf0a5de8e8c7fedda0724ad17409b29c2e1d Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 5 Aug 2026 10:29:17 -0500 Subject: [PATCH 4/5] chore: update Paykit to rc42 --- Bitkit.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Bitkit/Services/PubkyService.swift | 2 +- BitkitTests/PaykitSdkClientConfigTests.swift | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 3b9f89be1..ecda1896f 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1175,7 +1175,7 @@ repositoryURL = "https://github.com/pubky/paykit-rs"; requirement = { kind = exactVersion; - version = "0.1.0-rc41"; + version = "0.1.0-rc42"; }; }; 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 60ea04688..baa1fcc5e 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pubky/paykit-rs", "state" : { - "revision" : "f6d43c33ef479051a9e4b54c0d1c48e3b170cbaa", - "version" : "0.1.0-rc41" + "revision" : "192fc700897c3792ae4af221591572f3472089e1", + "version" : "0.1.0-rc42" } }, { diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift index 2957c325f..5ce423b8b 100644 --- a/Bitkit/Services/PubkyService.swift +++ b/Bitkit/Services/PubkyService.swift @@ -932,7 +932,7 @@ actor PaykitSdkService { nonisolated static func pubkyClientConfig(isLocalE2EBackend: Bool = Env.isLocalE2EBackend) -> PubkyClientConfig { var config = Paykit.defaultPubkyClientConfig() if isLocalE2EBackend { - config.environment = .localTestnet + config.localTestnetHost = "localhost" } return config } diff --git a/BitkitTests/PaykitSdkClientConfigTests.swift b/BitkitTests/PaykitSdkClientConfigTests.swift index 9d1453b29..2384b4108 100644 --- a/BitkitTests/PaykitSdkClientConfigTests.swift +++ b/BitkitTests/PaykitSdkClientConfigTests.swift @@ -6,14 +6,12 @@ final class PaykitSdkClientConfigTests: XCTestCase { func testProductionUsesDefaultPubkyClient() { let config = PaykitSdkService.pubkyClientConfig(isLocalE2EBackend: false) - XCTAssertEqual(config.environment, .production) - XCTAssertNil(config.testnetHost) + XCTAssertNil(config.localTestnetHost) } func testLocalE2EUsesLocalPubkyTestnet() { let config = PaykitSdkService.pubkyClientConfig(isLocalE2EBackend: true) - XCTAssertEqual(config.environment, .localTestnet) - XCTAssertNil(config.testnetHost) + XCTAssertEqual(config.localTestnetHost, "localhost") } } From 30868138a98cfd06615396344c6b66bf80c57d9d Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 5 Aug 2026 17:17:21 -0500 Subject: [PATCH 5/5] fix: address Pubky auth review --- Bitkit.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- Bitkit/Models/PubkyAuthRequest.swift | 10 ++++++- BitkitTests/PubkyAuthApprovalSheetTests.swift | 26 ++++++++++++------- BitkitTests/PubkyAuthRequestTests.swift | 15 +++++++++++ Docs/watch-only-account-claim-v1.md | 2 +- 6 files changed, 44 insertions(+), 15 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index b2fd14bbb..e2de4f22e 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1175,7 +1175,7 @@ repositoryURL = "https://github.com/pubky/paykit-rs"; requirement = { kind = exactVersion; - version = "0.1.0-rc42"; + version = "0.1.0-rc43"; }; }; 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 30374f26a..1386c2f43 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pubky/paykit-rs", "state" : { - "revision" : "192fc700897c3792ae4af221591572f3472089e1", - "version" : "0.1.0-rc42" + "revision" : "6b241878a9bba5cecea919c0298c3f90624be6ff", + "version" : "0.1.0-rc43" } }, { diff --git a/Bitkit/Models/PubkyAuthRequest.swift b/Bitkit/Models/PubkyAuthRequest.swift index 23efee576..6d04467f2 100644 --- a/Bitkit/Models/PubkyAuthRequest.swift +++ b/Bitkit/Models/PubkyAuthRequest.swift @@ -9,9 +9,17 @@ enum PubkyAuthClaim: String, Equatable { private static let watchOnlyAccountCapabilitySet = Set(watchOnlyAccountCapabilities.split(separator: ",").map(String.init)) static func matchesWatchOnlyAccountCapabilities(_ capabilities: String) -> Bool { - let requestedCapabilitySet = Set(capabilities.split(separator: ",").map(String.init)) + guard let requestedCapabilitySet = capabilitySet(capabilities) else { return false } return requestedCapabilitySet == watchOnlyAccountCapabilitySet } + + private static func capabilitySet(_ capabilities: String) -> Set? { + let entries = capabilities + .split(separator: ",", omittingEmptySubsequences: false) + .map { $0.trimmingCharacters(in: .whitespaces) } + guard !entries.contains(where: \.isEmpty) else { return nil } + return Set(entries) + } } enum PubkyAuthRequestError: Error, Equatable { diff --git a/BitkitTests/PubkyAuthApprovalSheetTests.swift b/BitkitTests/PubkyAuthApprovalSheetTests.swift index 1f67a08fe..cc1d17dc9 100644 --- a/BitkitTests/PubkyAuthApprovalSheetTests.swift +++ b/BitkitTests/PubkyAuthApprovalSheetTests.swift @@ -7,6 +7,12 @@ private let approvalTestXpub = "tpubDDWohsp5dx2iMJ9N7iHbgAEDhH4BJB9NWW1fEW3yA3AFNDREmpzteCXNqppMLUmKFY5q5e3" + "PXtS5CuqWCQbYcGhpPqYAgQSYdwknW9J6sQv" +private func approvalTestAuthUrl(secret: String = "e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s") -> String { + "pubkyauth://signin?caps=\(PubkyAuthClaim.watchOnlyAccountCapabilities)" + + "&relay=https://httprelay.pubky.app/inbox/&secret=\(secret)" + + "&x-bitkit-claim=watch-only-account-v1" +} + final class PubkyAuthApprovalSheetTests: XCTestCase { func testAuthDisplayPublicKeyOmitsPubkyPrefix() { XCTAssertEqual(pubkyAuthDisplayPublicKey("pubky3rsd123456789w5xg"), "3rsd...w5xg") @@ -16,7 +22,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { @MainActor func testWatchOnlyRequestStartsWithSeparateConsentBeforeAuthorization() throws { - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) var state = PubkyAuthApprovalSheet.initialState(for: request) @@ -97,7 +103,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() let manager = Bitkit.WatchOnlyAccountManager(defaults: defaults, node: node) @@ -134,7 +140,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() let manager = Bitkit.WatchOnlyAccountManager(defaults: defaults, node: node) @@ -168,8 +174,8 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let firstAuthUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" - let secondAuthUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=f3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let firstAuthUrl = approvalTestAuthUrl() + let secondAuthUrl = approvalTestAuthUrl(secret: "f3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s") let firstRequest = try PubkyAuthRequest.parse(url: firstAuthUrl) let secondRequest = try PubkyAuthRequest.parse(url: secondAuthUrl) let node = ApprovalFakeWatchOnlyAccountNode() @@ -229,7 +235,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() let manager = Bitkit.WatchOnlyAccountManager(defaults: defaults, node: node) @@ -258,7 +264,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() let manager = Bitkit.WatchOnlyAccountManager(defaults: defaults, node: node) @@ -298,7 +304,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() let initialManager = Bitkit.WatchOnlyAccountManager(defaults: defaults, node: node) @@ -349,7 +355,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() node.failNextTrackingPreparation = true @@ -379,7 +385,7 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) defer { defaults.removePersistentDomain(forName: suiteName) } - let authUrl = "pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1" + let authUrl = approvalTestAuthUrl() let request = try PubkyAuthRequest.parse(url: authUrl) let node = ApprovalFakeWatchOnlyAccountNode() node.checkCancellationWhenDisabling = true diff --git a/BitkitTests/PubkyAuthRequestTests.swift b/BitkitTests/PubkyAuthRequestTests.swift index a45b423ff..39c401f30 100644 --- a/BitkitTests/PubkyAuthRequestTests.swift +++ b/BitkitTests/PubkyAuthRequestTests.swift @@ -45,6 +45,15 @@ final class PubkyAuthRequestTests: XCTestCase { XCTAssertEqual(request.bitkitClaim, .watchOnlyAccountV1) } + func testParseUrlRecognizesWatchOnlyAccountClaimWithCapabilityWhitespace() throws { + let capabilities = PubkyAuthClaim.watchOnlyAccountCapabilities.replacingOccurrences(of: ",", with: " , ") + let url = authUrl(capabilities: capabilities, claimValues: [PubkyAuthClaim.watchOnlyAccountV1.rawValue]) + + let request = try PubkyAuthRequest.parse(url: url) + + XCTAssertEqual(request.bitkitClaim, .watchOnlyAccountV1) + } + func testParseUrlWithoutBitkitClaimPreservesNormalAuth() throws { let request = try PubkyAuthRequest.parse(url: authUrl(capabilities: "/pub/bitkit.to/:rw")) @@ -95,6 +104,12 @@ final class PubkyAuthRequestTests: XCTestCase { } } + func testWatchOnlyCapabilityMatcherRejectsEmptyCapability() { + let capabilities = "\(PubkyAuthClaim.watchOnlyAccountCapabilities)," + + XCTAssertFalse(PubkyAuthClaim.matchesWatchOnlyAccountCapabilities(capabilities)) + } + // MARK: - parseCapabilities func testParseCapabilitiesSingleEntry() { diff --git a/Docs/watch-only-account-claim-v1.md b/Docs/watch-only-account-claim-v1.md index bc043e916..71f15d3e0 100644 --- a/Docs/watch-only-account-claim-v1.md +++ b/Docs/watch-only-account-claim-v1.md @@ -5,7 +5,7 @@ This document records the client contract implemented by Bitkit iOS and Android ## Request - The Pubky Auth URL includes `x-bitkit-claim=watch-only-account-v1`. -- The exact capability is `/pub/paykit/v0/bitkit/server/:rw`. +- The exact capabilities are `/pub/paykit/v0/bitkit/server/:rw` and `/pub/paykit/v0/private/bitkit/server/:rw`. - Missing, unknown, mismatched, or duplicate companion-claim parameters are rejected. - Every distinct auth request creates a fresh native-SegWit account, beginning at BIP84 account index `1`. Account indexes increase monotonically and are never reused. Retrying the same logical auth request reuses its incomplete account even if query parameters are reordered. - Bitkit automatically names the account from the requesting service. The user can rename it later. The local name is not disclosed in the claim.