Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation Completeness #52

Merged
merged 4 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
),
],
...
Expand Down Expand Up @@ -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!
Expand Down
4 changes: 2 additions & 2 deletions Sources/WebPush/Errors/PushServiceError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..
Expand Down
4 changes: 2 additions & 2 deletions Sources/WebPush/Helpers/URL+Origin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebPush/Subscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebPush/Topic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebPush/VAPID/VAPID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
2 changes: 1 addition & 1 deletion Sources/WebPush/VAPID/VAPIDConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebPush/VAPID/VAPIDKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
26 changes: 13 additions & 13 deletions Sources/WebPush/VAPID/VAPIDToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
}
8 changes: 4 additions & 4 deletions Sources/WebPush/WebPushManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading