Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: iOS

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
contract-and-privacy:
name: Cave contract and privacy
runs-on: ubuntu-latest
steps:
- name: Check out Coven Memory
uses: actions/checkout@v4
- name: Check out Cave contract source
uses: actions/checkout@v4
with:
repository: OpenCoven/coven-cave
path: coven-cave
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Verify contract helpers
run: node --test scripts/check-mobile-contract.test.mjs
- name: Verify Cave response fixture parity
run: >-
node scripts/check-mobile-contract.mjs
coven-cave/tests/fixtures/mobile-canonical-memory-v1
apps/ios/CovenMemory/Tests/Fixtures/cave-mobile-memory-v1
- name: Verify iOS privacy scanner
run: bash scripts/check-ios-privacy.test.sh
- name: Scan source and fixtures
run: bash scripts/check-ios-privacy.sh

simulator:
name: iPhone simulator
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Install XcodeGen
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install xcodegen
- name: Generate project
working-directory: apps/ios/CovenMemory
run: xcodegen generate
- name: Run Swift and UI tests
working-directory: apps/ios/CovenMemory
run: >-
xcodebuild test
-project CovenMemory.xcodeproj
-scheme CovenMemory
-derivedDataPath build/DerivedData
-resultBundlePath build/CovenMemoryTests.xcresult
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'
- name: Scan generated and source artifacts
run: |
bash scripts/check-ios-privacy.sh
bash scripts/check-ios-privacy.sh apps/ios/CovenMemory/build/DerivedData/Build/Products/Debug-iphonesimulator/CovenMemory.app
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ The current dashboard provides:
Promotion, editing, deletion, approval, and supersession mutations are
intentionally absent until their authority contracts are available.

## Native iOS client

`apps/ios/CovenMemory` contains the read-only iPhone and iPad client. It pairs
through Coven Cave's **Open on phone** flow and reads the bearer-protected
`/api/mobile/coven-memory` contract over HTTPS; it does not connect directly to
Coven or persist memory content on the device. See
[`docs/ios/README.md`](docs/ios/README.md) for build, fixture, signing, pairing,
and privacy-gate instructions.

## Requirements

- Node.js 24 or newer
Expand Down Expand Up @@ -153,6 +162,7 @@ Coven, restarting the daemon, then retrying the dashboard.
```bash
pnpm check
pnpm audit:prod
pnpm test:ios-privacy
```

`pnpm check` runs lint, TypeScript, unit/component/integration tests, the
Expand Down
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ memory collection.
Browser and transport fixtures must be deterministic and synthetic. Real
memory must never be copied into tests, screenshots, traces, or issue notes.

## Native iOS boundary

The iOS client is a separate read-only presentation layer behind Coven Cave's
mobile bearer gate. Pairing accepts only a normalized HTTPS Cave URL with one
mobile access token. The connection record is stored in the device-only,
when-unlocked Keychain; memory summaries, details, and reveal state are never
written to a database, cache, preference, log, or analytics service.

Requests use an ephemeral URL session, reject redirects, and accept only the
versioned `/api/mobile/coven-memory` response shapes. Cave remains responsible
for mobile authorization and local Coven access. App lock, backgrounding,
protected-reader dismissal, pairing reset, and authentication/revocation
failures must clear the active memory graph and cover content immediately.

iOS cannot prevent a device owner from taking a screenshot or photographing a
screen. TestFlight notes, screenshots, recordings, crash reports, and review
packets must therefore contain synthetic status evidence only—never an invite,
endpoint, token, device identifier, or memory body. A lost device requires
rotation of the Cave mobile access secret and re-pairing trusted devices; local
**Pair again** removes only that device's stored connection.

## Enforcement layers (defense in depth)

