Commit 61a16cd9a0

61a16cd9a08bffa171f4a1e87ec5561b3e678b07

parent: bbb2bd6769

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-30 06:24 UTC

Explain the expiring-token refusal for keys and emails

An expiring token cannot create credentials (krz/gitbay#257), so adding
an SSH key or email address through the 90-day token the sign-in screen
suggests is always refused. Say what to do instead of showing the
command-shaped server message.

Layout: unified · split

gitbay/Networking/GitbayError.swift +4
@@ -32,6 +32,10 @@ nonisolated enum GitbayError: LocalizedError, Sendable {
3232 /// them loses the explanation.
3333 var userFacingMessage: String {
3434 switch self {
35 // Since krz/gitbay#257 an expiring token cannot create credentials
36 // (SSH keys, email addresses), and the sign-in hint mints one.
37 case .denied(let message) where message.contains("creates a credential"):
38 "This needs a token without an expiry. Add it on the web under Settings, or over SSH."
3539 case .denied(let message), .notFound(let message):
3640 message
3741 case .unauthorized:
gitbayTests/GitbayClientTests.swift +16
@@ -388,3 +388,19 @@ struct ETagStoreTests {
388388 #expect(store.count == 0)
389389 }
390390}
391
392struct ExpiringCredentialRefusalTests {
393
394 @Test func theRefusalSaysWhatToDoInstead() {
395 let error = GitbayError.denied(
396 "keys add creates a credential, and the one this request came with expires; use a token or key without an expiry")
397 #expect(error.userFacingMessage.contains("token without an expiry"))
398 #expect(error.userFacingMessage.contains("Settings"))
399 #expect(!error.userFacingMessage.contains("keys add"))
400 }
401
402 @Test func otherDenialsPassThrough() {
403 #expect(GitbayError.denied("you are not a maintainer of krz/gitbay").userFacingMessage
404 == "you are not a maintainer of krz/gitbay")
405 }
406}
gitbayUITests/LiveSmokeUITests.swift +3 −3
@@ -1006,10 +1006,10 @@ extension LiveSmokeUITests {
10061006 app.descendants(matching: .any).matching(identifier: "key-paste-submit")
10071007 .firstMatch.tap()
10081008 // An expiring token (the suite's are) is refused before the key is
1009 // parsed; either refusal is the server's text reaching the sheet.
1009 // parsed; either refusal reaching the sheet passes.
10101010 XCTAssertTrue(app.staticTexts
10111011 .containing(NSPredicate(format:
1012 "label CONTAINS 'not a valid public key' OR label CONTAINS 'creates a credential'"))
1012 "label CONTAINS 'not a valid public key' OR label CONTAINS 'token without an expiry'"))
10131013 .firstMatch.waitForExistence(timeout: 15), "invalid-key refusal not surfaced")
10141014 app.buttons["Cancel"].firstMatch.tap()
10151015
@@ -1069,7 +1069,7 @@ extension LiveSmokeUITests {
10691069 verify.tap()
10701070 XCTAssertTrue(app.staticTexts
10711071 .containing(NSPredicate(format:
1072 "label CONTAINS 'invalid, expired' OR label CONTAINS 'creates a credential'"))
1072 "label CONTAINS 'invalid, expired' OR label CONTAINS 'token without an expiry'"))
10731073 .firstMatch.waitForExistence(timeout: 15), "bad-code refusal not surfaced")
10741074 }
10751075}