Changeset ad10715 in Klonkt


Ignore:
Timestamp:
07/22/2026 07:12:11 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
053bf51
Parents:
dc4aa08
git-author:
Robin <roboburr@…> (07/22/2026 07:00:29 AM)
git-committer:
Robin <roboburr@…> (07/22/2026 07:12:11 AM)
Message:

Feature: web push slice 1, VAPID keys + subscription store

The foundation for background notifications (docs/webpush-design.md).

  • Dependency (approved): web-push for RFC 8292 VAPID JWTs and RFC 8291 payload encryption. Lazy import so a canary that autofollows before npm ci never crashes on boot (same pattern as @simplewebauthn/server).
  • VAPID keys: env (VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECT) wins, else auto-generated once into storage/.vapid (0600), never regenerated while the file exists: new keys would invalidate every subscription. Subject: PUBLIC_BASE_URL, else mailto from SMTP_FROM.
  • push_subscriptions table: one row per device, client keys for encrypted payloads, per-type alert preferences (follow/reply on, like/boost off, dm on by default), self-pruning on 404/410 in the send path.
  • notifyUser/notifySite: honour alert prefs, cap title/body length, fire-and-forget at call sites (slice 3 wires the triggers).

Changed files:
package.json, package-lock.json

  • web-push@3.6.7

src/config/database.js

  • push_subscriptions table (additive)

src/routes/posts.js

  • RESERVED_SLUGS: add 'push' (and the missing 'paid') so a post can't shadow the mounted routes

New file:
src/services/PushService.js

  • VAPID key resolve/persist, subscription CRUD, encrypted send with pruning, notifyUser/notifySite

test/push.test.js

  • key autogen (0600, persists, served=stored), subscription CRUD, upsert-not-duplicate, refuse incomplete payloads

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Files:
2 added
4 edited

Legend:

