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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ jobs:
--public-key Flowlight/Resources/RulePacks/official-rule-packs-v1-public.pem \
--expected-key-id flowlight-rule-packs-2026-01 --current-app-version "$APP_VERSION" \
--extension-protocol 1 --now "$(date +%s)"
python3 scripts/release_notes.py "$APP_VERSION" >/dev/null

- name: Check the site builds and is committed
run: |
Expand Down
30 changes: 30 additions & 0 deletions Flowlight/Analysis/RulePackCatalogComposition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

enum RulePackCatalogComposition {
/// The signed feed may replace a built-in with the same stable ID or add an official pack. Omission is not a
/// tombstone: every built-in remains available as the signed-app emergency baseline.
static func compose(_ catalog: VerifiedRulePackCatalog,
currentAppVersion: String,
builtIns: [RulePackDefinition] = BuiltInRulePacks.all) throws -> VerifiedRulePackCatalog {
try RulePackValidator.validate(builtIns, currentAppVersion: currentAppVersion)
try RulePackValidator.validate(catalog.definitions, currentAppVersion: currentAppVersion)

var merged: [String: RulePackDefinition] = [:]
for definition in builtIns {
guard merged.updateValue(definition, forKey: definition.id) == nil else {
throw RulePackValidationError.duplicatePack(definition.id)
}
}
for definition in catalog.definitions {
guard definition.id.hasPrefix("official.") else {
throw RulePackValidationError.invalidPackID(definition.id)
}
merged[definition.id] = definition
}

var composed = catalog
composed.definitions = merged.values.sorted { $0.id < $1.id }
try RulePackValidator.validate(composed.definitions, currentAppVersion: currentAppVersion)
return composed
}
}
71 changes: 71 additions & 0 deletions Flowlight/Analysis/RulePackCatalogState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

struct RulePackCatalogReceipt: Equatable, Sendable {
var catalogID: String
var sequence: Int64
var digest: String
var signerKeyID: String
var acceptedAt: Date
}

struct RulePackCatalogCache: Equatable, Sendable {
var catalogID: String
/// Exact signed bytes. Re-encoding would invalidate both the signature evidence and byte-for-byte recovery.
var rawEnvelope: Data
var etag: String?
var lastModified: String?
var verifiedAt: Date
}

struct RulePackCatalogUpdateState: Equatable, Sendable {
var catalogID: String
var automaticRefresh = true
var lastAttempt: Date?
var lastSuccess: Date?
var nextEligibleAttempt: Date?
var failureCount = 0
var lastStatus: String?
var lastError: String?
}

struct RulePackCatalogState: Equatable, Sendable {
var receipt: RulePackCatalogReceipt?
var cache: RulePackCatalogCache?
var update: RulePackCatalogUpdateState
}

/// The exact content shown by an editor. Publisher authentication and user consent are separate decisions.
struct RulePackReviewReceipt: Equatable, Sendable {
var packID: String
var version: String
var contentSHA256: String
var catalogSequence: Int64
var catalogDigest: String

func matches(_ definition: RulePackDefinition, catalogSequence: Int64, catalogDigest: String) -> Bool {
packID == definition.id
&& version == definition.manifest.version
&& contentSHA256 == definition.manifest.contentSHA256
&& self.catalogSequence == catalogSequence
&& self.catalogDigest == catalogDigest
}
}

enum RulePackCatalogPersistenceError: Error, Equatable {
case invalidCatalogID
case invalidReceipt
case rollback
case equivocation
case staleReview
}

struct RulePackCatalogAcceptance: Equatable, Sendable {
var state: RulePackCatalogState
/// All saved rows, including dormant IDs omitted by the currently active catalog.
var configurations: [RulePackConfiguration]
/// Durable user state is returned with every accepted catalog so an omitted and later reintroduced pack cannot
/// resume enforcement without the exceptions and history that still belong to its stable ID.
var overrides: [RulePackOverride]
var events: [RulePackEventRecord]
var deliveryStats: RulePackDeliveryStats
}
Loading
Loading