commit 1e65049b9f2b2fcaf16b32e8b8354cf462417c65
parent 7465b598a80fe63955e56a3b09d0afcfdb7e3b5b
Author: triesap <tyson@radroots.org>
Date: Fri, 2 Oct 2026 14:52:30 +0000
privacy: align current product declarations
- Declare linked public content and actual required API reasons
- Adopt the qualified local file capacity owner at its exact pin
- Package the approved support contact and honest removal language
- Qualify installed manifests permissions and fail-closed checks
Diffstat:
25 files changed, 644 insertions(+), 122 deletions(-)
diff --git a/Package.resolved b/Package.resolved
@@ -6,7 +6,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/radrootslabs/apple_kit.git",
"state" : {
- "revision" : "1126a77ed87387719a6c6d3b4ce580582af29553"
+ "revision" : "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2"
}
},
{
diff --git a/Package.swift b/Package.swift
@@ -14,7 +14,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/radrootslabs/apple_kit.git",
- revision: "1126a77ed87387719a6c6d3b4ce580582af29553"
+ revision: "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2"
),
],
targets: [
@@ -216,6 +216,7 @@ let package = Package(
"Views/TeraRecoveryUITestSurface.swift",
"Views/TeraSupportingViews.swift",
"Views/TeraSettingsView.swift",
+ "Views/TeraSupportSettingsSection.swift",
"Views/TeraStorageSettingsSection.swift",
"Views/TeraContextPicker.swift",
"Views/TeraTodayDiscoveryView.swift",
diff --git a/README.md b/README.md
@@ -429,3 +429,30 @@ files. Draft staging, pending operations, transfer receipts and backup media are
not cleanup targets. Invalidation must persist first: if the store is completely
full, free device space before trying again. Cache cleanup cannot resolve the
independent bounded transfer-receipt envelope limit.
+
+
+Public posts and profile updates are linked to the public Nostr key and may be
+retained by relays and other people. Only deliberately included location text
+(such as a public venue or address) is posted; camera/library images have
+sensitive metadata removed before staging. The app does not automatically
+publish device location and has no tracking or automatic telemetry endpoint.
+Its privacy manifest covers public names, identifiers, deliberately supplied
+addresses, photos and other posted content for application functionality.
+
+File metadata is accessed only for owned storage. The generic file owner
+checks available capacity locally before a write and reports the existing
+insufficient-space outcome. This check is conservative and does not reserve
+space or replace actual write/quota error handling. Capacity values stay local.
+
+Diagnostics exports contain bounded status codes and counts; prepare and review
+one before explicitly sharing it. [Radroots Support](mailto:support@radroots.org)
+is the canonical contact for support, privacy and removal inquiries. Settings
+opens the system email handler only after an explicit choice; no report or attachment
+is sent automatically. Provisioning/monitoring and operational reporting drills
+remain distribution prerequisites, not engineering-test outcomes.
+
+Removing a local signing key does not erase public copies. Review and stage any
+desired signed deletion requests before removing the key. Requests and relay
+acknowledgements cannot prove erasure by every recipient. The implemented local
+removal flow preserves its existing source and user-presence checks; it does not
+claim a decentralized-account policy exemption or App Store approval.
diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj
@@ -144,6 +144,7 @@
EF7293C553BF1DB45EEEBD49 /* local-social-personas.v1.json in Resources */ = {isa = PBXBuildFile; fileRef = 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */; };
F13090A3350CAE9CA3FFF3F3 /* TeraLateSigningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217E44A3221756EC904702D6 /* TeraLateSigningTests.swift */; };
F14217EA66C2A9397E36E3FA /* TeraEditingOperationProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9B19425D3374B899485C17 /* TeraEditingOperationProtectionTests.swift */; };
+ F211D24BC279D3FF4AC12094 /* TeraPrivacyPackagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFD344F51F015AE6775F03D3 /* TeraPrivacyPackagingTests.swift */; };
F4AF91E71B691FE30C191852 /* TeraAddObservationStartupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */; };
F8A87EA68BEF06EA7F0838D0 /* TeraRecoveryUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5928D5CAC9F8EF904547DF8 /* TeraRecoveryUITests.swift */; };
F8C87782857DC930D47E6A90 /* TeraRuntimeResourceTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */; };
@@ -304,6 +305,7 @@
EDE158F5F1E490847A5196EB /* TeraDiagnosticPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraDiagnosticPolicyTests.swift; sourceTree = "<group>"; };
EE1A300DCBB15E1911EA7D6C /* TeraCoordinateAdmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCoordinateAdmissionTests.swift; sourceTree = "<group>"; };
EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedMediaTests.swift; sourceTree = "<group>"; };
+ EFD344F51F015AE6775F03D3 /* TeraPrivacyPackagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraPrivacyPackagingTests.swift; sourceTree = "<group>"; };
EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayContextTests.swift; sourceTree = "<group>"; };
F06622DFA682235F4BFA0841 /* TeraKeyRemovalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraKeyRemovalTests.swift; sourceTree = "<group>"; };
F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceFixtures.swift; sourceTree = "<group>"; };
@@ -444,6 +446,7 @@
9B10B1F3C7FCD9A72F04FC00 /* TeraOpenedMediaTests.swift */,
9F8B650ED2D867EF94248F6F /* TeraOperationRecoveryTests.swift */,
E1BF69ADD9540F6800FD64B0 /* TeraPermissionRecoveryTests.swift */,
+ EFD344F51F015AE6775F03D3 /* TeraPrivacyPackagingTests.swift */,
7AF60EF9DCFDC4ECE77AED67 /* TeraProductStartupTests.swift */,
3B954B7006CC5904C98138C6 /* TeraPublicationEvidenceTests.swift */,
A195B277BEDC31DE3A0D7601 /* TeraPublicationNavigationTests.swift */,
@@ -778,6 +781,7 @@
6FED2D2255CC702B33CDC760 /* TeraOpenedMediaTests.swift in Sources */,
026981088C27CB79A49D9833 /* TeraOperationRecoveryTests.swift in Sources */,
C20BDCF02F461EE53B14B53E /* TeraPermissionRecoveryTests.swift in Sources */,
+ F211D24BC279D3FF4AC12094 /* TeraPrivacyPackagingTests.swift in Sources */,
C46F8EB5B196FFDD37E0279A /* TeraProductStartupTests.swift in Sources */,
B9E397E21B1654C108AF74BF /* TeraPublicDisclosureTests.swift in Sources */,
093A6AC6C9B724E465507004 /* TeraPublicationEvidenceTests.swift in Sources */,
@@ -1180,7 +1184,7 @@
repositoryURL = "https://github.com/radrootslabs/apple_kit.git";
requirement = {
kind = revision;
- revision = 1126a77ed87387719a6c6d3b4ce580582af29553;
+ revision = d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2;
};
};
/* End XCRemoteSwiftPackageReference section */
diff --git a/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,7 +6,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/radrootslabs/apple_kit.git",
"state" : {
- "revision" : "1126a77ed87387719a6c6d3b4ce580582af29553"
+ "revision" : "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2"
}
},
{
diff --git a/Tera/Info.plist b/Tera/Info.plist
@@ -21,7 +21,7 @@
<key>CFBundleVersion</key>
<string>1</string>
<key>NSCameraUsageDescription</key>
- <string>Tera uses the camera only when you choose to add a farm photo.</string>
+ <string>Tera uses the camera only when you choose to add a photo to a public post.</string>
<key>NSFaceIDUsageDescription</key>
<string>Tera uses Face ID only to unlock the local Nostr identity or approve a post or media upload you choose to sign.</string>
<key>NSLocalNetworkUsageDescription</key>
@@ -46,7 +46,6 @@
</array>
</dict>
</dict>
-
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
@@ -67,7 +66,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
-
<key>TERA_IOS_RUNTIME_MODE</key>
<string>$(TERA_IOS_RUNTIME_MODE)</string>
<key>TERA_IOS_NOSTR_RELAY_URLS</key>
@@ -76,5 +74,7 @@
<string>$(TERA_IOS_KEYCHAIN_SERVICE_PREFIX)</string>
<key>TERA_IOS_BLOSSOM_ORIGINS</key>
<string>$(TERA_IOS_BLOSSOM_ORIGINS)</string>
+ <key>TeraSupportEmail</key>
+ <string>support@radroots.org</string>
</dict>
</plist>
diff --git a/Tera/Resources/PrivacyInfo.xcprivacy b/Tera/Resources/PrivacyInfo.xcprivacy
@@ -7,11 +7,88 @@
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
- <array/>
+ <array>
+ <dict>
+ <key>NSPrivacyCollectedDataType</key>
+ <string>NSPrivacyCollectedDataTypeName</string>
+ <key>NSPrivacyCollectedDataTypeLinked</key>
+ <true/>
+ <key>NSPrivacyCollectedDataTypeTracking</key>
+ <false/>
+ <key>NSPrivacyCollectedDataTypePurposes</key>
+ <array>
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
+ </array>
+ </dict>
+ <dict>
+ <key>NSPrivacyCollectedDataType</key>
+ <string>NSPrivacyCollectedDataTypeUserID</string>
+ <key>NSPrivacyCollectedDataTypeLinked</key>
+ <true/>
+ <key>NSPrivacyCollectedDataTypeTracking</key>
+ <false/>
+ <key>NSPrivacyCollectedDataTypePurposes</key>
+ <array>
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
+ </array>
+ </dict>
+ <dict>
+ <key>NSPrivacyCollectedDataType</key>
+ <string>NSPrivacyCollectedDataTypePhysicalAddress</string>
+ <key>NSPrivacyCollectedDataTypeLinked</key>
+ <true/>
+ <key>NSPrivacyCollectedDataTypeTracking</key>
+ <false/>
+ <key>NSPrivacyCollectedDataTypePurposes</key>
+ <array>
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
+ </array>
+ </dict>
+ <dict>
+ <key>NSPrivacyCollectedDataType</key>
+ <string>NSPrivacyCollectedDataTypePhotosorVideos</string>
+ <key>NSPrivacyCollectedDataTypeLinked</key>
+ <true/>
+ <key>NSPrivacyCollectedDataTypeTracking</key>
+ <false/>
+ <key>NSPrivacyCollectedDataTypePurposes</key>
+ <array>
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
+ </array>
+ </dict>
+ <dict>
+ <key>NSPrivacyCollectedDataType</key>
+ <string>NSPrivacyCollectedDataTypeOtherUserContent</string>
+ <key>NSPrivacyCollectedDataTypeLinked</key>
+ <true/>
+ <key>NSPrivacyCollectedDataTypeTracking</key>
+ <false/>
+ <key>NSPrivacyCollectedDataTypePurposes</key>
+ <array>
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
+ </array>
+ </dict>
+ </array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
+ <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
+ <key>NSPrivacyAccessedAPITypeReasons</key>
+ <array>
+ <string>C617.1</string>
+ </array>
+ </dict>
+ <dict>
+ <key>NSPrivacyAccessedAPIType</key>
+ <string>NSPrivacyAccessedAPICategoryDiskSpace</string>
+ <key>NSPrivacyAccessedAPITypeReasons</key>
+ <array>
+ <string>E174.1</string>
+ </array>
+ </dict>
+ <dict>
+ <key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
diff --git a/Tera/Views/TeraSettingsView.swift b/Tera/Views/TeraSettingsView.swift
@@ -192,6 +192,7 @@ struct TeraSettingsView: View {
.disabled(addStore.isCheckingBlossom || addStore.blossomConfiguration == nil)
.accessibilityIdentifier("radroots.settings.retry.blossom")
}
+ TeraSupportSettingsSection()
Section("Runtime") {
LabeledContent("Crate", value: snapshot.crateName)
LabeledContent("Version", value: snapshot.crateVersion)
diff --git a/Tera/Views/TeraSupportSettingsSection.swift b/Tera/Views/TeraSupportSettingsSection.swift
@@ -0,0 +1,27 @@
+import Foundation
+import SwiftUI
+
+/// Read the approved packaged contact. No message or diagnostics are sent
+/// until the person chooses an external action and submits it themselves.
+enum TeraSupportContact {
+ static func mailURL(email: String?) -> URL? {
+ guard email == "support@radroots.org" else { return nil }
+ return URL(string: "mailto:support@radroots.org")
+ }
+
+ static var packagedMailURL: URL? {
+ mailURL(email: Bundle.main.object(forInfoDictionaryKey: "TeraSupportEmail") as? String)
+ }
+}
+
+struct TeraSupportSettingsSection: View {
+ var body: some View {
+ Section("Support and privacy") {
+ if let contact = TeraSupportContact.packagedMailURL {
+ Link("Contact Radroots Support", destination: contact)
+ .accessibilityIdentifier("tera.settings.support.contact")
+ }
+ Text("Share diagnostics only after reviewing an export. Removing a local signing key does not erase copies already held by relays or other people.")
+ }
+ }
+}
diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json
@@ -92,19 +92,19 @@
"sha256": "d7b5bce06f98b202c676e2022377d006459be8572aa1bccf4d902e5247b69238"
},
{
- "bytes": 138031,
+ "bytes": 138526,
"path": "source/aarch64-apple-darwin.json",
- "sha256": "c227dc0072bf5f333f0da087fd72906cb657d8b3be4442c032292b6ebc625321"
+ "sha256": "1492ea27ad780f31f2ff9c8fed2ccb736e82d8b735c487dddf4f3a776d289373"
},
{
- "bytes": 137875,
+ "bytes": 138370,
"path": "source/aarch64-apple-ios-sim.json",
- "sha256": "267f13ec71cfccda3d2d9bc6600fab446fde9fda4b7e0afd31011aeabe96785e"
+ "sha256": "dc03ba11437cc803c1321c10f686e24ea7cdbef7057d1827e578c6bb52b73c5f"
},
{
- "bytes": 137871,
+ "bytes": 138366,
"path": "source/aarch64-apple-ios.json",
- "sha256": "c1dc9e9a167368169dfd74cef909497c204a41c7da76d00a12f5ba4fbbcf7c85"
+ "sha256": "c0d7ae5c5101fa98040bc0d533e8703afdd98cb8f8b569d04f790a69cc198e2f"
}
],
"language": "swift",
@@ -112,7 +112,7 @@
"schema": "radroots.artifact-manifest.v2",
"source": {
"repository": "https://github.com/radrootslabs/tera",
- "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a"
+ "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67"
},
"source_records": {
"aarch64-apple-darwin": "source/aarch64-apple-darwin.json",
@@ -174,19 +174,19 @@
"sha256": "5d74060f97e52dad69ab7567c16d6b6b34acfab846f468450d89e2394a8a69d5"
},
{
- "bytes": 138031,
+ "bytes": 138526,
"path": "TeraFFI/source/aarch64-apple-darwin.json",
- "sha256": "c227dc0072bf5f333f0da087fd72906cb657d8b3be4442c032292b6ebc625321"
+ "sha256": "1492ea27ad780f31f2ff9c8fed2ccb736e82d8b735c487dddf4f3a776d289373"
},
{
- "bytes": 137875,
+ "bytes": 138370,
"path": "TeraFFI/source/aarch64-apple-ios-sim.json",
- "sha256": "267f13ec71cfccda3d2d9bc6600fab446fde9fda4b7e0afd31011aeabe96785e"
+ "sha256": "dc03ba11437cc803c1321c10f686e24ea7cdbef7057d1827e578c6bb52b73c5f"
},
{
- "bytes": 137871,
+ "bytes": 138366,
"path": "TeraFFI/source/aarch64-apple-ios.json",
- "sha256": "c1dc9e9a167368169dfd74cef909497c204a41c7da76d00a12f5ba4fbbcf7c85"
+ "sha256": "c0d7ae5c5101fa98040bc0d533e8703afdd98cb8f8b569d04f790a69cc198e2f"
}
],
"schema": "tera.installed-native-artifacts.v1"
diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock
@@ -1,7 +1,7 @@
schema = "tera.installed-source.v1"
repository = "https://github.com/radrootslabs/tera"
-source_tree = "8f9abc83606e9996524f519b8bfb18dc3348590a"
-manifest_sha256 = "feaf4681644f041ce4bf841cf1e3c29b03bba15c9de9858f1c0a1ccf63419993"
+source_tree = "5b2ddf887733ca6e86f26893ccc3c852ac2dee67"
+manifest_sha256 = "c0aebdf85e1295346b78d3b2b384cb0e64f5a9f0a2348e08f70398b6471fe862"
source_date_epoch = 1787871027
[foundation]
diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json
@@ -2812,10 +2812,16 @@
"sha256": "fff99e755f35fa9f00708427b1b29290c0ecc2733c0ea070f5807ec7c37f6f11"
},
"scripts/package_contract.py": {
- "bytes": 25795,
- "git_blob": "d68f5df2cc26d252be264fe55ece761070d02bee",
+ "bytes": 25574,
+ "git_blob": "ea94df8c0f106abc85e47295c2da7b023c168123",
"mode": "100755",
- "sha256": "b88df13f9c0da49fc535cf9997c668ec1357e9dbf6db2b472ac29fc3304c888c"
+ "sha256": "f00372d39a5f9849b6a535ed829a50f52810c3ac9e0695e80a71c802e23f0394"
+ },
+ "scripts/package_privacy.py": {
+ "bytes": 2718,
+ "git_blob": "a05951b332833f03520884ab6877b89f4ca88120",
+ "mode": "100644",
+ "sha256": "4540c5254ceb68634dc4d028d1056ad40558c4919c6d2be0d2d4c31958f46cbe"
},
"scripts/persona-verifier.sh": {
"bytes": 240,
@@ -2854,10 +2860,10 @@
"sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0"
},
"scripts/swift-quality.sh": {
- "bytes": 2080,
- "git_blob": "65b81671c82fe4102e6599532b4d2d802cedf4a0",
+ "bytes": 2143,
+ "git_blob": "0dea0e430dcc4e246bc951938faaa25db2596074",
"mode": "100755",
- "sha256": "15c98b26fdae1e6db8032b3201a8ab7e820800c721f0a4791ff374f81e6a004d"
+ "sha256": "9bb734eafe3c375ef30732e59a837ec345da438d96dc7882ec7f6ee1de43c13a"
},
"scripts/test_app_dependency_graph.py": {
"bytes": 4007,
@@ -2913,6 +2919,12 @@
"mode": "100644",
"sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f"
},
+ "scripts/test_package_privacy.py": {
+ "bytes": 4467,
+ "git_blob": "f1f821a0db6742ae2de3425c98d19297eb3463e9",
+ "mode": "100644",
+ "sha256": "651fc803fe3e3dd1636aec75f9ea79d81b9407317f93a02e4074a8369558bc33"
+ },
"scripts/test_xcode_selection.py": {
"bytes": 4745,
"git_blob": "b633afbccd765a106e2ad591881926a9add542ac",
@@ -2920,10 +2932,10 @@
"sha256": "a917dfbc1d8ecac7d1539670651e4b468059bc77dfe31aeefbc6c6eb21bebbfb"
},
"scripts/verify-package-contract.sh": {
- "bytes": 1496,
- "git_blob": "b21f0d5e48bcedd71460f7c0f4cf0c8f1070b27a",
+ "bytes": 1532,
+ "git_blob": "88f15a643269a8851d7fef68fe14208e71e2d843",
"mode": "100755",
- "sha256": "8ecb7b9fc1997e1cdb37d642d6dbfa59194bf9182218aceab470077f721fcd85"
+ "sha256": "028a8310c4a61a4cc04f1d0f86b8b953413b8efef5cb2059fbb0c84338483428"
},
"scripts/xcode.sh": {
"bytes": 22437,
@@ -2932,13 +2944,13 @@
"sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 186929,
- "git_blob": "39860837f76433ef305fde969d9162fefa98e579",
+ "bytes": 187553,
+ "git_blob": "c0ce4d1c11ad8683bf9c45029db89c77dc8fc4f7",
"mode": "100644",
- "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df"
+ "sha256": "e62d91d5b47a9565ca7cc46008be68cc780236cda46b7acb83234e59f538bcc7"
}
},
"policy": "staged_inputs",
- "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a"
+ "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67"
}
}
diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json
@@ -2808,10 +2808,16 @@
"sha256": "fff99e755f35fa9f00708427b1b29290c0ecc2733c0ea070f5807ec7c37f6f11"
},
"scripts/package_contract.py": {
- "bytes": 25795,
- "git_blob": "d68f5df2cc26d252be264fe55ece761070d02bee",
+ "bytes": 25574,
+ "git_blob": "ea94df8c0f106abc85e47295c2da7b023c168123",
"mode": "100755",
- "sha256": "b88df13f9c0da49fc535cf9997c668ec1357e9dbf6db2b472ac29fc3304c888c"
+ "sha256": "f00372d39a5f9849b6a535ed829a50f52810c3ac9e0695e80a71c802e23f0394"
+ },
+ "scripts/package_privacy.py": {
+ "bytes": 2718,
+ "git_blob": "a05951b332833f03520884ab6877b89f4ca88120",
+ "mode": "100644",
+ "sha256": "4540c5254ceb68634dc4d028d1056ad40558c4919c6d2be0d2d4c31958f46cbe"
},
"scripts/persona-verifier.sh": {
"bytes": 240,
@@ -2850,10 +2856,10 @@
"sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0"
},
"scripts/swift-quality.sh": {
- "bytes": 2080,
- "git_blob": "65b81671c82fe4102e6599532b4d2d802cedf4a0",
+ "bytes": 2143,
+ "git_blob": "0dea0e430dcc4e246bc951938faaa25db2596074",
"mode": "100755",
- "sha256": "15c98b26fdae1e6db8032b3201a8ab7e820800c721f0a4791ff374f81e6a004d"
+ "sha256": "9bb734eafe3c375ef30732e59a837ec345da438d96dc7882ec7f6ee1de43c13a"
},
"scripts/test_app_dependency_graph.py": {
"bytes": 4007,
@@ -2909,6 +2915,12 @@
"mode": "100644",
"sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f"
},
+ "scripts/test_package_privacy.py": {
+ "bytes": 4467,
+ "git_blob": "f1f821a0db6742ae2de3425c98d19297eb3463e9",
+ "mode": "100644",
+ "sha256": "651fc803fe3e3dd1636aec75f9ea79d81b9407317f93a02e4074a8369558bc33"
+ },
"scripts/test_xcode_selection.py": {
"bytes": 4745,
"git_blob": "b633afbccd765a106e2ad591881926a9add542ac",
@@ -2916,10 +2928,10 @@
"sha256": "a917dfbc1d8ecac7d1539670651e4b468059bc77dfe31aeefbc6c6eb21bebbfb"
},
"scripts/verify-package-contract.sh": {
- "bytes": 1496,
- "git_blob": "b21f0d5e48bcedd71460f7c0f4cf0c8f1070b27a",
+ "bytes": 1532,
+ "git_blob": "88f15a643269a8851d7fef68fe14208e71e2d843",
"mode": "100755",
- "sha256": "8ecb7b9fc1997e1cdb37d642d6dbfa59194bf9182218aceab470077f721fcd85"
+ "sha256": "028a8310c4a61a4cc04f1d0f86b8b953413b8efef5cb2059fbb0c84338483428"
},
"scripts/xcode.sh": {
"bytes": 22437,
@@ -2928,13 +2940,13 @@
"sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 186929,
- "git_blob": "39860837f76433ef305fde969d9162fefa98e579",
+ "bytes": 187553,
+ "git_blob": "c0ce4d1c11ad8683bf9c45029db89c77dc8fc4f7",
"mode": "100644",
- "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df"
+ "sha256": "e62d91d5b47a9565ca7cc46008be68cc780236cda46b7acb83234e59f538bcc7"
}
},
"policy": "staged_inputs",
- "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a"
+ "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67"
}
}
diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json
@@ -2808,10 +2808,16 @@
"sha256": "fff99e755f35fa9f00708427b1b29290c0ecc2733c0ea070f5807ec7c37f6f11"
},
"scripts/package_contract.py": {
- "bytes": 25795,
- "git_blob": "d68f5df2cc26d252be264fe55ece761070d02bee",
+ "bytes": 25574,
+ "git_blob": "ea94df8c0f106abc85e47295c2da7b023c168123",
"mode": "100755",
- "sha256": "b88df13f9c0da49fc535cf9997c668ec1357e9dbf6db2b472ac29fc3304c888c"
+ "sha256": "f00372d39a5f9849b6a535ed829a50f52810c3ac9e0695e80a71c802e23f0394"
+ },
+ "scripts/package_privacy.py": {
+ "bytes": 2718,
+ "git_blob": "a05951b332833f03520884ab6877b89f4ca88120",
+ "mode": "100644",
+ "sha256": "4540c5254ceb68634dc4d028d1056ad40558c4919c6d2be0d2d4c31958f46cbe"
},
"scripts/persona-verifier.sh": {
"bytes": 240,
@@ -2850,10 +2856,10 @@
"sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0"
},
"scripts/swift-quality.sh": {
- "bytes": 2080,
- "git_blob": "65b81671c82fe4102e6599532b4d2d802cedf4a0",
+ "bytes": 2143,
+ "git_blob": "0dea0e430dcc4e246bc951938faaa25db2596074",
"mode": "100755",
- "sha256": "15c98b26fdae1e6db8032b3201a8ab7e820800c721f0a4791ff374f81e6a004d"
+ "sha256": "9bb734eafe3c375ef30732e59a837ec345da438d96dc7882ec7f6ee1de43c13a"
},
"scripts/test_app_dependency_graph.py": {
"bytes": 4007,
@@ -2909,6 +2915,12 @@
"mode": "100644",
"sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f"
},
+ "scripts/test_package_privacy.py": {
+ "bytes": 4467,
+ "git_blob": "f1f821a0db6742ae2de3425c98d19297eb3463e9",
+ "mode": "100644",
+ "sha256": "651fc803fe3e3dd1636aec75f9ea79d81b9407317f93a02e4074a8369558bc33"
+ },
"scripts/test_xcode_selection.py": {
"bytes": 4745,
"git_blob": "b633afbccd765a106e2ad591881926a9add542ac",
@@ -2916,10 +2928,10 @@
"sha256": "a917dfbc1d8ecac7d1539670651e4b468059bc77dfe31aeefbc6c6eb21bebbfb"
},
"scripts/verify-package-contract.sh": {
- "bytes": 1496,
- "git_blob": "b21f0d5e48bcedd71460f7c0f4cf0c8f1070b27a",
+ "bytes": 1532,
+ "git_blob": "88f15a643269a8851d7fef68fe14208e71e2d843",
"mode": "100755",
- "sha256": "8ecb7b9fc1997e1cdb37d642d6dbfa59194bf9182218aceab470077f721fcd85"
+ "sha256": "028a8310c4a61a4cc04f1d0f86b8b953413b8efef5cb2059fbb0c84338483428"
},
"scripts/xcode.sh": {
"bytes": 22437,
@@ -2928,13 +2940,13 @@
"sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 186929,
- "git_blob": "39860837f76433ef305fde969d9162fefa98e579",
+ "bytes": 187553,
+ "git_blob": "c0ce4d1c11ad8683bf9c45029db89c77dc8fc4f7",
"mode": "100644",
- "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df"
+ "sha256": "e62d91d5b47a9565ca7cc46008be68cc780236cda46b7acb83234e59f538bcc7"
}
},
"policy": "staged_inputs",
- "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a"
+ "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67"
}
}
diff --git a/TeraTests/TeraPrivacyPackagingTests.swift b/TeraTests/TeraPrivacyPackagingTests.swift
@@ -0,0 +1,47 @@
+import CoreFoundation
+import Foundation
+@testable import TeraApp
+import XCTest
+
+final class TeraPrivacyPackagingTests: XCTestCase {
+ func testInstalledBundleContainsLinkedPublicContentAndReviewedReasonsWithoutTracking() throws {
+ let url = try XCTUnwrap(Bundle.main.url(forResource: "PrivacyInfo", withExtension: "xcprivacy"))
+ let value = try PropertyListSerialization.propertyList(from: Data(contentsOf: url), format: nil)
+ let manifest = try XCTUnwrap(value as? [String: Any])
+ let tracking = try XCTUnwrap(manifest["NSPrivacyTracking"] as? NSNumber)
+ XCTAssertEqual(CFGetTypeID(tracking), CFBooleanGetTypeID())
+ XCTAssertFalse(tracking.boolValue)
+ XCTAssertEqual(manifest["NSPrivacyTrackingDomains"] as? [String], [])
+ let dataTypes = try XCTUnwrap(manifest["NSPrivacyCollectedDataTypes"] as? [[String: Any]])
+ XCTAssertEqual(dataTypes.compactMap { $0["NSPrivacyCollectedDataType"] as? String }, [
+ "NSPrivacyCollectedDataTypeName", "NSPrivacyCollectedDataTypeUserID",
+ "NSPrivacyCollectedDataTypePhysicalAddress", "NSPrivacyCollectedDataTypePhotosorVideos",
+ "NSPrivacyCollectedDataTypeOtherUserContent",
+ ])
+ for type in dataTypes {
+ XCTAssertEqual(type["NSPrivacyCollectedDataTypeLinked"] as? Bool, true)
+ XCTAssertEqual(type["NSPrivacyCollectedDataTypeTracking"] as? Bool, false)
+ XCTAssertEqual(type["NSPrivacyCollectedDataTypePurposes"] as? [String], ["NSPrivacyCollectedDataTypePurposeAppFunctionality"])
+ }
+ let apis = try XCTUnwrap(manifest["NSPrivacyAccessedAPITypes"] as? [[String: Any]])
+ XCTAssertEqual(apis.compactMap { $0["NSPrivacyAccessedAPIType"] as? String }, [
+ "NSPrivacyAccessedAPICategoryFileTimestamp", "NSPrivacyAccessedAPICategoryDiskSpace", "NSPrivacyAccessedAPICategoryUserDefaults",
+ ])
+ XCTAssertEqual(apis.compactMap { $0["NSPrivacyAccessedAPITypeReasons"] as? [String] }, [["C617.1"], ["E174.1"], ["CA92.1"]])
+ }
+
+ func testInstalledPurposeAndSupportContactDescribeCurrentPublicPosting() throws {
+ XCTAssertEqual(Bundle.main.object(forInfoDictionaryKey: "NSCameraUsageDescription") as? String,
+ "Tera uses the camera only when you choose to add a photo to a public post.")
+ XCTAssertEqual(Bundle.main.object(forInfoDictionaryKey: "TeraSupportEmail") as? String, "support@radroots.org")
+ let mail = try XCTUnwrap(TeraSupportContact.packagedMailURL)
+ XCTAssertEqual(mail.absoluteString, "mailto:support@radroots.org")
+ }
+
+ func testContactRefusesUnreviewedOrInjectedRecipients() {
+ for email: String? in [nil, "", "operator@example.invalid", "support@radroots.org?subject=private", " support@radroots.org"] {
+ XCTAssertNil(TeraSupportContact.mailURL(email: email))
+ }
+ XCTAssertEqual(TeraSupportContact.mailURL(email: "support@radroots.org")?.absoluteString, "mailto:support@radroots.org")
+ }
+}
diff --git a/project.yml b/project.yml
@@ -13,7 +13,7 @@ packages:
path: .
RadrootsKit:
url: https://github.com/radrootslabs/apple_kit.git
- revision: 1126a77ed87387719a6c6d3b4ce580582af29553
+ revision: d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2
targets:
Tera:
@@ -164,6 +164,7 @@ targets:
- path: TeraTests/TeraCapacitySettingsTests.swift
- path: TeraTests/TeraLifecycleTests.swift
- path: TeraTests/TeraDiagnosticPolicyTests.swift
+ - path: TeraTests/TeraPrivacyPackagingTests.swift
- path: TeraTests/TeraClockTests.swift
- path: TeraTests/TeraCalendarFFICompatibilityTests.swift
- path: TeraTests/TeraCalendarTimingTests.swift
diff --git a/release/provenance.json b/release/provenance.json
@@ -2,11 +2,11 @@
"artifacts": {
"app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b",
"ffi_api_sha256": "5d74060f97e52dad69ab7567c16d6b6b34acfab846f468450d89e2394a8a69d5",
- "ffi_provenance_sha256": "feaf4681644f041ce4bf841cf1e3c29b03bba15c9de9858f1c0a1ccf63419993",
- "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef",
- "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12",
- "sbom_sha256": "9e243176e3eaec256f404207d284314e8c96d3c6861077acb8e760640047ee2f",
- "xcode_project_sha256": "6ad5a40e50a198c65afa5e5f78d229c951b618d8ee98e56e49aa5ee7596ff763"
+ "ffi_provenance_sha256": "c0aebdf85e1295346b78d3b2b384cb0e64f5a9f0a2348e08f70398b6471fe862",
+ "info_plist_sha256": "b55010dd73b34bf5ad910a888be0ea1ae45f2a5c14329fa6a57059a317bdbd42",
+ "privacy_manifest_sha256": "0f2103257ba596fbcaeb1d6a4dffe3ae021e84c010178852c6bf500a8cfb0f19",
+ "sbom_sha256": "f0a15c2c9ad7799539fbc52fd1fdbcf17f4046b6f86cfe0a9b8980adc0079e10",
+ "xcode_project_sha256": "a14bc2bd4f2b69020947721014432389804c0a8797c70aa90172810407649a2e"
},
"disposition": "unsigned",
"platforms": [
@@ -21,9 +21,9 @@
"consumer_source_lock_sha256": "cb660a5c2ba1a8e0ef5629152364a9be86eca83220fb40d5bff7f800bf9c32a0",
"lib_revision": "189c49b74b4bafc142b00b76b296477931139e72",
"source_date_epoch": 1787871027,
- "swift_package_lock_sha256": "322eaec80d4b85ef9eb20da95ff95c4d26d8555ca358ba0c8eb890adf0b6671e",
- "tera_ffi_source_tree": "8f9abc83606e9996524f519b8bfb18dc3348590a",
- "xcode_package_lock_sha256": "4e29fed46339d5e382fa78ec85b61be4296455ece4ddc40602d793c049e673b3"
+ "swift_package_lock_sha256": "8aec408132602a249111a65903c985cca5898ec316a45916d88ba756f08ea40d",
+ "tera_ffi_source_tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67",
+ "xcode_package_lock_sha256": "3656728f8251f3ea644aa150d154819766470ca7bd4f85b7c51fdf301d361d16"
},
"version": "0.1.0-alpha"
}
diff --git a/release/sbom.cdx.json b/release/sbom.cdx.json
@@ -7856,7 +7856,7 @@
"version": "1.0.23"
},
{
- "bom-ref": "swift:apple_kit@1126a77ed87387719a6c6d3b4ce580582af29553?source=https://github.com/radrootslabs/apple_kit.git",
+ "bom-ref": "swift:apple_kit@d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2?source=https://github.com/radrootslabs/apple_kit.git",
"name": "apple_kit",
"properties": [
{
@@ -7869,11 +7869,11 @@
},
{
"name": "radroots.package.revision",
- "value": "1126a77ed87387719a6c6d3b4ce580582af29553"
+ "value": "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2"
}
],
"type": "library",
- "version": "1126a77ed87387719a6c6d3b4ce580582af29553"
+ "version": "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2"
},
{
"bom-ref": "swift:swift-secp256k1@e70a10e036a55fffea31568f0af92d69b6d449cd?source=https://github.com/21-DOT-DEV/swift-secp256k1.git",
@@ -10770,14 +10770,14 @@
"cargo:tera_core@0.1.0-alpha?source=workspace",
"cargo:tera_ffi@0.1.0-alpha?source=workspace",
"cargo:tera_wasm@0.1.0-alpha?source=workspace",
- "swift:apple_kit@1126a77ed87387719a6c6d3b4ce580582af29553?source=https://github.com/radrootslabs/apple_kit.git",
+ "swift:apple_kit@d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2?source=https://github.com/radrootslabs/apple_kit.git",
"swift:swift-secp256k1@e70a10e036a55fffea31568f0af92d69b6d449cd?source=https://github.com/21-DOT-DEV/swift-secp256k1.git"
],
"ref": "pkg:generic/tera@0.1.0-alpha"
},
{
"dependsOn": [],
- "ref": "swift:apple_kit@1126a77ed87387719a6c6d3b4ce580582af29553?source=https://github.com/radrootslabs/apple_kit.git"
+ "ref": "swift:apple_kit@d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2?source=https://github.com/radrootslabs/apple_kit.git"
},
{
"dependsOn": [],
diff --git a/scripts/package_contract.py b/scripts/package_contract.py
@@ -19,6 +19,7 @@ from typing import Any
import app_dependency_graph
import legacy_identifiers
+import package_privacy
MAX_CONTRACT_BYTES = 2 * 1024 * 1024
GIT_REVISION = re.compile(r"^[0-9a-f]{40}$")
@@ -266,19 +267,10 @@ def _apple_dependency_revision(dependency: object) -> object | None:
def _validate_privacy(document: dict[str, Any]) -> None:
- _exact(document.get("NSPrivacyTracking"), False, "privacy tracking")
- _exact(document.get("NSPrivacyTrackingDomains"), [], "privacy tracking domains")
- _exact(document.get("NSPrivacyCollectedDataTypes"), [], "privacy collected data")
- _exact(
- document.get("NSPrivacyAccessedAPITypes"),
- [
- {
- "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
- "NSPrivacyAccessedAPITypeReasons": ["CA92.1"],
- }
- ],
- "privacy accessed APIs",
- )
+ try:
+ package_privacy.validate_manifest(document)
+ except ValueError as error:
+ raise PackageContractError(str(error)) from error
def _validate_app_plist(document: dict[str, Any]) -> None:
@@ -290,6 +282,10 @@ def _validate_app_plist(document: dict[str, Any]) -> None:
value = document.get(key)
if not isinstance(value, str) or not value.strip():
raise PackageContractError(f"required plist purpose is absent: {key}")
+ try:
+ package_privacy.validate_purposes(document)
+ except ValueError as error:
+ raise PackageContractError(str(error)) from error
_exact(
document.get("NSAppTransportSecurity"),
{"NSAllowsLocalNetworking": True},
diff --git a/scripts/package_privacy.py b/scripts/package_privacy.py
@@ -0,0 +1,70 @@
+"""Owned privacy and contact declarations for the implemented public product."""
+
+from __future__ import annotations
+
+from typing import Any
+
+SUPPORT_EMAIL = "support@radroots.org"
+CAMERA_PURPOSE = (
+ "Tera uses the camera only when you choose to add a photo to a public post."
+)
+
+
+def manifest() -> dict[str, Any]:
+ """Publicly posted identity/profile/content/media remain linked off device."""
+ data_types = [
+ "NSPrivacyCollectedDataTypeName",
+ "NSPrivacyCollectedDataTypeUserID",
+ "NSPrivacyCollectedDataTypePhysicalAddress",
+ "NSPrivacyCollectedDataTypePhotosorVideos",
+ "NSPrivacyCollectedDataTypeOtherUserContent",
+ ]
+ return {
+ "NSPrivacyTracking": False,
+ "NSPrivacyTrackingDomains": [],
+ "NSPrivacyCollectedDataTypes": [
+ {
+ "NSPrivacyCollectedDataType": value,
+ "NSPrivacyCollectedDataTypeLinked": True,
+ "NSPrivacyCollectedDataTypeTracking": False,
+ "NSPrivacyCollectedDataTypePurposes": [
+ "NSPrivacyCollectedDataTypePurposeAppFunctionality"
+ ],
+ }
+ for value in data_types
+ ],
+ "NSPrivacyAccessedAPITypes": [
+ {
+ "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",
+ "NSPrivacyAccessedAPITypeReasons": ["C617.1"],
+ },
+ {
+ "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace",
+ "NSPrivacyAccessedAPITypeReasons": ["E174.1"],
+ },
+ {
+ "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
+ "NSPrivacyAccessedAPITypeReasons": ["CA92.1"],
+ },
+ ],
+ }
+
+
+def validate_manifest(document: dict[str, Any]) -> None:
+ # JSON preserves boolean versus numeric types; Python equality aliases 0
+ # and False and cannot independently validate a privacy declaration.
+ import json
+
+ try:
+ actual = json.dumps(document, sort_keys=True, allow_nan=False)
+ except (TypeError, ValueError) as error:
+ raise ValueError("privacy manifest contains unsupported values") from error
+ if actual != json.dumps(manifest(), sort_keys=True):
+ raise ValueError("privacy manifest differs from implemented public product")
+
+
+def validate_purposes(document: dict[str, Any]) -> None:
+ if document.get("NSCameraUsageDescription") != CAMERA_PURPOSE:
+ raise ValueError("camera purpose differs from implemented public posting")
+ if document.get("TeraSupportEmail") != SUPPORT_EMAIL:
+ raise ValueError("support contact differs from approved canonical contact")
diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh
@@ -30,6 +30,8 @@ readonly -a PYTHON_QUALITY_PATHS=(
scripts/test_ffi_provenance.py
scripts/maintainability_ratchet.py
scripts/package_contract.py
+ scripts/package_privacy.py
+ scripts/test_package_privacy.py
scripts/legacy_identifiers.py
scripts/test_legacy_identifiers.py
scripts/app_dependency_graph.py
diff --git a/scripts/test_package_privacy.py b/scripts/test_package_privacy.py
@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+import copy
+import sys
+import unittest
+from pathlib import Path
+
+SCRIPTS = Path(__file__).resolve().parent
+if str(SCRIPTS) not in sys.path:
+ sys.path.insert(0, str(SCRIPTS))
+
+import package_contract as contract # noqa: E402
+import package_privacy as privacy # noqa: E402
+
+
+class PrivacyContractTests(unittest.TestCase):
+ def test_real_manifest_and_permission_contact_inputs_match_the_product(
+ self,
+ ) -> None:
+ root = SCRIPTS.parent
+ contract._validate_privacy(
+ contract._read_plist(root / "Tera/Resources/PrivacyInfo.xcprivacy")
+ )
+ contract._validate_app_plist(contract._read_plist(root / "Tera/Info.plist"))
+
+ def test_tracking_and_linkage_cannot_hide_behind_numeric_boolean_aliases(
+ self,
+ ) -> None:
+ for numeric in [0, 0.0, 1, 1.0]:
+ for target in ["tracking", "linked", "entry_tracking"]:
+ with self.subTest(numeric=numeric, target=target):
+ value = privacy.manifest()
+ if target == "tracking":
+ value["NSPrivacyTracking"] = numeric
+ elif target == "linked":
+ value["NSPrivacyCollectedDataTypes"][0][
+ "NSPrivacyCollectedDataTypeLinked"
+ ] = numeric
+ else:
+ value["NSPrivacyCollectedDataTypes"][0][
+ "NSPrivacyCollectedDataTypeTracking"
+ ] = numeric
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_privacy(value)
+
+ def test_missing_data_type_reason_or_tracking_declaration_fails_closed(
+ self,
+ ) -> None:
+ base = privacy.manifest()
+ for key in base:
+ with self.subTest(key=key):
+ value = copy.deepcopy(base)
+ del value[key]
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_privacy(value)
+ for inventory in ["NSPrivacyCollectedDataTypes", "NSPrivacyAccessedAPITypes"]:
+ for index in range(len(base[inventory])):
+ with self.subTest(inventory=inventory, index=index):
+ value = copy.deepcopy(base)
+ value[inventory].pop(index)
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_privacy(value)
+
+ def test_duplicate_foreign_purpose_or_unreviewed_api_reason_fails_closed(
+ self,
+ ) -> None:
+ for inventory in ["NSPrivacyCollectedDataTypes", "NSPrivacyAccessedAPITypes"]:
+ value = privacy.manifest()
+ value[inventory].append(copy.deepcopy(value[inventory][0]))
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_privacy(value)
+ for kind, key, replacement in [
+ (
+ "NSPrivacyCollectedDataTypes",
+ "NSPrivacyCollectedDataTypePurposes",
+ ["NSPrivacyCollectedDataTypePurposeAnalytics"],
+ ),
+ (
+ "NSPrivacyAccessedAPITypes",
+ "NSPrivacyAccessedAPITypeReasons",
+ ["3B52.1"],
+ ),
+ ]:
+ value = privacy.manifest()
+ value[kind][0][key] = replacement
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_privacy(value)
+
+ def test_stale_permission_and_foreign_contact_cannot_pass_nonempty_string_checks(
+ self,
+ ) -> None:
+ root = SCRIPTS.parent
+ base = contract._read_plist(root / "Tera/Info.plist")
+ for key, bad in [
+ ("NSCameraUsageDescription", "Add a farm photo"),
+ ("TeraSupportEmail", "operator@example.invalid"),
+ ("TeraSupportEmail", None),
+ ]:
+ value = {**base, key: bad}
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_app_plist(value)
+
+ def test_unsupported_or_nonfinite_values_return_a_stable_rejection(self) -> None:
+ for value in [b"private", float("nan"), float("inf")]:
+ document = privacy.manifest()
+ document["unexpected"] = value
+ with self.assertRaises(contract.PackageContractError):
+ contract._validate_privacy(document)
diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh
@@ -14,6 +14,7 @@ sh "$repo_root/scripts/ffi-provenance.sh" contract-check
uv run --project "$python_project" --offline --frozen \
python -m unittest \
scripts/test_package_contract.py \
+ scripts/test_package_privacy.py \
scripts/test_legacy_identifiers.py \
scripts/test_app_dependency_graph.py \
scripts/test_ffi_provenance.py \
diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json
@@ -126,9 +126,17 @@
},
{
"identifier": "Radroots",
- "category": "retired_producer_rejection",
+ "category": "public_namespace",
"occurrences": [
{
+ "path": "README.md",
+ "count": 1
+ },
+ {
+ "path": "Tera/Views/TeraSupportSettingsSection.swift",
+ "count": 1
+ },
+ {
"path": "scripts/test_legacy_identifiers.py",
"count": 1
}
@@ -4265,14 +4273,30 @@
"count": 2
},
{
+ "path": "README.md",
+ "count": 1
+ },
+ {
"path": "Tera/Config/Base.xcconfig",
"count": 2
},
{
+ "path": "Tera/Info.plist",
+ "count": 1
+ },
+ {
"path": "Tera/State/TeraConfigurationStore.swift",
"count": 1
},
{
+ "path": "Tera/Views/TeraSupportSettingsSection.swift",
+ "count": 2
+ },
+ {
+ "path": "TeraTests/TeraPrivacyPackagingTests.swift",
+ "count": 6
+ },
+ {
"path": "TeraTests/TeraStateMigrationTests.swift",
"count": 1
},
@@ -4297,6 +4321,10 @@
"count": 2
},
{
+ "path": "scripts/package_privacy.py",
+ "count": 1
+ },
+ {
"path": "scripts/test_local_social_fixture.py",
"count": 8
}
diff --git a/test-fixtures/maintainability-baseline.v1.json b/test-fixtures/maintainability-baseline.v1.json
@@ -8,47 +8,143 @@
"python_function_complexity": 10
},
"swift_file_exception": [
- {"path": "Tera/Runtime/TeraGeneratedRuntimeBackend.swift", "maximum_lines": 1188},
- {"path": "Tera/Runtime/TeraRuntimeClient.swift", "maximum_lines": 820},
- {"path": "Tera/Runtime/TeraRuntimeModels.swift", "maximum_lines": 1154},
- {"path": "Tera/State/TeraAddStore.swift", "maximum_lines": 783},
- {"path": "Tera/State/TeraConfigurationStore.swift", "maximum_lines": 759},
- {"path": "TeraTests/TeraAddStoreTests.swift", "maximum_lines": 1366},
- {"path": "TeraTests/TeraStateMigrationTests.swift", "maximum_lines": 718},
- {"path": "TeraUITests/TeraRemoteQualificationUITests.swift", "maximum_lines": 1992}
+ {
+ "path": "Tera/Runtime/TeraGeneratedRuntimeBackend.swift",
+ "maximum_lines": 1188
+ },
+ {
+ "path": "Tera/Runtime/TeraRuntimeClient.swift",
+ "maximum_lines": 820
+ },
+ {
+ "path": "Tera/Runtime/TeraRuntimeModels.swift",
+ "maximum_lines": 1154
+ },
+ {
+ "path": "Tera/State/TeraAddStore.swift",
+ "maximum_lines": 783
+ },
+ {
+ "path": "Tera/State/TeraConfigurationStore.swift",
+ "maximum_lines": 759
+ },
+ {
+ "path": "TeraTests/TeraAddStoreTests.swift",
+ "maximum_lines": 1366
+ },
+ {
+ "path": "TeraTests/TeraStateMigrationTests.swift",
+ "maximum_lines": 718
+ },
+ {
+ "path": "TeraUITests/TeraRemoteQualificationUITests.swift",
+ "maximum_lines": 1992
+ }
],
"python_file_exception": [
- {"path": "scripts/local-social-fixture.py", "maximum_lines": 2626},
- {"path": "scripts/test_local_social_fixture.py", "maximum_lines": 1188}
+ {
+ "path": "scripts/local-social-fixture.py",
+ "maximum_lines": 2626
+ },
+ {
+ "path": "scripts/test_local_social_fixture.py",
+ "maximum_lines": 1188
+ }
],
"python_complexity_exception": [
- {"function": "scripts/local-social-fixture.py:FixtureState._publish_persona_event", "maximum_complexity": 13},
- {"function": "scripts/local-social-fixture.py:FixtureState.upload", "maximum_complexity": 14},
- {"function": "scripts/local-social-fixture.py:RelayHandler.handle", "maximum_complexity": 26},
- {"function": "scripts/local-social-fixture.py:bounded_directory_inventory", "maximum_complexity": 12},
- {"function": "scripts/local-social-fixture.py:directory_digest", "maximum_complexity": 16},
- {"function": "scripts/local-social-fixture.py:exact_persona_test_node", "maximum_complexity": 11},
- {"function": "scripts/local-social-fixture.py:load_exported_persona_attachments", "maximum_complexity": 31},
- {"function": "scripts/local-social-fixture.py:matches", "maximum_complexity": 22},
- {"function": "scripts/local-social-fixture.py:photo_attempt_matches", "maximum_complexity": 12},
- {"function": "scripts/local-social-fixture.py:reconstruct_persona_result_v2", "maximum_complexity": 66},
- {"function": "scripts/local-social-fixture.py:simulator_metadata", "maximum_complexity": 23},
- {"function": "scripts/local-social-fixture.py:valid_blossom_authorization", "maximum_complexity": 29},
- {"function": "scripts/local-social-fixture.py:valid_bud11_server_domain", "maximum_complexity": 12},
- {"function": "scripts/local-social-fixture.py:valid_nostr_event", "maximum_complexity": 16},
- {"function": "scripts/local-social-fixture.py:validate_persona_attempt_evidence", "maximum_complexity": 59},
- {"function": "scripts/local-social-fixture.py:validate_persona_evidence", "maximum_complexity": 31},
- {"function": "scripts/local-social-fixture.py:validate_persona_result", "maximum_complexity": 43},
- {"function": "scripts/local-social-fixture.py:validate_persona_suite", "maximum_complexity": 28},
- {"function": "scripts/local-social-fixture.py:verify_bip340", "maximum_complexity": 11},
- {"function": "scripts/local-social-fixture.py:verify_persona", "maximum_complexity": 18},
- {"function": "scripts/test_local_social_fixture.py:mutate_bud11_event", "maximum_complexity": 29}
+ {
+ "function": "scripts/local-social-fixture.py:FixtureState._publish_persona_event",
+ "maximum_complexity": 13
+ },
+ {
+ "function": "scripts/local-social-fixture.py:FixtureState.upload",
+ "maximum_complexity": 14
+ },
+ {
+ "function": "scripts/local-social-fixture.py:RelayHandler.handle",
+ "maximum_complexity": 26
+ },
+ {
+ "function": "scripts/local-social-fixture.py:bounded_directory_inventory",
+ "maximum_complexity": 12
+ },
+ {
+ "function": "scripts/local-social-fixture.py:directory_digest",
+ "maximum_complexity": 16
+ },
+ {
+ "function": "scripts/local-social-fixture.py:exact_persona_test_node",
+ "maximum_complexity": 11
+ },
+ {
+ "function": "scripts/local-social-fixture.py:load_exported_persona_attachments",
+ "maximum_complexity": 31
+ },
+ {
+ "function": "scripts/local-social-fixture.py:matches",
+ "maximum_complexity": 22
+ },
+ {
+ "function": "scripts/local-social-fixture.py:photo_attempt_matches",
+ "maximum_complexity": 12
+ },
+ {
+ "function": "scripts/local-social-fixture.py:reconstruct_persona_result_v2",
+ "maximum_complexity": 66
+ },
+ {
+ "function": "scripts/local-social-fixture.py:simulator_metadata",
+ "maximum_complexity": 23
+ },
+ {
+ "function": "scripts/local-social-fixture.py:valid_blossom_authorization",
+ "maximum_complexity": 29
+ },
+ {
+ "function": "scripts/local-social-fixture.py:valid_bud11_server_domain",
+ "maximum_complexity": 12
+ },
+ {
+ "function": "scripts/local-social-fixture.py:valid_nostr_event",
+ "maximum_complexity": 16
+ },
+ {
+ "function": "scripts/local-social-fixture.py:validate_persona_attempt_evidence",
+ "maximum_complexity": 59
+ },
+ {
+ "function": "scripts/local-social-fixture.py:validate_persona_evidence",
+ "maximum_complexity": 31
+ },
+ {
+ "function": "scripts/local-social-fixture.py:validate_persona_result",
+ "maximum_complexity": 43
+ },
+ {
+ "function": "scripts/local-social-fixture.py:validate_persona_suite",
+ "maximum_complexity": 28
+ },
+ {
+ "function": "scripts/local-social-fixture.py:verify_bip340",
+ "maximum_complexity": 11
+ },
+ {
+ "function": "scripts/local-social-fixture.py:verify_persona",
+ "maximum_complexity": 18
+ },
+ {
+ "function": "scripts/test_local_social_fixture.py:mutate_bud11_event",
+ "maximum_complexity": 29
+ }
],
"bounded_module": [
"Tera/Runtime/TeraUserMessageClassifier.swift",
"Tera/Runtime/TeraUserMessages.swift",
+ "Tera/Views/TeraSupportSettingsSection.swift",
"scripts/maintainability_ratchet.py",
"scripts/package_contract.py",
- "scripts/test_package_contract.py"
+ "scripts/package_privacy.py",
+ "scripts/test_package_contract.py",
+ "scripts/test_package_privacy.py"
]
}