From 9353292a31d02cc077ab3a4be576bb76e68dfda9 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 16 Oct 2026 09:56:05 +0200 Subject: [PATCH 1/2] iPhone: security notices from FTW Cloud, and the car class in CloudView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notification extension reads a box-signed notice in an alert as an event of its kind, for a day, since a notice states a lasting fact. It checks the notice as Go's ReadNotice does: a 16-byte boot, and on agent.added a client's name of at most the consent page's 64 characters and its "…", with no control or format character. A notice kind sent as an event still reads as generic (#78). agent.added names the client when the box sent one: "Claude was given access to your box". An owner's phone registers for FTW Cloud's alerts whenever the box is linked, so notices reach it with the readable copy off. PushSeen keeps the numbers it showed per boot, the newest 64 above a floor, so a notice that arrives after a later event is shown, not quieted as a repeat. A file from before keeps its highest as the floor. CloudView holds the car class and joins a charger to its session's car by carId, the car winning where the two disagree. socReadAtMs dates the level from the car class's own send. The push vectors and the cloud vectors are srcfl/ftw's. Co-Authored-By: Claude Opus 5.5 --- .../Sources/FTWKit/Carrier/CloudView.swift | 53 ++++++++ .../Sources/FTWKit/Carrier/CloudWire.swift | 99 ++++++++++++++- .../Sources/FTWKit/State/SiteModel.swift | 24 ++-- .../FTWKit/Sources/FTWShared/PushAlert.swift | 56 ++++++++- .../FTWKit/Sources/FTWShared/PushSeen.swift | 29 +++-- .../FTWKit/Sources/FTWShared/PushText.swift | 23 ++-- .../Tests/FTWKitTests/CloudViewTests.swift | 52 ++++++++ .../Tests/FTWKitTests/CloudWireTests.swift | 32 ++++- .../FTWKitTests/Resources/cloud-vectors.json | 90 ++++++++++--- .../Tests/FTWSharedTests/PushAlertTests.swift | 4 +- .../Tests/FTWSharedTests/PushStoreTests.swift | 27 +++- .../Tests/FTWSharedTests/PushTextTests.swift | 23 ++++ .../Resources/cloud-vectors.json | 90 ++++++++++--- .../Resources/push-vectors.json | 119 ++++++++++++++++++ 14 files changed, 653 insertions(+), 68 deletions(-) diff --git a/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudView.swift b/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudView.swift index 9ec2899..0381317 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudView.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudView.swift @@ -80,6 +80,12 @@ public struct CloudView: Sendable { /// The newest of each class this phone shows. public private(set) var live: CloudWire.Live? public private(set) var lp: CloudWire.LP? + /// Each car's level and goal. A charger's `carId` joins it; where the + /// two disagree, this wins. + public private(set) var cars: CloudWire.Cars? + /// When the box sent the newest lp and car, by the phone's clock, when + /// a rule anchors it. + private var sentAt: [String: Int64] = [:] /// Kept for the screens and left out of `grade`, as in Go: a price /// window ages by its own slots, not by the box's sources. public private(set) var prices: CloudWire.Prices? @@ -115,6 +121,45 @@ public struct CloudView: Sendable { m.map { ($0.uptimeMs, $0.ages) } } + /// A charger's level and goal as its session's car has them, when the + /// car class holds that car; the charger as sent otherwise. The box + /// fills a charger's level fields from its car, so the two agree; where + /// they do not, as when one class arrived after the other, the car + /// wins (srcfl/ftw `docs/protocol-cloud.md`, lp). `socAgeS` is then the + /// car's, counted to the car class's send; `socReadAtMs` dates it. + public func joined(_ l: CloudWire.Loadpoint) -> CloudWire.Loadpoint { + guard let car = cars?.car(l.carId) else { return l } + var out = l + out.socPermille = car.socPermille + out.socAgeS = nil + switch true { + case car.socPermille == nil: out.socSource = "unknown" + case car.socSource == "assumed": out.socSource = "assumed" + case car.socEstimate || car.socSource != "vehicle": out.socSource = "inferred" + default: + out.socSource = "vehicle" + out.socAgeS = car.socAgeS + } + out.targetPermille = car.goal.flatMap { $0.targetPermille > 0 ? $0.targetPermille : nil } + out.recurring = (car.goal?.days ?? 0) != 0 + if car.goal == nil { out.readyByMs = nil } + out.surplusOnly = car.surplusOnly + return out + } + + /// When a charger's level was read, by the phone's clock: from its + /// car's class and that class's own send time when the class holds the + /// car, from the charger's message and its send otherwise. Nil without + /// an age, or when no rule anchors that send. + public func socReadAtMs(_ l: CloudWire.Loadpoint) -> Int64? { + if let car = cars?.car(l.carId) { + guard let age = car.socAgeS, let sent = sentAt[CloudWire.msgCar] else { return nil } + return sent &- age &* 1000 + } + guard let age = l.socAgeS, let sent = sentAt[CloudWire.msgLP] else { return nil } + return sent &- age &* 1000 + } + /// The boot the view holds, nil before the first message. public var boot: Bytes? { currentBoot.isEmpty ? nil : currentBoot } @@ -195,6 +240,8 @@ public struct CloudView: Sendable { } case .lp(let p): lp = p + case .cars(let c): + cars = c case .prices(let p): prices = p case .hb(let b): @@ -207,6 +254,7 @@ public struct CloudView: Sendable { break } seq[envelope.t] = h.seq + if envelope.t == CloudWire.msgLP || envelope.t == CloudWire.msgCar { sentAt[envelope.t] = send } if let send, send > newest { newest = send } } @@ -319,6 +367,7 @@ public struct CloudView: Sendable { private enum Message { case live(CloudWire.Live) case lp(CloudWire.LP) + case cars(CloudWire.Cars) case prices(CloudWire.Prices) case hb(CloudWire.HB) /// Classes the view only orders by seq. @@ -328,6 +377,7 @@ public struct CloudView: Sendable { switch t { case CloudWire.msgLive: self = .live(try CloudWire.Live(body: b)) case CloudWire.msgLP: self = .lp(try CloudWire.LP(body: b)) + case CloudWire.msgCar: self = .cars(try CloudWire.Cars(body: b)) case CloudWire.msgPrices: self = .prices(try CloudWire.Prices(body: b)) case CloudWire.msgHB: self = .hb(try CloudWire.HB(body: b)) default: self = .other(try CloudWire.Header(body: b)) @@ -338,6 +388,7 @@ public struct CloudView: Sendable { switch self { case .live(let l): return l.header case .lp(let p): return p.header + case .cars(let c): return c.header case .prices(let p): return p.header case .hb(let b): return b.header case .other(let h): return h @@ -360,6 +411,8 @@ public struct CloudView: Sendable { fieldSources = [:] live = nil lp = nil + cars = nil + sentAt = [:] prices = nil hb = nil m = nil diff --git a/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudWire.swift b/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudWire.swift index 4169334..81dc2ae 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudWire.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Carrier/CloudWire.swift @@ -54,10 +54,12 @@ public enum CloudWire { public static let msgEvent = "event" public static let msgTiles = "tiles" public static let msgHB = "hb" + /// Each car's level and goal, by opaque id (srcfl/ftw `docs/cars.md`). + public static let msgCar = "car" - /// Everything else the box sends (hello, roster, bye) is the service's - /// alone and never reaches a phone. - public static let phoneClasses: Set = [msgLive, msgLP, msgPrices, msgPlan, msgHealth, msgEvent, msgTiles, msgHB] + /// Everything else the box sends (hello, roster, bye, notice) is the + /// service's alone and never reaches a phone as a class. + public static let phoneClasses: Set = [msgLive, msgLP, msgCar, msgPrices, msgPlan, msgHealth, msgEvent, msgTiles, msgHB] // The app session's own types. All unsigned. /// Handshake message 1, `{hz}`. @@ -251,6 +253,12 @@ public enum CloudWire { opaqueID(grantKey, "lp\0", loadpoint) } + /// A car's opaque id, cut the same way with "car\0" from the id the + /// owner's app minted, so it differs per home. + static func carID(grantKey: Bytes, car: String) -> String { + opaqueID(grantKey, "car\0", car) + } + private static func opaqueID(_ key: Bytes, _ tag: String, _ name: String) -> String { Base64url.encode(Array(Primitives.hmacSHA256(key: key, Bytes(tag.utf8) + Bytes(name.utf8)).prefix(8))) } @@ -453,6 +461,57 @@ public enum CloudWire { public var nextStartMs: Int64? public var nextEndMs: Int64? public var nextWh: Int64? + /// Opaque id of the session's car, the join to `Cars`. Empty while + /// nothing is plugged in or the box does not know the car. With it, + /// the level and goal fields above are views of that car, and where + /// the two disagree the car class wins. + public var carId: String = "" + /// charger, car, only, tap, asking or none (`docs/cars.md`). + public var carIdentifiedBy: String = "" + public var carCorrected: Bool = false + public var carCorrectedFrom: String = "" + /// While asking: the cars a tap may pick, at most four. + public var choose: [String] = [] + } + + /// Each car's level and goal on this box. Nothing here names a car; the + /// names come from the box over Noise. + public struct Cars: Equatable, Sendable { + public var header: Header + public var cars: [Car] + + /// The car with this opaque id, as a charger's `carId` names it. + public func car(_ id: String) -> Car? { + id.isEmpty ? nil : cars.first { $0.id == id } + } + } + + public struct Car: Equatable, Sendable { + /// Opaque car id. + public var id: String + /// The car's level; the car's or a person's own number while + /// `socEstimate` is false. Nil when the box has none. + public var socPermille: Int64? + /// vehicle, manual or assumed; empty without a level. + public var socSource: String + /// Seconds since the car reported the level or someone set it, at + /// send. + public var socAgeS: Int64? + /// Energy into the car since then; negative after V2X. + public var energySinceWh: Int64 + public var socEstimate: Bool + public var chargeLimitPermille: Int64? + public var goal: CarGoal? + public var surplusOnly: Bool + } + + public struct CarGoal: Equatable, Sendable { + public var targetPermille: Int64 + /// Minutes after midnight in the box's time zone. + public var readyByMin: Int64 + /// ISO weekday mask, Monday in bit 0; 0 is once, 127 every day. + public var days: Int64 + public var finishAtLimit: Bool } /// A manual charging hold. @@ -581,7 +640,39 @@ extension CloudWire.Loadpoint { boost: try f.bool("boost"), nextStartMs: try f.optionalInt("nextStartMs"), nextEndMs: try f.optionalInt("nextEndMs"), - nextWh: try f.optionalInt("nextWh") + nextWh: try f.optionalInt("nextWh"), + carId: try f.text("carId"), + carIdentifiedBy: try f.text("carIdentifiedBy"), + carCorrected: try f.bool("carCorrected"), + carCorrectedFrom: try f.text("carCorrectedFrom"), + choose: try CloudWire.texts(f["choose"], "choose") + ) + } +} + +extension CloudWire.Cars { + public init(body: Bytes) throws { + let f = try CloudWire.Fields(body: body) + self.init(header: try CloudWire.Header(f), + cars: try f.list("cars") { try CloudWire.Car(CloudWire.Fields($0, "cars")) }) + } +} + +extension CloudWire.Car { + init(_ f: CloudWire.Fields) throws { + self.init( + id: try f.text("id"), + socPermille: try f.optionalInt("socPermille"), + socSource: try f.text("socSource"), + socAgeS: try f.optionalInt("socAgeS"), + energySinceWh: try f.int("energySinceWh"), + socEstimate: try f.bool("socEstimate"), + chargeLimitPermille: try f.optionalInt("chargeLimitPermille"), + goal: try f.optional("goal").map { g in + CloudWire.CarGoal(targetPermille: try g.int("targetPermille"), readyByMin: try g.int("readyByMin"), + days: try g.int("days"), finishAtLimit: try g.bool("finishAtLimit")) + }, + surplusOnly: try f.bool("surplusOnly") ) } } diff --git a/appleApp/FTWKit/Sources/FTWKit/State/SiteModel.swift b/appleApp/FTWKit/Sources/FTWKit/State/SiteModel.swift index 04e3983..c81462c 100644 --- a/appleApp/FTWKit/Sources/FTWKit/State/SiteModel.swift +++ b/appleApp/FTWKit/Sources/FTWKit/State/SiteModel.swift @@ -1138,7 +1138,7 @@ extension SiteModel { cloud = c cloudHome = home cloudStatus = c.status - if cloudOn { installPushSink() } + if takesAlerts { installPushSink() } c.start() } @@ -1194,11 +1194,18 @@ extension SiteModel { cloudDirty = true persistNow() startCloud() - // Alerts come with the readable copy, and only then is the person - // asked to allow them. - if cloudOn, cloud != nil { installPushSink() } else if !cloudOn { uninstallPushSink() } + // Alerts come with the readable copy, or to an owner's phone for the + // security notices whenever the box is linked, and only then is the + // person asked to allow them. + if takesAlerts, cloud != nil { installPushSink() } else if !takesAlerts { uninstallPushSink() } } + /// Whether this phone registers for FTW Cloud's alerts: an owner's + /// whenever the box is linked, since security notices go to every owner + /// with the readable copy on or off; another phone only while the copy + /// carries its events. + private var takesAlerts: Bool { cloudOn || canConfigure } + // MARK: Pushes /// The App Group files the notification extension reads. None in the @@ -1288,7 +1295,7 @@ extension SiteModel { guard var view = cloudView, (try? view.accept(env, arrivalMs: Int64(at))) != nil else { return false } cloudView = view switch env.t { - case CloudWire.msgLP: takeCloudCars(at) + case CloudWire.msgLP, CloudWire.msgCar: takeCloudCars(at) case CloudWire.msgPrices: takeCloudPrices(at) default: break } @@ -1397,13 +1404,16 @@ extension SiteModel { guard let view = cloudView, let lp = view.lp, let identity = cloudIdentity else { return } let now = scheduler.nowMs let g = view.grade(nowMs: Int64(now)) - let cars = lp.loadpoints.map { l in + let cars = lp.loadpoints.map(view.joined).map { l in CarEntry.Car( loadpointID: identity.loadpoints[l.id] ?? l.id, pluggedIn: l.plugged, socPct: l.socPermille.map { (Double($0) / 10).rounded() }, socSource: l.socSource, - socReadAtMs: l.socAgeS.map { now - Double($0) * 1000 }, + // Dated from the message that carried the age: the car + // class's own send for a car. Without an anchor, a charger + // without a car is dated from now, as before cars. + socReadAtMs: view.socReadAtMs(l).map(Double.init) ?? (l.carId.isEmpty ? l.socAgeS.map { now - Double($0) * 1000 } : nil), powerW: Double(l.powerW), targetSocPct: l.targetPermille.map { (Double($0) / 10).rounded() } ) diff --git a/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift b/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift index 72e681a..b793d5c 100644 --- a/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift +++ b/appleApp/FTWKit/Sources/FTWShared/PushAlert.swift @@ -27,7 +27,8 @@ public enum PushKind { public static let cloudGranted = "cloud.granted" public static let cloudRevoked = "cloud.revoked" /// Access from outside the house, for every owner (docs/agents.md in - /// srcfl/ftw). They come as the box's `notice`, never as an event. + /// srcfl/ftw). They come as the box's `notice`, never as an event, and + /// FTW Cloud brings them with the readable copy on or off. public static let agentAdded = "agent.added" public static let agentRequest = "agent.request" public static let agentExpiring = "agent.expiring" @@ -98,6 +99,8 @@ public struct PushAlert: Sendable, Equatable { static let clockLeadMs: Int64 = 2_000 /// How old an event may be and still get its text and buttons. static let maxAgeMs: Int64 = 10 * 60_000 + /// How old a notice may be: a day. + static let noticeMaxAgeMs: Int64 = 24 * 60 * 60_000 /// How long rule U's anchor stays usable after it was last learned. static let ruleULifetimeMs: Int64 = 24 * 60 * 60_000 /// Rule U's drift margin: 0.01 % of the time since it was learned. @@ -138,16 +141,22 @@ public struct PushAlert: Sendable, Equatable { guard let event = a.event, !event.isEmpty else { return a.kind == PushKind.boxUnreachable ? PushAlert(.unreachable, home: home) : generic } + // A notice reads as an event of its kind, with the client's name as + // its one argument when it carries one. guard a.kind == nil || a.kind == "", - let env = Envelope(Array(event)), env.t == "event", + let env = Envelope(Array(event)), env.t == "event" || env.t == "notice", let pk = boxKey(home), verify(pk, home: home, env), - let ev = Event(env.b), var at = sendTime(ev, offset: offset(home), nowMs: nowMs) else { + let ev = env.t == "event" ? Event(env.b) : Event(notice: env.b), + var at = sendTime(ev, offset: offset(home), nowMs: nowMs) else { return generic } if let ts = ev.ts { at = min(at, ts) } let age = nowMs.subtractingReportingOverflow(at) - guard PushKind.all.contains(ev.kind), ev.kind != PushKind.boxUnreachable, !PushKind.notices.contains(ev.kind), - !age.overflow, age.partialValue <= maxAgeMs else { + // A notice kind comes only as a notice, and a notice states a + // lasting fact, so it lasts a day. + let notice = env.t == "notice" + guard PushKind.all.contains(ev.kind), ev.kind != PushKind.boxUnreachable, PushKind.notices.contains(ev.kind) == notice, + !age.overflow, age.partialValue <= (notice ? noticeMaxAgeMs : maxAgeMs) else { return generic } return PushAlert(.event, home: home, kind: ev.kind, args: ev.args, src: ev.src, lp: ev.lp, boot: ev.boot.map { Data($0) }, seq: ev.seq, atMs: at) @@ -202,6 +211,17 @@ public struct PushAlert: Sendable, Equatable { } } + /// A notice's client name may be at most this many characters: the + /// consent page keeps 64 and marks a cut name with "…", as in Go. + static let noticeClientMaxRunes = 65 + + /// A client name as Go's NoticeClientOK takes it: no control or format + /// character, and not too long. + static func clientOK(_ c: String) -> Bool { + !c.isEmpty && c.unicodeScalars.count <= noticeClientMaxRunes && + !c.unicodeScalars.contains { $0.properties.generalCategory == .control || $0.properties.generalCategory == .format } + } + /// The event body. A field may be missing or null; one of the wrong /// type makes the whole body unreadable, as it does in Go. struct Event { @@ -238,6 +258,32 @@ public struct PushAlert: Sendable, Equatable { return nil } } + + /// A notice body, read as ReadNotice in Go reads it: the header, a + /// notice kind and, on agent.added only, the client's name. Any + /// other field makes it no notice, so it cannot carry a reading. + init?(notice b: [UInt8]) { + let allowed: Set = ["seq", "boot", "uptimeMs", "wallMs", "kind", "client"] + guard let m = (try? PushCBOR.decode(b))?.textMap, Set(m.keys).isSubset(of: allowed) else { return nil } + func field(_ key: String, _ read: (PushCBOR) -> T?) throws -> T? { + guard let v = m[key], v != .null else { return nil } + guard let out = read(v) else { throw PushCBOR.Malformed() } + return out + } + do { + seq = try field("seq", \.uint) ?? 0 + boot = try field("boot", \.byteString) + uptimeMs = try field("uptimeMs", \.int64) ?? 0 + wallMs = try field("wallMs", \.int64) + kind = try field("kind", \.text) ?? "" + let client = try field("client", \.text) ?? "" + guard PushKind.notices.contains(kind), boot?.count == 16, uptimeMs >= 0, + client.isEmpty || (kind == PushKind.agentAdded && PushAlert.clientOK(client)) else { return nil } + if !client.isEmpty { args = ["client": client] } + } catch { + return nil + } + } } } diff --git a/appleApp/FTWKit/Sources/FTWShared/PushSeen.swift b/appleApp/FTWKit/Sources/FTWShared/PushSeen.swift index af67a2b..b3e12b9 100644 --- a/appleApp/FTWKit/Sources/FTWShared/PushSeen.swift +++ b/appleApp/FTWKit/Sources/FTWShared/PushSeen.swift @@ -4,15 +4,21 @@ import Synchronization /// Events this phone has shown, so a push that arrives twice is shown once. /// /// The box numbers what it sends: `seq` rises by one per message within a -/// boot. Per home and boot the extension keeps the highest `seq` it showed; -/// an event at or below it is a duplicate or older than one already shown. -/// A new boot starts its own count. The file keeps the most recent few -/// boots and is in the App Group, beside the cache. +/// boot. Events and notices share the count, and a notice can arrive after +/// a later event, so per home and boot the extension keeps the numbers it +/// showed, the newest `recent` of them, and a floor below which everything +/// counts as shown: the numbers that fell out, and for a file from before +/// this list, the highest shown. A new boot starts its own count. The file +/// keeps the most recent few boots and is in the App Group, beside the +/// cache. public struct PushSeen: Sendable { public let file: URL /// Homes times boots worth remembering. Each box starts a new boot on /// every restart; the oldest goes first. public static let limit = 32 + /// Numbers kept per boot. Far more than a box raises in a day under + /// FTW Cloud's caps; a notice lives a day. + public static let recent = 64 public init(file: URL) { self.file = file @@ -25,7 +31,11 @@ public struct PushSeen: Sendable { struct Entry: Codable, Equatable { var home: String var boot: Data + /// The floor: anything at or below it counts as shown. var seq: UInt64 + /// The numbers shown above the floor, rising. Absent in a file from + /// before them, whose `seq` is the highest shown. + var shown: [UInt64]? /// The phone's clock when it was last moved, to drop the oldest. var atMs: Double } @@ -41,11 +51,16 @@ public struct PushSeen: Sendable { return Self.lock.withLock { _ in var entries = load() if let i = entries.firstIndex(where: { $0.home == alert.home && $0.boot == boot }) { - guard alert.seq > entries[i].seq else { return false } - entries[i].seq = alert.seq + var shown = entries[i].shown ?? [] + guard alert.seq > entries[i].seq, !shown.contains(alert.seq) else { return false } + shown.insert(alert.seq, at: shown.firstIndex { $0 > alert.seq } ?? shown.endIndex) + if shown.count > Self.recent { + entries[i].seq = max(entries[i].seq, shown.removeFirst()) + } + entries[i].shown = shown entries[i].atMs = nowMs } else { - entries.append(Entry(home: alert.home, boot: boot, seq: alert.seq, atMs: nowMs)) + entries.append(Entry(home: alert.home, boot: boot, seq: 0, shown: [alert.seq], atMs: nowMs)) } entries.sort { $0.atMs > $1.atMs } try? save(Array(entries.prefix(Self.limit))) diff --git a/appleApp/FTWKit/Sources/FTWShared/PushText.swift b/appleApp/FTWKit/Sources/FTWShared/PushText.swift index e5beca7..3845bf2 100644 --- a/appleApp/FTWKit/Sources/FTWShared/PushText.swift +++ b/appleApp/FTWKit/Sources/FTWShared/PushText.swift @@ -29,11 +29,14 @@ public enum PushText { } /// One kind's sentence. `{name}` marks an argument; `without` is the - /// body to show when an argument is missing or will not do. + /// body to show when an argument is missing or will not do. `named` is + /// the phone's own body for a notice that names the client given + /// access; without the name, `body` shows. struct Sentence { var title: String var body: String var without: String? + var named: String? } /// The line for anything that did not check out: no signature, an old @@ -57,6 +60,7 @@ public enum PushText { } guard let s = sentences(language)[kind] else { return generic(language) } let values = arguments(alert.args, name: sourceName, decimalSeparator: decimalSeparator) + if let named = s.named, let body = fill(named, values) { return Content(title: s.title, body: body) } guard let body = fill(s.body, values) else { return Content(title: s.title, body: s.without ?? s.body) } return Content(title: s.title, body: body) } @@ -76,7 +80,8 @@ public enum PushText { PushKind.boxUnreachable: Sentence(title: "Your box is out of reach", body: "It stopped answering. Power cut, or the internet at home is down."), PushKind.cloudGranted: Sentence(title: "FTW Cloud is on", body: "Your box now shares a copy of its readings with FTW Cloud. You can turn it off in the app or on the box."), PushKind.cloudRevoked: Sentence(title: "FTW Cloud is off", body: "Your box stopped sharing with FTW Cloud and asked it to delete its copy."), - PushKind.agentAdded: Sentence(title: "New access to your box", body: "You gave an assistant or Sourceful access. See who has access in the app."), + PushKind.agentAdded: Sentence(title: "New access to your box", body: "You gave an assistant or Sourceful access. See who has access in the app.", + named: "{client} was given access to your box. See who has access in the app."), PushKind.agentRequest: Sentence(title: "Waiting for your answer", body: "An assistant asks to change a setting. Open FTW to confirm or decline."), PushKind.agentExpiring: Sentence(title: "Access ends tomorrow", body: "An assistant's or Sourceful's access to your box runs out in a day. Renew it in the app if it should go on."), PushKind.approvalKeySet: Sentence(title: "A phone can now approve access", body: "A phone set up the key it uses to approve access to your box. If that was not you, remove the phone in the app."), @@ -95,7 +100,8 @@ public enum PushText { PushKind.boxUnreachable: Sentence(title: "Din box går inte att nå", body: "Den har slutat svara. Strömavbrott, eller så är internet hemma nere."), PushKind.cloudGranted: Sentence(title: "FTW Cloud är på", body: "Din box delar nu en kopia av sina mätvärden med FTW Cloud. Du kan stänga av det i appen eller på boxen."), PushKind.cloudRevoked: Sentence(title: "FTW Cloud är av", body: "Din box har slutat dela med FTW Cloud och bett den radera sin kopia."), - PushKind.agentAdded: Sentence(title: "Ny åtkomst till din box", body: "En assistent eller Sourceful har fått åtkomst. Se vem som har åtkomst i appen."), + PushKind.agentAdded: Sentence(title: "Ny åtkomst till din box", body: "En assistent eller Sourceful har fått åtkomst. Se vem som har åtkomst i appen.", + named: "{client} har fått åtkomst till din box. Se vem som har åtkomst i appen."), PushKind.agentRequest: Sentence(title: "Väntar på ditt svar", body: "En assistent vill ändra en inställning. Öppna FTW för att godkänna eller avböja."), PushKind.agentExpiring: Sentence(title: "Åtkomsten upphör i morgon", body: "En assistents eller Sourcefuls åtkomst till din box går ut om ett dygn. Förnya den i appen om den ska fortsätta."), PushKind.approvalKeySet: Sentence(title: "En telefon kan nu godkänna åtkomst", body: "En telefon har satt upp nyckeln den använder för att godkänna åtkomst till din box. Var det inte du, ta bort telefonen i appen."), @@ -119,16 +125,19 @@ public enum PushText { out["by"] = by } for key in ["version", "phase"] { - if let v = args[key].flatMap(plain) { out[key] = v } + if let v = args[key].flatMap({ plain($0) }) { out[key] = v } } - if let name = name.flatMap(plain) { out["name"] = name } + // A client's name as the consent page cut it: up to 64 characters + // and "…". + if let c = args["client"].flatMap({ plain($0, upTo: PushAlert.noticeClientMaxRunes) }) { out["client"] = c } + if let name = name.flatMap({ plain($0) }) { out["name"] = name } return out } /// A short line of text, or nil. - static func plain(_ s: String) -> String? { + static func plain(_ s: String, upTo most: Int = 40) -> String? { let t = s.trimmingCharacters(in: .whitespacesAndNewlines) - guard (1...40).contains(t.count), t.unicodeScalars.allSatisfy({ !CharacterSet.controlCharacters.contains($0) && !CharacterSet.newlines.contains($0) }) else { return nil } + guard (1...most).contains(t.count), t.unicodeScalars.allSatisfy({ $0.properties.generalCategory != .format }), t.unicodeScalars.allSatisfy({ !CharacterSet.controlCharacters.contains($0) && !CharacterSet.newlines.contains($0) }) else { return nil } return t } diff --git a/appleApp/FTWKit/Tests/FTWKitTests/CloudViewTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/CloudViewTests.swift index eef8b00..3753c28 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/CloudViewTests.swift +++ b/appleApp/FTWKit/Tests/FTWKitTests/CloudViewTests.swift @@ -464,3 +464,55 @@ private func header(_ box: inout FakeCloudBox, at: Int64) throws -> CloudWire.He #expect(v.sentMs(ahead, arrivalMs: 3_000_100) == 3_000_000) } } + +/// The car class (srcfl/ftw docs/cars.md): the view holds it like the other +/// classes, and a charger's level and goal read from its session's car, the +/// car winning where the two disagree. +@Suite struct CloudViewCarTests { + @Test func aChargerReadsItsCarAndTheCarWins() throws { + let vectors = try CloudVectors.load() + var v = CloudView(home: vectors.home, boxKey: Bytes(hex: vectors.boxKey.pk)) + let arrival: Int64 = 1_791_820_900_000 + for name in ["lp car asking", "car", "lp car tapped"] { + try v.accept(vectors.envelope(name), arrivalMs: arrival) + } + let cars = try #require(v.cars) + #expect(cars.cars.count == 2) + let tapped = try #require(v.lp?.loadpoints.first) + let tesla = try #require(cars.car(tapped.carId)) + // The box filled the charger from the car: joined, nothing moves + // but the age, which each class counts to its own send. + var same = tapped + same.socAgeS = tesla.socAgeS + #expect(v.joined(tapped) == same) + #expect(tesla.socPermille == tapped.socPermille) + // Dated from the car class's own send, not from the lp that came + // later, nor from now. + let carSent = try #require(v.sentMs(cars.header, arrivalMs: arrival)) + #expect(v.socReadAtMs(tapped) == carSent - 540_000) + + // A newer car class says otherwise: the car wins. + var box = try FakeCloudBox(boot: 0xb0) + box.seq = 20 + let at = arrival - 1_000 + let car = CBOR.map([ + ("id", .text(tesla.id)), ("socPermille", .int(300)), ("socSource", .text("manual")), ("socAgeS", .int(5)), + ("energySinceWh", .int(0)), ("socEstimate", .bool(false)), ("surplusOnly", .bool(true)), + ("goal", .map([("targetPermille", .int(600)), ("readyByMin", .int(390)), ("days", .int(0)), ("finishAtLimit", .bool(false))])), + ]) + try v.accept(box.seal(CloudWire.msgCar, box.header(at) + [("cars", .array([car]))]), arrivalMs: arrival) + let joined = v.joined(tapped) + #expect(joined.socPermille == 300 && joined.socSource == "inferred" && joined.socAgeS == nil) + #expect(v.socReadAtMs(tapped) == at - 5_000) + #expect(joined.targetPermille == 600 && !joined.recurring && joined.surplusOnly) + #expect(joined.powerW == tapped.powerW && joined.carId == tapped.carId) + + // A charger with no car, or a car the class does not hold, is as + // sent. + var visitor = tapped + visitor.carId = "" + #expect(v.joined(visitor) == visitor) + visitor.carId = "unknown-car" + #expect(v.joined(visitor) == visitor) + } +} diff --git a/appleApp/FTWKit/Tests/FTWKitTests/CloudWireTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/CloudWireTests.swift index f55c563..c2d246a 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/CloudWireTests.swift +++ b/appleApp/FTWKit/Tests/FTWKitTests/CloudWireTests.swift @@ -118,7 +118,7 @@ func cloudSign(_ key: P256.Signing.PrivateKey, home: String, t: String, b: Bytes let v = try CloudVectors.load() let boxPK = Bytes(hex: v.boxKey.pk) let servicePK = Bytes(hex: v.serviceKey.pk) - #expect(v.messages.count == 22) + #expect(v.messages.count == 27) for m in v.messages { let frame = Bytes(hex: m.frame) @@ -156,11 +156,13 @@ func cloudSign(_ key: P256.Signing.PrivateKey, home: String, t: String, b: Bytes #expect(Bytes(service.publicKey.x963Representation).hex == v.serviceKey.pk) let grantKey = Bytes(hex: v.grantKey) - #expect(v.ids.count == 4) + #expect(v.ids.count == 6) for id in v.ids { - let got = id.kind == "src" - ? CloudWire.sourceID(grantKey: grantKey, driver: id.name) - : CloudWire.loadpointID(grantKey: grantKey, loadpoint: id.name) + let got = switch id.kind { + case "src": CloudWire.sourceID(grantKey: grantKey, driver: id.name) + case "car": CloudWire.carID(grantKey: grantKey, car: id.name) + default: CloudWire.loadpointID(grantKey: grantKey, loadpoint: id.name) + } #expect(got == id.id, "\(id.kind) \(id.name)") } // A loadpoint and a source with one name do not share an id. @@ -209,6 +211,26 @@ func cloudSign(_ key: P256.Signing.PrivateKey, home: String, t: String, b: Bytes #expect(lp.feedback == [CloudWire.Feedback(kind: "ev", src: charger, status: "following", reason: "plan", severity: "ok", evidence: "measured", fresh: true, actualW: 2100, ageMs: 900)]) + // Cars (srcfl/ftw docs/cars.md): the class by opaque id, and a + // tapped charger that names its car. + let tesla = CloudWire.carID(grantKey: grantKey, car: "5f0c1e2d3a4b5c6d7e8f90a1b2c3d4e5") + let volvo = CloudWire.carID(grantKey: grantKey, car: "0123456789abcdef0123456789abcdef") + let cars = try CloudWire.Cars(body: v.envelope("car").b) + #expect(cars.header.seq == 16) + #expect(cars.cars == [ + CloudWire.Car(id: tesla, socPermille: 140, socSource: "vehicle", socAgeS: 540, energySinceWh: 0, socEstimate: false, + chargeLimitPermille: 800, goal: CloudWire.CarGoal(targetPermille: 800, readyByMin: 420, days: 0x7f, finishAtLimit: false), + surplusOnly: false), + CloudWire.Car(id: volvo, socPermille: 620, socSource: "vehicle", socAgeS: 30_600, energySinceWh: 0, socEstimate: true, + chargeLimitPermille: nil, goal: nil, surplusOnly: true), + ]) + let asking = try #require(try CloudWire.LP(body: v.envelope("lp car asking").b).loadpoints.first) + #expect(asking.carId.isEmpty && asking.carIdentifiedBy == "asking" && asking.choose == [tesla, volvo]) + let tapped = try #require(try CloudWire.LP(body: v.envelope("lp car tapped").b).loadpoints.first) + #expect(tapped.carId == tesla && tapped.carIdentifiedBy == "tap" && !tapped.carCorrected && tapped.carCorrectedFrom.isEmpty) + #expect(cars.car(tapped.carId)?.socPermille == tapped.socPermille) + #expect(car.carId.isEmpty && car.choose.isEmpty) + let prices = try CloudWire.Prices(body: v.envelope("prices").b) #expect(prices.zone == "SE3" && prices.currency == "SEK") #expect(prices.t0Ms == 1_791_820_800_000 && prices.slotMs == 900_000) diff --git a/appleApp/FTWKit/Tests/FTWKitTests/Resources/cloud-vectors.json b/appleApp/FTWKit/Tests/FTWKitTests/Resources/cloud-vectors.json index 7a9dd78..52a05b7 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/Resources/cloud-vectors.json +++ b/appleApp/FTWKit/Tests/FTWKitTests/Resources/cloud-vectors.json @@ -29,6 +29,16 @@ "kind": "lp", "name": "garage", "id": "j3HZORNCWHs" + }, + { + "kind": "car", + "name": "5f0c1e2d3a4b5c6d7e8f90a1b2c3d4e5", + "id": "FNKibitfeU0" + }, + { + "kind": "car", + "name": "0123456789abcdef0123456789abcdef", + "id": "Dm4Qg3Dz3bc" } ], "messages": [ @@ -66,11 +76,11 @@ "name": "roster", "signer": "box", "t": "roster", - "body": "a9637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104", - "signedInputSha256": "54a45d8ab064d1d9fda8dfd3d43708dca9294daefe67078b15ab17ae1c3c086a", - "sig": "26de4f6b491496569361fce489167aa3a445c91235689a79f5499f5a65d4ed5e35ecd601251013d345f8171904bd5b508e0a05d543dc25f2335b2d7a743ad916", - "envelope": "a361625901c5a9637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584026de4f6b491496569361fce489167aa3a445c91235689a79f5499f5a65d4ed5e35ecd601251013d345f8171904bd5b508e0a05d543dc25f2335b2d7a743ad916", - "frame": "01010000021aa361625901c5a9637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584026de4f6b491496569361fce489167aa3a445c91235689a79f5499f5a65d4ed5e35ecd601251013d345f8171904bd5b508e0a05d543dc25f2335b2d7a743ad916" + "body": "aa637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104", + "signedInputSha256": "f47b850d31bfffb3f6352ab20c3ba7788cc95f0df22a38f1b8b44184a383399e", + "sig": "96a9ec9cde7b6b2b62bf55ed8d74aeaba0a8d0ded9e5ec32d1f868f758051c146e323ee9aca2c7a009b78190cde63c4b9c882ec29843f07fd15608ed9cf4dd11", + "envelope": "a361625901edaa637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584096a9ec9cde7b6b2b62bf55ed8d74aeaba0a8d0ded9e5ec32d1f868f758051c146e323ee9aca2c7a009b78190cde63c4b9c882ec29843f07fd15608ed9cf4dd11", + "frame": "010100000242a361625901edaa637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584096a9ec9cde7b6b2b62bf55ed8d74aeaba0a8d0ded9e5ec32d1f868f758051c146e323ee9aca2c7a009b78190cde63c4b9c882ec29843f07fd15608ed9cf4dd11" }, { "name": "live full", @@ -196,21 +206,21 @@ "name": "roster tunnel only", "signer": "box", "t": "roster", - "body": "a9637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105", - "signedInputSha256": "30ae9e4de78ae0f21421f8a7b172759f3b244da825c0072f2781cb49a73a907c", - "sig": "f9b4b3e144492cf784a07fce0272fe0252d3d71164645b092ebc086a4eab2f0f4e8c11e20aec3567e454a5641a06a97166eaf20ccdd8a022f1eaaa2a18a52e8b", - "envelope": "a36162590168a9637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840f9b4b3e144492cf784a07fce0272fe0252d3d71164645b092ebc086a4eab2f0f4e8c11e20aec3567e454a5641a06a97166eaf20ccdd8a022f1eaaa2a18a52e8b", - "frame": "0101000001bda36162590168a9637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840f9b4b3e144492cf784a07fce0272fe0252d3d71164645b092ebc086a4eab2f0f4e8c11e20aec3567e454a5641a06a97166eaf20ccdd8a022f1eaaa2a18a52e8b" + "body": "aa637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105", + "signedInputSha256": "03e4ceaeb5d8abcbd48e0d4a30d53d4145df97c2986d8029e71b1823effd4d3c", + "sig": "729ecdf8ab1a41194cc3da99bf86d5e3593f9bb9e417e70512905e0a6c664f2bfa390316d794493370c9beb8da904e8a01f11af360503b1705cf463834502ac7", + "envelope": "a36162590190aa637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840729ecdf8ab1a41194cc3da99bf86d5e3593f9bb9e417e70512905e0a6c664f2bfa390316d794493370c9beb8da904e8a01f11af360503b1705cf463834502ac7", + "frame": "0101000001e5a36162590190aa637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840729ecdf8ab1a41194cc3da99bf86d5e3593f9bb9e417e70512905e0a6c664f2bfa390316d794493370c9beb8da904e8a01f11af360503b1705cf463834502ac7" }, { "name": "roster revoking", "signer": "box", "t": "roster", - "body": "a9637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106", - "signedInputSha256": "d8775b10218f6c69d9b7c9064d1cea8713058eaa9d91aeeba2e2f29419b7f9cc", - "sig": "4332ec4bf6f166175f50eecea08ecbc981d762c10b37f9e676d7e0a763b5d73e58533683fd6b566cf0a33feff903af40d4bf70bedc5245b2696fa85a3191186f", - "envelope": "a3616258f5a9637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f737465726373696758404332ec4bf6f166175f50eecea08ecbc981d762c10b37f9e676d7e0a763b5d73e58533683fd6b566cf0a33feff903af40d4bf70bedc5245b2696fa85a3191186f", - "frame": "010100000149a3616258f5a9637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f737465726373696758404332ec4bf6f166175f50eecea08ecbc981d762c10b37f9e676d7e0a763b5d73e58533683fd6b566cf0a33feff903af40d4bf70bedc5245b2696fa85a3191186f" + "body": "aa637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106", + "signedInputSha256": "322a030b3148bdb71722a8b7ddc7e54d4feff792bb06467125ea6a81d98e387c", + "sig": "c96e88f759f66bb580111e4d63cb45bb4b160d9cd17e211a394df3901b07c2e5317aab96aad398b1a1aca1827a68ba3fe7632ba3b12e886a9968d2c617b70d3c", + "envelope": "a3616259011daa637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f73746572637369675840c96e88f759f66bb580111e4d63cb45bb4b160d9cd17e211a394df3901b07c2e5317aab96aad398b1a1aca1827a68ba3fe7632ba3b12e886a9968d2c617b70d3c", + "frame": "010100000172a3616259011daa637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f73746572637369675840c96e88f759f66bb580111e4d63cb45bb4b160d9cd17e211a394df3901b07c2e5317aab96aad398b1a1aca1827a68ba3fe7632ba3b12e886a9968d2c617b70d3c" }, { "name": "purged copy", @@ -251,6 +261,56 @@ "sig": "", "envelope": "a3616256a3616440636465766871354c77305a6231637369640761746374756e6373696740", "frame": "010100000025a3616256a3616440636465766871354c77305a6231637369640761746374756e6373696740" + }, + { + "name": "roster with an agent", + "signer": "box", + "t": "roster", + "body": "aa637365710e64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7de80676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a7626964686f4b47696f36536c636b65795820a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a06374756ef464726f6c6567737570706f7274656167656e74a5636f7073816e6c6f6164706f696e742e686f6c646463617073a3646d617857191ce865686f6c6453190384667065724d696e066673636f70657383716674772e64697370617463682e686f6c646d6674772e6c6976652e726561646d6674772e6c6f67732e72656164697072696e636970616ca366636c69656e7471536f7572636566756c20737570706f7274667469636b6574674654572d31323368636c69656e744964783268747470733a2f2f636c6f75642e6674772e656e657267792f636c69656e74732f736f7572636566756c2d737570706f72746b6578706972657341744d731b000001a17720ca006565706f6368016673636f706573806770656e64696e678068757074696d654d731a000128e069726f7374657253657107", + "signedInputSha256": "24ade7fd185b91e8c5c954c34805ebed51cbbf8d6e36097c7a903ebbfe5db7aa", + "sig": "f2f15bfcafd92e077fb41218fb5eca3735d45484cab4074541f0908b8e6a1107a31f8bf3caf605405bd7cbd02963ff4466a40de915aa9d9af96166e5fc5965a7", + "envelope": "a36162590256aa637365710e64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7de80676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a7626964686f4b47696f36536c636b65795820a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a06374756ef464726f6c6567737570706f7274656167656e74a5636f7073816e6c6f6164706f696e742e686f6c646463617073a3646d617857191ce865686f6c6453190384667065724d696e066673636f70657383716674772e64697370617463682e686f6c646d6674772e6c6976652e726561646d6674772e6c6f67732e72656164697072696e636970616ca366636c69656e7471536f7572636566756c20737570706f7274667469636b6574674654572d31323368636c69656e744964783268747470733a2f2f636c6f75642e6674772e656e657267792f636c69656e74732f736f7572636566756c2d737570706f72746b6578706972657341744d731b000001a17720ca006565706f6368016673636f706573806770656e64696e678068757074696d654d731a000128e069726f7374657253657107617466726f73746572637369675840f2f15bfcafd92e077fb41218fb5eca3735d45484cab4074541f0908b8e6a1107a31f8bf3caf605405bd7cbd02963ff4466a40de915aa9d9af96166e5fc5965a7", + "frame": "0101000002aba36162590256aa637365710e64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7de80676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a7626964686f4b47696f36536c636b65795820a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a06374756ef464726f6c6567737570706f7274656167656e74a5636f7073816e6c6f6164706f696e742e686f6c646463617073a3646d617857191ce865686f6c6453190384667065724d696e066673636f70657383716674772e64697370617463682e686f6c646d6674772e6c6976652e726561646d6674772e6c6f67732e72656164697072696e636970616ca366636c69656e7471536f7572636566756c20737570706f7274667469636b6574674654572d31323368636c69656e744964783268747470733a2f2f636c6f75642e6674772e656e657267792f636c69656e74732f736f7572636566756c2d737570706f72746b6578706972657341744d731b000001a17720ca006565706f6368016673636f706573806770656e64696e678068757074696d654d731a000128e069726f7374657253657107617466726f73746572637369675840f2f15bfcafd92e077fb41218fb5eca3735d45484cab4074541f0908b8e6a1107a31f8bf3caf605405bd7cbd02963ff4466a40de915aa9d9af96166e5fc5965a7" + }, + { + "name": "lp car asking", + "signer": "box", + "t": "lp", + "body": "a7637365710f64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7de8068666565646261636b8068757074696d654d731a000128e06a636f6e74726f6c52657619032d6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46663686f6f7365826b464e4b69626974666555306b446d34516733447a336263666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553f6696e657874456e644d73f669726561647942794d73f669726563757272696e67f46973657373696f6e57680069736f63536f7572636567756e6b6e6f776e6a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65f66b737572706c75734f6e6c79f56c677269644465666572726564f46e7461726765745065726d696c6c65f66f6361724964656e74696669656442796661736b696e676f636f6d6d616e646564526561736f6e7070765f737572706c75735f7061757365", + "signedInputSha256": "895b15e9dd2992642f09e346998147e6c89162a1c6a84e2c1fe9aba9349f9a82", + "sig": "fa1b746e25aa7f02deb3fbc543e233315f26f31e0325f4921fb0d5a9ef01e249c129184c88d23396bcc1dc3193bdf78fa2f5805760dd9bea3d05137343bbe420", + "envelope": "a361625901bda7637365710f64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7de8068666565646261636b8068757074696d654d731a000128e06a636f6e74726f6c52657619032d6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46663686f6f7365826b464e4b69626974666555306b446d34516733447a336263666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553f6696e657874456e644d73f669726561647942794d73f669726563757272696e67f46973657373696f6e57680069736f63536f7572636567756e6b6e6f776e6a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65f66b737572706c75734f6e6c79f56c677269644465666572726564f46e7461726765745065726d696c6c65f66f6361724964656e74696669656442796661736b696e676f636f6d6d616e646564526561736f6e7070765f737572706c75735f70617573656174626c70637369675840fa1b746e25aa7f02deb3fbc543e233315f26f31e0325f4921fb0d5a9ef01e249c129184c88d23396bcc1dc3193bdf78fa2f5805760dd9bea3d05137343bbe420", + "frame": "01010000020ea361625901bda7637365710f64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7de8068666565646261636b8068757074696d654d731a000128e06a636f6e74726f6c52657619032d6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46663686f6f7365826b464e4b69626974666555306b446d34516733447a336263666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553f6696e657874456e644d73f669726561647942794d73f669726563757272696e67f46973657373696f6e57680069736f63536f7572636567756e6b6e6f776e6a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65f66b737572706c75734f6e6c79f56c677269644465666572726564f46e7461726765745065726d696c6c65f66f6361724964656e74696669656442796661736b696e676f636f6d6d616e646564526561736f6e7070765f737572706c75735f70617573656174626c70637369675840fa1b746e25aa7f02deb3fbc543e233315f26f31e0325f4921fb0d5a9ef01e249c129184c88d23396bcc1dc3193bdf78fa2f5805760dd9bea3d05137343bbe420" + }, + { + "name": "car", + "signer": "box", + "t": "car", + "body": "a5637365711064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0646361727382a96269646b464e4b696269746665553064676f616ca46464617973187f6a726561647942794d696e1901a46d66696e69736841744c696d6974f46e7461726765745065726d696c6c6519032067736f634167655319021c69736f63536f757263656776656869636c656b736f63457374696d617465f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65190320a96269646b446d34516733447a33626364676f616cf667736f634167655319778869736f63536f757263656776656869636c656b736f63457374696d617465f56b736f635065726d696c6c6519026c6b737572706c75734f6e6c79f56d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65f66677616c6c4d731b000001a130d7de8a68757074696d654d731a000128ea", + "signedInputSha256": "855da2d9302e228ac2acc0c4d900d6a3074675c8b5fb1350c2942bce438b9fc4", + "sig": "7ee438570fc0f1072201594add376235e687cb87168f882c88e94e1e087b595be184505d295ecc901cb8c8a424a8fa3df0b65b28b1c2352a7ecfc4cb0b53ae01", + "envelope": "a36162590177a5637365711064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0646361727382a96269646b464e4b696269746665553064676f616ca46464617973187f6a726561647942794d696e1901a46d66696e69736841744c696d6974f46e7461726765745065726d696c6c6519032067736f634167655319021c69736f63536f757263656776656869636c656b736f63457374696d617465f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65190320a96269646b446d34516733447a33626364676f616cf667736f634167655319778869736f63536f757263656776656869636c656b736f63457374696d617465f56b736f635065726d696c6c6519026c6b737572706c75734f6e6c79f56d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65f66677616c6c4d731b000001a130d7de8a68757074696d654d731a000128ea6174636361726373696758407ee438570fc0f1072201594add376235e687cb87168f882c88e94e1e087b595be184505d295ecc901cb8c8a424a8fa3df0b65b28b1c2352a7ecfc4cb0b53ae01", + "frame": "0101000001c9a36162590177a5637365711064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0646361727382a96269646b464e4b696269746665553064676f616ca46464617973187f6a726561647942794d696e1901a46d66696e69736841744c696d6974f46e7461726765745065726d696c6c6519032067736f634167655319021c69736f63536f757263656776656869636c656b736f63457374696d617465f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65190320a96269646b446d34516733447a33626364676f616cf667736f634167655319778869736f63536f757263656776656869636c656b736f63457374696d617465f56b736f635065726d696c6c6519026c6b737572706c75734f6e6c79f56d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65f66677616c6c4d731b000001a130d7de8a68757074696d654d731a000128ea6174636361726373696758407ee438570fc0f1072201594add376235e687cb87168f882c88e94e1e087b595be184505d295ecc901cb8c8a424a8fa3df0b65b28b1c2352a7ecfc4cb0b53ae01" + }, + { + "name": "event car which", + "signer": "box", + "t": "event", + "body": "ac626c706b6a33485a4f524e435748736274731b000001a130d7de306373657111637372636b4455595757516d6e2d58676461726773a16773657373696f6e703764316630633261396233653466353064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06463617273826b464e4b69626974666555306b446d34516733447a336263646b696e64696361722e77686963686677616c6c4d731b000001a130d7de94676576656e7449646c643268705932686a5958497868736576657269747964696e666f68757074696d654d731a000128f4", + "signedInputSha256": "e576ea700f4abd28075bef81b88d28414de2979912ea333d3503ec45a31cb27d", + "sig": "70a21cc4e288f019d1ffeb59456f8555b07c9f450f6182e6d2cddb5dcca37c31a4a3465c5080c429d6f01063c571edf44e4012dd0198b6bdc1fdfcc6ca3e4b88", + "envelope": "a3616258d4ac626c706b6a33485a4f524e435748736274731b000001a130d7de306373657111637372636b4455595757516d6e2d58676461726773a16773657373696f6e703764316630633261396233653466353064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06463617273826b464e4b69626974666555306b446d34516733447a336263646b696e64696361722e77686963686677616c6c4d731b000001a130d7de94676576656e7449646c643268705932686a5958497868736576657269747964696e666f68757074696d654d731a000128f46174656576656e7463736967584070a21cc4e288f019d1ffeb59456f8555b07c9f450f6182e6d2cddb5dcca37c31a4a3465c5080c429d6f01063c571edf44e4012dd0198b6bdc1fdfcc6ca3e4b88", + "frame": "010100000127a3616258d4ac626c706b6a33485a4f524e435748736274731b000001a130d7de306373657111637372636b4455595757516d6e2d58676461726773a16773657373696f6e703764316630633261396233653466353064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06463617273826b464e4b69626974666555306b446d34516733447a336263646b696e64696361722e77686963686677616c6c4d731b000001a130d7de94676576656e7449646c643268705932686a5958497868736576657269747964696e666f68757074696d654d731a000128f46174656576656e7463736967584070a21cc4e288f019d1ffeb59456f8555b07c9f450f6182e6d2cddb5dcca37c31a4a3465c5080c429d6f01063c571edf44e4012dd0198b6bdc1fdfcc6ca3e4b88" + }, + { + "name": "lp car tapped", + "signer": "box", + "t": "lp", + "body": "a7637365711264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7f20868666565646261636b8068757074696d654d731a00013c686a636f6e74726f6c52657619032e6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46563617249646b464e4b6962697466655530666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553190221696e657874456e644d73f669726561647942794d731b000001a133a1bc8069726563757272696e67f56973657373696f6e57680069736f63536f757263656776656869636c656a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46c677269644465666572726564f46e7461726765745065726d696c6c651903206f6361724964656e7469666965644279637461706f636f6d6d616e646564526561736f6e64706c616e", + "signedInputSha256": "daabad621023f8fb32b67179de6f06ec1ed7b72dd9c83de533aa3b625fa7b439", + "sig": "89b4a6faa2af341cdc4da9ad0247aa2135f40e1aec94554cf1855392d22412cb2191b2a594084e4490faada200f68abbdcdd80b7b0bc3e7fb4ff65e5572f3fcf", + "envelope": "a361625901ada7637365711264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7f20868666565646261636b8068757074696d654d731a00013c686a636f6e74726f6c52657619032e6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46563617249646b464e4b6962697466655530666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553190221696e657874456e644d73f669726561647942794d731b000001a133a1bc8069726563757272696e67f56973657373696f6e57680069736f63536f757263656776656869636c656a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46c677269644465666572726564f46e7461726765745065726d696c6c651903206f6361724964656e7469666965644279637461706f636f6d6d616e646564526561736f6e64706c616e6174626c7063736967584089b4a6faa2af341cdc4da9ad0247aa2135f40e1aec94554cf1855392d22412cb2191b2a594084e4490faada200f68abbdcdd80b7b0bc3e7fb4ff65e5572f3fcf", + "frame": "0101000001fea361625901ada7637365711264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7f20868666565646261636b8068757074696d654d731a00013c686a636f6e74726f6c52657619032e6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46563617249646b464e4b6962697466655530666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553190221696e657874456e644d73f669726561647942794d731b000001a133a1bc8069726563757272696e67f56973657373696f6e57680069736f63536f757263656776656869636c656a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46c677269644465666572726564f46e7461726765745065726d696c6c651903206f6361724964656e7469666965644279637461706f636f6d6d616e646564526561736f6e64706c616e6174626c7063736967584089b4a6faa2af341cdc4da9ad0247aa2135f40e1aec94554cf1855392d22412cb2191b2a594084e4490faada200f68abbdcdd80b7b0bc3e7fb4ff65e5572f3fcf" } ] } diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift b/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift index 5c5568a..3f544d0 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift +++ b/appleApp/FTWKit/Tests/FTWSharedTests/PushAlertTests.swift @@ -64,7 +64,7 @@ import Testing @Test func everyPushVectorReadsAsTheGoReferenceReadsIt() throws { let file = try Self.load("push-vectors", as: PushFile.self) - #expect(file.cases.count == 18) + #expect(file.cases.count == 28) let keys = Dictionary(uniqueKeysWithValues: file.keys.map { ($0.home, Data(hex: $0.pk)) }) for c in file.cases { let payload = Data(c.payload.utf8) @@ -100,7 +100,7 @@ import Testing let file = try Self.load("cloud-vectors", as: CloudFile.self) let home = try #require(file.boxKey.home) let keys = ["box": Data(hex: file.boxKey.pk), "service": Data(hex: file.serviceKey.pk)] - #expect(file.messages.count == 22) + #expect(file.messages.count == 27) for m in file.messages { let env = try #require(PushAlert.Envelope(Array(Data(hex: m.envelope))), "\(m.name)") #expect(env.t == m.t, "\(m.name)") diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/PushStoreTests.swift b/appleApp/FTWKit/Tests/FTWSharedTests/PushStoreTests.swift index af563aa..94a71d0 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/PushStoreTests.swift +++ b/appleApp/FTWKit/Tests/FTWSharedTests/PushStoreTests.swift @@ -68,7 +68,8 @@ import Testing let seen = PushSeen(file: dir.appendingPathComponent("push-seen")) #expect(seen.accept(Self.event(seq: 9), nowMs: 1)) #expect(!seen.accept(Self.event(seq: 9), nowMs: 2)) - // Older than one shown. + // A notice numbered before an event shown, and late: shown once. + #expect(seen.accept(Self.event(seq: 8), nowMs: 3)) #expect(!seen.accept(Self.event(seq: 8), nowMs: 3)) #expect(seen.accept(Self.event(seq: 10), nowMs: 4)) // A restart counts again; another home counts on its own. @@ -82,6 +83,30 @@ import Testing #expect(seen.accept(PushAlert(.generic, home: Self.home), nowMs: 10)) } + /// Past `recent` numbers the oldest fall below the floor, which counts + /// as shown; a file from before the list keeps its highest as the + /// floor. + @Test func theNumbersKeptAreBounded() throws { + let dir = Self.temp() + defer { try? FileManager.default.removeItem(at: dir) } + let seen = PushSeen(file: dir.appendingPathComponent("push-seen")) + for seq in UInt64(1)...UInt64(PushSeen.recent + 1) { + #expect(seen.accept(Self.event(seq: seq * 2), nowMs: Double(seq))) + } + let entry = try #require(seen.load().first) + #expect(entry.shown?.count == PushSeen.recent && entry.seq == 2) + #expect(!seen.accept(Self.event(seq: 1), nowMs: 100)) + #expect(seen.accept(Self.event(seq: 5), nowMs: 101)) + + let legacy = PushSeen(file: dir.appendingPathComponent("legacy")) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + let boot = try #require(Self.event(seq: 1).boot) + try JSONSerialization.data(withJSONObject: [["home": Self.home, "boot": boot.base64EncodedString(), "seq": 9, "atMs": 1]]) + .write(to: legacy.file) + #expect(!legacy.accept(Self.event(seq: 8), nowMs: 2)) + #expect(legacy.accept(Self.event(seq: 10), nowMs: 3)) + } + @Test func theFileStaysSmall() { let dir = Self.temp() defer { try? FileManager.default.removeItem(at: dir) } diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift b/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift index c892f7d..11961e4 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift +++ b/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift @@ -241,4 +241,27 @@ import Testing #expect(again.thread == "fjzyr67buzpyncqmjliwgezvde") #expect(connected.sound && unreachable.sound) } + + /// A notice that names the client says so; without a name that will + /// do, the catalogue's sentence shows. No notice gets buttons. + @Test func aNoticeNamesTheClientWhenItCan() { + let named = Self.event(PushKind.agentAdded, args: ["client": "Claude"]) + #expect(PushText.content(named, language: .en) == .init(title: "New access to your box", + body: "Claude was given access to your box. See who has access in the app.")) + #expect(PushText.content(named, language: .sv) == .init(title: "Ny åtkomst till din box", + body: "Claude har fått åtkomst till din box. Se vem som har åtkomst i appen.")) + // A name the consent page cut: 64 characters and "…". + let long = String(repeating: "ö", count: 64) + "…" + #expect(PushText.content(Self.event(PushKind.agentAdded, args: ["client": long]), language: .en).body + == "\(long) was given access to your box. See who has access in the app.") + for client in ["", "Claude\nPay here", "Claude\u{202E}", String(repeating: "c", count: 66)] { + #expect(PushText.content(Self.event(PushKind.agentAdded, args: ["client": client]), language: .en).body + == "You gave an assistant or Sourceful access. See who has access in the app.", "\(client)") + } + #expect(PushText.content(Self.event(PushKind.ownerAdded, args: ["client": "Claude"]), language: .sv) + == .init(title: "En ny ägare", body: "En telefon har parats som ägare av din box. Var det inte du, ta bort den i appen.")) + for kind in PushKind.notices { + #expect(PushNotice(Self.event(kind), home: nil, language: .en).category == "", "\(kind)") + } + } } diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/Resources/cloud-vectors.json b/appleApp/FTWKit/Tests/FTWSharedTests/Resources/cloud-vectors.json index 7a9dd78..52a05b7 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/Resources/cloud-vectors.json +++ b/appleApp/FTWKit/Tests/FTWSharedTests/Resources/cloud-vectors.json @@ -29,6 +29,16 @@ "kind": "lp", "name": "garage", "id": "j3HZORNCWHs" + }, + { + "kind": "car", + "name": "5f0c1e2d3a4b5c6d7e8f90a1b2c3d4e5", + "id": "FNKibitfeU0" + }, + { + "kind": "car", + "name": "0123456789abcdef0123456789abcdef", + "id": "Dm4Qg3Dz3bc" } ], "messages": [ @@ -66,11 +76,11 @@ "name": "roster", "signer": "box", "t": "roster", - "body": "a9637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104", - "signedInputSha256": "54a45d8ab064d1d9fda8dfd3d43708dca9294daefe67078b15ab17ae1c3c086a", - "sig": "26de4f6b491496569361fce489167aa3a445c91235689a79f5499f5a65d4ed5e35ecd601251013d345f8171904bd5b508e0a05d543dc25f2335b2d7a743ad916", - "envelope": "a361625901c5a9637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584026de4f6b491496569361fce489167aa3a445c91235689a79f5499f5a65d4ed5e35ecd601251013d345f8171904bd5b508e0a05d543dc25f2335b2d7a743ad916", - "frame": "01010000021aa361625901c5a9637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584026de4f6b491496569361fce489167aa3a445c91235689a79f5499f5a65d4ed5e35ecd601251013d345f8171904bd5b508e0a05d543dc25f2335b2d7a743ad916" + "body": "aa637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104", + "signedInputSha256": "f47b850d31bfffb3f6352ab20c3ba7788cc95f0df22a38f1b8b44184a383399e", + "sig": "96a9ec9cde7b6b2b62bf55ed8d74aeaba0a8d0ded9e5ec32d1f868f758051c146e323ee9aca2c7a009b78190cde63c4b9c882ec29843f07fd15608ed9cf4dd11", + "envelope": "a361625901edaa637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584096a9ec9cde7b6b2b62bf55ed8d74aeaba0a8d0ded9e5ec32d1f868f758051c146e323ee9aca2c7a009b78190cde63c4b9c882ec29843f07fd15608ed9cf4dd11", + "frame": "010100000242a361625901edaa637365710264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f706573826d6674772e6c6976652e726561646f6674772e6576656e74732e7265616468707572706f736573816573657276656a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7a00a676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef464726f6c65656f776e65726565706f6368016673636f706573856d6674772e6c6976652e72656164706674772e686973746f72792e726561646d6674772e706c616e2e726561646f6674772e6865616c74682e726561646f6674772e6576656e74732e72656164a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef464726f6c65667669657765726565706f6368026673636f706573816d6674772e6c6976652e726561646770656e64696e678068757074696d654d7319ea6a69726f7374657253657104617466726f7374657263736967584096a9ec9cde7b6b2b62bf55ed8d74aeaba0a8d0ded9e5ec32d1f868f758051c146e323ee9aca2c7a009b78190cde63c4b9c882ec29843f07fd15608ed9cf4dd11" }, { "name": "live full", @@ -196,21 +206,21 @@ "name": "roster tunnel only", "signer": "box", "t": "roster", - "body": "a9637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105", - "signedInputSha256": "30ae9e4de78ae0f21421f8a7b172759f3b244da825c0072f2781cb49a73a907c", - "sig": "f9b4b3e144492cf784a07fce0272fe0252d3d71164645b092ebc086a4eab2f0f4e8c11e20aec3567e454a5641a06a97166eaf20ccdd8a022f1eaaa2a18a52e8b", - "envelope": "a36162590168a9637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840f9b4b3e144492cf784a07fce0272fe0252d3d71164645b092ebc086a4eab2f0f4e8c11e20aec3567e454a5641a06a97166eaf20ccdd8a022f1eaaa2a18a52e8b", - "frame": "0101000001bda36162590168a9637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840f9b4b3e144492cf784a07fce0272fe0252d3d71164645b092ebc086a4eab2f0f4e8c11e20aec3567e454a5641a06a97166eaf20ccdd8a022f1eaaa2a18a52e8b" + "body": "aa637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105", + "signedInputSha256": "03e4ceaeb5d8abcbd48e0d4a30d53d4145df97c2986d8029e71b1823effd4d3c", + "sig": "729ecdf8ab1a41194cc3da99bf86d5e3593f9bb9e417e70512905e0a6c664f2bfa390316d794493370c9beb8da904e8a01f11af360503b1705cf463834502ac7", + "envelope": "a36162590190aa637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840729ecdf8ab1a41194cc3da99bf86d5e3593f9bb9e417e70512905e0a6c664f2bfa390316d794493370c9beb8da904e8a01f11af360503b1705cf463834502ac7", + "frame": "0101000001e5a36162590190aa637365710c64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7d6b0676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a6626964685a66303978513261636b6579582066666666666666666666666666666666666666666666666666666666666666666374756ef564726f6c65667669657765726565706f6368026673636f706573806770656e64696e6781a264686173685820b4e707c0ac2241df0e0325b42b951b3c71bbd5099bc3c6f2894a48ee23fa35636474746c5319025868757074696d654d731a0001211069726f7374657253657105617466726f73746572637369675840729ecdf8ab1a41194cc3da99bf86d5e3593f9bb9e417e70512905e0a6c664f2bfa390316d794493370c9beb8da904e8a01f11af360503b1705cf463834502ac7" }, { "name": "roster revoking", "signer": "box", "t": "roster", - "body": "a9637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106", - "signedInputSha256": "d8775b10218f6c69d9b7c9064d1cea8713058eaa9d91aeeba2e2f29419b7f9cc", - "sig": "4332ec4bf6f166175f50eecea08ecbc981d762c10b37f9e676d7e0a763b5d73e58533683fd6b566cf0a33feff903af40d4bf70bedc5245b2696fa85a3191186f", - "envelope": "a3616258f5a9637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f737465726373696758404332ec4bf6f166175f50eecea08ecbc981d762c10b37f9e676d7e0a763b5d73e58533683fd6b566cf0a33feff903af40d4bf70bedc5245b2696fa85a3191186f", - "frame": "010100000149a3616258f5a9637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c6677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f737465726373696758404332ec4bf6f166175f50eecea08ecbc981d762c10b37f9e676d7e0a763b5d73e58533683fd6b566cf0a33feff903af40d4bf70bedc5245b2696fa85a3191186f" + "body": "aa637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106", + "signedInputSha256": "322a030b3148bdb71722a8b7ddc7e54d4feff792bb06467125ea6a81d98e387c", + "sig": "c96e88f759f66bb580111e4d63cb45bb4b160d9cd17e211a394df3901b07c2e5317aab96aad398b1a1aca1827a68ba3fe7632ba3b12e886a9968d2c617b70d3c", + "envelope": "a3616259011daa637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f73746572637369675840c96e88f759f66bb580111e4d63cb45bb4b160d9cd17e211a394df3901b07c2e5317aab96aad398b1a1aca1827a68ba3fe7632ba3b12e886a9968d2c617b70d3c", + "frame": "010100000172a3616259011daa637365710d64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a462696450444444444444444444444444444444446673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7da98676465766963657381a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f706573806770656e64696e678068757074696d654d731a000124f869726f7374657253657106617466726f73746572637369675840c96e88f759f66bb580111e4d63cb45bb4b160d9cd17e211a394df3901b07c2e5317aab96aad398b1a1aca1827a68ba3fe7632ba3b12e886a9968d2c617b70d3c" }, { "name": "purged copy", @@ -251,6 +261,56 @@ "sig": "", "envelope": "a3616256a3616440636465766871354c77305a6231637369640761746374756e6373696740", "frame": "010100000025a3616256a3616440636465766871354c77305a6231637369640761746374756e6373696740" + }, + { + "name": "roster with an agent", + "signer": "box", + "t": "roster", + "body": "aa637365710e64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7de80676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a7626964686f4b47696f36536c636b65795820a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a06374756ef464726f6c6567737570706f7274656167656e74a5636f7073816e6c6f6164706f696e742e686f6c646463617073a3646d617857191ce865686f6c6453190384667065724d696e066673636f70657383716674772e64697370617463682e686f6c646d6674772e6c6976652e726561646d6674772e6c6f67732e72656164697072696e636970616ca366636c69656e7471536f7572636566756c20737570706f7274667469636b6574674654572d31323368636c69656e744964783268747470733a2f2f636c6f75642e6674772e656e657267792f636c69656e74732f736f7572636566756c2d737570706f72746b6578706972657341744d731b000001a17720ca006565706f6368016673636f706573806770656e64696e678068757074696d654d731a000128e069726f7374657253657107", + "signedInputSha256": "24ade7fd185b91e8c5c954c34805ebed51cbbf8d6e36097c7a903ebbfe5db7aa", + "sig": "f2f15bfcafd92e077fb41218fb5eca3735d45484cab4074541f0908b8e6a1107a31f8bf3caf605405bd7cbd02963ff4466a40de915aa9d9af96166e5fc5965a7", + "envelope": "a36162590256aa637365710e64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7de80676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a7626964686f4b47696f36536c636b65795820a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a06374756ef464726f6c6567737570706f7274656167656e74a5636f7073816e6c6f6164706f696e742e686f6c646463617073a3646d617857191ce865686f6c6453190384667065724d696e066673636f70657383716674772e64697370617463682e686f6c646d6674772e6c6976652e726561646d6674772e6c6f67732e72656164697072696e636970616ca366636c69656e7471536f7572636566756c20737570706f7274667469636b6574674654572d31323368636c69656e744964783268747470733a2f2f636c6f75642e6674772e656e657267792f636c69656e74732f736f7572636566756c2d737570706f72746b6578706972657341744d731b000001a17720ca006565706f6368016673636f706573806770656e64696e678068757074696d654d731a000128e069726f7374657253657107617466726f73746572637369675840f2f15bfcafd92e077fb41218fb5eca3735d45484cab4074541f0908b8e6a1107a31f8bf3caf605405bd7cbd02963ff4466a40de915aa9d9af96166e5fc5965a7", + "frame": "0101000002aba36162590256aa637365710e64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a4626964406673636f7065738068707572706f736573806a636f6e74726f6c6c657269736f7572636566756c656e6f697365582077777777777777777777777777777777777777777777777777777777777777776677616c6c4d731b000001a130d7de80676465766963657382a66269646871354c77305a6231636b6579582055555555555555555555555555555555555555555555555555555555555555556374756ef564726f6c65656f776e65726565706f6368016673636f70657380a7626964686f4b47696f36536c636b65795820a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a06374756ef464726f6c6567737570706f7274656167656e74a5636f7073816e6c6f6164706f696e742e686f6c646463617073a3646d617857191ce865686f6c6453190384667065724d696e066673636f70657383716674772e64697370617463682e686f6c646d6674772e6c6976652e726561646d6674772e6c6f67732e72656164697072696e636970616ca366636c69656e7471536f7572636566756c20737570706f7274667469636b6574674654572d31323368636c69656e744964783268747470733a2f2f636c6f75642e6674772e656e657267792f636c69656e74732f736f7572636566756c2d737570706f72746b6578706972657341744d731b000001a17720ca006565706f6368016673636f706573806770656e64696e678068757074696d654d731a000128e069726f7374657253657107617466726f73746572637369675840f2f15bfcafd92e077fb41218fb5eca3735d45484cab4074541f0908b8e6a1107a31f8bf3caf605405bd7cbd02963ff4466a40de915aa9d9af96166e5fc5965a7" + }, + { + "name": "lp car asking", + "signer": "box", + "t": "lp", + "body": "a7637365710f64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7de8068666565646261636b8068757074696d654d731a000128e06a636f6e74726f6c52657619032d6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46663686f6f7365826b464e4b69626974666555306b446d34516733447a336263666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553f6696e657874456e644d73f669726561647942794d73f669726563757272696e67f46973657373696f6e57680069736f63536f7572636567756e6b6e6f776e6a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65f66b737572706c75734f6e6c79f56c677269644465666572726564f46e7461726765745065726d696c6c65f66f6361724964656e74696669656442796661736b696e676f636f6d6d616e646564526561736f6e7070765f737572706c75735f7061757365", + "signedInputSha256": "895b15e9dd2992642f09e346998147e6c89162a1c6a84e2c1fe9aba9349f9a82", + "sig": "fa1b746e25aa7f02deb3fbc543e233315f26f31e0325f4921fb0d5a9ef01e249c129184c88d23396bcc1dc3193bdf78fa2f5805760dd9bea3d05137343bbe420", + "envelope": "a361625901bda7637365710f64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7de8068666565646261636b8068757074696d654d731a000128e06a636f6e74726f6c52657619032d6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46663686f6f7365826b464e4b69626974666555306b446d34516733447a336263666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553f6696e657874456e644d73f669726561647942794d73f669726563757272696e67f46973657373696f6e57680069736f63536f7572636567756e6b6e6f776e6a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65f66b737572706c75734f6e6c79f56c677269644465666572726564f46e7461726765745065726d696c6c65f66f6361724964656e74696669656442796661736b696e676f636f6d6d616e646564526561736f6e7070765f737572706c75735f70617573656174626c70637369675840fa1b746e25aa7f02deb3fbc543e233315f26f31e0325f4921fb0d5a9ef01e249c129184c88d23396bcc1dc3193bdf78fa2f5805760dd9bea3d05137343bbe420", + "frame": "01010000020ea361625901bda7637365710f64626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7de8068666565646261636b8068757074696d654d731a000128e06a636f6e74726f6c52657619032d6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46663686f6f7365826b464e4b69626974666555306b446d34516733447a336263666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553f6696e657874456e644d73f669726561647942794d73f669726563757272696e67f46973657373696f6e57680069736f63536f7572636567756e6b6e6f776e6a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65f66b737572706c75734f6e6c79f56c677269644465666572726564f46e7461726765745065726d696c6c65f66f6361724964656e74696669656442796661736b696e676f636f6d6d616e646564526561736f6e7070765f737572706c75735f70617573656174626c70637369675840fa1b746e25aa7f02deb3fbc543e233315f26f31e0325f4921fb0d5a9ef01e249c129184c88d23396bcc1dc3193bdf78fa2f5805760dd9bea3d05137343bbe420" + }, + { + "name": "car", + "signer": "box", + "t": "car", + "body": "a5637365711064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0646361727382a96269646b464e4b696269746665553064676f616ca46464617973187f6a726561647942794d696e1901a46d66696e69736841744c696d6974f46e7461726765745065726d696c6c6519032067736f634167655319021c69736f63536f757263656776656869636c656b736f63457374696d617465f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65190320a96269646b446d34516733447a33626364676f616cf667736f634167655319778869736f63536f757263656776656869636c656b736f63457374696d617465f56b736f635065726d696c6c6519026c6b737572706c75734f6e6c79f56d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65f66677616c6c4d731b000001a130d7de8a68757074696d654d731a000128ea", + "signedInputSha256": "855da2d9302e228ac2acc0c4d900d6a3074675c8b5fb1350c2942bce438b9fc4", + "sig": "7ee438570fc0f1072201594add376235e687cb87168f882c88e94e1e087b595be184505d295ecc901cb8c8a424a8fa3df0b65b28b1c2352a7ecfc4cb0b53ae01", + "envelope": "a36162590177a5637365711064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0646361727382a96269646b464e4b696269746665553064676f616ca46464617973187f6a726561647942794d696e1901a46d66696e69736841744c696d6974f46e7461726765745065726d696c6c6519032067736f634167655319021c69736f63536f757263656776656869636c656b736f63457374696d617465f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65190320a96269646b446d34516733447a33626364676f616cf667736f634167655319778869736f63536f757263656776656869636c656b736f63457374696d617465f56b736f635065726d696c6c6519026c6b737572706c75734f6e6c79f56d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65f66677616c6c4d731b000001a130d7de8a68757074696d654d731a000128ea6174636361726373696758407ee438570fc0f1072201594add376235e687cb87168f882c88e94e1e087b595be184505d295ecc901cb8c8a424a8fa3df0b65b28b1c2352a7ecfc4cb0b53ae01", + "frame": "0101000001c9a36162590177a5637365711064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0646361727382a96269646b464e4b696269746665553064676f616ca46464617973187f6a726561647942794d696e1901a46d66696e69736841744c696d6974f46e7461726765745065726d696c6c6519032067736f634167655319021c69736f63536f757263656776656869636c656b736f63457374696d617465f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65190320a96269646b446d34516733447a33626364676f616cf667736f634167655319778869736f63536f757263656776656869636c656b736f63457374696d617465f56b736f635065726d696c6c6519026c6b737572706c75734f6e6c79f56d656e6572677953696e6365576800736368617267654c696d69745065726d696c6c65f66677616c6c4d731b000001a130d7de8a68757074696d654d731a000128ea6174636361726373696758407ee438570fc0f1072201594add376235e687cb87168f882c88e94e1e087b595be184505d295ecc901cb8c8a424a8fa3df0b65b28b1c2352a7ecfc4cb0b53ae01" + }, + { + "name": "event car which", + "signer": "box", + "t": "event", + "body": "ac626c706b6a33485a4f524e435748736274731b000001a130d7de306373657111637372636b4455595757516d6e2d58676461726773a16773657373696f6e703764316630633261396233653466353064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06463617273826b464e4b69626974666555306b446d34516733447a336263646b696e64696361722e77686963686677616c6c4d731b000001a130d7de94676576656e7449646c643268705932686a5958497868736576657269747964696e666f68757074696d654d731a000128f4", + "signedInputSha256": "e576ea700f4abd28075bef81b88d28414de2979912ea333d3503ec45a31cb27d", + "sig": "70a21cc4e288f019d1ffeb59456f8555b07c9f450f6182e6d2cddb5dcca37c31a4a3465c5080c429d6f01063c571edf44e4012dd0198b6bdc1fdfcc6ca3e4b88", + "envelope": "a3616258d4ac626c706b6a33485a4f524e435748736274731b000001a130d7de306373657111637372636b4455595757516d6e2d58676461726773a16773657373696f6e703764316630633261396233653466353064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06463617273826b464e4b69626974666555306b446d34516733447a336263646b696e64696361722e77686963686677616c6c4d731b000001a130d7de94676576656e7449646c643268705932686a5958497868736576657269747964696e666f68757074696d654d731a000128f46174656576656e7463736967584070a21cc4e288f019d1ffeb59456f8555b07c9f450f6182e6d2cddb5dcca37c31a4a3465c5080c429d6f01063c571edf44e4012dd0198b6bdc1fdfcc6ca3e4b88", + "frame": "010100000127a3616258d4ac626c706b6a33485a4f524e435748736274731b000001a130d7de306373657111637372636b4455595757516d6e2d58676461726773a16773657373696f6e703764316630633261396233653466353064626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06463617273826b464e4b69626974666555306b446d34516733447a336263646b696e64696361722e77686963686677616c6c4d731b000001a130d7de94676576656e7449646c643268705932686a5958497868736576657269747964696e666f68757074696d654d731a000128f46174656576656e7463736967584070a21cc4e288f019d1ffeb59456f8555b07c9f450f6182e6d2cddb5dcca37c31a4a3465c5080c429d6f01063c571edf44e4012dd0198b6bdc1fdfcc6ca3e4b88" + }, + { + "name": "lp car tapped", + "signer": "box", + "t": "lp", + "body": "a7637365711264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7f20868666565646261636b8068757074696d654d731a00013c686a636f6e74726f6c52657619032e6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46563617249646b464e4b6962697466655530666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553190221696e657874456e644d73f669726561647942794d731b000001a133a1bc8069726563757272696e67f56973657373696f6e57680069736f63536f757263656776656869636c656a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46c677269644465666572726564f46e7461726765745065726d696c6c651903206f6361724964656e7469666965644279637461706f636f6d6d616e646564526561736f6e64706c616e", + "signedInputSha256": "daabad621023f8fb32b67179de6f06ec1ed7b72dd9c83de533aa3b625fa7b439", + "sig": "89b4a6faa2af341cdc4da9ad0247aa2135f40e1aec94554cf1855392d22412cb2191b2a594084e4490faada200f68abbdcdd80b7b0bc3e7fb4ff65e5572f3fcf", + "envelope": "a361625901ada7637365711264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7f20868666565646261636b8068757074696d654d731a00013c686a636f6e74726f6c52657619032e6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46563617249646b464e4b6962697466655530666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553190221696e657874456e644d73f669726561647942794d731b000001a133a1bc8069726563757272696e67f56973657373696f6e57680069736f63536f757263656776656869636c656a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46c677269644465666572726564f46e7461726765745065726d696c6c651903206f6361724964656e7469666965644279637461706f636f6d6d616e646564526561736f6e64706c616e6174626c7063736967584089b4a6faa2af341cdc4da9ad0247aa2135f40e1aec94554cf1855392d22412cb2191b2a594084e4490faada200f68abbdcdd80b7b0bc3e7fb4ff65e5572f3fcf", + "frame": "0101000001fea361625901ada7637365711264626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06677616c6c4d731b000001a130d7f20868666565646261636b8068757074696d654d731a00013c686a636f6e74726f6c52657619032e6a6c6f6164706f696e747381b8186269646b6a33485a4f524e43574873637372636b4455595757516d6e2d586765626f6f7374f46563617249646b464e4b6962697466655530666d616e75616cf6666e6578745768f666706f776572570067706c7567676564f56773657373696f6e703764316630633261396233653466353067736f6341676553190221696e657874456e644d73f669726561647942794d731b000001a133a1bc8069726563757272696e67f56973657373696f6e57680069736f63536f757263656776656869636c656a636f6d6d616e64656457f66b6e65787453746172744d73f66b706c616e50656e64696e67f46b736f635065726d696c6c65188c6b737572706c75734f6e6c79f46c677269644465666572726564f46e7461726765745065726d696c6c651903206f6361724964656e7469666965644279637461706f636f6d6d616e646564526561736f6e64706c616e6174626c7063736967584089b4a6faa2af341cdc4da9ad0247aa2135f40e1aec94554cf1855392d22412cb2191b2a594084e4490faada200f68abbdcdd80b7b0bc3e7fb4ff65e5572f3fcf" } ] } diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/Resources/push-vectors.json b/appleApp/FTWKit/Tests/FTWSharedTests/Resources/push-vectors.json index e6b4ac6..55b8145 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/Resources/push-vectors.json +++ b/appleApp/FTWKit/Tests/FTWSharedTests/Resources/push-vectors.json @@ -252,6 +252,125 @@ "verdict": "generic", "home": "fjzyr67buzpyncqmjliwgezvde" } + }, + { + "name": "notice: agent added", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWEulY3NlcRFkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRrYWdlbnQuYWRkZWRmd2FsbE1zGwAAAaEw19qpaHVwdGltZU1zGgABJQlhdGZub3RpY2Vjc2lnWEAbAyFDum/1cvsa/UZ6W4Ycr9Vi0fi4Za3eybeuZVPnU2TKhbb8isxPSpwOBLfLQVgHhoY+bkI6oQUgQQ2R9+3v\"}}", + "sig": "1b032143ba6ff572fb1afd467a5b861cafd562d1f8b865addec9b7ae6553e75364ca85b6fc8acc4f4a9c0e04b7cb41580786863e6e423aa10520410d91f7edef", + "nowMs": 1791820816217, + "want": { + "verdict": "event", + "home": "fjzyr67buzpyncqmjliwgezvde", + "kind": "agent.added", + "boot": "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", + "seq": 17, + "atMs": 1791820815017 + } + }, + { + "name": "notice: agent added, with the client's name", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWFmmY3NlcRJkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRrYWdlbnQuYWRkZWRmY2xpZW50ZkNsYXVkZWZ3YWxsTXMbAAABoTDX2qpodXB0aW1lTXMaAAElCmF0Zm5vdGljZWNzaWdYQE4+GuWl/LzZJLZfra214MRS7FcNFraI3uxiFd8KifedDll34e209yxx4ShX4EnuMw3pZ/saTXZbanwGvvq7OwA=\"}}", + "sig": "4e3e1ae5a5fcbcd924b65fadadb5e0c452ec570d16b688deec6215df0a89f79d0e5977e1edb4f72c71e12857e049ee330de967fb1a4d765b6a7c06befabb3b00", + "nowMs": 1791820816218, + "want": { + "verdict": "event", + "home": "fjzyr67buzpyncqmjliwgezvde", + "kind": "agent.added", + "args": { + "client": "Claude" + }, + "boot": "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", + "seq": 18, + "atMs": 1791820815018 + } + }, + { + "name": "notice: owner added", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWEulY3NlcRNkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRrb3duZXIuYWRkZWRmd2FsbE1zGwAAAaEw19qraHVwdGltZU1zGgABJQthdGZub3RpY2Vjc2lnWEA1UrmPsIrWKWG9fjxWr25VeJlNItvdxLisM+pnQm6hiBlJP04wSC+YWHHEc7ONCX7RaG9AuXgy9YYIC861ofPC\"}}", + "sig": "3552b98fb08ad62961bd7e3c56af6e5578994d22dbddc4b8ac33ea67426ea18819493f4e30482f985871c473b38d097ed1686f40b97832f586080bceb5a1f3c2", + "nowMs": 1791820816219, + "want": { + "verdict": "event", + "home": "fjzyr67buzpyncqmjliwgezvde", + "kind": "owner.added", + "boot": "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", + "seq": 19, + "atMs": 1791820815019 + } + }, + { + "name": "notice of a kind that is not a notice", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWFKlY3NlcRRkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRyY2hhcmdpbmcuY29ubmVjdGVkZndhbGxNcxsAAAGhMNfarGh1cHRpbWVNcxoAASUMYXRmbm90aWNlY3NpZ1hAEgkMM7TKQ7T+9OGA0ghvgzsbClOTmAZDKyjx5+2GbmhLfEnPDyrf8j7+/XJtPUpRAYtG08d0fJwFx4N25Fj7ig==\"}}", + "sig": "12090c33b4ca43b4fef4e180d2086f833b1b0a53939806432b28f1e7ed866e684b7c49cf0f2adff23efefd726d3d4a51018b46d3c7747c9c05c78376e458fb8a", + "nowMs": 1791820816220, + "want": { + "verdict": "generic", + "home": "fjzyr67buzpyncqmjliwgezvde" + } + }, + { + "name": "notice with a client's name on another kind", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWFumY3NlcRVkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRtYWdlbnQucmVxdWVzdGZjbGllbnRmQ2xhdWRlZndhbGxNcxsAAAGhMNfarWh1cHRpbWVNcxoAASUNYXRmbm90aWNlY3NpZ1hAs6NjZVEBVln2Oa0fswegEGi/WY4oZE+ttBlos/7klFaXvtNQ0LhlWjs1WsNg6F5A1SSN5nruJ1Djpl7T/SaG8w==\"}}", + "sig": "b3a3636551015659f639ad1fb307a01068bf598e28644fadb41968b3fee4945697bed350d0b8655a3b355ac360e85e40d5248de67aee2750e3a65ed3fd2686f3", + "nowMs": 1791820816221, + "want": { + "verdict": "generic", + "home": "fjzyr67buzpyncqmjliwgezvde" + } + }, + { + "name": "notice that carries a reading", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWFmmY3NlcRZkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRtYWdlbnQucmVxdWVzdGZmaWVsZHOhARkJYGZ3YWxsTXMbAAABoTDX2q5odXB0aW1lTXMaAAElDmF0Zm5vdGljZWNzaWdYQLAkL9JgkAuamo13qXA+F1jPe7kikT0U7zv65Audx/TaPylfZhJ+A5ozM0YLiSoi8bceFQJeYGVP9Qtl/y1WStM=\"}}", + "sig": "b0242fd260900b9a9a8d77a9703e1758cf7bb922913d14ef3bfae40b9dc7f4da3f295f66127e039a3333460b892a22f1b71e15025e60654ff50b65ff2d564ad3", + "nowMs": 1791820816222, + "want": { + "verdict": "generic", + "home": "fjzyr67buzpyncqmjliwgezvde" + } + }, + { + "name": "a notice kind sent as an event", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWHSoY3NlcRdkYXJnc6BkYm9vdFCwsLCwsLCwsLCwsLCwsLCwZGtpbmRrYWdlbnQuYWRkZWRmd2FsbE1zGwAAAaEw19qvZ2V2ZW50SWRsYm05MGFXTmxaWFl4aHNldmVyaXR5ZGluZm9odXB0aW1lTXMaAAElD2F0ZWV2ZW50Y3NpZ1hAdYhzcPtaXW1REa8z71XlON6LANr0PDuWzx2axo7CkNzZSYNx4OF/juTHPf4LKSjQW+dwwkAeE2vIjFjpLhJjEw==\"}}", + "sig": "75887370fb5a5d6d5111af33ef55e538de8b00daf43c3b96cf1d9ac68ec290dcd9498371e0e17f8ee4c73dfe0b2928d05be770c2401e136bc88c58e92e126313", + "nowMs": 1791820816223, + "want": { + "verdict": "generic", + "home": "fjzyr67buzpyncqmjliwgezvde" + } + }, + { + "name": "notice: twenty hours old is still shown", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWFGlY3NlcRgYZGJvb3RQsLCwsLCwsLCwsLCwsLCwsGRraW5kcGFwcHJvdmFsLmtleS5zZXRmd2FsbE1zGwAAAaEw19qwaHVwdGltZU1zGgABJRBhdGZub3RpY2Vjc2lnWEBslMBAqiznLiaedLlM28EGe661MxcbIVAadoY7xwF5cpHybeQU8pFgluWy6XoaOX7zvtEPOxBLFj0qzgfEaBLE\"}}", + "sig": "6c94c040aa2ce72e269e74b94cdbc1067baeb533171b21501a76863bc701797291f26de414f2916096e5b2e97a1a397ef3bed10f3b104b163d2ace07c46812c4", + "nowMs": 1791892815024, + "want": { + "verdict": "event", + "home": "fjzyr67buzpyncqmjliwgezvde", + "kind": "approval.key.set", + "boot": "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", + "seq": 24, + "atMs": 1791820815024 + } + }, + { + "name": "notice: over a day old", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWFGlY3NlcRgYZGJvb3RQsLCwsLCwsLCwsLCwsLCwsGRraW5kcGFwcHJvdmFsLmtleS5zZXRmd2FsbE1zGwAAAaEw19qwaHVwdGltZU1zGgABJRBhdGZub3RpY2Vjc2lnWEDhgeh5sDs7qhAO/vie0bMhUWZJeajyBGqqql6TIH2c2i4nNqnWOFdaTnGigUzmM+Vvw13M9baTp+8wqH6gnRcg\"}}", + "sig": "e181e879b03b3baa100efef89ed1b32151664979a8f2046aaaaa5e93207d9cda2e2736a9d638575a4e71a2814ce633e56fc35dccf5b693a7ef30a87ea09d1720", + "nowMs": 1791907215025, + "want": { + "verdict": "generic", + "home": "fjzyr67buzpyncqmjliwgezvde" + } + }, + { + "name": "notice with a short boot", + "payload": "{\"aps\":{\"alert\":{\"title-loc-key\":\"push.title\",\"loc-key\":\"push.generic\"},\"mutable-content\":1,\"sound\":\"default\"},\"ftw\":{\"v\":1,\"home\":\"fjzyr67buzpyncqmjliwgezvde\",\"event\":\"o2FiWESlY3NlcRgZZGJvb3RIsLCwsLCwsLBka2luZGtvd25lci5hZGRlZGZ3YWxsTXMbAAABoTDX2rFodXB0aW1lTXMaAAElEWF0Zm5vdGljZWNzaWdYQB0GZm45ROf59oQ4Kl/xD8+TQaF3B+mNoBN3uHrYHTvqGTD1378qCy/FI34or24G6uAl7HB4/RN3QQTNMdDU0KI=\"}}", + "sig": "1d06666e3944e7f9f684382a5ff10fcf9341a17707e98da01377b87ad81d3bea1930f5dfbf2a0b2fc5237e28af6e06eae025ec7078fd13774104cd31d0d4d0a2", + "nowMs": 1791820816225, + "want": { + "verdict": "generic", + "home": "fjzyr67buzpyncqmjliwgezvde" + } } ] } From 54a1f1183d9a2d7dff081ae2c85a5bea1f15f9cb Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 16 Oct 2026 12:03:11 +0200 Subject: [PATCH 2/2] push: agent.added says access was given, not who gave it The catalogue from srcfl/ftw-webapp#94, and the same in Swedish. The named line stays the phone's own, from the notice's client. Co-Authored-By: Claude Opus 5.5 --- appleApp/FTWKit/Sources/FTWShared/PushText.swift | 4 ++-- appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift | 2 +- protocol/push-catalogue.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/appleApp/FTWKit/Sources/FTWShared/PushText.swift b/appleApp/FTWKit/Sources/FTWShared/PushText.swift index 3845bf2..9c3b52f 100644 --- a/appleApp/FTWKit/Sources/FTWShared/PushText.swift +++ b/appleApp/FTWKit/Sources/FTWShared/PushText.swift @@ -80,7 +80,7 @@ public enum PushText { PushKind.boxUnreachable: Sentence(title: "Your box is out of reach", body: "It stopped answering. Power cut, or the internet at home is down."), PushKind.cloudGranted: Sentence(title: "FTW Cloud is on", body: "Your box now shares a copy of its readings with FTW Cloud. You can turn it off in the app or on the box."), PushKind.cloudRevoked: Sentence(title: "FTW Cloud is off", body: "Your box stopped sharing with FTW Cloud and asked it to delete its copy."), - PushKind.agentAdded: Sentence(title: "New access to your box", body: "You gave an assistant or Sourceful access. See who has access in the app.", + PushKind.agentAdded: Sentence(title: "New access to your box", body: "An assistant or Sourceful was given access to your box. See who has access in the app.", named: "{client} was given access to your box. See who has access in the app."), PushKind.agentRequest: Sentence(title: "Waiting for your answer", body: "An assistant asks to change a setting. Open FTW to confirm or decline."), PushKind.agentExpiring: Sentence(title: "Access ends tomorrow", body: "An assistant's or Sourceful's access to your box runs out in a day. Renew it in the app if it should go on."), @@ -100,7 +100,7 @@ public enum PushText { PushKind.boxUnreachable: Sentence(title: "Din box går inte att nå", body: "Den har slutat svara. Strömavbrott, eller så är internet hemma nere."), PushKind.cloudGranted: Sentence(title: "FTW Cloud är på", body: "Din box delar nu en kopia av sina mätvärden med FTW Cloud. Du kan stänga av det i appen eller på boxen."), PushKind.cloudRevoked: Sentence(title: "FTW Cloud är av", body: "Din box har slutat dela med FTW Cloud och bett den radera sin kopia."), - PushKind.agentAdded: Sentence(title: "Ny åtkomst till din box", body: "En assistent eller Sourceful har fått åtkomst. Se vem som har åtkomst i appen.", + PushKind.agentAdded: Sentence(title: "Ny åtkomst till din box", body: "En assistent eller Sourceful har fått åtkomst till din box. Se vem som har åtkomst i appen.", named: "{client} har fått åtkomst till din box. Se vem som har åtkomst i appen."), PushKind.agentRequest: Sentence(title: "Väntar på ditt svar", body: "En assistent vill ändra en inställning. Öppna FTW för att godkänna eller avböja."), PushKind.agentExpiring: Sentence(title: "Åtkomsten upphör i morgon", body: "En assistents eller Sourcefuls åtkomst till din box går ut om ett dygn. Förnya den i appen om den ska fortsätta."), diff --git a/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift b/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift index 11961e4..a998d11 100644 --- a/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift +++ b/appleApp/FTWKit/Tests/FTWSharedTests/PushTextTests.swift @@ -256,7 +256,7 @@ import Testing == "\(long) was given access to your box. See who has access in the app.") for client in ["", "Claude\nPay here", "Claude\u{202E}", String(repeating: "c", count: 66)] { #expect(PushText.content(Self.event(PushKind.agentAdded, args: ["client": client]), language: .en).body - == "You gave an assistant or Sourceful access. See who has access in the app.", "\(client)") + == "An assistant or Sourceful was given access to your box. See who has access in the app.", "\(client)") } #expect(PushText.content(Self.event(PushKind.ownerAdded, args: ["client": "Claude"]), language: .sv) == .init(title: "En ny ägare", body: "En telefon har parats som ägare av din box. Var det inte du, ta bort den i appen.")) diff --git a/protocol/push-catalogue.yaml b/protocol/push-catalogue.yaml index 0c9e8c5..3a56367 100644 --- a/protocol/push-catalogue.yaml +++ b/protocol/push-catalogue.yaml @@ -65,7 +65,7 @@ events: # the release check access to the box. Access from outside the house is # news every owner should hear, as with cloud.granted. title: New access to your box - body: "You gave an assistant or Sourceful access. See who has access in the app." + body: "An assistant or Sourceful was given access to your box. See who has access in the app." - kind: agent.request # To every owner, when an agent asks for a durable change. It waits for # one of them to confirm it, and runs out after twelve hours.