Unmodified
Added
Removed
  • package-lock.json

    rdc4aa08 rad10715  
    3030        "nodemailer": "^6.9.15",
    3131        "sanitize-html": "^2.17.3",
    32         "uuid": "^9.0.1"
     32        "uuid": "^9.0.1",
     33        "web-push": "^3.6.7"
    3334      },
    3435      "engines": {
     
    551552      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
    552553      "license": "MIT"
     554    },
     555    "node_modules/asn1.js": {
     556      "version": "5.4.1",
     557      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
     558      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
     559      "license": "MIT",
     560      "dependencies": {
     561        "bn.js": "^4.0.0",
     562        "inherits": "^2.0.1",
     563        "minimalistic-assert": "^1.0.0",
     564        "safer-buffer": "^2.1.0"
     565      }
    553566    },
    554567    "node_modules/asn1js": {
     
    635648      }
    636649    },
     650    "node_modules/bn.js": {
     651      "version": "4.12.5",
     652      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz",
     653      "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==",
     654      "license": "MIT"
     655    },
    637656    "node_modules/body-parser": {
    638657      "version": "1.20.5",
     
    692711      }
    693712    },
     713    "node_modules/buffer-equal-constant-time": {
     714      "version": "1.0.1",
     715      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
     716      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
     717      "license": "BSD-3-Clause"
     718    },
    694719    "node_modules/buffer-from": {
    695720      "version": "1.1.2",
     
    10071032      "engines": {
    10081033        "node": ">= 0.4"
     1034      }
     1035    },
     1036    "node_modules/ecdsa-sig-formatter": {
     1037      "version": "1.0.11",
     1038      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
     1039      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
     1040      "license": "Apache-2.0",
     1041      "dependencies": {
     1042        "safe-buffer": "^5.0.1"
    10091043      }
    10101044    },
     
    14481482      "license": "0BSD"
    14491483    },
     1484    "node_modules/http_ece": {
     1485      "version": "1.2.0",
     1486      "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
     1487      "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
     1488      "license": "MIT",
     1489      "engines": {
     1490        "node": ">=16"
     1491      }
     1492    },
    14501493    "node_modules/http-errors": {
    14511494      "version": "2.0.1",
     
    16041647      }
    16051648    },
     1649    "node_modules/jwa": {
     1650      "version": "2.0.1",
     1651      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
     1652      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
     1653      "license": "MIT",
     1654      "dependencies": {
     1655        "buffer-equal-constant-time": "^1.0.1",
     1656        "ecdsa-sig-formatter": "1.0.11",
     1657        "safe-buffer": "^5.0.1"
     1658      }
     1659    },
     1660    "node_modules/jws": {
     1661      "version": "4.0.1",
     1662      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
     1663      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
     1664      "license": "MIT",
     1665      "dependencies": {
     1666        "jwa": "^2.0.1",
     1667        "safe-buffer": "^5.0.1"
     1668      }
     1669    },
    16061670    "node_modules/marked": {
    16071671      "version": "11.2.0",
     
    16961760        "url": "https://github.com/sponsors/sindresorhus"
    16971761      }
     1762    },
     1763    "node_modules/minimalistic-assert": {
     1764      "version": "1.0.1",
     1765      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
     1766      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
     1767      "license": "ISC"
    16981768    },
    16991769    "node_modules/minimatch": {
     
    25122582      }
    25132583    },
     2584    "node_modules/web-push": {
     2585      "version": "3.6.7",
     2586      "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
     2587      "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
     2588      "license": "MPL-2.0",
     2589      "dependencies": {
     2590        "asn1.js": "^5.3.0",
     2591        "http_ece": "1.2.0",
     2592        "https-proxy-agent": "^7.0.0",
     2593        "jws": "^4.0.0",
     2594        "minimist": "^1.2.5"
     2595      },
     2596      "bin": {
     2597        "web-push": "src/cli.js"
     2598      },
     2599      "engines": {
     2600        "node": ">= 16"
     2601      }
     2602    },
     2603    "node_modules/web-push/node_modules/agent-base": {
     2604      "version": "7.1.4",
     2605      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
     2606      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
     2607      "license": "MIT",
     2608      "engines": {
     2609        "node": ">= 14"
     2610      }
     2611    },
     2612    "node_modules/web-push/node_modules/debug": {
     2613      "version": "4.4.3",
     2614      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
     2615      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
     2616      "license": "MIT",
     2617      "dependencies": {
     2618        "ms": "^2.1.3"
     2619      },
     2620      "engines": {
     2621        "node": ">=6.0"
     2622      },
     2623      "peerDependenciesMeta": {
     2624        "supports-color": {
     2625          "optional": true
     2626        }
     2627      }
     2628    },
     2629    "node_modules/web-push/node_modules/https-proxy-agent": {
     2630      "version": "7.0.6",
     2631      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
     2632      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
     2633      "license": "MIT",
     2634      "dependencies": {
     2635        "agent-base": "^7.1.2",
     2636        "debug": "4"
     2637      },
     2638      "engines": {
     2639        "node": ">= 14"
     2640      }
     2641    },
     2642    "node_modules/web-push/node_modules/ms": {
     2643      "version": "2.1.3",
     2644      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
     2645      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
     2646      "license": "MIT"
     2647    },
    25142648    "node_modules/which": {
    25152649      "version": "1.3.1",
  • package.json

    rdc4aa08 rad10715  
    3535    "nodemailer": "^6.9.15",
    3636    "sanitize-html": "^2.17.3",
    37     "uuid": "^9.0.1"
     37    "uuid": "^9.0.1",
     38    "web-push": "^3.6.7"
    3839  },
    3940  "engines": {
  • src/config/database.js

    rdc4aa08 rad10715  
    335335      expires_at INTEGER NOT NULL,      -- unix seconds; re-link after
    336336      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
     337    );
     338    -- Web Push (docs/webpush-design.md): one row per browser/device the owner
     339    -- enabled notifications on. Payloads are encrypted to p256dh/auth (RFC 8291).
     340    CREATE TABLE IF NOT EXISTS push_subscriptions (
     341      endpoint TEXT PRIMARY KEY,       -- push-service URL for this device
     342      user_id TEXT NOT NULL,
     343      p256dh TEXT NOT NULL,            -- client public key
     344      auth TEXT NOT NULL,              -- client auth secret
     345      alert_types TEXT,                -- JSON {follow,reply,like,boost,dm}
     346      ua_label TEXT,
     347      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     348      last_ok_at DATETIME
    337349    );
    338350    CREATE TABLE IF NOT EXISTS ap_outbox (
  • src/routes/posts.js

    rdc4aa08 rad10715  
    152152  'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
    153153  'authorize_interaction', 'fediverse', 'news', 'following', 'notifications', 'blocking',
     154  'paid', 'push',
    154155]);
    155156
Note: See TracChangeset for help on using the changeset viewer.