{"id":22821932,"url":"https://github.com/mochidev/swift-webpush","last_synced_at":"2025-04-14T01:55:13.342Z","repository":{"id":266829417,"uuid":"897836668","full_name":"mochidev/swift-webpush","owner":"mochidev","description":"WebPush server implementation in Swift","archived":false,"fork":false,"pushed_at":"2025-03-02T12:26:53.000Z","size":155,"stargazers_count":41,"open_issues_count":8,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-14T01:55:04.195Z","etag":null,"topics":["swift","swift-6","swift-server","webpush"],"latest_commit_sha":null,"homepage":"https://swiftpackageindex.com/mochidev/swift-webpush/documentation","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mochidev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-03T10:23:24.000Z","updated_at":"2025-03-27T19:18:06.000Z","dependencies_parsed_at":"2025-03-01T09:35:00.041Z","dependency_job_id":null,"html_url":"https://github.com/mochidev/swift-webpush","commit_stats":null,"previous_names":["mochidev/swift-webpush"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mochidev%2Fswift-webpush","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mochidev%2Fswift-webpush/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mochidev%2Fswift-webpush/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mochidev%2Fswift-webpush/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mochidev","download_url":"https://codeload.github.com/mochidev/swift-webpush/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248809032,"owners_count":21164895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["swift","swift-6","swift-server","webpush"],"created_at":"2024-12-12T16:09:48.785Z","updated_at":"2025-04-14T01:55:13.327Z","avatar_url":"https://github.com/mochidev.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Swift WebPush\n\n\u003cp align=\"center\"\u003e\n    \u003ca href=\"https://swiftpackageindex.com/mochidev/swift-webpush\"\u003e\n        \u003cimg src=\"https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmochidev%2Fswift-webpush%2Fbadge%3Ftype%3Dswift-versions\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://swiftpackageindex.com/mochidev/swift-webpush\"\u003e\n        \u003cimg src=\"https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmochidev%2Fswift-webpush%2Fbadge%3Ftype%3Dplatforms\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://github.com/mochidev/swift-webpush/actions?query=workflow%3A%22Test+WebPush%22\"\u003e\n        \u003cimg src=\"https://github.com/mochidev/swift-webpush/workflows/Test%20WebPush/badge.svg\" alt=\"Test Status\" /\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\nA server-side Swift implementation of the WebPush standard.\n\n## Quick Links\n\n- [Documentation](https://swiftpackageindex.com/mochidev/swift-webpush/documentation)\n- [Symbol Exploration](https://swiftinit.org/docs/mochidev.swift-webpush)\n- [Updates on Mastodon](https://mastodon.social/tags/SwiftWebPush)\n\n## Installation\n\nAdd `WebPush` as a dependency in your `Package.swift` file to start using it. Then, add `import WebPush` to any file you wish to use the library in.\n\nPlease check the [releases](https://github.com/mochidev/swift-webpush/releases) for recommended versions.\n\n```swift\ndependencies: [\n    .package(\n        url: \"https://github.com/mochidev/swift-webpush.git\", \n        .upToNextMinor(from: \"0.4.1\")\n    ),\n],\n...\ntargets: [\n    .target(\n        name: \"MyPackage\",\n        dependencies: [\n            .product(name: \"WebPush\", package: \"swift-webpush\"),\n            ...\n        ]\n    ),\n    .testTarget(\n        name: \"MyPackageTests\",\n        dependencies: [\n            .product(name: \"WebPushTesting\", package: \"swift-webpush\"),\n            ...\n        ]\n    ),\n]\n```\n\n## Usage\n\n### Terminology and Core Concepts\n\nIf you are unfamiliar with the WebPush standard, we suggest you first familiarize yourself with the following core concepts:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSubscriber\u003c/strong\u003e\u003c/summary\u003e\n\nA **Subscriber** represents a device that has opted in to receive push messages from your service. \n\n\u003e [!IMPORTANT]\n\u003e A subscriber should not be conflated with a user — a single user may be logged in on multiple devices, while a subscriber may be shared by multiple users on a single device. It is up to you to manage this complexity and ensure user information remains secure across session boundaries by registering, unregistering, and updating the subscriber when a user logs in or out.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eApplication Server\u003c/strong\u003e\u003c/summary\u003e\n\nThe **Application Server** is a server you run to manage subscriptions and send push notifications. The actual servers that perform these roles may be different, but they must all use the same VAPID keys to function correctly.\n\n\u003e [!CAUTION]\n\u003e Using a VAPID key that wasn't registered with a subscription \u003cstrong\u003ewill\u003c/strong\u003e result in push messages failing to reach their subscribers.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eVAPID Key\u003c/strong\u003e\u003c/summary\u003e\n\n**VAPID**, or _Voluntary Application Server Identification_, describes a standard for letting your application server introduce itself at time of subscription registration so that the subscription returned back to you may only be used by your service, and can't be shared with other unrelated services.\n\nThis is made possible by generating a VAPID key pair to represent your server with. At time of registration, the public key is shared with the browser, and the subscription that is returned is locked to this key. When sending a push message, the private key is used to identify your application server to the push service so that it knows who you are before forwarding messages to subscribers.\n\n\u003e [!CAUTION]\n\u003e It is important to note that you should strive to use the same key for as long as possible for a given subscriber — you won't be able to send messages to existing subscribers if you ever regenerate this key, so keep it secure!\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ePush Service\u003c/strong\u003e\u003c/summary\u003e\n\nA **Push Service** is run by browsers to coordinate delivery of messages to subscribers on your behalf.\n\n\u003c/details\u003e\n\n\n### Generating Keys\n\nBefore integrating WebPush into your server, you must generate one time VAPID keys to identify your server to push services with. To help we this, we provide `vapid-key-generator`, which you can install and use as needed:\n```zsh\n% git clone https://github.com/mochidev/swift-webpush.git\n% cd swift-webpush/vapid-key-generator\n% swift package experimental-install\n```\n\nTo uninstall the generator:\n```zsh\n% swift package experimental-uninstall vapid-key-generator\n```\n\nTo update the generator, uninstall it and re-install it after pulling from main:\n```zsh\n% swift package experimental-uninstall vapid-key-generator\n% swift package experimental-install\n```\n\nOnce installed, a new configuration can be generated as needed. Here, we generate a configuration with `https://example.com` as our support URL for push service administrators to use to contact us when issues occur:\n```\n% ~/.swiftpm/bin/vapid-key-generator https://example.com\nVAPID.Configuration: {\"contactInformation\":\"https://example.com\",\"expirationDuration\":79200,\"primaryKey\":\"6PSSAJiMj7uOvtE4ymNo5GWcZbT226c5KlV6c+8fx5g=\",\"validityDuration\":72000}\n\n\nExample Usage:\n    // TODO: Load this data from .env or from file system\n    let configurationData = Data(#\" {\"contactInformation\":\"https://example.com\",\"expirationDuration\":79200,\"primaryKey\":\"6PSSAJiMj7uOvtE4ymNo5GWcZbT226c5KlV6c+8fx5g=\",\"validityDuration\":72000} \"#.utf8)\n    let vapidConfiguration = try JSONDecoder().decode(VAPID.Configuration.self, from: configurationData)\n```\n\nOnce generated, the configuration JSON should be added to your deployment's `.env` and kept secure so it can be accessed at runtime by your application server, and _only_ by your application server. Make sure this key does not leak and is not stored alongside subscriber information.\n\n\u003e [!NOTE]\n\u003e You can specify either a support URL or an email for administrators of push services to contact you with if problems are encountered, or you can generate keys only if you prefer to configure contact information at runtime:\n\n```zsh\n% ~/.swiftpm/bin/vapid-key-generator -h\nOVERVIEW: Generate VAPID Keys.\n\nGenerates VAPID keys and configurations suitable for use on your server. Keys should generally only be generated once\nand kept secure.\n\nUSAGE: vapid-key-generator \u003csupport-url\u003e\n       vapid-key-generator --email \u003cemail\u003e\n       vapid-key-generator --key-only\n\nARGUMENTS:\n  \u003csupport-url\u003e           The fully-qualified HTTPS support URL administrators of push services may contact you at:\n                          https://example.com/support\n\nOPTIONS:\n  -k, --key-only          Only generate a VAPID key.\n  -s, --silent            Output raw JSON only so this tool can be piped with others in scripts.\n  -p, --pretty            Output JSON with spacing. Has no effect when generating keys only.\n  --email \u003cemail\u003e         Parse the input as an email address.\n  -h, --help              Show help information.\n```\n\n\u003e [!IMPORTANT]\n\u003e If you only need to change the contact information, you can do so in the JSON directly — a key should _not_ be regenerated when doing this as it will invalidate all existing subscriptions.\n\n\u003e [!TIP]\n\u003e If you prefer, you can also generate keys in your own code by calling `VAPID.Key()`, but remember, the key should be persisted and re-used from that point forward!\n\n\n### Setup\n\nDuring the setup stage of your application server, decode the VAPID configuration you created above and initialize a `WebPushManager` with it:\n\n```swift\nimport WebPush\n\n...\n\nguard\n    let rawVAPIDConfiguration = ProcessInfo.processInfo.environment[\"VAPID-CONFIG\"],\n    let vapidConfiguration = try? JSONDecoder().decode(VAPID.Configuration.self, from: Data(rawVAPIDConfiguration.utf8))\nelse { fatalError(\"VAPID keys are unavailable, please generate one and add it to the environment.\") }\n\nlet manager = WebPushManager(\n    vapidConfiguration: vapidConfiguration,\n    backgroundActivityLogger: logger\n    /// If you customized the event loop group your app uses, you can set it here:\n    // eventLoopGroupProvider: .shared(app.eventLoopGroup)\n)\n\ntry await ServiceGroup(\n    services: [\n        /// Your other services here\n        manager\n    ],\n    gracefulShutdownSignals: [.sigint],\n    logger: logger\n).run()\n```\n\nIf you are not yet using [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle), you can skip adding it to the service group, and it'll shut down on deinit instead. This however may be too late to finish sending over all in-flight messages, so prefer to use a ServiceGroup for all your services if you can.\n\nYou'll also want to serve a `serviceWorker.mjs` file at the root of your server (it can be anywhere, but there are scoping restrictions that are simplified by serving it at the root) to handle incoming notifications:\n\n```js\nself.addEventListener('push', function(event) {\n    const data = event.data?.json() ?? {};\n    event.waitUntil((async () =\u003e {\n        const notification = data?.notification ?? {}\n        /// Try parsing the data, otherwise use fallback content. DO NOT skip sending the notification, as you must display one for every push message that is received or your subscription will be dropped.\n        const title = notification.title ?? \"Your App Name\";\n        const body = notification.body ?? \"New Content Available!\";\n        \n        await self.registration.showNotification(title, { \n            body,\n            requireInteraction: notification.require_interaction ?? false,\n            ...notification,\n        });\n    })());\n});\n```\n\nIn the example above, we are using the new **Declarative Push Notification** format for our message payload so the browser can automatically skip requiring the service worker, but you can send send any data payload and interpret it in your service worker should you choose. Note that doing so will require more resources on your user's devices when Declarative Push Notifications are otherwise supported.\n\n\u003e [!NOTE]\n\u003e `.mjs` here allows your code to import other js modules as needed. If you are not using Vapor, please make sure your server uses the correct mime type for this file extension.\n\n\n### Registering Subscribers\n\nTo register a subscriber, you'll need backend code to provide your VAPID key, and frontend code to ask the browser for a subscription on behalf of the user.\n\nOn the backend (we are assuming Vapor here), register a route that returns your VAPID public key:\n\n```swift\nimport WebPush\n\n...\n\n/// Listen somewhere for a VAPID key request. This path can be anything you want, and should be available to all parties you with to serve push messages to.\napp.get(\"vapidKey\", use: loadVapidKey)\n\n...\n\n/// A wrapper for the VAPID key that Vapor can encode.\nstruct WebPushOptions: Codable, Content, Hashable, Sendable {\n    static let defaultContentType = HTTPMediaType(type: \"application\", subType: \"webpush-options+json\")\n\n    var vapid: VAPID.Key.ID\n}\n\n/// The route handler, usually part of a route controller.\n@Sendable func loadVapidKey(request: Request) async throws -\u003e WebPushOptions {\n    WebPushOptions(vapid: manager.nextVAPIDKeyID)\n}\n```\n\nAlso register a route for persisting `Subscriber`'s:\n\n```swift\nimport WebPush\n\n...\n\n/// Listen somewhere for new registrations. This path can be anything you want, and should be available to all parties you with to serve push messages to.\napp.get(\"registerSubscription\", use: registerSubscription)\n\n...\n\n/// A custom type for communicating the status of your subscription. Fill this out with any options you'd like to communicate back to the user.\nstruct SubscriptionStatus: Codable, Content, Hashable, Sendable {\n    var subscribed = true\n}\n\n/// The route handler, usually part of a route controller.\n@Sendable func registerSubscription(request: Request) async throws -\u003e SubscriptionStatus {\n    let subscriptionRequest = try request.content.decode(Subscriber.self, as: .jsonAPI)\n    \n    // TODO: Persist subscriptionRequest!\n    \n    return SubscriptionStatus()\n}\n```\n\n\u003e [!NOTE]\n\u003e `WebPushManager` (`manager` here) is fully sendable, and should be shared with your controllers using dependency injection. This allows you to fully test your application server by relying on the provided `WebPushTesting` library in your unit tests to mock keys, verify delivery, and simulate errors.\n\nOn the frontend, register your service worker, fetch your vapid key, and subscribe on behalf of the user:\n\n```js\nconst serviceRegistration = await navigator.serviceWorker?.register(\"/serviceWorker.mjs\", { type: \"module\" });\nlet subscription = await registration?.pushManager?.getSubscription();\n\n/// Wait for the user to interact with the page to request a subscription.\ndocument.getElementById(\"notificationsSwitch\").addEventListener(\"click\", async ({ currentTarget }) =\u003e {\n    try {\n        /// If we couldn't load a subscription, now's the time to ask for one.\n        if (!subscription) {\n            const applicationServerKey = await loadVAPIDKey();\n            subscription = await serviceRegistration.pushManager.subscribe({\n                userVisibleOnly: true,\n                applicationServerKey,\n            });\n        }\n        \n        /// It is safe to re-register the same subscription.\n        const subscriptionStatusResponse = await registerSubscription(subscription);\n        \n        /// Do something with your registration. Some may use it to store notification settings and display those back to the user.\n        ...\n    } catch (error) {\n        /// Tell the user something went wrong here.\n        console.error(error);\n    }\n});\n\n...\n\nasync function loadVAPIDKey() {\n    /// Make sure this is the same route used above.\n    const httpResponse = await fetch(`/vapidKey`);\n\n    const webPushOptions = await httpResponse.json();\n    if (httpResponse.status != 200) throw new Error(webPushOptions.reason);\n\n    return webPushOptions.vapid;\n}\n\nexport async function registerSubscription(subscription) {\n    /// Make sure this is the same route used above.\n    const subscriptionStatusResponse = await fetch(`/registerSubscription`, {\n        method: \"POST\",\n        body: {\n            ...subscription.toJSON(),\n            /// It is good practice to provide the applicationServerKey back here so we can track which one was used if multiple were provided during configuration.\n            applicationServerKey: subscription.options.applicationServerKey,\n        },\n    });\n    \n    /// Do something with your registration. Some may use it to store notification settings and display those back to the user.\n    ...\n}\n```\n\n\n### Sending Messages\n\nTo send a message, call one of the `send()` methods on `WebPushManager` with a `Subscriber`:\n\n```swift\nimport WebPush\n\n...\n\ndo {\n    try await manager.send(\n        /// We use a declarative push notification to allow newer browsers to deliver the notification to users on our behalf.\n        notification: PushMessage.Notification(\n            destination: URL(string: \"https://example.com/notificationDetails\")!,\n            title: \"Test Notification\",\n            body: \"Hello, World!\"\n        ),\n        to: subscriber\n        /// If sent from a request, pass the request's logger here to maintain its metadata.\n        // logger: request.logger\n    )\n} catch is BadSubscriberError {\n    /// The subscription is no longer valid and should be removed.\n} catch is MessageTooLargeError {\n    /// The message was too long and should be shortened.\n} catch let error as PushServiceError {\n    /// The push service ran into trouble. error.response may help here.\n} catch {\n    /// An unknown error occurred.\n}\n```\n\nYour service worker will receive this message, decode it, and present it to the user.\n\nYou can also send JSON (`send(json: ...)`), data (`send(data: ...)`), or text (`send(string: ...)`) and have your service worker interpret the payload as it sees fit.\n\n\u003e [!NOTE]\n\u003e Although the spec supports it, most browsers do not support silent notifications, and will drop a subscription if they are used.\n\n\n### Testing\n\nThe `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.\n\n\u003e [!IMPORTANT]\n\u003e Only import `WebPushTesting` in your testing targets.\n\n```swift\nimport Testing\nimport WebPushTesting\n\n@Test func sendSuccessfulNotifications() async throws {\n    try await confirmation { requestWasMade in\n        let mockedManager = WebPushManager.makeMockedManager { message, subscriber, topic, expiration, urgency in\n            #expect(message.string == \"hello\")\n            #expect(subscriber.endpoint.absoluteString == \"https://example.com/expectedSubscriber\")\n            #expect(subscriber.vapidKeyID == .mockedKeyID1)\n            #expect(topic == nil)\n            #expect(expiration == .recommendedMaximum)\n            #expect(urgency == .high)\n            requestWasMade()\n        }\n        \n        let myController = MyController(pushManager: mockedManager)\n        try await myController.sendNotifications()\n    }\n}\n\n@Test func catchBadSubscriptions() async throws {\n    /// Mocked managers accept multiple handlers, and will cycle through them each time a push message is sent:\n    let mockedManager = WebPushManager.makeMockedManager(messageHandlers:\n        { _, _, _, _, _ in throw BadSubscriberError() },\n        { _, _, _, _, _ in },\n        { _, _, _, _, _ in throw BadSubscriberError() },\n    )\n    \n    let myController = MyController(pushManager: mockedManager)\n    #expect(myController.subscribers.count == 3)\n    try await myController.sendNotifications()\n    #expect(myController.subscribers.count == 1)\n}\n```\n\n## Specifications\n\n### RFC Standards\n\n- [RFC 6454 — The Web Origin Concept](https://datatracker.ietf.org/doc/html/rfc6454)\n- [RFC 7515 — JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515)\n- [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)\n- [RFC 8030 — Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030)\n- [RFC 8188 — Encrypted Content-Encoding for HTTP](https://datatracker.ietf.org/doc/html/rfc8188)\n- [RFC 8291 — Message Encryption for Web Push](https://datatracker.ietf.org/doc/html/rfc8291)\n- [RFC 8292 — Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292)\n\n### W3C Standards\n\n- [Push API Working Draft](https://www.w3.org/TR/push-api/)\n- [Push API Editor's Draft — `declarative-push` branch](https://raw.githubusercontent.com/w3c/push-api/refs/heads/declarative-push/index.html)\n\n### WHATWG Standards\n\n- [Notifications API — Living Standard](https://notifications.spec.whatwg.org/)\n- [Notifications API — PR #213 — Allow notifications and actions to specify a navigable URL](https://whatpr.org/notifications/213.html)\n\n## Other Resources\n\n- [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)\n- [WWDC22 — Meet Web Push for Safari](https://developer.apple.com/videos/play/wwdc2022/10098/)\n- [WebKit — Meet Web Push](https://webkit.org/blog/12945/meet-web-push/)\n- [WebKit — Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)\n- [MDN — Notification](https://developer.mozilla.org/en-US/docs/Web/API/Notification)\n- [MDN — Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)\n- [MDN — Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)\n- [web.dev — The Web Push Protocol](https://web.dev/articles/push-notifications-web-push-protocol)\n- [Sample Code — ServiceWorker Cookbook](https://github.com/mdn/serviceworker-cookbook/tree/master/push-simple)\n- [Web Push: Data Encryption Test Page](https://mozilla-services.github.io/WebPushDataTestPage/)\n\n## Contributing\n\nContribution 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!\n\nPlease make sure that all submissions have clean commit histories, are well documented, and thoroughly tested. **Please rebase your PR** before submission rather than merge in `main`. Linear histories are required, so merge commits in PRs will not be accepted.\n\n## Support\n\nTo support this project, consider following [@dimitribouniol](https://mastodon.social/@dimitribouniol) on Mastodon, listening to Spencer and Dimitri on [Code Completion](https://mastodon.social/@codecompletion), or downloading Linh and Dimitri's apps:\n- [Not Phở](https://notpho.app/)\n- [Jiiiii](https://jiiiii.moe/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmochidev%2Fswift-webpush","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmochidev%2Fswift-webpush","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmochidev%2Fswift-webpush/lists"}