feat(ios): device enrollment flow + silent cert rotation (B3)
Wire the SecureEnclave enroll library into a real flow (login->bearer->CSR->
/device/enroll->keychain identity), presented on the existing mTLS path; add a
rotation scheduler. Atomic keychain replace (add-before-delete); renew body is
{csr}-only; renewal-failing surfaced in the UI. ClientTLS 48 tests pass.
This commit is contained in:
@@ -75,9 +75,13 @@ struct StackRootView: View {
|
||||
viewModel: coordinator.sessionList,
|
||||
onOpen: { coordinator.open($0) },
|
||||
onAddHost: { coordinator.presentAddHost() },
|
||||
onDeviceCert: { coordinator.presentDeviceCert() }
|
||||
onDeviceCert: { coordinator.presentDeviceCert() },
|
||||
onEnroll: { coordinator.presentEnrollment() }
|
||||
)
|
||||
.safeAreaInset(edge: .bottom) { continueLastBanner }
|
||||
// B3 (HIGH) · A silently-failing device-cert renewal is surfaced here
|
||||
// (top inset), so it is observable instead of buried in os.Logger.
|
||||
.safeAreaInset(edge: .top) { certRenewalWarningBanner }
|
||||
// 横幅出现/消失走 DS 动效(reduceMotion 时塌成瞬切,无位移)。
|
||||
.animation(
|
||||
DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion),
|
||||
@@ -98,6 +102,15 @@ struct StackRootView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Device-cert renewal warning (B3 HIGH observability fix)
|
||||
|
||||
@ViewBuilder private var certRenewalWarningBanner: some View {
|
||||
if coordinator.isCertificateRenewalFailing {
|
||||
CertRenewalWarningBanner()
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal push
|
||||
|
||||
private var terminalBinding: Binding<Bool> {
|
||||
@@ -155,6 +168,32 @@ struct ContinueLastBanner: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Device-cert renewal warning (B3 HIGH observability fix)
|
||||
|
||||
/// A quiet, non-blocking warning that the silent device-certificate renewal is
|
||||
/// failing — surfaced from `AppCoordinator.isCertificateRenewalFailing` so a
|
||||
/// failing renewal is observable in the UI instead of only in os.Logger. Amber
|
||||
/// (the `waiting`/needs-me status color) + an SF Symbol so meaning is never
|
||||
/// carried by color alone. Non-interactive: the existing cert stays valid until
|
||||
/// expiry, and the next foreground retries automatically.
|
||||
struct CertRenewalWarningBanner: View {
|
||||
var body: some View {
|
||||
Label(RootCopy.certRenewalFailing, systemImage: "exclamationmark.shield")
|
||||
.font(DS.Typography.caption)
|
||||
.foregroundStyle(DS.Palette.statusWaiting)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, DS.Space.lg16)
|
||||
.padding(.vertical, DS.Space.sm8)
|
||||
.background(.regularMaterial)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(DS.Palette.hairline)
|
||||
.frame(height: DS.Stroke.hairline)
|
||||
}
|
||||
.accessibilityIdentifier("root.certRenewalWarning")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Projects toolbar item (shared stack + split, DRY)
|
||||
|
||||
/// 「项目」leading 工具栏入口 —— stack 与 split 共用(同 disabled 条件、同
|
||||
@@ -180,4 +219,6 @@ enum RootCopy {
|
||||
static let continueLast = "继续上次会话"
|
||||
static let projects = "项目"
|
||||
static let done = "完成"
|
||||
/// B3 (HIGH) · Shown when the silent device-cert renewal keeps failing.
|
||||
static let certRenewalFailing = "设备证书自动续期失败,将在下次前台重试"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user