diff --git a/README.md b/README.md index 7b8703d..2903f0a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Please check the [releases](https://github.com/mochidev/swift-webpush/releases) dependencies: [ .package( url: "https://github.com/mochidev/swift-webpush.git", - .upToNextMinor(from: "0.3.3") + .upToNextMinor(from: "0.4.0") ), ], ... @@ -373,20 +373,73 @@ Your service worker will receive this message, decode it, and present it to the ### Testing -The `WebPushTesting` module can be used to obtain a mocked `WebPushManager` instance that allows you to capture all messages that are sent out, or throw your own errors to validate your code functions appropriately. Only import `WebPushTesting` in your testing targets. +The `WebPushTesting` module can be used to obtain a mocked `WebPushManager` instance that allows you to capture all messages that are sent out, or throw your own errors to validate your code functions appropriately. +> [!IMPORTANT] +> Only import `WebPushTesting` in your testing targets. + +```swift +import Testing +import WebPushTesting + +@Test func sendSuccessfulNotifications() async throws { + try await confirmation { requestWasMade in + let mockedManager = WebPushManager.makeMockedManager { message, subscriber, topic, expiration, urgency in + #expect(message.string == "hello") + #expect(subscriber.endpoint.absoluteString == "https://example.com/expectedSubscriber") + #expect(subscriber.vapidKeyID == .mockedKeyID1) + #expect(topic == nil) + #expect(expiration == .recommendedMaximum) + #expect(urgency == .high) + requestWasMade() + } + + let myController = MyController(pushManager: mockedManager) + try await myController.sendNotifications() + } +} + +@Test func catchBadSubscriptions() async throws { + /// Mocked managers accept multiple handlers, and will cycle through them each time a push message is sent: + let mockedManager = WebPushManager.makeMockedManager(messageHandlers: + { _, _, _, _, _ in throw BadSubscriberError() }, + { _, _, _, _, _ in }, + { _, _, _, _, _ in throw BadSubscriberError() }, + ) + + let myController = MyController(pushManager: mockedManager) + #expect(myController.subscribers.count == 3) + try await myController.sendNotifications() + #expect(myController.subscribers.count == 1) +} +``` ## Specifications -- [RFC 7515 JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515) -- [RFC 7519 JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) -- [RFC 8030 Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030) -- [RFC 8188 Encrypted Content-Encoding for HTTP](https://datatracker.ietf.org/doc/html/rfc8188) -- [RFC 8291 Message Encryption for Web Push](https://datatracker.ietf.org/doc/html/rfc8291) -- [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) +- [RFC 6454 — The Web Origin Concept](https://datatracker.ietf.org/doc/html/rfc6454) +- [RFC 7515 — JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515) +- [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) +- [RFC 8030 — Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030) +- [RFC 8188 — Encrypted Content-Encoding for HTTP](https://datatracker.ietf.org/doc/html/rfc8188) +- [RFC 8291 — Message Encryption for Web Push](https://datatracker.ietf.org/doc/html/rfc8291) +- [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) + - [Push API Working Draft](https://www.w3.org/TR/push-api/) + +## Other Resources + +- [Apple Developer — Sending web push notifications in web apps and browsers](https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers) +- [WWDC22 — Meet Web Push for Safari](https://developer.apple.com/videos/play/wwdc2022/10098/) +- [WebKit — Meet Web Push](https://webkit.org/blog/12945/meet-web-push/) +- [WebKit — Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/) +- [MDN — Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) +- [MDN — Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) +- [web.dev — The Web Push Protocol](https://web.dev/articles/push-notifications-web-push-protocol) +- [Sample Code — ServiceWorker Cookbook](https://github.com/mdn/serviceworker-cookbook/tree/master/push-simple) +- [Web Push: Data Encryption Test Page](https://mozilla-services.github.io/WebPushDataTestPage/) + ## Contributing Contribution is welcome! Please take a look at the issues already available, or start a new discussion to propose a new feature. Although guarantees can't be made regarding feature requests, PRs that fit within the goals of the project and that have been discussed beforehand are more than welcome! diff --git a/Sources/WebPush/Errors/PushServiceError.swift b/Sources/WebPush/Errors/PushServiceError.swift index e6b76f8..a4fda6b 100644 --- a/Sources/WebPush/Errors/PushServiceError.swift +++ b/Sources/WebPush/Errors/PushServiceError.swift @@ -11,8 +11,8 @@ import Foundation /// An unknown Push Service error was encountered. /// -/// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030) -/// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) +/// - SeeAlso: [RFC 8030 — Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030) +/// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) /// - SeeAlso: [Sending web push notifications in web apps and browsers — Review responses for push notification errors](https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers#Review-responses-for-push-notification-errors) public struct PushServiceError: LocalizedError, Sendable { /// The HTTP response that was returned from the push service.. diff --git a/Sources/WebPush/Helpers/URL+Origin.swift b/Sources/WebPush/Helpers/URL+Origin.swift index 612c541..3a6e645 100644 --- a/Sources/WebPush/Helpers/URL+Origin.swift +++ b/Sources/WebPush/Helpers/URL+Origin.swift @@ -13,8 +13,8 @@ extension URL { /// /// This implementation is similar to the [WHATWG Standard](https://url.spec.whatwg.org/#concept-url-origin), except that it uses the unicode form of the host, and is limited to HTTP and HTTPS schemas. /// - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) - /// - SeeAlso: [RFC6454 The Web Origin Concept §6.1. Unicode Serialization of an Origin](https://datatracker.ietf.org/doc/html/rfc6454#section-6.1) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) + /// - SeeAlso: [RFC 6454 — The Web Origin Concept §6.1. Unicode Serialization of an Origin](https://datatracker.ietf.org/doc/html/rfc6454#section-6.1) var origin: String { /// Note that we need the unicode variant, which only URLComponents provides. let components = URLComponents(url: self, resolvingAgainstBaseURL: true) diff --git a/Sources/WebPush/Subscriber.swift b/Sources/WebPush/Subscriber.swift index 2a6fdc5..8844608 100644 --- a/Sources/WebPush/Subscriber.swift +++ b/Sources/WebPush/Subscriber.swift @@ -31,7 +31,7 @@ public protocol SubscriberProtocol: Sendable { /// The set of cryptographic secrets shared by the browser (is. user agent) along with a subscription. /// -/// - SeeAlso: [RFC 8291 Message Encryption for Web Push §2.1. Key and Secret Distribution](https://datatracker.ietf.org/doc/html/rfc8291#section-2.1) +/// - SeeAlso: [RFC 8291 — Message Encryption for Web Push §2.1. Key and Secret Distribution](https://datatracker.ietf.org/doc/html/rfc8291#section-2.1) public struct UserAgentKeyMaterial: Sendable { /// The underlying type of an authentication secret. public typealias Salt = Data diff --git a/Sources/WebPush/Topic.swift b/Sources/WebPush/Topic.swift index c8991bc..8ffc959 100644 --- a/Sources/WebPush/Topic.swift +++ b/Sources/WebPush/Topic.swift @@ -15,7 +15,7 @@ import Foundation /// /// - Important: Since topics are sent in the clear to push services, they must be securely hashed. You must use a stable random value for this, such as the subscriber's ``UserAgentKeyMaterial/authenticationSecret``. This is fine for most applications, though you may wish to use a different key if your application requires it. /// -/// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP §5.4. Replacing Push Messages](https://datatracker.ietf.org/doc/html/rfc8030#section-5.4) +/// - SeeAlso: [RFC 8030 — Generic Event Delivery Using HTTP §5.4. Replacing Push Messages](https://datatracker.ietf.org/doc/html/rfc8030#section-5.4) public struct Topic: Hashable, Sendable, CustomStringConvertible { /// The topic value to use. public let topic: String diff --git a/Sources/WebPush/VAPID/VAPID.swift b/Sources/WebPush/VAPID/VAPID.swift index b361d4f..47a7ed2 100644 --- a/Sources/WebPush/VAPID/VAPID.swift +++ b/Sources/WebPush/VAPID/VAPID.swift @@ -13,5 +13,5 @@ public typealias VoluntaryApplicationServerIdentification = VAPID /// A set of types for Voluntary Application Server Identification, also known as VAPID. /// -/// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) +/// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) public enum VAPID: Sendable {} diff --git a/Sources/WebPush/VAPID/VAPIDConfiguration.swift b/Sources/WebPush/VAPID/VAPIDConfiguration.swift index 9cda94b..fa77ff1 100644 --- a/Sources/WebPush/VAPID/VAPIDConfiguration.swift +++ b/Sources/WebPush/VAPID/VAPIDConfiguration.swift @@ -214,7 +214,7 @@ extension VAPID.Configuration { /// This allows administrators of push services to contact you should an issue arise with your application server. /// /// - Note: Although the specification notes that this field is optional, some push services may refuse connection from serers without contact information. - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2.1. Application Server Contact Information](https://datatracker.ietf.org/doc/html/rfc8292#section-2.1) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2.1. Application Server Contact Information](https://datatracker.ietf.org/doc/html/rfc8292#section-2.1) public enum ContactInformation: Hashable, Codable, Sendable { /// A URL-based contact method, such as a support page on your website. case url(URL) diff --git a/Sources/WebPush/VAPID/VAPIDKey.swift b/Sources/WebPush/VAPID/VAPIDKey.swift index 3ca349e..29a9f3e 100644 --- a/Sources/WebPush/VAPID/VAPIDKey.swift +++ b/Sources/WebPush/VAPID/VAPIDKey.swift @@ -98,7 +98,7 @@ extension VAPID.Key: Identifiable { /// The public key component in a format suitable for user agents to consume. /// /// - SeeAlso: [Push API Working Draft §7.2. `PushSubscriptionOptions` Interface](https://www.w3.org/TR/push-api/#dom-pushsubscriptionoptions-applicationserverkey) - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §3.2. Public Key Parameter ("k")](https://datatracker.ietf.org/doc/html/rfc8292#section-3.2) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §3.2. Public Key Parameter ("k")](https://datatracker.ietf.org/doc/html/rfc8292#section-3.2) public var id: ID { ID(privateKey.publicKey.x963Representation.base64URLEncodedString()) } diff --git a/Sources/WebPush/VAPID/VAPIDToken.swift b/Sources/WebPush/VAPID/VAPIDToken.swift index 460e702..430177b 100644 --- a/Sources/WebPush/VAPID/VAPIDToken.swift +++ b/Sources/WebPush/VAPID/VAPIDToken.swift @@ -12,9 +12,9 @@ import Foundation extension VAPID { /// An internal representation the token and authorization headers used self-identification. /// - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) - /// - SeeAlso: [RFC 7515 JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515) - ///- SeeAlso: [RFC 7519 JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) + /// - SeeAlso: [RFC 7515 — JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515) + ///- SeeAlso: [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) struct Token: Hashable, Codable, Sendable { /// The coding keys used to encode the token. enum CodingKeys: String, CodingKey { @@ -26,25 +26,25 @@ extension VAPID { /// The audience claim, which encodes the origin of the ``Subscriber/endpoint`` /// /// - SeeAlso: ``/Foundation/URL/origin`` - /// - SeeAlso: [RFC 7519 JSON Web Token (JWT) §4.1.3. "aud" (Audience) Claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3) - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) + /// - SeeAlso: [RFC 7519 — JSON Web Token (JWT) §4.1.3. "aud" (Audience) Claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) var audience: String /// The subject claim, which encodes contact information for the application server. /// - /// - SeeAlso: [RFC 7519 JSON Web Token (JWT) §4.1.2. "sub" (Subject) Claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2) - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2.1. Application Server Contact Information](https://datatracker.ietf.org/doc/html/rfc8292#section-2.1) + /// - SeeAlso: [RFC 7519 — JSON Web Token (JWT) §4.1.2. "sub" (Subject) Claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2.1. Application Server Contact Information](https://datatracker.ietf.org/doc/html/rfc8292#section-2.1) var subject: Configuration.ContactInformation /// The expiry claim, which encodes the number of seconds after 1970/01/01 when the token expires. /// - /// - SeeAlso: [RFC 7519 JSON Web Token (JWT) §4.1.4. "exp" (Expiration Time) Claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) + /// - SeeAlso: [RFC 7519 — JSON Web Token (JWT) §4.1.4. "exp" (Expiration Time) Claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) var expiration: Int /// The standard header including the type and algorithm. /// - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §2. Application Server Self-Identification](https://datatracker.ietf.org/doc/html/rfc8292#section-2) static let jwtHeader = Array(#"{"typ":"JWT","alg":"ES256"}"#.utf8).base64URLEncodedString() /// Initialize a token with the specified claims. @@ -93,7 +93,7 @@ extension VAPID { self = token } - /// - SeeAlso: [RFC 7515 JSON Web Signature (JWS) §3. JSON Web Signature (JWS) Overview](https://datatracker.ietf.org/doc/html/rfc7515#section-3) + /// - SeeAlso: [RFC 7515 — JSON Web Signature (JWS) §3. JSON Web Signature (JWS) Overview](https://datatracker.ietf.org/doc/html/rfc7515#section-3) func generateJWT(signedBy signingKey: some VAPIDKeyProtocol) throws -> String { let header = Self.jwtHeader @@ -108,7 +108,7 @@ extension VAPID { /// Generate an `Authorization` header. /// - /// - SeeAlso: [RFC 8292 Voluntary Application Server Identification (VAPID) for Web Push §3. VAPID Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc8292#section-3) + /// - SeeAlso: [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push §3. VAPID Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc8292#section-3) func generateAuthorization(signedBy signingKey: some VAPIDKeyProtocol) throws -> String { let token = try generateJWT(signedBy: signingKey) let key = signingKey.id @@ -123,6 +123,6 @@ protocol VAPIDKeyProtocol: Identifiable, Sendable { associatedtype Signature: ContiguousBytes /// Returns a JWS signature for the message. - /// - SeeAlso: [RFC 7515 JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515) + /// - SeeAlso: [RFC 7515 — JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515) func signature(for message: some DataProtocol) throws -> Signature } diff --git a/Sources/WebPush/WebPushManager.swift b/Sources/WebPush/WebPushManager.swift index e7c327b..cb0ada1 100644 --- a/Sources/WebPush/WebPushManager.swift +++ b/Sources/WebPush/WebPushManager.swift @@ -637,7 +637,7 @@ public actor WebPushManager: Sendable { .withUnsafeBytes(AES.GCM.Nonce.init(data:)) /// Encrypt the padded payload into a single record. - /// - SeeAlso: [RFC 8188 Encrypted Content-Encoding for HTTP](https://datatracker.ietf.org/doc/html/rfc8188) + /// - SeeAlso: [RFC 8188 — Encrypted Content-Encoding for HTTP](https://datatracker.ietf.org/doc/html/rfc8188) let encryptedRecord = try AES.GCM.seal(paddedPayload, using: contentEncryptionKey, nonce: nonce) /// Attach the header with our public key and salt, along with the authentication tag. @@ -784,7 +784,7 @@ extension WebPushManager: Service { extension WebPushManager { /// A duration in seconds used to express when push messages will expire. /// - /// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP §5.2. Push Message Time-To-Live](https://datatracker.ietf.org/doc/html/rfc8030#section-5.2) + /// - SeeAlso: [RFC 8030 — Generic Event Delivery Using HTTP §5.2. Push Message Time-To-Live](https://datatracker.ietf.org/doc/html/rfc8030#section-5.2) public struct Expiration: Hashable, Comparable, Codable, ExpressibleByIntegerLiteral, AdditiveArithmetic, Sendable { /// The number of seconds represented by this expiration. public let seconds: Int @@ -800,7 +800,7 @@ extension WebPushManager { /// /// If the user agent is unavailable, a push message with a zero TTL expires and is never delivered. /// - /// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP §5.2. Push Message Time-To-Live](https://datatracker.ietf.org/doc/html/rfc8030#section-5.2) + /// - SeeAlso: [RFC 8030 — Generic Event Delivery Using HTTP §5.2. Push Message Time-To-Live](https://datatracker.ietf.org/doc/html/rfc8030#section-5.2) public static let dropIfUndeliverable: Self = .zero /// Initialize an expiration with a number of seconds. @@ -868,7 +868,7 @@ extension WebPushManager { extension WebPushManager { /// The urgency with which to deliver a push message. /// - /// - SeeAlso: [RFC 8030 Generic Event Delivery Using HTTP §5.3. Push Message Urgency](https://datatracker.ietf.org/doc/html/rfc8030#section-5.3) + /// - SeeAlso: [RFC 8030 — Generic Event Delivery Using HTTP §5.3. Push Message Urgency](https://datatracker.ietf.org/doc/html/rfc8030#section-5.3) public struct Urgency: Hashable, Comparable, Sendable, CustomStringConvertible { /// The internal raw value that is encoded in this type's place when calling ``description``. let rawValue: String