• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 29509563508

16 Jul 2026 03:05PM UTC coverage: 83.856% (+0.1%) from 83.725%
29509563508

Pull #1130

github

web-flow
Merge f73f7e3a8 into 0bbdbecba
Pull Request #1130: fix(client,functions): warn instead of throw on unrecognized sb_ key subtype; never send new-format key as Bearer

31 of 43 new or added lines in 2 files covered. (72.09%)

8108 of 9669 relevant lines covered (83.86%)

38.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.73
/Sources/Helpers/APIKeyFormat.swift
1
//
2
//  APIKeyFormat.swift
3
//  Supabase
4
//
5
//  Created by Claude on 16/07/26.
6
//
7

8
import ConcurrencyExtras
9
import IssueReporting
10

11
/// Classifies and validates the format of a Supabase API key.
12
///
13
/// New-format keys (`sb_publishable_…` / `sb_secret_…`) are not JWTs and must never be sent
14
/// as a Bearer token — they belong only in the `apikey` header. Legacy JWT keys (no `sb_`
15
/// prefix) and platform-issued temporary keys (`sb_temp_…`) keep the existing Bearer fallback.
16
package enum APIKeyFormat {
17
  private static let newFormatPrefixes = ["sb_publishable_", "sb_secret_"]
18
  private static let temporaryKeyPrefix = "sb_temp_"
19

20
  /// Whether `key` is a new-format key (`sb_publishable_…` / `sb_secret_…`).
21
  package static func isNew(_ key: String) -> Bool {
21✔
22
    newFormatPrefixes.contains { key.hasPrefix($0) }
37✔
23
  }
21✔
24

25
  private static let warnedSubtypes = LockIsolated<Set<String>>([])
1✔
26

27
  /// Warns (once per subtype) when `key` has the `sb_` prefix but an unrecognized subtype.
28
  /// Never throws — the server, not the SDK, decides key validity. The key value is never
29
  /// included in the message.
30
  package static func checkFormat(_ key: String) {
8✔
31
    guard shouldWarn(for: key) else { return }
8✔
NEW
32
    reportIssue(
×
NEW
33
      """
×
NEW
34
      Unrecognized Supabase API key format. The client will proceed and send this key as-is; \
×
NEW
35
      if you see authentication errors you may need to upgrade supabase-swift to a version \
×
NEW
36
      that recognizes this key type.
×
NEW
37
      """
×
NEW
38
    )
×
NEW
39
  }
×
40

41
  /// Decides whether `checkFormat` should warn for `key`, and records the subtype as warned
42
  /// as a side effect. Split out from `checkFormat` so the classification/dedup logic is
43
  /// unit-testable without exercising `reportIssue` directly (calling `reportIssue` from a
44
  /// Swift Testing `@Test` function is known to crash the Xcode test runner — see the
45
  /// `clientInitWithCustomAccessToken` test comment in Tests/SupabaseTests/SupabaseClientTests.swift
46
  /// for a documented instance of the same constraint).
47
  package static func shouldWarn(for key: String) -> Bool {
19✔
48
    guard key.hasPrefix("sb_"), !isNew(key), !key.hasPrefix(temporaryKeyPrefix) else {
19✔
49
      return false
13✔
50
    }
13✔
51
    let subtype = subtypeToken(for: key)
6✔
52
    return warnedSubtypes.withValue { $0.insert(subtype).inserted }
6✔
53
  }
19✔
54

55
  /// The `Authorization` Bearer token an Edge Functions request should use, given the raw
56
  /// `accessToken` fallback logic and the client's `supabaseKey`. New-format keys must never
57
  /// appear as a Bearer token: when there is no real session and `accessToken` is just the
58
  /// raw key fallback, this returns `nil` so the caller omits the header entirely instead of
59
  /// sending the new-format key as a Bearer token. A genuine session token (which will never
60
  /// equal `supabaseKey`) and legacy JWT key fallbacks are returned unchanged.
61
  package static func functionsBearerToken(accessToken: String, supabaseKey: String) -> String? {
3✔
62
    accessToken == supabaseKey && isNew(supabaseKey) ? nil : accessToken
3✔
63
  }
3✔
64

65
  private static func subtypeToken(for key: String) -> String {
6✔
66
    let remainder = key.dropFirst("sb_".count)
6✔
67
    guard let underscoreIndex = remainder.firstIndex(of: "_") else { return "unknown" }
6✔
68
    let candidate = remainder[..<underscoreIndex]
4✔
69
    guard !candidate.isEmpty, candidate.allSatisfy({ $0.isLetter || $0.isNumber }) else {
56✔
NEW
70
      return "unknown"
×
71
    }
4✔
72
    return String(candidate)
4✔
73
  }
6✔
74
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc