From b3ab9cac766c37c23968e830547a4d73b3f8a249 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 5 Nov 2024 00:03:03 +0100 Subject: [PATCH 1/7] Test localization --- Tests/OrdersTests/OrdersTests.swift | 4 ++++ .../Templates/it-IT.lproj/pet_store_logo.png | Bin 0 -> 11659 bytes Tests/PassesTests/PassesTests.swift | 20 +++++++++++++++--- .../Templates/it-IT.lproj/logo.png | Bin 0 -> 3593 bytes .../it-IT.lproj/personalizationLogo.png | Bin 0 -> 3593 bytes 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 Tests/OrdersTests/Templates/it-IT.lproj/pet_store_logo.png create mode 100644 Tests/PassesTests/Templates/it-IT.lproj/logo.png create mode 100644 Tests/PassesTests/Templates/it-IT.lproj/personalizationLogo.png diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index b5afb84..a92d77d 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -25,6 +25,9 @@ struct OrdersTests { #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/pet_store_logo.png"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/it-IT.lproj/pet_store_logo.png"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/order.json"))) let orderJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) let orderJSON = try decoder.decode(OrderJSONData.self, from: orderJSONData!) @@ -37,6 +40,7 @@ struct OrdersTests { let iconData = try Data(contentsOf: orderFolder.appendingPathComponent("/icon.png")) #expect(manifestJSON["icon.png"] == SHA256.hash(data: iconData).hex) #expect(manifestJSON["pet_store_logo.png"] != nil) + #expect(manifestJSON["it-IT.lproj/pet_store_logo.png"] != nil) } } diff --git a/Tests/OrdersTests/Templates/it-IT.lproj/pet_store_logo.png b/Tests/OrdersTests/Templates/it-IT.lproj/pet_store_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a0dc1184668c0f81f30de58e34d11dcbd593ba GIT binary patch literal 11659 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 HoqPWWk?Qmp literal 0 HcmV?d00001 diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 5bccc26..28cd0e6 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -25,6 +25,11 @@ struct PassesTests { #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) @@ -39,6 +44,8 @@ struct PassesTests { #expect(manifestJSON["icon.png"] == Insecure.SHA1.hash(data: iconData).hex) #expect(manifestJSON["logo.png"] != nil) #expect(manifestJSON["personalizationLogo.png"] != nil) + #expect(manifestJSON["it-IT.lproj/logo.png"] != nil) + #expect(manifestJSON["it-IT.lproj/personalizationLogo.png"] != nil) } } @@ -78,7 +85,12 @@ struct PassesTests { try Zip.unzipFile(passURL, destination: passFolder) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) - + + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) @@ -93,8 +105,10 @@ struct PassesTests { let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) - let iconData = try Data(contentsOf: passFolder.appendingPathComponent("/personalizationLogo.png")) - #expect(manifestJSON["personalizationLogo.png"] == Insecure.SHA1.hash(data: iconData).hex) + let personalizationLogoData = try Data(contentsOf: passFolder.appendingPathComponent("/personalizationLogo.png")) + let personalizationLogoHash = Insecure.SHA1.hash(data: personalizationLogoData).hex + #expect(manifestJSON["personalizationLogo.png"] == personalizationLogoHash) + #expect(manifestJSON["it-IT.lproj/personalizationLogo.png"] == personalizationLogoHash) } } diff --git a/Tests/PassesTests/Templates/it-IT.lproj/logo.png b/Tests/PassesTests/Templates/it-IT.lproj/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0b96c3171cfb1804309c8658cf385b2cc245acfc GIT binary patch literal 3593 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 literal 0 HcmV?d00001 diff --git a/Tests/PassesTests/Templates/it-IT.lproj/personalizationLogo.png b/Tests/PassesTests/Templates/it-IT.lproj/personalizationLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..0b96c3171cfb1804309c8658cf385b2cc245acfc GIT binary patch literal 3593 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 literal 0 HcmV?d00001 From 92a02f38fe8b25af3fc0c933b8b6de71c146eb23 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 5 Nov 2024 14:19:03 +0100 Subject: [PATCH 2/7] Simplify signing --- .spi.yml | 3 +- .../Orders.docc/Extensions/OrdersService.md | 2 +- Sources/Orders/Orders.docc/GettingStarted.md | 14 +- Sources/Orders/OrdersDelegate.swift | 15 - Sources/Orders/OrdersError.swift | 32 +- Sources/Orders/OrdersService.swift | 38 +-- Sources/Orders/OrdersServiceCustom.swift | 225 ++++++------- Sources/PassKit/Testing/TestCertificate.swift | 115 +++++++ .../Passes.docc/Extensions/PassesService.md | 4 +- Sources/Passes/Passes.docc/GettingStarted.md | 18 +- Sources/Passes/Passes.docc/Personalization.md | 2 +- Sources/Passes/PassesDelegate.swift | 13 - Sources/Passes/PassesError.swift | 32 +- Sources/Passes/PassesService.swift | 43 ++- Sources/Passes/PassesServiceCustom.swift | 313 +++++++----------- Tests/Certificates/WWDR.pem | 26 -- Tests/Certificates/certificate.pem | 18 - Tests/Certificates/encryptedcert.pem | 16 - Tests/Certificates/encryptedkey.pem | 11 - Tests/Certificates/key.pem | 28 -- Tests/OrdersTests/OrdersTests.swift | 18 +- Tests/OrdersTests/withApp.swift | 10 +- Tests/PassesTests/PassesTests.swift | 19 +- Tests/PassesTests/withApp.swift | 10 +- 24 files changed, 451 insertions(+), 574 deletions(-) create mode 100644 Sources/PassKit/Testing/TestCertificate.swift delete mode 100644 Tests/Certificates/WWDR.pem delete mode 100644 Tests/Certificates/certificate.pem delete mode 100644 Tests/Certificates/encryptedcert.pem delete mode 100644 Tests/Certificates/encryptedkey.pem delete mode 100644 Tests/Certificates/key.pem diff --git a/.spi.yml b/.spi.yml index eaf12d0..ad36f6c 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,5 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [PassKit, Passes, Orders] - swift_version: 6.0 \ No newline at end of file + - documentation_targets: [PassKit, Passes, Orders] \ No newline at end of file diff --git a/Sources/Orders/Orders.docc/Extensions/OrdersService.md b/Sources/Orders/Orders.docc/Extensions/OrdersService.md index d59b09c..937c328 100644 --- a/Sources/Orders/Orders.docc/Extensions/OrdersService.md +++ b/Sources/Orders/Orders.docc/Extensions/OrdersService.md @@ -4,7 +4,7 @@ ### Essentials -- ``generateOrderContent(for:on:)`` +- ``build(order:on:)`` - ``register(migrations:)`` ### Push Notifications diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md index 9870283..ec20edf 100644 --- a/Sources/Orders/Orders.docc/GettingStarted.md +++ b/Sources/Orders/Orders.docc/GettingStarted.md @@ -147,8 +147,6 @@ final class OrderDelegate: OrdersDelegate { Next, initialize the ``OrdersService`` inside the `configure.swift` file. This will implement all of the routes that Apple Wallet expects to exist on your server. -In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files. -If they are named like that you're good to go, otherwise you have to specify the custom name. > Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders. @@ -164,7 +162,9 @@ public func configure(_ app: Application) async throws { let ordersService = try OrdersService( app: app, delegate: orderDelegate, - signingFilesDirectory: "Certificates/Orders/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -203,7 +203,9 @@ public func configure(_ app: Application) async throws { >( app: app, delegate: orderDelegate, - signingFilesDirectory: "Certificates/Orders/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -284,7 +286,7 @@ struct OrdersController: RouteCollection { > Note: You'll have to register the `OrdersController` in the `configure.swift` file, in order to pass it the ``OrdersService`` object. -Then use the object inside your route handlers to generate the order bundle with the ``OrdersService/generateOrderContent(for:on:)`` method and distribute it with the "`application/vnd.apple.order`" MIME type. +Then use the object inside your route handlers to generate the order bundle with the ``OrdersService/build(order:on:)`` method and distribute it with the "`application/vnd.apple.order`" MIME type. ```swift fileprivate func orderHandler(_ req: Request) async throws -> Response { @@ -297,7 +299,7 @@ fileprivate func orderHandler(_ req: Request) async throws -> Response { throw Abort(.notFound) } - let bundle = try await ordersService.generateOrderContent(for: orderData.order, on: req.db) + let bundle = try await ordersService.build(order: orderData.order, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.order") diff --git a/Sources/Orders/OrdersDelegate.swift b/Sources/Orders/OrdersDelegate.swift index dc40cdc..e63979c 100644 --- a/Sources/Orders/OrdersDelegate.swift +++ b/Sources/Orders/OrdersDelegate.swift @@ -25,15 +25,6 @@ public protocol OrdersDelegate: AnyObject, Sendable { /// - Returns: A URL path which points to the template data for the order. func template(for order: O, db: any Database) async throws -> String - /// Generates the SSL `signature` file. - /// - /// If you need to implement custom S/Mime signing you can use this - /// method to do so. You must generate a detached DER signature of the `manifest.json` file. - /// - /// - Parameter root: The location of the `manifest.json` and where to write the `signature` to. - /// - Returns: Return `true` if you generated a custom `signature`, otherwise `false`. - func generateSignatureFile(in root: URL) -> Bool - /// Encode the order into JSON. /// /// This method should generate the entire order JSON. You are provided with @@ -51,9 +42,3 @@ public protocol OrdersDelegate: AnyObject, Sendable { order: O, db: any Database, encoder: JSONEncoder ) async throws -> Data } - -extension OrdersDelegate { - public func generateSignatureFile(in root: URL) -> Bool { - return false - } -} diff --git a/Sources/Orders/OrdersError.swift b/Sources/Orders/OrdersError.swift index 3c827b9..eee43c7 100644 --- a/Sources/Orders/OrdersError.swift +++ b/Sources/Orders/OrdersError.swift @@ -10,10 +10,8 @@ public struct OrdersError: Error, Sendable { /// The type of the errors that can be thrown by Apple Wallet orders. public struct ErrorType: Sendable, Hashable, CustomStringConvertible { enum Base: String, Sendable { - case templateNotDirectory - case pemCertificateMissing - case pemPrivateKeyMissing - case opensslBinaryMissing + case noSourceFiles + case noOpenSSLExecutable } let base: Base @@ -22,14 +20,10 @@ public struct OrdersError: Error, Sendable { self.base = base } - /// The template path is not a directory. - public static let templateNotDirectory = Self(.templateNotDirectory) - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(.pemCertificateMissing) - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(.pemPrivateKeyMissing) - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(.opensslBinaryMissing) + /// The path for the source files is not a directory. + public static let noSourceFiles = Self(.noSourceFiles) + /// The `openssl` executable is missing. + public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable) /// A textual representation of this error. public var description: String { @@ -54,17 +48,11 @@ public struct OrdersError: Error, Sendable { self.backing = .init(errorType: errorType) } - /// The template path is not a directory. - public static let templateNotDirectory = Self(errorType: .templateNotDirectory) + /// The path for the source files is not a directory. + public static let noSourceFiles = Self(errorType: .noSourceFiles) - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(errorType: .pemCertificateMissing) - - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(errorType: .pemPrivateKeyMissing) - - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(errorType: .opensslBinaryMissing) + /// The `openssl` executable is missing. + public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable) } extension OrdersError: CustomStringConvertible { diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift index 0a9d1e0..81a711c 100644 --- a/Sources/Orders/OrdersService.swift +++ b/Sources/Orders/OrdersService.swift @@ -17,37 +17,34 @@ public final class OrdersService: Sendable { /// - Parameters: /// - app: The `Vapor.Application` to use in route handlers and APNs. /// - delegate: The ``OrdersDelegate`` to use for order generation. - /// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located. - /// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`. - /// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`. - /// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`. - /// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - sslBinary: The location of the `openssl` command as a file path. /// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered. /// - logger: The `Logger` to use. + /// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format. + /// - pemCertificate: The PEM Certificate for signing orders. + /// - pemPrivateKey: The PEM Certificate's private key for signing orders. + /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. + /// - openSSLURL: The location of the `openssl` command as a file path. public init( app: Application, delegate: any OrdersDelegate, - signingFilesDirectory: String, - wwdrCertificate: String = "WWDR.pem", - pemCertificate: String = "certificate.pem", - pemPrivateKey: String = "key.pem", - pemPrivateKeyPassword: String? = nil, - sslBinary: String = "/usr/bin/openssl", pushRoutesMiddleware: (any Middleware)? = nil, - logger: Logger? = nil + logger: Logger? = nil, + pemWWDRCertificate: String, + pemCertificate: String, + pemPrivateKey: String, + pemPrivateKeyPassword: String? = nil, + openSSLURL: String = "/usr/bin/openssl" ) throws { self.service = try .init( app: app, delegate: delegate, - signingFilesDirectory: signingFilesDirectory, - wwdrCertificate: wwdrCertificate, + pushRoutesMiddleware: pushRoutesMiddleware, + logger: logger, + pemWWDRCertificate: pemWWDRCertificate, pemCertificate: pemCertificate, pemPrivateKey: pemPrivateKey, pemPrivateKeyPassword: pemPrivateKeyPassword, - sslBinary: sslBinary, - pushRoutesMiddleware: pushRoutesMiddleware, - logger: logger + openSSLURL: openSSLURL ) } @@ -56,9 +53,10 @@ public final class OrdersService: Sendable { /// - Parameters: /// - order: The order to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated order content. - public func generateOrderContent(for order: Order, on db: any Database) async throws -> Data { - try await service.generateOrderContent(for: order, on: db) + public func build(order: Order, on db: any Database) async throws -> Data { + try await service.build(order: order, on: db) } /// Adds the migrations for Wallet orders models. diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 4f741f7..664ea1a 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -26,13 +26,14 @@ public final class OrdersServiceCustom Data { + private func manifest(for directory: URL) throws -> Data { var manifest: [String: String] = [:] - let paths = try FileManager.default.subpathsOfDirectory(atPath: root.path) + + let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) for relativePath in paths { - let file = URL(fileURLWithPath: relativePath, relativeTo: root) - guard !file.hasDirectoryPath else { continue } - manifest[relativePath] = try SHA256.hash(data: Data(contentsOf: file)).hex + let file = URL(fileURLWithPath: relativePath, relativeTo: directory) + guard !file.hasDirectoryPath else { + continue + } + + let hash = try SHA256.hash(data: Data(contentsOf: file)) + manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() } - // Write the manifest file to the root directory - // and return the data for using it in signing. - let data = try encoder.encode(manifest) - try data.write(to: root.appendingPathComponent("manifest.json")) - return data - } - private func generateSignatureFile(for manifest: Data, in root: URL) throws { - // If the caller's delegate generated a file we don't have to do it. - if delegate.generateSignatureFile(in: root) { return } + return try encoder.encode(manifest) + } + private func signature(for manifest: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. - if let password = self.pemPrivateKeyPassword { - let sslBinary = self.sslBinary - guard FileManager.default.fileExists(atPath: sslBinary.path) else { - throw OrdersError.opensslBinaryMissing - } - - let proc = Process() - proc.currentDirectoryURL = self.signingFilesDirectory - proc.executableURL = sslBinary - proc.arguments = [ + if let pemPrivateKeyPassword { + let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: dir) } + + try manifest.write(to: dir.appendingPathComponent("manifest.json")) + try self.pemWWDRCertificate.write(to: dir.appendingPathComponent("wwdr.pem"), atomically: true, encoding: .utf8) + try self.pemCertificate.write(to: dir.appendingPathComponent("certificate.pem"), atomically: true, encoding: .utf8) + try self.pemPrivateKey.write(to: dir.appendingPathComponent("private.pem"), atomically: true, encoding: .utf8) + + let process = Process() + process.currentDirectoryURL = dir + process.executableURL = self.openSSLURL + process.arguments = [ "smime", "-binary", "-sign", - "-certfile", self.wwdrCertificate, - "-signer", self.pemCertificate, - "-inkey", self.pemPrivateKey, - "-in", root.appendingPathComponent("manifest.json").path, - "-out", root.appendingPathComponent("signature").path, + "-certfile", dir.appendingPathComponent("wwdr.pem").path, + "-signer", dir.appendingPathComponent("certificate.pem").path, + "-inkey", dir.appendingPathComponent("private.pem").path, + "-in", dir.appendingPathComponent("manifest.json").path, + "-out", dir.appendingPathComponent("signature").path, "-outform", "DER", - "-passin", "pass:\(password)", + "-passin", "pass:\(pemPrivateKeyPassword)", ] - try proc.run() - proc.waitUntilExit() - return - } + try process.run() + process.waitUntilExit() - let signature = try CMS.sign( - manifest, - signatureAlgorithm: .sha256WithRSAEncryption, - additionalIntermediateCertificates: [ - Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.wwdrCertificate) - ) - ) - ], - certificate: Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemCertificate) - ) - ), - privateKey: .init( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemPrivateKey) - ) - ), - signingTime: Date() - ) - try Data(signature).write(to: root.appendingPathComponent("signature")) + return try Data(contentsOf: dir.appendingPathComponent("signature")) + } else { + let signature = try CMS.sign( + manifest, + signatureAlgorithm: .sha256WithRSAEncryption, + additionalIntermediateCertificates: [ + Certificate(pemEncoded: self.pemWWDRCertificate) + ], + certificate: Certificate(pemEncoded: self.pemCertificate), + privateKey: .init(pemEncoded: self.pemPrivateKey), + signingTime: Date() + ) + return Data(signature) + } } /// Generates the order content bundle for a given order. @@ -462,28 +441,42 @@ extension OrdersServiceCustom { /// - Parameters: /// - order: The order to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated order content as `Data`. - public func generateOrderContent(for order: O, on db: any Database) async throws -> Data { - let templateDirectory = try await URL(fileURLWithPath: delegate.template(for: order, db: db), isDirectory: true) + public func build(order: O, on db: any Database) async throws -> Data { + let filesDirectory = try await URL(fileURLWithPath: delegate.template(for: order, db: db), isDirectory: true) guard - (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { - throw OrdersError.templateNotDirectory + throw OrdersError.noSourceFiles } - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.copyItem(at: templateDirectory, to: root) - defer { _ = try? FileManager.default.removeItem(at: root) } + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.copyItem(at: filesDirectory, to: tempDir) + defer { try? FileManager.default.removeItem(at: tempDir) } + + var files: [ArchiveFile] = [] + + let orderJSON = try await self.delegate.encode(order: order, db: db, encoder: self.encoder) + try orderJSON.write(to: tempDir.appendingPathComponent("order.json")) + files.append(ArchiveFile(filename: "order.json", data: orderJSON)) - try await self.delegate.encode(order: order, db: db, encoder: self.encoder) - .write(to: root.appendingPathComponent("order.json")) + let manifest = try self.manifest(for: tempDir) + files.append(ArchiveFile(filename: "manifest.json", data: manifest)) + try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest))) - try self.generateSignatureFile(for: Self.generateManifestFile(using: self.encoder, in: root), in: root) + let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path) + for relativePath in paths { + let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir) + guard !file.hasDirectoryPath else { + continue + } + + try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file))) + } - var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) - files.append(URL(fileURLWithPath: "order.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "manifest.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "signature", relativeTo: root)) - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) + let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).order") + try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) + return try Data(contentsOf: zipFile) } } diff --git a/Sources/PassKit/Testing/TestCertificate.swift b/Sources/PassKit/Testing/TestCertificate.swift new file mode 100644 index 0000000..73e1cc4 --- /dev/null +++ b/Sources/PassKit/Testing/TestCertificate.swift @@ -0,0 +1,115 @@ +package enum TestCertificate { + package static let pemWWDRCertificate = """ + -----BEGIN CERTIFICATE----- + MIIEVTCCAz2gAwIBAgIUE9x3lVJx5T3GMujM/+Uh88zFztIwDQYJKoZIhvcNAQEL + BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsT + HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBS + b290IENBMB4XDTIwMTIxNjE5MzYwNFoXDTMwMTIxMDAwMDAwMFowdTFEMEIGA1UE + Aww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNh + dGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc0MRMwEQYDVQQKDApBcHBsZSBJbmMu + MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAf + eKp6JzKwRl/nF3bYoJ0OKY6tPTKlxGs3yeRBkWq3eXFdDDQEYHX3rkOPR8SGHgjo + v9Y5Ui8eZ/xx8YJtPH4GUnadLLzVQ+mxtLxAOnhRXVGhJeG+bJGdayFZGEHVD41t + QSo5SiHgkJ9OE0/QjJoyuNdqkh4laqQyziIZhQVg3AJK8lrrd3kCfcCXVGySjnYB + 5kaP5eYq+6KwrRitbTOFOCOL6oqW7Z+uZk+jDEAnbZXQYojZQykn/e2kv1MukBVl + PNkuYmQzHWxq3Y4hqqRfFcYw7V/mjDaSlLfcOQIA+2SM1AyB8j/VNJeHdSbCb64D + YyEMe9QbsWLFApy9/a8CAwEAAaOB7zCB7DASBgNVHRMBAf8ECDAGAQH/AgEAMB8G + A1UdIwQYMBaAFCvQaUeUdgn+9GuNLkCm90dNfwheMEQGCCsGAQUFBwEBBDgwNjA0 + BggrBgEFBQcwAYYoaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJv + b3RjYTAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vY3JsLmFwcGxlLmNvbS9yb290 + LmNybDAdBgNVHQ4EFgQUW9n6HeeaGgujmXYiUIY+kchbd6gwDgYDVR0PAQH/BAQD + AgEGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQA/Vj2e5bbD + eeZFIGi9v3OLLBKeAuOugCKMBB7DUshwgKj7zqew1UJEggOCTwb8O0kU+9h0UoWv + p50h5wESA5/NQFjQAde/MoMrU1goPO6cn1R2PWQnxn6NHThNLa6B5rmluJyJlPef + x4elUWY0GzlxOSTjh2fvpbFoe4zuPfeutnvi0v/fYcZqdUmVIkSoBPyUuAsuORFJ + EtHlgepZAE9bPFo22noicwkJac3AfOriJP6YRLj477JxPxpd1F1+M02cHSS+APCQ + A1iZQT0xWmJArzmoUUOSqwSonMJNsUvSq3xKX+udO7xPiEAGE/+QF4oIRynoYpgp + pU8RBWk6z/Kf + -----END CERTIFICATE----- + """ + + package static let pemCertificate = """ + -----BEGIN CERTIFICATE----- + MIIC8TCCAdmgAwIBAgICbE8wDQYJKoZIhvcNAQENBQAwGDEWMBQGA1UEAwwNUHVz + aHlUZXN0Um9vdDAgFw0xNzA0MTcwMDUzMzBaGA8yMTE3MDMyNDAwNTMzMFowHzEd + MBsGA1UEAwwUY29tLnJlbGF5cmlkZXMucHVzaHkwggEiMA0GCSqGSIb3DQEBAQUA + A4IBDwAwggEKAoIBAQDHZkZBnDKM4Gt+WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c36 + 41/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTkKljDGe8tuDncT1qSrp/UuikgdIAAiCXA + /vClWPYqZcHAUc9/OcfRiyK5AmJdzz+UbY803ArSPHjz3+Mk6C9tnzBXzG8oJq9o + EKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7U + j8wRstdr2xWhPg1fdIVHzudYubJ7M/h95JQFKtwqEevtLUa4BJgi8SKvRX5NnkGE + QMui1ercRuklVURTeoGDQYENiFnzTyI0J2tw3T+dAgMBAAGjPDA6MAkGA1UdEwQC + MAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD + ATANBgkqhkiG9w0BAQ0FAAOCAQEAnHHYMvBWglQLOUmNOalCMopmk9yKHM7+Sc9h + KsTWJW+YohF5zkRhnwUFxW85Pc63rRVA0qyI5zHzRtwYlcZHU57KttJyDGe1rm/0 + ZUqXharurJzyI09jcwRpDY8EGktrGirE1iHDqQTHNDHyS8iMVU6aPCo0xur63G5y + XzoIVhQXsBuwoU4VKb3n5CrxKEVcmE/nYF/Tk0rTtCrZF7TR3y/oxrp359goJ1b2 + /OjXN4dlqND41SbVTTL0FyXU3ebaS4DALA3pyVa1Rijw7vgEbFabsuMaAbdvlprn + RwUjsrRVu3Tx7sp/NqmeBLVru5nH/yHStDjSdvQtI2ipNGK/9w== + -----END CERTIFICATE----- + """ + + package static let pemPrivateKey = """ + -----BEGIN PRIVATE KEY----- + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHZkZBnDKM4Gt+ + WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c3641/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTk + KljDGe8tuDncT1qSrp/UuikgdIAAiCXA/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+U + bY803ArSPHjz3+Mk6C9tnzBXzG8oJq9oEKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69 + m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7Uj8wRstdr2xWhPg1fdIVHzudYubJ7M/h9 + 5JQFKtwqEevtLUa4BJgi8SKvRX5NnkGEQMui1ercRuklVURTeoGDQYENiFnzTyI0 + J2tw3T+dAgMBAAECggEBAMOsIZWQ6ipEsDe1R+vuq9Z6XeP8nwb7C2FXaKGji0Gz + 78YcCruln7KsHKkkD3UVw0Wa2Q1S8Kbf6A9fXutWL9f1yRHg7Ui0BDSE2ob2zAW5 + lRLnGs+nlSnV4WQQ5EY9NVDz8IcNR+o2znWhbb65kATvQuJO+l/lWWWBqbb+7rW+ + RHy43p7U8cK63nXJy9eHZ7eOgGGUMUX+Yg0g47RGYxlIeSDrtPCXlNuwwAJY7Ecp + LVltCAyCJEaLVwQpz61PTdmkb9HCvkwiuL6cnjtpoAdXCWX7tV61UNweNkvALIWR + kMukFFE/H6JlAkcbw4na1KwQ3glWIIB2H/vZyMNdnyECgYEA78VEXo+iAQ6or4bY + xUQFd/hIibIYMzq8PxDMOmD86G78u5Ho0ytetht5Xk1xmhv402FZCL1LsAEWpCBs + a9LUwo30A23KaTA7Oy5oo5Md1YJejSNOCR+vs5wAo0SZov5tQaxVMoj3vZZqnJzJ + 3A+XUgYZddIFkn8KJjgU/QVapTMCgYEA1OV1okYF2u5VW90RkVdvQONNcUvlKEL4 + UMSF3WJnORmtUL3Dt8AFt9b7pfz6WtVr0apT5SSIFA1+305PTpjjaw25m1GftL3U + 5QwkmgTKxnPD/YPC6tImp+OUXHmk+iTgmQ9HaBpEplcyjD0EP2LQsIc6qiku/P2n + OT8ArOkk5+8CgYEA7B98wRL6G8hv3swRVdMy/36HEPNOWcUR9Zl5RlSVO+FxCtca + Tjt7viM4VuI1aer6FFDd+XlRvDaWMXOs0lKCLEbXczkACK7y5clCSzRqQQVuT9fg + 1aNayKptBlxcYOPmfLJWBLpWH2KuAyV0tT61apWPJTR7QFXTjOfV44cOSXkCgYAH + CvAxRg+7hlbcixuhqzrK8roFHXWfN1fvlBC5mh/AC9Fn8l8fHQMTadE5VH0TtCu0 + 6+WKlwLJZwjjajvFZdlgGTwinzihSgZY7WXoknAC0KGTKWCxU/Jja2vlA0Ep5T5o + 0dCS6QuMVSYe7YXOcv5kWJTgPCyJwfpeMm9bSPsnkQKBgQChy4vU3J6CxGzwuvd/ + 011kszao+cHn1DdMTyUhvA/O/paB+BAVktHm+o/i+kOk4OcPjhRqewzZZdf7ie5U + hUC8kIraXM4aZt69ThQkAIER89wlhxsFXUmGf7ZMXm8f7pvM6/MDaMW3mEsfbL0U + Y3jy0E30W5s1XCW3gmZ1Vg2xAg== + -----END PRIVATE KEY----- + """ + + package static let encryptedPemCertificate = """ + -----BEGIN CERTIFICATE----- + MIICdDCCAVwCCQCtBOr7dtQS6zANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJB + VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 + cyBQdHkgTHRkMRAwDgYDVQQDDAdQYXNza2l0MSIwIAYJKoZIhvcNAQkBFhNub3Jl + cGx5QGV4YW1wbGUuY29tMB4XDTE5MDEzMTE2NTYzNloXDTI0MDEzMDE2NTYzNlow + RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu + dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC1 + +NQj0QzX5Vu9JMZVntP8i+JYAfOxzeP+MWUL/VaOxGaRp7DSiWAOd8bXDjJZjET0 + 4SPZzKvy0a6Suk9aIxCfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHd5jKTM4smJ + b4CoVY2PwYogb+bI4AtpUBV1QlsDrb1xMBHQ6zLf+JhRMya2MqJR+5hDKywrN4bC + j3LY87ir5aJFFaBMs9h0sCEoQKs0cnksf6Gq2pVJF9G+Aki4UF9r7jxoQwXjbtS3 + m6ptezzKYvMcw5rKKhtZRgDT1uuy5hgOCapZrV1s0byRv3W6IcdzOD3cWZEuxz2D + AVZCwIvqThqMaAs3Fvs3L3aQsDiOJpZ65gNnBU6j21liMZ3q7txD3eCzuXWMLPI5 + O7C4Sxy+LF4XAfd1/0nmHC2HBgA6CSMgncEzU6PLRR6bXH1daKWlcMAvF+STbLUJ + 79kQMXh2OCs= + -----END CERTIFICATE----- + """ + + package static let encryptedPemPrivateKey = """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIIBpjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMBNCLGhiuR4CAggA + MBQGCCqGSIb3DQMHBAjXF3m+2fdMRgSCAWAaGMyNREsNYTuTE0Zf/GIORBQH1Vjc + pNTvxV0B/YUHfzthOkotQjL8mfbbCWVixEdDE41Rn66WVrVmgFDVIKoGhjsMLGYd + angmocOnZ77ZYXi0f0/8fZYuQF2dF/zOfsdxyNl2gi4MGbKqt8m9vDcFAWEZsN/r + 5l1QJYNpF4OXKwNg4dnf7Ugo3PMWrVxKzKn+KtUvQd+mdYJ3xBjr1yLjLacbCXh0 + 4Kh1kWeV6yyaZswYPPItyAeg4smLdDTEqFI+GHIT7NFQ0GIojIqz2Ug8KWZaMwZs + iRCYXHDECkC7zqgcxJKRtjDmCJIxfIFcnwJ8DmMf7bpawtowcfM/z7TGSzAUeptA + bH9rS4Zf5/5Sx/yFRr2esClwli5BJG1uISQBpA0DePhePTiW6LesvAt3YZ3p2BCI + OE0HwdbAr24Nw7LRCuobRsTKFnBmM+uqtGyJhKE6hC1q4CPjZ09F8njX + -----END ENCRYPTED PRIVATE KEY----- + """ +} diff --git a/Sources/Passes/Passes.docc/Extensions/PassesService.md b/Sources/Passes/Passes.docc/Extensions/PassesService.md index 8fe801a..93f9f2a 100644 --- a/Sources/Passes/Passes.docc/Extensions/PassesService.md +++ b/Sources/Passes/Passes.docc/Extensions/PassesService.md @@ -4,8 +4,8 @@ ### Essentials -- ``generatePassContent(for:on:)`` -- ``generatePassesContent(for:on:)`` +- ``build(pass:on:)`` +- ``build(passes:on:)`` - ``register(migrations:)`` ### Push Notifications diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md index f7c679e..d56b243 100644 --- a/Sources/Passes/Passes.docc/GettingStarted.md +++ b/Sources/Passes/Passes.docc/GettingStarted.md @@ -171,8 +171,6 @@ final class PassDelegate: PassesDelegate { Next, initialize the ``PassesService`` inside the `configure.swift` file. This will implement all of the routes that Apple Wallet expects to exist on your server. -In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files. -If they are named like that you're good to go, otherwise you have to specify the custom name. > Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). @@ -188,7 +186,9 @@ public func configure(_ app: Application) async throws { let passesService = try PassesService( app: app, delegate: passDelegate, - signingFilesDirectory: "Certificates/Passes/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -228,7 +228,9 @@ public func configure(_ app: Application) async throws { >( app: app, delegate: passDelegate, - signingFilesDirectory: "Certificates/Passes/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -309,7 +311,7 @@ struct PassesController: RouteCollection { > Note: You'll have to register the `PassesController` in the `configure.swift` file, in order to pass it the ``PassesService`` object. -Then use the object inside your route handlers to generate the pass bundle with the ``PassesService/generatePassContent(for:on:)`` method and distribute it with the "`application/vnd.apple.pkpass`" MIME type. +Then use the object inside your route handlers to generate the pass bundle with the ``PassesService/build(pass:on:)`` method and distribute it with the "`application/vnd.apple.pkpass`" MIME type. ```swift fileprivate func passHandler(_ req: Request) async throws -> Response { @@ -322,7 +324,7 @@ fileprivate func passHandler(_ req: Request) async throws -> Response { throw Abort(.notFound) } - let bundle = try await passesService.generatePassContent(for: passData.pass, on: req.db) + let bundle = try await passesService.build(pass: passData.pass, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpass") @@ -336,7 +338,7 @@ fileprivate func passHandler(_ req: Request) async throws -> Response { ### Create a Bundle of Passes You can also create a bundle of passes to enable your user to download multiple passes at once. -Use the ``PassesService/generatePassesContent(for:on:)`` method to generate the bundle and serve it to the user. +Use the ``PassesService/build(passes:on:)`` method to generate the bundle and serve it to the user. The MIME type for a bundle of passes is "`application/vnd.apple.pkpasses`". > Note: You can have up to 10 passes or 150 MB for a bundle of passes. @@ -347,7 +349,7 @@ fileprivate func passesHandler(_ req: Request) async throws -> Response { let passesData = try await PassData.query(on: req.db).with(\.$pass).all() let passes = passesData.map { $0.pass } - let bundle = try await passesService.generatePassesContent(for: passes, on: req.db) + let bundle = try await passesService.build(passes: passes, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpasses") diff --git a/Sources/Passes/Passes.docc/Personalization.md b/Sources/Passes/Passes.docc/Personalization.md index c828d2c..7756781 100644 --- a/Sources/Passes/Passes.docc/Personalization.md +++ b/Sources/Passes/Passes.docc/Personalization.md @@ -97,7 +97,7 @@ Initializing the ``PassesService`` will automatically set up the endpoints that Adding the ``PassesService/register(migrations:)`` method to your `configure.swift` file will automatically set up the database table that stores the user personalization data. -Generate the pass bundle with ``PassesService/generatePassContent(for:on:)`` as usual and distribute it. +Generate the pass bundle with ``PassesService/build(pass:on:)`` as usual and distribute it. The user will be prompted to provide the required personal information when they add the pass. Wallet will then send the user personal information to your server, which will be saved in the ``UserPersonalization`` table. Immediately after that, Wallet will request the updated pass. diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift index ca3464b..7b9c46d 100644 --- a/Sources/Passes/PassesDelegate.swift +++ b/Sources/Passes/PassesDelegate.swift @@ -47,15 +47,6 @@ public protocol PassesDelegate: AnyObject, Sendable { /// - Returns: A URL path which points to the template data for the pass. func template(for pass: P, db: any Database) async throws -> String - /// Generates the SSL `signature` file. - /// - /// If you need to implement custom S/Mime signing you can use this - /// method to do so. You must generate a detached DER signature of the `manifest.json` file. - /// - /// - Parameter root: The location of the `manifest.json` and where to write the `signature` to. - /// - Returns: Return `true` if you generated a custom `signature`, otherwise `false`. - func generateSignatureFile(in root: URL) -> Bool - /// Encode the pass into JSON. /// /// This method should generate the entire pass JSON. You are provided with @@ -90,10 +81,6 @@ public protocol PassesDelegate: AnyObject, Sendable { } extension PassesDelegate { - public func generateSignatureFile(in root: URL) -> Bool { - return false - } - public func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { return nil } diff --git a/Sources/Passes/PassesError.swift b/Sources/Passes/PassesError.swift index 2bacc68..e93072b 100644 --- a/Sources/Passes/PassesError.swift +++ b/Sources/Passes/PassesError.swift @@ -10,10 +10,8 @@ public struct PassesError: Error, Sendable, Equatable { /// The type of the errors that can be thrown by PassKit passes. public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { enum Base: String, Sendable, Equatable { - case templateNotDirectory - case pemCertificateMissing - case pemPrivateKeyMissing - case opensslBinaryMissing + case noSourceFiles + case noOpenSSLExecutable case invalidNumberOfPasses } @@ -23,14 +21,10 @@ public struct PassesError: Error, Sendable, Equatable { self.base = base } - /// The template path is not a directory. - public static let templateNotDirectory = Self(.templateNotDirectory) - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(.pemCertificateMissing) - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(.pemPrivateKeyMissing) - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(.opensslBinaryMissing) + /// The path for the source files is not a directory. + public static let noSourceFiles = Self(.noSourceFiles) + /// The `openssl` executable is missing. + public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable) /// The number of passes to bundle is invalid. public static let invalidNumberOfPasses = Self(.invalidNumberOfPasses) @@ -61,17 +55,11 @@ public struct PassesError: Error, Sendable, Equatable { self.backing = .init(errorType: errorType) } - /// The template path is not a directory. - public static let templateNotDirectory = Self(errorType: .templateNotDirectory) + /// The path for the source files is not a directory. + public static let noSourceFiles = Self(errorType: .noSourceFiles) - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(errorType: .pemCertificateMissing) - - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(errorType: .pemPrivateKeyMissing) - - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(errorType: .opensslBinaryMissing) + /// The `openssl` executable is missing. + public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable) /// The number of passes to bundle is invalid. public static let invalidNumberOfPasses = Self(errorType: .invalidNumberOfPasses) diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift index 572f07f..74c6040 100644 --- a/Sources/Passes/PassesService.swift +++ b/Sources/Passes/PassesService.swift @@ -38,37 +38,34 @@ public final class PassesService: Sendable { /// - Parameters: /// - app: The `Vapor.Application` to use in route handlers and APNs. /// - delegate: The ``PassesDelegate`` to use for pass generation. - /// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located. - /// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`. - /// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`. - /// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`. - /// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - sslBinary: The location of the `openssl` command as a file path. /// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered. /// - logger: The `Logger` to use. + /// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format. + /// - pemCertificate: The PEM Certificate for signing passes. + /// - pemPrivateKey: The PEM Certificate's private key for signing passes. + /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. + /// - openSSLURL: The location of the `openssl` command as a file path. public init( app: Application, delegate: any PassesDelegate, - signingFilesDirectory: String, - wwdrCertificate: String = "WWDR.pem", - pemCertificate: String = "certificate.pem", - pemPrivateKey: String = "key.pem", - pemPrivateKeyPassword: String? = nil, - sslBinary: String = "/usr/bin/openssl", pushRoutesMiddleware: (any Middleware)? = nil, - logger: Logger? = nil + logger: Logger? = nil, + pemWWDRCertificate: String, + pemCertificate: String, + pemPrivateKey: String, + pemPrivateKeyPassword: String? = nil, + openSSLURL: String = "/usr/bin/openssl" ) throws { self.service = try .init( app: app, delegate: delegate, - signingFilesDirectory: signingFilesDirectory, - wwdrCertificate: wwdrCertificate, + pushRoutesMiddleware: pushRoutesMiddleware, + logger: logger, + pemWWDRCertificate: pemWWDRCertificate, pemCertificate: pemCertificate, pemPrivateKey: pemPrivateKey, pemPrivateKeyPassword: pemPrivateKeyPassword, - sslBinary: sslBinary, - pushRoutesMiddleware: pushRoutesMiddleware, - logger: logger + openSSLURL: openSSLURL ) } @@ -77,9 +74,10 @@ public final class PassesService: Sendable { /// - Parameters: /// - pass: The pass to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated pass content as `Data`. - public func generatePassContent(for pass: Pass, on db: any Database) async throws -> Data { - try await service.generatePassContent(for: pass, on: db) + public func build(pass: Pass, on db: any Database) async throws -> Data { + try await service.build(pass: pass, on: db) } /// Generates a bundle of passes to enable your user to download multiple passes at once. @@ -91,9 +89,10 @@ public final class PassesService: Sendable { /// - Parameters: /// - passes: The passes to include in the bundle. /// - db: The `Database` to use. + /// /// - Returns: The bundle of passes as `Data`. - public func generatePassesContent(for passes: [Pass], on db: any Database) async throws -> Data { - try await service.generatePassesContent(for: passes, on: db) + public func build(passes: [Pass], on db: any Database) async throws -> Data { + try await service.build(passes: passes, on: db) } /// Adds the migrations for PassKit passes models. diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 56db552..5500c8b 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -23,19 +23,18 @@ import Zip /// - Device Type /// - Registration Type /// - Error Log Type -public final class PassesServiceCustom< - P, U, D, R: PassesRegistrationModel, E: ErrorLogModel ->: Sendable +public final class PassesServiceCustom: Sendable where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { private unowned let app: Application private unowned let delegate: any PassesDelegate - private let signingFilesDirectory: URL - private let wwdrCertificate: String + private let logger: Logger? + + private let pemWWDRCertificate: String private let pemCertificate: String private let pemPrivateKey: String private let pemPrivateKeyPassword: String? - private let sslBinary: URL - private let logger: Logger? + private let openSSLURL: URL + private let encoder = JSONEncoder() /// Initializes the service and registers all the routes required for PassKit to work. @@ -43,65 +42,54 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { /// - Parameters: /// - app: The `Vapor.Application` to use in route handlers and APNs. /// - delegate: The ``PassesDelegate`` to use for pass generation. - /// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located. - /// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`. - /// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`. - /// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`. - /// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - sslBinary: The location of the `openssl` command as a file path. /// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered. /// - logger: The `Logger` to use. + /// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format. + /// - pemCertificate: The PEM Certificate for signing passes. + /// - pemPrivateKey: The PEM Certificate's private key for signing passes. + /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. + /// - openSSLURL: The location of the `openssl` command as a file path. public init( app: Application, delegate: any PassesDelegate, - signingFilesDirectory: String, - wwdrCertificate: String = "WWDR.pem", - pemCertificate: String = "certificate.pem", - pemPrivateKey: String = "key.pem", - pemPrivateKeyPassword: String? = nil, - sslBinary: String = "/usr/bin/openssl", pushRoutesMiddleware: (any Middleware)? = nil, - logger: Logger? = nil + logger: Logger? = nil, + pemWWDRCertificate: String, + pemCertificate: String, + pemPrivateKey: String, + pemPrivateKeyPassword: String? = nil, + openSSLURL: String = "/usr/bin/openssl" ) throws { self.app = app self.delegate = delegate - self.signingFilesDirectory = URL(fileURLWithPath: signingFilesDirectory, isDirectory: true) - self.wwdrCertificate = wwdrCertificate + self.logger = logger + + self.pemWWDRCertificate = pemWWDRCertificate self.pemCertificate = pemCertificate self.pemPrivateKey = pemPrivateKey self.pemPrivateKeyPassword = pemPrivateKeyPassword - self.sslBinary = URL(fileURLWithPath: sslBinary) - self.logger = logger + self.openSSLURL = URL(fileURLWithPath: openSSLURL) - let privateKeyPath = URL(fileURLWithPath: pemPrivateKey, relativeTo: self.signingFilesDirectory).path - guard FileManager.default.fileExists(atPath: privateKeyPath) else { - throw PassesError.pemPrivateKeyMissing - } - let pemPath = URL(fileURLWithPath: pemCertificate, relativeTo: self.signingFilesDirectory).path - guard FileManager.default.fileExists(atPath: pemPath) else { - throw PassesError.pemCertificateMissing - } + let privateKeyBytes = pemPrivateKey.data(using: .utf8)!.map { UInt8($0) } + let certificateBytes = pemCertificate.data(using: .utf8)!.map { UInt8($0) } let apnsConfig: APNSClientConfiguration - if let password = pemPrivateKeyPassword { + if let pemPrivateKeyPassword { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( privateKey: .privateKey( - NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { passphraseCallback in - passphraseCallback(password.utf8) - }), - certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { - .certificate($0) - } + NIOSSLPrivateKey(bytes: privateKeyBytes, format: .pem) { passphraseCallback in + passphraseCallback(pemPrivateKeyPassword.utf8) + } + ), + certificateChain: NIOSSLCertificate.fromPEMBytes(certificateBytes).map { .certificate($0) } ), environment: .production ) } else { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( - privateKey: .privateKey(NIOSSLPrivateKey(file: privateKeyPath, format: .pem)), - certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { - .certificate($0) - } + privateKey: .privateKey(NIOSSLPrivateKey(bytes: privateKeyBytes, format: .pem)), + certificateChain: NIOSSLCertificate.fromPEMBytes(certificateBytes).map { .certificate($0) } ), environment: .production ) @@ -266,7 +254,7 @@ extension PassesServiceCustom { return try await Response( status: .ok, headers: headers, - body: Response.Body(data: self.generatePassContent(for: pass, on: req.db)) + body: Response.Body(data: self.build(pass: pass, on: req.db)) ) } @@ -344,72 +332,14 @@ extension PassesServiceCustom { pass._$userPersonalization.id = try userPersonalization.requireID() try await pass.update(on: req.db) - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { _ = try? FileManager.default.removeItem(at: root) } - guard let token = userInfo.personalizationToken.data(using: .utf8) else { throw Abort(.internalServerError) } - let signature: Data - if let password = self.pemPrivateKeyPassword { - let sslBinary: URL = self.sslBinary - guard FileManager.default.fileExists(atPath: sslBinary.path) else { - throw PassesError.opensslBinaryMissing - } - - let tokenURL = root.appendingPathComponent("personalizationToken") - try token.write(to: tokenURL) - - let proc = Process() - proc.currentDirectoryURL = self.signingFilesDirectory - proc.executableURL = sslBinary - proc.arguments = [ - "smime", "-binary", "-sign", - "-certfile", self.wwdrCertificate, - "-signer", self.pemCertificate, - "-inkey", self.pemPrivateKey, - "-in", tokenURL.path, - "-out", root.appendingPathComponent("signature").path, - "-outform", "DER", - "-passin", "pass:\(password)", - ] - try proc.run() - proc.waitUntilExit() - signature = try Data(contentsOf: root.appendingPathComponent("signature")) - } else { - let signatureBytes = try CMS.sign( - token, - signatureAlgorithm: .sha256WithRSAEncryption, - additionalIntermediateCertificates: [ - Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.wwdrCertificate) - ) - ) - ], - certificate: Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemCertificate) - ) - ), - privateKey: .init( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemPrivateKey) - ) - ), - signingTime: Date() - ) - signature = Data(signatureBytes) - } var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/octet-stream") headers.add(name: .contentTransferEncoding, value: "binary") - return Response(status: .ok, headers: headers, body: Response.Body(data: signature)) + return try Response(status: .ok, headers: headers, body: Response.Body(data: self.signature(for: token))) } // MARK: - Push Routes @@ -490,76 +420,70 @@ extension PassesServiceCustom { // MARK: - pkpass file generation extension PassesServiceCustom { - private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { + private func manifest(for directory: URL) throws -> Data { var manifest: [String: String] = [:] - let paths = try FileManager.default.subpathsOfDirectory(atPath: root.path) + + let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) for relativePath in paths { - let file = URL(fileURLWithPath: relativePath, relativeTo: root) - guard !file.hasDirectoryPath else { continue } - manifest[relativePath] = try Insecure.SHA1.hash(data: Data(contentsOf: file)).hex + let file = URL(fileURLWithPath: relativePath, relativeTo: directory) + guard !file.hasDirectoryPath else { + continue + } + + let hash = try Insecure.SHA1.hash(data: Data(contentsOf: file)) + manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() } - // Write the manifest file to the root directory - // and return the data for using it in signing. - let data = try encoder.encode(manifest) - try data.write(to: root.appendingPathComponent("manifest.json")) - return data - } - private func generateSignatureFile(for manifest: Data, in root: URL) throws { - // If the caller's delegate generated a file we don't have to do it. - if delegate.generateSignatureFile(in: root) { return } + return try encoder.encode(manifest) + } + // We use this function to sign the personalization token too. + private func signature(for manifest: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. - if let password = self.pemPrivateKeyPassword { - let sslBinary = self.sslBinary - guard FileManager.default.fileExists(atPath: sslBinary.path) else { - throw PassesError.opensslBinaryMissing + if let pemPrivateKeyPassword { + guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { + throw PassesError.noOpenSSLExecutable } - let proc = Process() - proc.currentDirectoryURL = self.signingFilesDirectory - proc.executableURL = sslBinary - proc.arguments = [ + let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: dir) } + + try manifest.write(to: dir.appendingPathComponent("manifest.json")) + try self.pemWWDRCertificate.write(to: dir.appendingPathComponent("wwdr.pem"), atomically: true, encoding: .utf8) + try self.pemCertificate.write(to: dir.appendingPathComponent("certificate.pem"), atomically: true, encoding: .utf8) + try self.pemPrivateKey.write(to: dir.appendingPathComponent("private.pem"), atomically: true, encoding: .utf8) + + let process = Process() + process.currentDirectoryURL = dir + process.executableURL = self.openSSLURL + process.arguments = [ "smime", "-binary", "-sign", - "-certfile", self.wwdrCertificate, - "-signer", self.pemCertificate, - "-inkey", self.pemPrivateKey, - "-in", root.appendingPathComponent("manifest.json").path, - "-out", root.appendingPathComponent("signature").path, + "-certfile", dir.appendingPathComponent("wwdr.pem").path, + "-signer", dir.appendingPathComponent("certificate.pem").path, + "-inkey", dir.appendingPathComponent("private.pem").path, + "-in", dir.appendingPathComponent("manifest.json").path, + "-out", dir.appendingPathComponent("signature").path, "-outform", "DER", - "-passin", "pass:\(password)", + "-passin", "pass:\(pemPrivateKeyPassword)", ] - try proc.run() - proc.waitUntilExit() - return - } + try process.run() + process.waitUntilExit() - let signature = try CMS.sign( - manifest, - signatureAlgorithm: .sha256WithRSAEncryption, - additionalIntermediateCertificates: [ - Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.wwdrCertificate) - ) - ) - ], - certificate: Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemCertificate) - ) - ), - privateKey: .init( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemPrivateKey) - ) - ), - signingTime: Date() - ) - try Data(signature).write(to: root.appendingPathComponent("signature")) + return try Data(contentsOf: dir.appendingPathComponent("signature")) + } else { + let signature = try CMS.sign( + manifest, + signatureAlgorithm: .sha256WithRSAEncryption, + additionalIntermediateCertificates: [ + Certificate(pemEncoded: self.pemWWDRCertificate) + ], + certificate: Certificate(pemEncoded: self.pemCertificate), + privateKey: .init(pemEncoded: self.pemPrivateKey), + signingTime: Date() + ) + return Data(signature) + } } /// Generates the pass content bundle for a given pass. @@ -567,36 +491,50 @@ extension PassesServiceCustom { /// - Parameters: /// - pass: The pass to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated pass content as `Data`. - public func generatePassContent(for pass: P, on db: any Database) async throws -> Data { - let templateDirectory = try await URL(fileURLWithPath: delegate.template(for: pass, db: db), isDirectory: true) + public func build(pass: P, on db: any Database) async throws -> Data { + let filesDirectory = try await URL(fileURLWithPath: delegate.template(for: pass, db: db), isDirectory: true) guard - (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { - throw PassesError.templateNotDirectory + throw PassesError.noSourceFiles } - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.copyItem(at: templateDirectory, to: root) - defer { _ = try? FileManager.default.removeItem(at: root) } + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.copyItem(at: filesDirectory, to: tempDir) + defer { try? FileManager.default.removeItem(at: tempDir) } - try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) - .write(to: root.appendingPathComponent("pass.json")) + var files: [ArchiveFile] = [] - var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) + let passJSON = try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) + try passJSON.write(to: tempDir.appendingPathComponent("pass.json")) + files.append(ArchiveFile(filename: "pass.json", data: passJSON)) // Pass Personalization if let personalizationJSON = try await self.delegate.personalizationJSON(for: pass, db: db) { - try self.encoder.encode(personalizationJSON).write(to: root.appendingPathComponent("personalization.json")) - files.append(URL(fileURLWithPath: "personalization.json", relativeTo: root)) + let personalizationJSONData = try self.encoder.encode(personalizationJSON) + try personalizationJSONData.write(to: tempDir.appendingPathComponent("personalization.json")) + files.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData)) } - try self.generateSignatureFile(for: Self.generateManifestFile(using: self.encoder, in: root), in: root) + let manifest = try self.manifest(for: tempDir) + files.append(ArchiveFile(filename: "manifest.json", data: manifest)) + try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest))) - files.append(URL(fileURLWithPath: "pass.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "manifest.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "signature", relativeTo: root)) - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) + let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path) + for relativePath in paths { + let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir) + guard !file.hasDirectoryPath else { + continue + } + + try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file))) + } + + let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).pkpass") + try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) + return try Data(contentsOf: zipFile) } /// Generates a bundle of passes to enable your user to download multiple passes at once. @@ -608,23 +546,20 @@ extension PassesServiceCustom { /// - Parameters: /// - passes: The passes to include in the bundle. /// - db: The `Database` to use. + /// /// - Returns: The bundle of passes as `Data`. - public func generatePassesContent(for passes: [P], on db: any Database) async throws -> Data { + public func build(passes: [P], on db: any Database) async throws -> Data { guard passes.count > 1 && passes.count <= 10 else { throw PassesError.invalidNumberOfPasses } - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { _ = try? FileManager.default.removeItem(at: root) } - - var files: [URL] = [] + var files: [ArchiveFile] = [] for (i, pass) in passes.enumerated() { - let name = "pass\(i).pkpass" - try await self.generatePassContent(for: pass, on: db) - .write(to: root.appendingPathComponent(name)) - files.append(URL(fileURLWithPath: name, relativeTo: root)) + try await files.append(ArchiveFile(filename: "pass\(i).pkpass", data: self.build(pass: pass, on: db))) } - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) + + let zipFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") + try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) + return try Data(contentsOf: zipFile) } } diff --git a/Tests/Certificates/WWDR.pem b/Tests/Certificates/WWDR.pem deleted file mode 100644 index 7202de0..0000000 --- a/Tests/Certificates/WWDR.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEVTCCAz2gAwIBAgIUE9x3lVJx5T3GMujM/+Uh88zFztIwDQYJKoZIhvcNAQEL -BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsT -HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBS -b290IENBMB4XDTIwMTIxNjE5MzYwNFoXDTMwMTIxMDAwMDAwMFowdTFEMEIGA1UE -Aww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc0MRMwEQYDVQQKDApBcHBsZSBJbmMu -MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAf -eKp6JzKwRl/nF3bYoJ0OKY6tPTKlxGs3yeRBkWq3eXFdDDQEYHX3rkOPR8SGHgjo -v9Y5Ui8eZ/xx8YJtPH4GUnadLLzVQ+mxtLxAOnhRXVGhJeG+bJGdayFZGEHVD41t -QSo5SiHgkJ9OE0/QjJoyuNdqkh4laqQyziIZhQVg3AJK8lrrd3kCfcCXVGySjnYB -5kaP5eYq+6KwrRitbTOFOCOL6oqW7Z+uZk+jDEAnbZXQYojZQykn/e2kv1MukBVl -PNkuYmQzHWxq3Y4hqqRfFcYw7V/mjDaSlLfcOQIA+2SM1AyB8j/VNJeHdSbCb64D -YyEMe9QbsWLFApy9/a8CAwEAAaOB7zCB7DASBgNVHRMBAf8ECDAGAQH/AgEAMB8G -A1UdIwQYMBaAFCvQaUeUdgn+9GuNLkCm90dNfwheMEQGCCsGAQUFBwEBBDgwNjA0 -BggrBgEFBQcwAYYoaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJv -b3RjYTAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vY3JsLmFwcGxlLmNvbS9yb290 -LmNybDAdBgNVHQ4EFgQUW9n6HeeaGgujmXYiUIY+kchbd6gwDgYDVR0PAQH/BAQD -AgEGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQA/Vj2e5bbD -eeZFIGi9v3OLLBKeAuOugCKMBB7DUshwgKj7zqew1UJEggOCTwb8O0kU+9h0UoWv -p50h5wESA5/NQFjQAde/MoMrU1goPO6cn1R2PWQnxn6NHThNLa6B5rmluJyJlPef -x4elUWY0GzlxOSTjh2fvpbFoe4zuPfeutnvi0v/fYcZqdUmVIkSoBPyUuAsuORFJ -EtHlgepZAE9bPFo22noicwkJac3AfOriJP6YRLj477JxPxpd1F1+M02cHSS+APCQ -A1iZQT0xWmJArzmoUUOSqwSonMJNsUvSq3xKX+udO7xPiEAGE/+QF4oIRynoYpgp -pU8RBWk6z/Kf ------END CERTIFICATE----- diff --git a/Tests/Certificates/certificate.pem b/Tests/Certificates/certificate.pem deleted file mode 100644 index c300118..0000000 --- a/Tests/Certificates/certificate.pem +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC8TCCAdmgAwIBAgICbE8wDQYJKoZIhvcNAQENBQAwGDEWMBQGA1UEAwwNUHVz -aHlUZXN0Um9vdDAgFw0xNzA0MTcwMDUzMzBaGA8yMTE3MDMyNDAwNTMzMFowHzEd -MBsGA1UEAwwUY29tLnJlbGF5cmlkZXMucHVzaHkwggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQDHZkZBnDKM4Gt+WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c36 -41/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTkKljDGe8tuDncT1qSrp/UuikgdIAAiCXA -/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+UbY803ArSPHjz3+Mk6C9tnzBXzG8oJq9o -EKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7U -j8wRstdr2xWhPg1fdIVHzudYubJ7M/h95JQFKtwqEevtLUa4BJgi8SKvRX5NnkGE -QMui1ercRuklVURTeoGDQYENiFnzTyI0J2tw3T+dAgMBAAGjPDA6MAkGA1UdEwQC -MAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD -ATANBgkqhkiG9w0BAQ0FAAOCAQEAnHHYMvBWglQLOUmNOalCMopmk9yKHM7+Sc9h -KsTWJW+YohF5zkRhnwUFxW85Pc63rRVA0qyI5zHzRtwYlcZHU57KttJyDGe1rm/0 -ZUqXharurJzyI09jcwRpDY8EGktrGirE1iHDqQTHNDHyS8iMVU6aPCo0xur63G5y -XzoIVhQXsBuwoU4VKb3n5CrxKEVcmE/nYF/Tk0rTtCrZF7TR3y/oxrp359goJ1b2 -/OjXN4dlqND41SbVTTL0FyXU3ebaS4DALA3pyVa1Rijw7vgEbFabsuMaAbdvlprn -RwUjsrRVu3Tx7sp/NqmeBLVru5nH/yHStDjSdvQtI2ipNGK/9w== ------END CERTIFICATE----- diff --git a/Tests/Certificates/encryptedcert.pem b/Tests/Certificates/encryptedcert.pem deleted file mode 100644 index 7a9d7cc..0000000 --- a/Tests/Certificates/encryptedcert.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICdDCCAVwCCQCtBOr7dtQS6zANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRAwDgYDVQQDDAdQYXNza2l0MSIwIAYJKoZIhvcNAQkBFhNub3Jl -cGx5QGV4YW1wbGUuY29tMB4XDTE5MDEzMTE2NTYzNloXDTI0MDEzMDE2NTYzNlow -RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu -dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC1 -+NQj0QzX5Vu9JMZVntP8i+JYAfOxzeP+MWUL/VaOxGaRp7DSiWAOd8bXDjJZjET0 -4SPZzKvy0a6Suk9aIxCfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHd5jKTM4smJ -b4CoVY2PwYogb+bI4AtpUBV1QlsDrb1xMBHQ6zLf+JhRMya2MqJR+5hDKywrN4bC -j3LY87ir5aJFFaBMs9h0sCEoQKs0cnksf6Gq2pVJF9G+Aki4UF9r7jxoQwXjbtS3 -m6ptezzKYvMcw5rKKhtZRgDT1uuy5hgOCapZrV1s0byRv3W6IcdzOD3cWZEuxz2D -AVZCwIvqThqMaAs3Fvs3L3aQsDiOJpZ65gNnBU6j21liMZ3q7txD3eCzuXWMLPI5 -O7C4Sxy+LF4XAfd1/0nmHC2HBgA6CSMgncEzU6PLRR6bXH1daKWlcMAvF+STbLUJ -79kQMXh2OCs= ------END CERTIFICATE----- diff --git a/Tests/Certificates/encryptedkey.pem b/Tests/Certificates/encryptedkey.pem deleted file mode 100644 index 615dcc0..0000000 --- a/Tests/Certificates/encryptedkey.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -MIIBpjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMBNCLGhiuR4CAggA -MBQGCCqGSIb3DQMHBAjXF3m+2fdMRgSCAWAaGMyNREsNYTuTE0Zf/GIORBQH1Vjc -pNTvxV0B/YUHfzthOkotQjL8mfbbCWVixEdDE41Rn66WVrVmgFDVIKoGhjsMLGYd -angmocOnZ77ZYXi0f0/8fZYuQF2dF/zOfsdxyNl2gi4MGbKqt8m9vDcFAWEZsN/r -5l1QJYNpF4OXKwNg4dnf7Ugo3PMWrVxKzKn+KtUvQd+mdYJ3xBjr1yLjLacbCXh0 -4Kh1kWeV6yyaZswYPPItyAeg4smLdDTEqFI+GHIT7NFQ0GIojIqz2Ug8KWZaMwZs -iRCYXHDECkC7zqgcxJKRtjDmCJIxfIFcnwJ8DmMf7bpawtowcfM/z7TGSzAUeptA -bH9rS4Zf5/5Sx/yFRr2esClwli5BJG1uISQBpA0DePhePTiW6LesvAt3YZ3p2BCI -OE0HwdbAr24Nw7LRCuobRsTKFnBmM+uqtGyJhKE6hC1q4CPjZ09F8njX ------END ENCRYPTED PRIVATE KEY----- diff --git a/Tests/Certificates/key.pem b/Tests/Certificates/key.pem deleted file mode 100644 index db02c80..0000000 --- a/Tests/Certificates/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHZkZBnDKM4Gt+ -WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c3641/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTk -KljDGe8tuDncT1qSrp/UuikgdIAAiCXA/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+U -bY803ArSPHjz3+Mk6C9tnzBXzG8oJq9oEKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69 -m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7Uj8wRstdr2xWhPg1fdIVHzudYubJ7M/h9 -5JQFKtwqEevtLUa4BJgi8SKvRX5NnkGEQMui1ercRuklVURTeoGDQYENiFnzTyI0 -J2tw3T+dAgMBAAECggEBAMOsIZWQ6ipEsDe1R+vuq9Z6XeP8nwb7C2FXaKGji0Gz -78YcCruln7KsHKkkD3UVw0Wa2Q1S8Kbf6A9fXutWL9f1yRHg7Ui0BDSE2ob2zAW5 -lRLnGs+nlSnV4WQQ5EY9NVDz8IcNR+o2znWhbb65kATvQuJO+l/lWWWBqbb+7rW+ -RHy43p7U8cK63nXJy9eHZ7eOgGGUMUX+Yg0g47RGYxlIeSDrtPCXlNuwwAJY7Ecp -LVltCAyCJEaLVwQpz61PTdmkb9HCvkwiuL6cnjtpoAdXCWX7tV61UNweNkvALIWR -kMukFFE/H6JlAkcbw4na1KwQ3glWIIB2H/vZyMNdnyECgYEA78VEXo+iAQ6or4bY -xUQFd/hIibIYMzq8PxDMOmD86G78u5Ho0ytetht5Xk1xmhv402FZCL1LsAEWpCBs -a9LUwo30A23KaTA7Oy5oo5Md1YJejSNOCR+vs5wAo0SZov5tQaxVMoj3vZZqnJzJ -3A+XUgYZddIFkn8KJjgU/QVapTMCgYEA1OV1okYF2u5VW90RkVdvQONNcUvlKEL4 -UMSF3WJnORmtUL3Dt8AFt9b7pfz6WtVr0apT5SSIFA1+305PTpjjaw25m1GftL3U -5QwkmgTKxnPD/YPC6tImp+OUXHmk+iTgmQ9HaBpEplcyjD0EP2LQsIc6qiku/P2n -OT8ArOkk5+8CgYEA7B98wRL6G8hv3swRVdMy/36HEPNOWcUR9Zl5RlSVO+FxCtca -Tjt7viM4VuI1aer6FFDd+XlRvDaWMXOs0lKCLEbXczkACK7y5clCSzRqQQVuT9fg -1aNayKptBlxcYOPmfLJWBLpWH2KuAyV0tT61apWPJTR7QFXTjOfV44cOSXkCgYAH -CvAxRg+7hlbcixuhqzrK8roFHXWfN1fvlBC5mh/AC9Fn8l8fHQMTadE5VH0TtCu0 -6+WKlwLJZwjjajvFZdlgGTwinzihSgZY7WXoknAC0KGTKWCxU/Jja2vlA0Ep5T5o -0dCS6QuMVSYe7YXOcv5kWJTgPCyJwfpeMm9bSPsnkQKBgQChy4vU3J6CxGzwuvd/ -011kszao+cHn1DdMTyUhvA/O/paB+BAVktHm+o/i+kOk4OcPjhRqewzZZdf7ie5U -hUC8kIraXM4aZt69ThQkAIER89wlhxsFXUmGf7ZMXm8f7pvM6/MDaMW3mEsfbL0U -Y3jy0E30W5s1XCW3gmZ1Vg2xAg== ------END PRIVATE KEY----- diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index a92d77d..9b843e3 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -17,7 +17,7 @@ struct OrdersTests { let orderData = OrderData(title: "Test Order") try await orderData.create(on: app.db) let order = try await orderData.$order.get(on: app.db) - let data = try await ordersService.generateOrderContent(for: order, on: app.db) + let data = try await ordersService.build(order: order, on: app.db) let orderURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).order") try data.write(to: orderURL) let orderFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -396,19 +396,7 @@ struct OrdersTests { @Test("OrdersError") func ordersError() { - #expect(OrdersError.templateNotDirectory.description == "OrdersError(errorType: templateNotDirectory)") - #expect(OrdersError.pemCertificateMissing.description == "OrdersError(errorType: pemCertificateMissing)") - #expect(OrdersError.pemPrivateKeyMissing.description == "OrdersError(errorType: pemPrivateKeyMissing)") - #expect(OrdersError.opensslBinaryMissing.description == "OrdersError(errorType: opensslBinaryMissing)") - } - - @Test("Default OrdersDelegate Properties") - func defaultDelegate() { - final class DefaultOrdersDelegate: OrdersDelegate { - func template(for order: O, db: any Database) async throws -> String { "" } - func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data { Data() } - } - - #expect(!DefaultOrdersDelegate().generateSignatureFile(in: URL(fileURLWithPath: ""))) + #expect(OrdersError.noSourceFiles.description == "OrdersError(errorType: noSourceFiles)") + #expect(OrdersError.noOpenSSLExecutable.description == "OrdersError(errorType: noOpenSSLExecutable)") } } diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/withApp.swift index 5075c87..8928800 100644 --- a/Tests/OrdersTests/withApp.swift +++ b/Tests/OrdersTests/withApp.swift @@ -23,12 +23,12 @@ func withApp( let ordersService = try OrdersService( app: app, delegate: delegate, - signingFilesDirectory: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - pemCertificate: useEncryptedKey ? "encryptedcert.pem" : "certificate.pem", - pemPrivateKey: useEncryptedKey ? "encryptedkey.pem" : "key.pem", - pemPrivateKeyPassword: useEncryptedKey ? "password" : nil, pushRoutesMiddleware: SecretMiddleware(secret: "foo"), - logger: app.logger + logger: app.logger, + pemWWDRCertificate: TestCertificate.pemWWDRCertificate, + pemCertificate: useEncryptedKey ? TestCertificate.encryptedPemCertificate : TestCertificate.pemCertificate, + pemPrivateKey: useEncryptedKey ? TestCertificate.encryptedPemPrivateKey : TestCertificate.pemPrivateKey, + pemPrivateKeyPassword: useEncryptedKey ? "password" : nil ) app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 28cd0e6..34848cf 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -17,7 +17,7 @@ struct PassesTests { let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) - let data = try await passesService.generatePassContent(for: pass, on: app.db) + let data = try await passesService.build(pass: pass, on: app.db) let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") try data.write(to: passURL) let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -60,11 +60,11 @@ struct PassesTests { try await passData2.create(on: app.db) let pass2 = try await passData2._$pass.get(on: app.db) - let data = try await passesService.generatePassesContent(for: [pass1, pass2], on: app.db) + let data = try await passesService.build(passes: [pass1, pass2], on: app.db) #expect(data != nil) do { - let data = try await passesService.generatePassesContent(for: [pass1], on: app.db) + let data = try await passesService.build(passes: [pass1], on: app.db) Issue.record("Expected error, got \(data)") } catch let error as PassesError { #expect(error == .invalidNumberOfPasses) @@ -78,19 +78,19 @@ struct PassesTests { let passData = PassData(title: "Personalize") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) - let data = try await passesService.generatePassContent(for: pass, on: app.db) + let data = try await passesService.build(pass: pass, on: app.db) let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") try data.write(to: passURL) let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) try Zip.unzipFile(passURL, destination: passFolder) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) - + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/logo.png"))) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/personalizationLogo.png"))) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/logo.png"))) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/personalizationLogo.png"))) - + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) @@ -534,10 +534,8 @@ struct PassesTests { @Test("PassesError") func passesError() { - #expect(PassesError.templateNotDirectory.description == "PassesError(errorType: templateNotDirectory)") - #expect(PassesError.pemCertificateMissing.description == "PassesError(errorType: pemCertificateMissing)") - #expect(PassesError.pemPrivateKeyMissing.description == "PassesError(errorType: pemPrivateKeyMissing)") - #expect(PassesError.opensslBinaryMissing.description == "PassesError(errorType: opensslBinaryMissing)") + #expect(PassesError.noSourceFiles.description == "PassesError(errorType: noSourceFiles)") + #expect(PassesError.noOpenSSLExecutable.description == "PassesError(errorType: noOpenSSLExecutable)") #expect(PassesError.invalidNumberOfPasses.description == "PassesError(errorType: invalidNumberOfPasses)") } @@ -549,7 +547,6 @@ struct PassesTests { } let defaultDelegate = DefaultPassesDelegate() - #expect(!defaultDelegate.generateSignatureFile(in: URL(fileURLWithPath: ""))) try await withApp { app, passesService in let passData = PassData(title: "Test Pass") diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/withApp.swift index 52a051f..4681421 100644 --- a/Tests/PassesTests/withApp.swift +++ b/Tests/PassesTests/withApp.swift @@ -23,12 +23,12 @@ func withApp( let passesService = try PassesService( app: app, delegate: delegate, - signingFilesDirectory: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - pemCertificate: useEncryptedKey ? "encryptedcert.pem" : "certificate.pem", - pemPrivateKey: useEncryptedKey ? "encryptedkey.pem" : "key.pem", - pemPrivateKeyPassword: useEncryptedKey ? "password" : nil, pushRoutesMiddleware: SecretMiddleware(secret: "foo"), - logger: app.logger + logger: app.logger, + pemWWDRCertificate: TestCertificate.pemWWDRCertificate, + pemCertificate: useEncryptedKey ? TestCertificate.encryptedPemCertificate : TestCertificate.pemCertificate, + pemPrivateKey: useEncryptedKey ? TestCertificate.encryptedPemPrivateKey : TestCertificate.pemPrivateKey, + pemPrivateKeyPassword: useEncryptedKey ? "password" : nil ) app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite) From a8f2827df93234adb3f96ef9d19a88a5d332cbab Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 5 Nov 2024 15:34:35 +0100 Subject: [PATCH 3/7] Simplify notifications API --- Sources/Orders/OrdersService.swift | 10 ------ Sources/Orders/OrdersServiceCustom.swift | 44 ++++++++++++++---------- Sources/Passes/PassesService.swift | 10 ------ Sources/Passes/PassesServiceCustom.swift | 44 ++++++++++++++---------- Tests/OrdersTests/OrdersTests.swift | 2 +- Tests/PassesTests/PassesTests.swift | 2 +- 6 files changed, 54 insertions(+), 58 deletions(-) diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift index 81a711c..b6f944c 100644 --- a/Sources/Orders/OrdersService.swift +++ b/Sources/Orders/OrdersService.swift @@ -69,16 +69,6 @@ public final class OrdersService: Sendable { migrations.add(OrdersErrorLog()) } - /// Sends push notifications for a given order. - /// - /// - Parameters: - /// - id: The `UUID` of the order to send the notifications for. - /// - typeIdentifier: The type identifier of the order. - /// - db: The `Database` to use. - public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - try await service.sendPushNotificationsForOrder(id: id, of: typeIdentifier, on: db) - } - /// Sends push notifications for a given order. /// /// - Parameters: diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 664ea1a..89b16ad 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -306,7 +306,16 @@ extension OrdersServiceCustom { } let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")! - try await sendPushNotificationsForOrder(id: id, of: orderTypeIdentifier, on: req.db) + guard + let order = try await O.query(on: req.db) + .filter(\._$id == id) + .filter(\._$typeIdentifier == orderTypeIdentifier) + .first() + else { + throw Abort(.notFound) + } + + try await sendPushNotifications(for: order, on: req.db) return .noContent } @@ -318,7 +327,16 @@ extension OrdersServiceCustom { } let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")! - return try await Self.registrationsForOrder(id: id, of: orderTypeIdentifier, on: req.db).map { $0.device.pushToken } + guard + let order = try await O.query(on: req.db) + .filter(\._$id == id) + .filter(\._$typeIdentifier == orderTypeIdentifier) + .first() + else { + throw Abort(.notFound) + } + + return try await Self.registrations(for: order, on: req.db).map { $0.device.pushToken } } } @@ -327,11 +345,10 @@ extension OrdersServiceCustom { /// Sends push notifications for a given order. /// /// - Parameters: - /// - id: The `UUID` of the order to send the notifications for. - /// - typeIdentifier: The type identifier of the order. + /// - order: The order to send the notifications for. /// - db: The `Database` to use. - public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - let registrations = try await Self.registrationsForOrder(id: id, of: typeIdentifier, on: db) + public func sendPushNotifications(for order: O, on db: any Database) async throws { + let registrations = try await Self.registrations(for: order, on: db) for reg in registrations { let backgroundNotification = APNSBackgroundNotification( expiration: .immediately, @@ -350,16 +367,7 @@ extension OrdersServiceCustom { } } - /// Sends push notifications for a given order. - /// - /// - Parameters: - /// - order: The order to send the notifications for. - /// - db: The `Database` to use. - public func sendPushNotifications(for order: O, on db: any Database) async throws { - try await sendPushNotificationsForOrder(id: order.requireID(), of: order.typeIdentifier, on: db) - } - - private static func registrationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrations(for order: O, on db: any Database) async throws -> [R] { // This could be done by enforcing the caller to have a Siblings property wrapper, // but there's not really any value to forcing that on them when we can just do the query ourselves like this. try await R.query(on: db) @@ -367,8 +375,8 @@ extension OrdersServiceCustom { .join(parent: \._$device) .with(\._$order) .with(\._$device) - .filter(O.self, \._$typeIdentifier == typeIdentifier) - .filter(O.self, \._$id == id) + .filter(O.self, \._$typeIdentifier == order._$typeIdentifier.value!) + .filter(O.self, \._$id == order.requireID()) .all() } } diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift index 74c6040..4243277 100644 --- a/Sources/Passes/PassesService.swift +++ b/Sources/Passes/PassesService.swift @@ -106,16 +106,6 @@ public final class PassesService: Sendable { migrations.add(PassesErrorLog()) } - /// Sends push notifications for a given pass. - /// - /// - Parameters: - /// - id: The `UUID` of the pass to send the notifications for. - /// - typeIdentifier: The type identifier of the pass. - /// - db: The `Database` to use. - public func sendPushNotificationsForPass(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - try await service.sendPushNotificationsForPass(id: id, of: typeIdentifier, on: db) - } - /// Sends push notifications for a given pass. /// /// - Parameters: diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 5500c8b..8d14a18 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -351,7 +351,16 @@ extension PassesServiceCustom { } let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! - try await sendPushNotificationsForPass(id: id, of: passTypeIdentifier, on: req.db) + guard + let pass = try await P.query(on: req.db) + .filter(\._$id == id) + .filter(\._$typeIdentifier == passTypeIdentifier) + .first() + else { + throw Abort(.notFound) + } + + try await sendPushNotifications(for: pass, on: req.db) return .noContent } @@ -363,7 +372,16 @@ extension PassesServiceCustom { } let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! - return try await Self.registrationsForPass(id: id, of: passTypeIdentifier, on: req.db).map { $0.device.pushToken } + guard + let pass = try await P.query(on: req.db) + .filter(\._$id == id) + .filter(\._$typeIdentifier == passTypeIdentifier) + .first() + else { + throw Abort(.notFound) + } + + return try await Self.registrations(for: pass, on: req.db).map { $0.device.pushToken } } } @@ -372,11 +390,10 @@ extension PassesServiceCustom { /// Sends push notifications for a given pass. /// /// - Parameters: - /// - id: The `UUID` of the pass to send the notifications for. - /// - typeIdentifier: The type identifier of the pass. + /// - pass: The pass to send the notifications for. /// - db: The `Database` to use. - public func sendPushNotificationsForPass(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - let registrations = try await Self.registrationsForPass(id: id, of: typeIdentifier, on: db) + public func sendPushNotifications(for pass: P, on db: any Database) async throws { + let registrations = try await Self.registrations(for: pass, on: db) for reg in registrations { let backgroundNotification = APNSBackgroundNotification( expiration: .immediately, @@ -395,16 +412,7 @@ extension PassesServiceCustom { } } - /// Sends push notifications for a given pass. - /// - /// - Parameters: - /// - pass: The pass to send the notifications for. - /// - db: The `Database` to use. - public func sendPushNotifications(for pass: P, on db: any Database) async throws { - try await sendPushNotificationsForPass(id: pass.requireID(), of: pass.typeIdentifier, on: db) - } - - private static func registrationsForPass(id: UUID, of typeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrations(for pass: P, on db: any Database) async throws -> [R] { // This could be done by enforcing the caller to have a Siblings property wrapper, // but there's not really any value to forcing that on them when we can just do the query ourselves like this. try await R.query(on: db) @@ -412,8 +420,8 @@ extension PassesServiceCustom { .join(parent: \._$device) .with(\._$pass) .with(\._$device) - .filter(P.self, \._$typeIdentifier == typeIdentifier) - .filter(P.self, \._$id == id) + .filter(P.self, \._$typeIdentifier == pass._$typeIdentifier.value!) + .filter(P.self, \._$id == pass.requireID()) .all() } } diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index 9b843e3..ab61f1b 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -327,7 +327,7 @@ struct OrdersTests { try await orderData.create(on: app.db) let order = try await orderData._$order.get(on: app.db) - try await ordersService.sendPushNotificationsForOrder(id: order.requireID(), of: order.typeIdentifier, on: app.db) + try await ordersService.sendPushNotifications(for: order, on: app.db) let deviceLibraryIdentifier = "abcdefg" let pushToken = "1234567890" diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 34848cf..fc6b7fa 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -465,7 +465,7 @@ struct PassesTests { try await passData.create(on: app.db) let pass = try await passData._$pass.get(on: app.db) - try await passesService.sendPushNotificationsForPass(id: pass.requireID(), of: pass.typeIdentifier, on: app.db) + try await passesService.sendPushNotifications(for: pass, on: app.db) let deviceLibraryIdentifier = "abcdefg" let pushToken = "1234567890" From 00a2cd4bf48d485d54a294b772c181d35e40c5b2 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 5 Nov 2024 15:50:52 +0100 Subject: [PATCH 4/7] Update DocC --- Sources/Orders/Orders.docc/Extensions/OrdersService.md | 1 - Sources/Passes/Passes.docc/Extensions/PassesService.md | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/Orders/Orders.docc/Extensions/OrdersService.md b/Sources/Orders/Orders.docc/Extensions/OrdersService.md index 937c328..5716063 100644 --- a/Sources/Orders/Orders.docc/Extensions/OrdersService.md +++ b/Sources/Orders/Orders.docc/Extensions/OrdersService.md @@ -10,4 +10,3 @@ ### Push Notifications - ``sendPushNotifications(for:on:)`` -- ``sendPushNotificationsForOrder(id:of:on:)`` diff --git a/Sources/Passes/Passes.docc/Extensions/PassesService.md b/Sources/Passes/Passes.docc/Extensions/PassesService.md index 93f9f2a..6073ecd 100644 --- a/Sources/Passes/Passes.docc/Extensions/PassesService.md +++ b/Sources/Passes/Passes.docc/Extensions/PassesService.md @@ -11,4 +11,3 @@ ### Push Notifications - ``sendPushNotifications(for:on:)`` -- ``sendPushNotificationsForPass(id:of:on:)`` From 0c7ea5aba05591625cae9d37f5d4a8623f9467ae Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 7 Nov 2024 12:43:54 +0100 Subject: [PATCH 5/7] Update Zip --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2a5323a..4bfb12f 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( .package(url: "https://github.com/vapor/vapor.git", from: "4.106.1"), .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"), .package(url: "https://github.com/vapor/apns.git", from: "4.2.0"), - .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.3"), + .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.4"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.6.1"), // used in tests .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"), From bb90049b41ccf332e64c66067baf41d412bc4586 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 30 Nov 2024 11:54:19 +0100 Subject: [PATCH 6/7] Rename `openSSLURL` to `openSSLPath` --- Sources/Orders/OrdersService.swift | 6 +++--- Sources/Orders/OrdersServiceCustom.swift | 10 +++++++--- Sources/Passes/PassesService.swift | 6 +++--- Sources/Passes/PassesServiceCustom.swift | 6 +++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift index b6f944c..e915472 100644 --- a/Sources/Orders/OrdersService.swift +++ b/Sources/Orders/OrdersService.swift @@ -23,7 +23,7 @@ public final class OrdersService: Sendable { /// - pemCertificate: The PEM Certificate for signing orders. /// - pemPrivateKey: The PEM Certificate's private key for signing orders. /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - openSSLURL: The location of the `openssl` command as a file path. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any OrdersDelegate, @@ -33,7 +33,7 @@ public final class OrdersService: Sendable { pemCertificate: String, pemPrivateKey: String, pemPrivateKeyPassword: String? = nil, - openSSLURL: String = "/usr/bin/openssl" + openSSLPath: String = "/usr/bin/openssl" ) throws { self.service = try .init( app: app, @@ -44,7 +44,7 @@ public final class OrdersService: Sendable { pemCertificate: pemCertificate, pemPrivateKey: pemPrivateKey, pemPrivateKeyPassword: pemPrivateKeyPassword, - openSSLURL: openSSLURL + openSSLPath: openSSLPath ) } diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 89b16ad..c6e50f9 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -47,7 +47,7 @@ where O == R.OrderType, D == R.DeviceType { /// - pemCertificate: The PEM Certificate for signing orders. /// - pemPrivateKey: The PEM Certificate's private key for signing orders. /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - openSSLURL: The location of the `openssl` command as a file path. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any OrdersDelegate, @@ -57,7 +57,7 @@ where O == R.OrderType, D == R.DeviceType { pemCertificate: String, pemPrivateKey: String, pemPrivateKeyPassword: String? = nil, - openSSLURL: String = "/usr/bin/openssl" + openSSLPath: String = "/usr/bin/openssl" ) throws { self.app = app self.delegate = delegate @@ -67,7 +67,7 @@ where O == R.OrderType, D == R.DeviceType { self.pemCertificate = pemCertificate self.pemPrivateKey = pemPrivateKey self.pemPrivateKeyPassword = pemPrivateKeyPassword - self.openSSLURL = URL(fileURLWithPath: openSSLURL) + self.openSSLURL = URL(fileURLWithPath: openSSLPath) let privateKeyBytes = pemPrivateKey.data(using: .utf8)!.map { UInt8($0) } let certificateBytes = pemCertificate.data(using: .utf8)!.map { UInt8($0) } @@ -403,6 +403,10 @@ extension OrdersServiceCustom { private func signature(for manifest: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. if let pemPrivateKeyPassword { + guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { + throw OrdersError.noOpenSSLExecutable + } + let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift index 4243277..d32f433 100644 --- a/Sources/Passes/PassesService.swift +++ b/Sources/Passes/PassesService.swift @@ -44,7 +44,7 @@ public final class PassesService: Sendable { /// - pemCertificate: The PEM Certificate for signing passes. /// - pemPrivateKey: The PEM Certificate's private key for signing passes. /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - openSSLURL: The location of the `openssl` command as a file path. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any PassesDelegate, @@ -54,7 +54,7 @@ public final class PassesService: Sendable { pemCertificate: String, pemPrivateKey: String, pemPrivateKeyPassword: String? = nil, - openSSLURL: String = "/usr/bin/openssl" + openSSLPath: String = "/usr/bin/openssl" ) throws { self.service = try .init( app: app, @@ -65,7 +65,7 @@ public final class PassesService: Sendable { pemCertificate: pemCertificate, pemPrivateKey: pemPrivateKey, pemPrivateKeyPassword: pemPrivateKeyPassword, - openSSLURL: openSSLURL + openSSLPath: openSSLPath ) } diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 8d14a18..34388e6 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -48,7 +48,7 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { /// - pemCertificate: The PEM Certificate for signing passes. /// - pemPrivateKey: The PEM Certificate's private key for signing passes. /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - openSSLURL: The location of the `openssl` command as a file path. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any PassesDelegate, @@ -58,7 +58,7 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { pemCertificate: String, pemPrivateKey: String, pemPrivateKeyPassword: String? = nil, - openSSLURL: String = "/usr/bin/openssl" + openSSLPath: String = "/usr/bin/openssl" ) throws { self.app = app self.delegate = delegate @@ -68,7 +68,7 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { self.pemCertificate = pemCertificate self.pemPrivateKey = pemPrivateKey self.pemPrivateKeyPassword = pemPrivateKeyPassword - self.openSSLURL = URL(fileURLWithPath: openSSLURL) + self.openSSLURL = URL(fileURLWithPath: openSSLPath) let privateKeyBytes = pemPrivateKey.data(using: .utf8)!.map { UInt8($0) } let certificateBytes = pemCertificate.data(using: .utf8)!.map { UInt8($0) } From 1ea069cadc25cfb5d0cdb52b36027aecb3e27c85 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 1 Dec 2024 17:18:42 +0100 Subject: [PATCH 7/7] Unify error types --- Sources/Orders/Orders.docc/Orders.md | 4 -- Sources/Orders/OrdersError.swift | 62 ------------------- Sources/Orders/OrdersServiceCustom.swift | 4 +- .../WalletError.swift} | 16 ++--- Sources/Passes/Passes.docc/Passes.md | 4 -- Sources/Passes/PassesServiceCustom.swift | 6 +- Tests/OrdersTests/OrdersTests.swift | 6 -- Tests/PassesTests/PassesTests.swift | 15 +++-- 8 files changed, 22 insertions(+), 95 deletions(-) delete mode 100644 Sources/Orders/OrdersError.swift rename Sources/{Passes/PassesError.swift => PassKit/WalletError.swift} (80%) diff --git a/Sources/Orders/Orders.docc/Orders.md b/Sources/Orders/Orders.docc/Orders.md index 9311362..952a12b 100644 --- a/Sources/Orders/Orders.docc/Orders.md +++ b/Sources/Orders/Orders.docc/Orders.md @@ -34,7 +34,3 @@ For information on Apple Wallet orders, see the [Apple Developer Documentation]( - ``OrderModel`` - ``OrdersRegistrationModel`` - ``OrderDataModel`` - -### Errors - -- ``OrdersError`` \ No newline at end of file diff --git a/Sources/Orders/OrdersError.swift b/Sources/Orders/OrdersError.swift deleted file mode 100644 index eee43c7..0000000 --- a/Sources/Orders/OrdersError.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// OrdersError.swift -// PassKit -// -// Created by Francesco Paolo Severino on 04/07/24. -// - -/// Errors that can be thrown by Apple Wallet orders. -public struct OrdersError: Error, Sendable { - /// The type of the errors that can be thrown by Apple Wallet orders. - public struct ErrorType: Sendable, Hashable, CustomStringConvertible { - enum Base: String, Sendable { - case noSourceFiles - case noOpenSSLExecutable - } - - let base: Base - - private init(_ base: Base) { - self.base = base - } - - /// The path for the source files is not a directory. - public static let noSourceFiles = Self(.noSourceFiles) - /// The `openssl` executable is missing. - public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable) - - /// A textual representation of this error. - public var description: String { - base.rawValue - } - } - - private struct Backing: Sendable { - fileprivate let errorType: ErrorType - - init(errorType: ErrorType) { - self.errorType = errorType - } - } - - private var backing: Backing - - /// The type of this error. - public var errorType: ErrorType { backing.errorType } - - private init(errorType: ErrorType) { - self.backing = .init(errorType: errorType) - } - - /// The path for the source files is not a directory. - public static let noSourceFiles = Self(errorType: .noSourceFiles) - - /// The `openssl` executable is missing. - public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable) -} - -extension OrdersError: CustomStringConvertible { - public var description: String { - "OrdersError(errorType: \(self.errorType))" - } -} diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index c6e50f9..3dde8f0 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -404,7 +404,7 @@ extension OrdersServiceCustom { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. if let pemPrivateKeyPassword { guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { - throw OrdersError.noOpenSSLExecutable + throw WalletError.noOpenSSLExecutable } let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -460,7 +460,7 @@ extension OrdersServiceCustom { guard (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { - throw OrdersError.noSourceFiles + throw WalletError.noSourceFiles } let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) diff --git a/Sources/Passes/PassesError.swift b/Sources/PassKit/WalletError.swift similarity index 80% rename from Sources/Passes/PassesError.swift rename to Sources/PassKit/WalletError.swift index e93072b..6209a7a 100644 --- a/Sources/Passes/PassesError.swift +++ b/Sources/PassKit/WalletError.swift @@ -1,13 +1,13 @@ // -// PassesError.swift +// WalletError.swift // PassKit // // Created by Francesco Paolo Severino on 04/07/24. // -/// Errors that can be thrown by PassKit passes. -public struct PassesError: Error, Sendable, Equatable { - /// The type of the errors that can be thrown by PassKit passes. +/// Errors that can be thrown by Apple Wallet passes and orders. +public struct WalletError: Error, Sendable, Equatable { + /// The type of the errors that can be thrown by Apple Wallet passes and orders. public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { enum Base: String, Sendable, Equatable { case noSourceFiles @@ -41,7 +41,7 @@ public struct PassesError: Error, Sendable, Equatable { self.errorType = errorType } - static func == (lhs: PassesError.Backing, rhs: PassesError.Backing) -> Bool { + static func == (lhs: WalletError.Backing, rhs: WalletError.Backing) -> Bool { lhs.errorType == rhs.errorType } } @@ -64,13 +64,13 @@ public struct PassesError: Error, Sendable, Equatable { /// The number of passes to bundle is invalid. public static let invalidNumberOfPasses = Self(errorType: .invalidNumberOfPasses) - public static func == (lhs: PassesError, rhs: PassesError) -> Bool { + public static func == (lhs: WalletError, rhs: WalletError) -> Bool { lhs.backing == rhs.backing } } -extension PassesError: CustomStringConvertible { +extension WalletError: CustomStringConvertible { public var description: String { - "PassesError(errorType: \(self.errorType))" + "WalletError(errorType: \(self.errorType))" } } diff --git a/Sources/Passes/Passes.docc/Passes.md b/Sources/Passes/Passes.docc/Passes.md index 19d5cab..3d3ed98 100644 --- a/Sources/Passes/Passes.docc/Passes.md +++ b/Sources/Passes/Passes.docc/Passes.md @@ -43,10 +43,6 @@ For information on Apple Wallet passes, see the [Apple Developer Documentation]( - ``PassesRegistrationModel`` - ``PassDataModel`` -### Errors - -- ``PassesError`` - ### Personalized Passes (⚠️ WIP) - diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 34388e6..6c6fb31 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -450,7 +450,7 @@ extension PassesServiceCustom { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. if let pemPrivateKeyPassword { guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { - throw PassesError.noOpenSSLExecutable + throw WalletError.noOpenSSLExecutable } let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -506,7 +506,7 @@ extension PassesServiceCustom { guard (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { - throw PassesError.noSourceFiles + throw WalletError.noSourceFiles } let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -558,7 +558,7 @@ extension PassesServiceCustom { /// - Returns: The bundle of passes as `Data`. public func build(passes: [P], on db: any Database) async throws -> Data { guard passes.count > 1 && passes.count <= 10 else { - throw PassesError.invalidNumberOfPasses + throw WalletError.invalidNumberOfPasses } var files: [ArchiveFile] = [] diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index ab61f1b..4d41358 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -393,10 +393,4 @@ struct OrdersTests { } } } - - @Test("OrdersError") - func ordersError() { - #expect(OrdersError.noSourceFiles.description == "OrdersError(errorType: noSourceFiles)") - #expect(OrdersError.noOpenSSLExecutable.description == "OrdersError(errorType: noOpenSSLExecutable)") - } } diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index fc6b7fa..9bc3637 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -66,7 +66,7 @@ struct PassesTests { do { let data = try await passesService.build(passes: [pass1], on: app.db) Issue.record("Expected error, got \(data)") - } catch let error as PassesError { + } catch let error as WalletError { #expect(error == .invalidNumberOfPasses) } } @@ -532,11 +532,14 @@ struct PassesTests { } } - @Test("PassesError") - func passesError() { - #expect(PassesError.noSourceFiles.description == "PassesError(errorType: noSourceFiles)") - #expect(PassesError.noOpenSSLExecutable.description == "PassesError(errorType: noOpenSSLExecutable)") - #expect(PassesError.invalidNumberOfPasses.description == "PassesError(errorType: invalidNumberOfPasses)") + @Test("WalletError") + func walletError() { + #expect(WalletError.noSourceFiles.description == "WalletError(errorType: noSourceFiles)") + #expect(WalletError.noOpenSSLExecutable.description == "WalletError(errorType: noOpenSSLExecutable)") + #expect(WalletError.invalidNumberOfPasses.description == "WalletError(errorType: invalidNumberOfPasses)") + + #expect(WalletError.noSourceFiles == WalletError.noSourceFiles) + #expect(WalletError.noOpenSSLExecutable != WalletError.invalidNumberOfPasses) } @Test("Default PassesDelegate Properties")