feat(account): add managed credits and cloud gateway

Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
This commit is contained in:
Rocky
2026-08-20 11:43:21 +08:00
parent 0f9280bd00
commit 498f407585
301 changed files with 19221 additions and 10891 deletions
@@ -0,0 +1,146 @@
// ManagedGatewayModels.swift
// OSGKeyboard · Shared
//
// Scope-limited credentials and transport models for the managed gateway.
import Foundation
public enum ManagedGatewayCapability: String, Codable, CaseIterable, Sendable {
case polish
case assistant = "ai"
case agent
case asr
}
/// Stable task contract shared with the account gateway. The app identifies
/// user intent; the server remains authoritative for model and feature policy.
public enum ManagedGatewayTaskKind: String, Codable, CaseIterable, Sendable {
case dictationPolish = "dictation_polish"
case translation
case editLastInput = "edit_last_input"
case aiQuestion = "ai_question"
case clipboardTransform = "clipboard_transform"
case customSkill = "custom_skill"
case agentPlanning = "agent_planning"
}
public struct ManagedGatewayGrantCredentials: Codable, Equatable, Sendable {
public static let maximumAccessLifetime: TimeInterval = 5 * 60
public let grantId: String
public let scopes: Set<ManagedGatewayCapability>
public let accessToken: String
public let accessExpiresAt: Date
public let refreshToken: String
public let refreshExpiresAt: Date
public let receivedAt: Date
public init(
grantId: String,
scopes: Set<ManagedGatewayCapability>,
accessToken: String,
accessExpiresAt: Date,
refreshToken: String,
refreshExpiresAt: Date,
receivedAt: Date = Date()
) {
self.grantId = grantId
self.scopes = scopes
self.accessToken = accessToken
self.accessExpiresAt = accessExpiresAt
self.refreshToken = refreshToken
self.refreshExpiresAt = refreshExpiresAt
self.receivedAt = receivedAt
}
/// Never trust an unexpectedly long access expiry. The gateway contract
/// deliberately limits extension-readable bearer credentials to five minutes.
public var effectiveAccessExpiresAt: Date {
min(accessExpiresAt, receivedAt.addingTimeInterval(Self.maximumAccessLifetime))
}
public func hasUsableAccessToken(
for scope: ManagedGatewayCapability,
at now: Date = Date(),
refreshLeeway: TimeInterval = 30
) -> Bool {
scopes.contains(scope)
&& !accessToken.isEmpty
&& effectiveAccessExpiresAt.timeIntervalSince(now) > refreshLeeway
}
public func hasUsableRefreshToken(at now: Date = Date()) -> Bool {
!refreshToken.isEmpty && refreshExpiresAt > now
}
}
public enum ManagedGatewayError: Error, LocalizedError, Equatable, Sendable {
case missingGrant
case scopeNotGranted(ManagedGatewayCapability)
case invalidGrant
case insufficientCredits
case timeout
case server(code: String, status: Int, requestId: String?)
public var errorDescription: String? {
switch self {
case .missingGrant:
return SharedL10n.string("managed.error.grantUnavailable")
case .scopeNotGranted(let scope):
return SharedL10n.format(
"managed.error.scopeNotGranted",
language: nil,
scope.rawValue
)
case .invalidGrant:
return SharedL10n.string("managed.error.grantRejected")
case .insufficientCredits:
return SharedL10n.string("managed.error.insufficientCredits")
case .timeout:
return SharedL10n.string("managed.error.timeout")
case .server(let code, let status, _):
return SharedL10n.format(
"managed.error.server",
language: nil,
code,
status
)
}
}
}
struct ManagedGatewayGrantTokenResponse: Decodable {
let grantId: String
let scopes: Set<ManagedGatewayCapability>
let accessToken: String
let accessExpiresAt: Date
let refreshToken: String
let refreshExpiresAt: Date
func credentials(receivedAt: Date) -> ManagedGatewayGrantCredentials {
ManagedGatewayGrantCredentials(
grantId: grantId,
scopes: scopes,
accessToken: accessToken,
accessExpiresAt: accessExpiresAt,
refreshToken: refreshToken,
refreshExpiresAt: refreshExpiresAt,
receivedAt: receivedAt
)
}
}
struct ManagedGatewayErrorResponse: Decodable, Sendable {
let code: String
let message: String
let requestId: String
}
struct ManagedGatewayTextRequest: Encodable, Sendable {
let input: String
let context: String?
let maxOutputTokens: Int
let temperature: Double
let stream: Bool
let taskKind: ManagedGatewayTaskKind
}