1. **Pre-commit hook** — `scripts/guard-scan.sh --staged`: gitleaks (with
Expand Down
60 changes: 48 additions & 12 deletions apps/ios/CovenMemory/CovenMemory.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"images" : [
{
"filename" : "AppIcon-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.945",
"green" : "0.604",
"red" : "0.478"
"blue" : "0.718",
"green" : "0.345",
"red" : "0.239"
}
},
"idiom" : "universal"
Expand All @@ -24,8 +24,8 @@
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.710",
"red" : "0.650"
"green" : "0.651",
"red" : "0.561"
}
},
"idiom" : "universal"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.118",
"green" : "0.149",
"red" : "0.702"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.380",
"green" : "0.412",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.340",
"green" : "0.300",
"red" : "0.300"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.800",
"green" : "0.750",
"red" : "0.750"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
14 changes: 14 additions & 0 deletions apps/ios/CovenMemory/Resources/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
</dict>
</plist>
5 changes: 4 additions & 1 deletion apps/ios/CovenMemory/Sources/App/LaunchCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,10 @@ private actor SessionInvalidatingCaveMemoryService: CaveMemoryServicing {
try validity.check()
return value
} catch let error as NetworkError {
if hasServedDetail || detailRequestsInFlight > 0 {
if error == .authenticationRequired
|| hasServedDetail
|| detailRequestsInFlight > 0
{
await invalidateSession(for: error)
}
try validity.check()
Expand Down
16 changes: 10 additions & 6 deletions apps/ios/CovenMemory/Sources/App/LaunchViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private struct LaunchProgressView: View {
ProgressView()
.controlSize(.large)
Text(message)
.foregroundStyle(.secondary)
.foregroundStyle(CovenTheme.secondary)
}
.multilineTextAlignment(.center)
.padding(CovenTheme.regularMargin)
Expand Down Expand Up @@ -162,7 +162,7 @@ private struct PairingView: View {
+ "then scan its QR code."
)
.font(.body)
.foregroundStyle(.secondary)
.foregroundStyle(CovenTheme.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
Expand All @@ -175,6 +175,7 @@ private struct PairingView: View {
scannerPresented = true
}
.buttonStyle(.borderedProminent)
.foregroundStyle(CovenTheme.prominentForeground)
.controlSize(.large)
.frame(
maxWidth: .infinity,
Expand All @@ -185,7 +186,7 @@ private struct PairingView: View {
if let scannerStatus {
Text(scannerStatus)
.font(.footnote)
.foregroundStyle(.secondary)
.foregroundStyle(CovenTheme.secondary)
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier("scanner-status")
} else if !QRScannerSheet.isSupported {
Expand All @@ -194,7 +195,7 @@ private struct PairingView: View {
+ "Paste the invite link instead."
)
.font(.footnote)
.foregroundStyle(.secondary)
.foregroundStyle(CovenTheme.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
Expand Down Expand Up @@ -275,7 +276,7 @@ private struct ConnectionReadyView: View {
Text(host)
.font(.headline)
Text("The native library interface is the next phase.")
.foregroundStyle(.secondary)
.foregroundStyle(CovenTheme.secondary)
ViewThatFits {
HStack {
actions
Expand All @@ -300,6 +301,7 @@ private struct ConnectionReadyView: View {
.controlSize(.large)
Button("Lock", action: lock)
.buttonStyle(.borderedProminent)
.foregroundStyle(CovenTheme.prominentForeground)
.controlSize(.large)
}
}
Expand All @@ -320,11 +322,12 @@ private struct LaunchFailureView: View {
Text(title)
.font(.title2.bold())
Text(message)
.foregroundStyle(.secondary)
.foregroundStyle(CovenTheme.secondary)

if canRetry {
Button("Retry", action: retry)
.buttonStyle(.borderedProminent)
.foregroundStyle(CovenTheme.prominentForeground)
.controlSize(.large)
}
if canRetry {
Expand All @@ -334,6 +337,7 @@ private struct LaunchFailureView: View {
} else {
Button("Pair again", action: pairAgain)
.buttonStyle(.borderedProminent)
.foregroundStyle(CovenTheme.prominentForeground)
.controlSize(.large)
}
}
Expand Down
3 changes: 3 additions & 0 deletions apps/ios/CovenMemory/Sources/DesignSystem/CovenTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import SwiftUI

enum CovenTheme {
static let accent = Color("CovenAccent")
static let failure = Color("CovenFailure")
static let prominentForeground = Color("CovenProminentForeground")
static let secondary = Color("CovenSecondary")
static let compactMargin: CGFloat = 16
static let regularMargin: CGFloat = 24
static let readerMaximumWidth: CGFloat = 720
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct ExceptionNotice: View {
var color: Color {
switch self {
case .warning, .unavailable, .incompatible: .orange
case .failure: .red
case .failure: CovenTheme.failure
}
}
}
Expand Down
Loading
Loading