Merge branch 'v1.3-dev' as v1.3.5 v1.3.5
authorJustin Wind <justin.wind+git@gmail.com>
Thu, 24 Feb 2022 01:00:53 +0000 (17:00 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Thu, 24 Feb 2022 01:00:53 +0000 (17:00 -0800)
27 files changed:
CHANGELOG.md
README.md
package-lock.json
package.json
server.js
src/communication.js
src/db/postgres/index.js
src/db/postgres/listener.js
src/db/postgres/sql/schema/1.0.2/apply.sql [new file with mode: 0644]
src/db/postgres/sql/schema/1.0.2/revert.sql [new file with mode: 0644]
src/db/postgres/sql/schema/1.0.3/apply.sql [new file with mode: 0644]
src/db/postgres/sql/schema/1.0.3/revert.sql [new file with mode: 0644]
src/db/postgres/sql/subscription-delivery-success.sql
src/db/postgres/sql/topic-set-content-history.sql [new file with mode: 0644]
src/db/sqlite/index.js
src/db/sqlite/sql/schema/1.0.2/apply.sql [new file with mode: 0644]
src/db/sqlite/sql/schema/1.0.2/revert.sql [new file with mode: 0644]
src/db/sqlite/sql/schema/1.0.3/apply.sql [new file with mode: 0644]
src/db/sqlite/sql/schema/1.0.3/revert.sql [new file with mode: 0644]
src/db/sqlite/sql/subscription-delivery-success.sql
src/db/sqlite/sql/topic-set-content-history.sql [new file with mode: 0644]
src/service.js
src/template/root-html.js
test/src/db/integration.js
test/src/db/postgres.js
test/src/db/sqlite.js
test/src/service.js

index a55c42a2d8728f9b66494c9370d8035f6c3597b5..99ae8071e4b4e3af6a3ccc1c78a5ee49ee3c911e 100644 (file)
@@ -2,7 +2,22 @@
 
 Releases and notable changes to this project are documented here.
 
+## [v1.3.5] - 2022-02-23
+
+### Added
+- IndieAuth profile logins now support latest spec, id est metadata discovery and issuer validation.
+- Topic content update history is now tracked.
+
+### Fixed
+
+- Fixed potential race condition which could cause a subscriber to miss an update.
+- Fixed postfix listener to more properly deal with errors.
+- Removed accidental logging of response body during HEAD requests.
+- Fixed session logout link on root page.
+- Dependency updates.
+
 ## [v1.3.4] - 2022-01-23
+
 ### Fixed
 
 - Dependency updates.
@@ -109,7 +124,9 @@ Releases and notable changes to this project are documented here.
 
 ---
 
-[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.2
+[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.5
+[v1.3.5]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.5;hp=v1.3.4
+[v1.3.4]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.4;hp=v1.3.3
 [v1.3.3]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.3;hp=v1.3.2
 [v1.3.2]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.2;hp=v1.3.1
 [v1.3.1]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.1;hp=v1.3.0
index cd5939e895009b5fd6227726961aebd682aca347..138e4583cbd3c6d247d29d8a934beb1692b1823b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -129,7 +129,6 @@ This implementation is built atop an in-house API framework, for Reasons.  It wo
   - *.js - environment specific values, edit these as needed
 - server.js - launches the application server
 - src/
-  - authenticator.js - interact with credentials and validation mechanisms
   - common.js - utility functions
   - communication.js - outgoing requests and associated logic
   - db/
@@ -150,7 +149,6 @@ This implementation is built atop an in-house API framework, for Reasons.  It wo
   - logger.js - a very simple logging class
   - manager.js - process incoming requests
   - service.js - defines incoming endpoints, linking the API server framework to the manager methods
-  - session-manager.js - process login/logout requests
   - template/ - HTML content
   - worker.js - maintains a pool of tasks in progress, for sending out updates, performing verifications, et cetera
 - static/ - static assets
index d50f130c77e860b9a4d209db257a3600d2498f86..a5089d4b14cbfa378944b67c05f3522efee83cfa 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/websub-hub",
-  "version": "1.3.4",
+  "version": "1.3.5",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
       }
     },
     "@eslint/eslintrc": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz",
-      "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz",
+      "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==",
       "dev": true,
       "requires": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
-        "espree": "^9.2.0",
+        "espree": "^9.3.1",
         "globals": "^13.9.0",
         "ignore": "^4.0.6",
         "import-fresh": "^3.2.1",
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.9.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz",
-      "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==",
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz",
+      "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==",
       "dev": true,
       "requires": {
         "@humanwhocodes/object-schema": "^1.2.1",
       }
     },
     "@sinonjs/fake-timers": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz",
-      "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==",
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.0.0.tgz",
+      "integrity": "sha512-+shXA2X7KNP7H7qNbQTJ3SA+NQc0pZDSBrdvFSRwF8sAo/ohw+ZQFD8Moc+gnz51+1eRXtEQBpKWPiQ4jsRC/w==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.7.0"
       }
     },
     "@sinonjs/samsam": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz",
-      "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==",
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz",
+      "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.6.0",
       }
     },
     "@squeep/authentication-module": {
-      "version": "git+https://git.squeep.com/squeep-authentication-module/#afa866e6bb4d5af652379295c9497c2caf4ecc92",
-      "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.2",
+      "version": "git+https://git.squeep.com/squeep-authentication-module/#92658b114da01ab5537c53dee7ff5ad6385fe179",
+      "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
       "requires": {
         "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
-        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1",
-        "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.2",
+        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
+        "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
         "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.3",
-        "argon2": "^0.28.3",
+        "argon2": "^0.28.4",
         "node-linux-pam": "^0.2.1"
-      },
-      "dependencies": {
-        "@squeep/html-template-helper": {
-          "version": "git+https://git.squeep.com/squeep-html-template-helper#d3f76b9e76b8f133e8158c1087bb01b32c38d9bb",
-          "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1"
-        }
       }
     },
     "@squeep/html-template-helper": {
       "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2"
     },
     "@squeep/indieauth-helper": {
-      "version": "git+https://git.squeep.com/squeep-indieauth-helper/#a15c4051aee22b76eca268e6a53b0944a9e40d0c",
-      "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.2",
+      "version": "git+https://git.squeep.com/squeep-indieauth-helper/#7ece3489799b5349e22e95e3bd9fe7a30a985ebf",
+      "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
       "requires": {
         "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
-        "axios": "^0.25.0",
+        "axios": "^0.26.0",
         "iconv": "^3.0.1",
         "microformats-parser": "^1.4.1"
       }
       }
     },
     "argon2": {
-      "version": "0.28.3",
-      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
-      "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
+      "version": "0.28.4",
+      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.4.tgz",
+      "integrity": "sha512-WsfqiDp/tf5+eieLc1+S7RtO7Y3cAiZQ1F6GIaskENoJy/6xuCN5WGBIc8dG7QVPDavy6jUSads8zwZTtrHVag==",
       "optional": true,
       "requires": {
-        "@mapbox/node-pre-gyp": "^1.0.7",
+        "@mapbox/node-pre-gyp": "^1.0.8",
         "@phc/format": "^1.0.0",
-        "node-addon-api": "^4.2.0",
+        "node-addon-api": "^4.3.0",
         "opencollective-postinstall": "^2.0.3"
       }
     },
       "optional": true
     },
     "axios": {
-      "version": "0.25.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
-      "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
+      "version": "0.26.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
+      "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
       "requires": {
-        "follow-redirects": "^1.14.7"
+        "follow-redirects": "^1.14.8"
       }
     },
     "balanced-match": {
       }
     },
     "chokidar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
-      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
       "dev": true,
       "requires": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
+        "fsevents": "~2.3.2",
         "glob-parent": "~5.1.2",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
     },
     "eslint": {
-      "version": "8.7.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.7.0.tgz",
-      "integrity": "sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w==",
+      "version": "8.9.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz",
+      "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==",
       "dev": true,
       "requires": {
-        "@eslint/eslintrc": "^1.0.5",
+        "@eslint/eslintrc": "^1.1.0",
         "@humanwhocodes/config-array": "^0.9.2",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "debug": "^4.3.2",
         "doctrine": "^3.0.0",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.1.0",
+        "eslint-scope": "^7.1.1",
         "eslint-utils": "^3.0.0",
-        "eslint-visitor-keys": "^3.2.0",
-        "espree": "^9.3.0",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
         "esquery": "^1.4.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
       }
     },
     "eslint-plugin-sonarjs": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.11.0.tgz",
-      "integrity": "sha512-ei/WuZiL0wP+qx2KrxKyZs3+eDbxiGAhFSm3GKCOOAUkg+G2ny6TSXDB2j67tvyqHefi+eoQsAgGQvz+nEtIBw==",
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.12.0.tgz",
+      "integrity": "sha512-soxjK67hoYxO8hesKqXWN50GSM+oG2r35N5WnAMehetahO6zoVpv3HZbdziP0jYWNopEe6te/BFUZOYAZI+qhg==",
       "dev": true
     },
     "eslint-scope": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
-      "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+      "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
       "dev": true,
       "requires": {
         "esrecurse": "^4.3.0",
       }
     },
     "eslint-visitor-keys": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz",
-      "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+      "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
       "dev": true
     },
     "espree": {
-      "version": "9.3.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz",
-      "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==",
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
+      "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==",
       "dev": true,
       "requires": {
         "acorn": "^8.7.0",
         "acorn-jsx": "^5.3.1",
-        "eslint-visitor-keys": "^3.1.0"
+        "eslint-visitor-keys": "^3.3.0"
       }
     },
     "esprima": {
       }
     },
     "flatted": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz",
-      "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+      "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
       "dev": true
     },
     "follow-redirects": {
-      "version": "1.14.7",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
-      "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
+      "version": "1.14.8",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
+      "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
     },
     "foreground-child": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
     },
+    "fsevents": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "dev": true,
+      "optional": true
+    },
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       }
     },
     "globals": {
-      "version": "13.12.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
-      "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
+      "version": "13.12.1",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz",
+      "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==",
       "dev": true,
       "requires": {
         "type-fest": "^0.20.2"
       "optional": true
     },
     "mocha": {
-      "version": "9.1.4",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.4.tgz",
-      "integrity": "sha512-+q2aV5VlJZuLgCWoBvGI5zEwPF9eEI0kr/sAA9Jm4xMND7RfIEyF8JE7C0JIg8WXRG+P1sdIAb5ccoHPlXLzcw==",
+      "version": "9.2.1",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.1.tgz",
+      "integrity": "sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ==",
       "dev": true,
       "requires": {
         "@ungap/promise-all-settled": "1.1.2",
         "ansi-colors": "4.1.1",
         "browser-stdout": "1.3.1",
-        "chokidar": "3.5.2",
-        "debug": "4.3.2",
+        "chokidar": "3.5.3",
+        "debug": "4.3.3",
         "diff": "5.0.0",
         "escape-string-regexp": "4.0.0",
         "find-up": "5.0.0",
-        "glob": "7.1.7",
+        "glob": "7.2.0",
         "growl": "1.10.5",
         "he": "1.2.0",
         "js-yaml": "4.1.0",
         "log-symbols": "4.1.0",
         "minimatch": "3.0.4",
         "ms": "2.1.3",
-        "nanoid": "3.1.25",
+        "nanoid": "3.2.0",
         "serialize-javascript": "6.0.0",
         "strip-json-comments": "3.1.1",
         "supports-color": "8.1.1",
         "which": "2.0.2",
-        "workerpool": "6.1.5",
+        "workerpool": "6.2.0",
         "yargs": "16.2.0",
         "yargs-parser": "20.2.4",
         "yargs-unparser": "2.0.0"
           "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
           "dev": true
         },
+        "debug": {
+          "version": "4.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+          "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          },
+          "dependencies": {
+            "ms": {
+              "version": "2.1.2",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+              "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+              "dev": true
+            }
+          }
+        },
         "escape-string-regexp": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
           "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
           "dev": true
         },
+        "glob": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+          "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
         "has-flag": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "nanoid": {
-      "version": "3.1.25",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
-      "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
+      "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
       "dev": true
     },
     "napi-build-utils": {
       "dev": true
     },
     "nise": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz",
-      "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz",
+      "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==",
       "dev": true,
       "requires": {
-        "@sinonjs/commons": "^1.7.0",
-        "@sinonjs/fake-timers": "^7.0.4",
+        "@sinonjs/commons": "^1.8.3",
+        "@sinonjs/fake-timers": ">=5",
         "@sinonjs/text-encoding": "^0.7.1",
         "just-extend": "^4.0.2",
         "path-to-regexp": "^1.7.0"
-      },
-      "dependencies": {
-        "@sinonjs/fake-timers": {
-          "version": "7.1.2",
-          "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
-          "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
-          "dev": true,
-          "requires": {
-            "@sinonjs/commons": "^1.7.0"
-          }
-        }
       }
     },
     "no-case": {
       }
     },
     "sinon": {
-      "version": "12.0.1",
-      "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz",
-      "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==",
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz",
+      "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.8.3",
-        "@sinonjs/fake-timers": "^8.1.0",
-        "@sinonjs/samsam": "^6.0.2",
+        "@sinonjs/fake-timers": "^9.0.0",
+        "@sinonjs/samsam": "^6.1.1",
         "diff": "^5.0.0",
-        "nise": "^5.1.0",
+        "nise": "^5.1.1",
         "supports-color": "^7.2.0"
       },
       "dependencies": {
       "dev": true
     },
     "workerpool": {
-      "version": "6.1.5",
-      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
-      "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
+      "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==",
       "dev": true
     },
     "wrap-ansi": {
index 6595792ddf29f9793ba6a2e49f44ec6e19991510..71843a3b4c1a6baa1744b30ae73a153a96181866 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/websub-hub",
-  "version": "1.3.4",
+  "version": "1.3.5",
   "description": "A WebSub Hub server implementation.",
   "main": "server.js",
   "scripts": {
   ],
   "dependencies": {
     "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
-    "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.2",
+    "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
     "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
-    "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.2",
     "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
-    "axios": "^0.25.0",
+    "axios": "^0.26.0",
     "feedparser": "^2.2.10",
     "htmlparser2": "^7.2.0",
     "iconv": "^3.0.1"
     "pg-promise": "^10.11.1"
   },
   "devDependencies": {
-    "eslint": "^8.7.0",
+    "eslint": "^8.9.0",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-security": "^1.4.0",
-    "eslint-plugin-sonarjs": "^0.11.0",
+    "eslint-plugin-sonarjs": "^0.12.0",
     "html-minifier-lint": "^2.0.0",
-    "mocha": "^9.1.4",
+    "mocha": "^9.2.1",
     "mocha-steps": "^1.3.0",
     "nyc": "^15.1.0",
     "pre-commit": "^1.2.2",
-    "sinon": "^12.0.1"
+    "sinon": "^13.0.1"
   }
 }
index cef54a918fbce09797f290bd3a3f8fe49176e577..49ce8f6ea6971169ae958ef0c4d4d9227133a8d2 100644 (file)
--- a/server.js
+++ b/server.js
@@ -26,12 +26,13 @@ const ADDR = process.env.LISTEN_ADDR || '127.0.0.1';
       service.dispatch(req, res);
     }).listen(PORT, ADDR, (err) => {
       if (err) {
-        logger.error(_scope, 'error starting server', err);
+        logger.error(_scope, 'error creating server', err);
         throw err;
       }
       logger.info(_scope, 'server started', { version, listenAddress: ADDR, listenPort: PORT });
     });
   } catch (e) {
     (logger || console).error(_scope, 'error starting server', e);
+    db && db.listener && await db.listener.stop();
   }
 })();
\ No newline at end of file
index 03cc670dd7154ccc64a50b87f4d7bb1d5eddafb3..dc4d464c70111b4ea2a8971575940c099d14b379 100644 (file)
@@ -565,7 +565,7 @@ class Communication {
 
       await this.db.transaction(dbCtx, async (txCtx) => {
         await this.db.verificationInsert(txCtx, verification);
-        await this.db.subscriptionDeliveryComplete(txCtx, subscription.callback, subscription.topicId);
+        await this.db.subscriptionDeliveryComplete(txCtx, subscription.callback, subscription.topicId, topic.contentUpdated);
       });
       this.logger.info(_scope, 'update unsubscription for deleted topic', logInfoData);
       return;
@@ -617,7 +617,7 @@ class Communication {
         return;
     }
 
-    await this.db.subscriptionDeliveryComplete(dbCtx, subscription.callback, subscription.topicId);
+    await this.db.subscriptionDeliveryComplete(dbCtx, subscription.callback, subscription.topicId, topic.contentUpdated);
     this.logger.info(_scope, 'update success', logInfoData);
   }
 
index 213fa1031c3c44fd29c48a46a8af86ff8bed6217..d950d4a8488924ea2999a9c03b03d93f38ab4591 100644 (file)
@@ -30,7 +30,7 @@ const schemaVersionsSupported = {
   max: {
     major: 1,
     minor: 0,
-    patch: 1,
+    patch: 3,
   },
 };
 
@@ -162,10 +162,16 @@ class DatabasePostgres extends Database {
     this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
     for (const v of migrationsWanted) {
       const fPath = path.join(__dirname, 'sql', 'schema', v, 'apply.sql');
-      const migrationSql = _queryFile(fPath);
-      const results = await this.db.multiResult(migrationSql);
-      this.logger.debug(_scope, 'executed migration sql', { version: v, results });
-      this.logger.info(_scope, 'applied migration', { version: v });
+      try {
+        const migrationSql = _queryFile(fPath);
+        this.logger.debug(_scope, 'applying migration', { version: v });
+        const results = await this.db.multiResult(migrationSql);
+        this.logger.debug(_scope, 'migration results', { results });
+        this.logger.info(_scope, 'applied migration', { version: v });
+      } catch (e) {
+        this.logger.error(_scope, 'migration failed', { error: e, fPath, version: v });
+        throw e;
+      }
     }
   }
 
@@ -473,14 +479,14 @@ class DatabasePostgres extends Database {
   }
 
 
-  async subscriptionDeliveryComplete(dbCtx, callback, topicId) {
+  async subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated) {
     const _scope = _fileScope('subscriptionDeliveryComplete');
-    this.logger.debug(_scope, 'called', { callback, topicId });
+    this.logger.debug(_scope, 'called', { callback, topicId, topicContentUpdated });
 
     let result;
     try {
       await dbCtx.txIf(async (txCtx) => {
-        result = await txCtx.result(this.statement.subscriptionDeliverySuccess, { callback, topicId });
+        result = await txCtx.result(this.statement.subscriptionDeliverySuccess, { callback, topicId, topicContentUpdated });
         if (result.rowCount != 1) {
           throw new DBErrors.UnexpectedResult('did not set subscription delivery success');
         }
@@ -490,7 +496,7 @@ class DatabasePostgres extends Database {
         }
       });
     } catch (e) {
-      this.logger.error(_scope, 'failed', { error: e, callback, topicId });
+      this.logger.error(_scope, 'failed', { error: e, callback, topicId, topicContentUpdated });
       throw e;
     }
   }
@@ -898,6 +904,10 @@ class DatabasePostgres extends Database {
       if (result.rowCount !=  1) {
         throw new DBErrors.UnexpectedResult('did not set topic content');
       }
+      result = await dbCtx.result(this.statement.topicSetContentHistory, { topicId: data.topicId, contentHash: data.contentHash, contentSize: data.content.length });
+      if (result.rowCount != 1) {
+        throw new DBErrors.UnexpectedResult('did not set topic content history');
+      }
       this.logger.debug(_scope, 'success', { ...logData });
       return this._engineInfo(result);
     } catch (e) {
index 6ad387aa49ea2e11a91e29a6de60094eea4024aa..4baf5d082de3bc1d507ef441ddc795af014e8470 100644 (file)
@@ -54,7 +54,7 @@ class PostgresListener {
       delete this.reconnectPending;
     }
     if (this.connection) {
-      this.connection.client.removeListener(this.notificationEventName, this.onNotificationBound);
+      this.connection.client.removeListener(this.notificationEventName, this._onNotificationBound);
       this.connection.done();
       this.connection = null;
       await this.options.connectionLostCallback();
@@ -106,7 +106,7 @@ class PostgresListener {
     this.logger.error(_scope, 'listener connection lost', { error, event });
     this.connection = null;
     try {
-      event.client.removeListener(this.notificationEventName, this.onNotificationBound);
+      event.client.removeListener(this.notificationEventName, this._onNotificationBound);
     } catch (e) {
       this.logger.error(_scope, 'failed to remove listener', { error: e });
       // That's okay, it was probably just gone anyhow.
diff --git a/src/db/postgres/sql/schema/1.0.2/apply.sql b/src/db/postgres/sql/schema/1.0.2/apply.sql
new file mode 100644 (file)
index 0000000..6da0924
--- /dev/null
@@ -0,0 +1,28 @@
+BEGIN;
+       -- track when content was delivered as separate from latest content delivered
+       -- content_delivered continues to be the time the content was delivered, but becomes only informational
+       -- latest_content_delivered is date on topic content delivered
+       ALTER TABLE subscription
+               ADD COLUMN latest_content_delivered TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT '-infinity'::timestamptz
+       ;
+       CREATE INDEX subscription_latest_content_delivered_idx ON subscription(latest_content_delivered);
+       -- migrate existing values
+       UPDATE subscription SET latest_content_delivered = content_delivered;
+       -- no need for this index
+       DROP INDEX subscription_content_delivered_idx;
+       -- update the view to use latest_cotnent_delivered
+       CREATE OR REPLACE VIEW subscription_delivery_needed AS
+               SELECT s.*
+               FROM subscription s JOIN topic t ON s.topic_id = t.id
+               WHERE
+                       s.expires > now()
+               AND
+                       s.latest_content_delivered < t.content_updated
+               AND
+                       s.delivery_next_attempt < now()
+               AND
+                       s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+       ;
+
+       INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 2);
+COMMIT;
diff --git a/src/db/postgres/sql/schema/1.0.2/revert.sql b/src/db/postgres/sql/schema/1.0.2/revert.sql
new file mode 100644 (file)
index 0000000..2752e6d
--- /dev/null
@@ -0,0 +1,21 @@
+BEGIN;
+       DROP INDEX subscription_latest_content_delivered_idx;
+       CREATE INDEX subscription_content_delivered_idx ON subscription(content_delivered);
+       DROP VIEW subscription_delivery_needed;
+       ALTER TABLE subscription
+               DROP COLUMN latest_content_delivered
+       ;
+       CREATE OR REPLACE VIEW subscription_delivery_needed AS
+               SELECT s.*
+               FROM subscription s JOIN topic t ON s.topic_id = t.id
+               WHERE
+                       s.expires > now()
+               AND
+                       s.content_delivered < t.content_updated
+               AND
+                       s.delivery_next_attempt < now()
+               AND
+                       s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+       ;
+       DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 2;
+COMMIT;
diff --git a/src/db/postgres/sql/schema/1.0.3/apply.sql b/src/db/postgres/sql/schema/1.0.3/apply.sql
new file mode 100644 (file)
index 0000000..04b49c7
--- /dev/null
@@ -0,0 +1,13 @@
+BEGIN;
+       -- Track all content updates over time.
+       CREATE TABLE topic_content_history (
+               topic_id UUID NOT NULL REFERENCES topic(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+               content_updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+               content_size INTEGER NOT NULL,
+               content_hash TEXT NOT NULL
+       );
+       CREATE INDEX topic_content_history_topic_id_idx ON topic_content_history(topic_id);
+       CREATE INDEX topic_content_history_content_updated_idx ON topic_content_history(content_updated);
+
+       INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 3);
+COMMIT;
diff --git a/src/db/postgres/sql/schema/1.0.3/revert.sql b/src/db/postgres/sql/schema/1.0.3/revert.sql
new file mode 100644 (file)
index 0000000..a838ade
--- /dev/null
@@ -0,0 +1,7 @@
+BEGIN;
+       DROP INDEX topic_content_history_topic_id_idx;
+       DROP INDEX topic_content_history_content_updated_idx;
+       DROP TABLE topic_content_history;
+
+       DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 3;
+COMMIT;
index 9ff01e0395ef7c689b3ae6d1af33d586d77b683a..b19b376593f54405e2a6ef7532bcef912d0f1ebe 100644 (file)
@@ -1,6 +1,7 @@
 --
 UPDATE subscription SET
        content_delivered = now(),
+       latest_content_delivered = $(topicContentUpdated),
        delivery_attempts_since_success = 0,
        delivery_next_attempt = '-infinity'::timestamptz
 WHERE
diff --git a/src/db/postgres/sql/topic-set-content-history.sql b/src/db/postgres/sql/topic-set-content-history.sql
new file mode 100644 (file)
index 0000000..d24f844
--- /dev/null
@@ -0,0 +1,5 @@
+--
+INSERT INTO topic_content_history
+       (topic_id, content_size, content_hash)
+VALUES
+       ($(topicId), $(contentSize), $(contentHash))
index 07d6633311113dcdaf2eae05dabcc912f94fdece..b8826a4f15ee424113e72fd790b41b8212bf3cf8 100644 (file)
@@ -20,12 +20,14 @@ const schemaVersionsSupported = {
   max: {
     major: 1,
     minor: 0,
-    patch: 1,
+    patch: 3,
   },
 };
 
 // max of signed int64 (2^63 - 1), should be enough
 const EPOCH_FOREVER = BigInt('9223372036854775807');
+const epochToDate = (epoch) => new Date(Number(epoch) * 1000);
+const dateToEpoch = (date) => Math.round(date.getTime() / 1000);
 
 class DatabaseSQLite extends Database {
   constructor(logger, options) {
@@ -85,10 +87,17 @@ class DatabaseSQLite extends Database {
     this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
     migrationsWanted.forEach((v) => {
       const fPath = path.join(__dirname, 'sql', 'schema', v, 'apply.sql');
-      // eslint-disable-next-line security/detect-non-literal-fs-filename
-      const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
-      this.logger.info(_scope, 'applying migration', { version: v });
-      this.db.exec(fSql);
+      try {
+        // eslint-disable-next-line security/detect-non-literal-fs-filename
+        const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
+        this.logger.debug(_scope, 'applying migration', { version: v });
+        const results = this.db.exec(fSql);
+        this.logger.debug(_scope, 'migration results', { results });
+        this.logger.info(_scope, 'applied migration', { version: v });
+      } catch (e) {
+        this.logger.error(_scope, 'migration failed', { error: e, fPath, version: v });
+        throw e;
+      }
     });
   }
 
@@ -304,7 +313,6 @@ class DatabaseSQLite extends Database {
    * @param {Object} data
    */
   static _subscriptionDataToNative(data) {
-    const epochToDate = (epoch) => new Date(Number(epoch) * 1000);
     if (data) {
       ['created', 'verified', 'expires', 'contentDelivered'].forEach((field) => {
         // eslint-disable-next-line security/detect-object-injection
@@ -414,14 +422,15 @@ class DatabaseSQLite extends Database {
   }
 
 
-  subscriptionDeliveryComplete(dbCtx, callback, topicId) {
+  subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated) {
     const _scope = _fileScope('subscriptionDeliveryComplete');
-    this.logger.debug(_scope, 'called', { callback, topicId });
+    this.logger.debug(_scope, 'called', { callback, topicId, topicContentUpdated });
 
     let result;
     try {
       this.db.transaction(() => {
-        result = this.statement.subscriptionDeliverySuccess.run({ callback, topicId });
+        topicContentUpdated = dateToEpoch(topicContentUpdated);
+        result = this.statement.subscriptionDeliverySuccess.run({ callback, topicId, topicContentUpdated });
         if (result.changes != 1) {
           throw new DBErrors.UnexpectedResult('did not set subscription delivery success');
         }
@@ -432,7 +441,7 @@ class DatabaseSQLite extends Database {
       })();
       return this._engineInfo(result);
     } catch (e) {
-      this.logger.error(_scope, 'failed', { error: e, callback, topicId });
+      this.logger.error(_scope, 'failed', { error: e, callback, topicId, topicContentUpdated });
       throw e;
     }
   }
@@ -698,7 +707,6 @@ class DatabaseSQLite extends Database {
    * @param {Object} data
    */
   static _topicDataToNative(data) {
-    const epochToDate = (epoch) => new Date(Number(epoch) * 1000);
     if (data) {
       data.isActive = !!data.isActive;
       data.isDeleted = !!data.isDeleted;
@@ -861,6 +869,10 @@ class DatabaseSQLite extends Database {
       if (result.changes !=  1) {
         throw new DBErrors.UnexpectedResult('did not set topic content');
       }
+      result = this.statement.topicSetContentHistory.run({ topicId: data.topicId, contentHash: data.contentHash, contentSize: data.content.length });
+      if (result.changes != 1) {
+        throw new DBErrors.UnexpectedResult('did not set topic content history');
+      }
       return this._engineInfo(result);
     } catch (e) {
       this.logger.error(_scope, 'failed', { error: e, ...logData });
diff --git a/src/db/sqlite/sql/schema/1.0.2/apply.sql b/src/db/sqlite/sql/schema/1.0.2/apply.sql
new file mode 100644 (file)
index 0000000..3a7598e
--- /dev/null
@@ -0,0 +1,29 @@
+BEGIN;
+       -- track when content was delivered as separate from latest content delivered
+       -- content_delivered continues to be the time the content was delivered, but becomes only informational
+       -- latest_content_delivered is date on topic content delivered
+       ALTER TABLE subscription
+               ADD COLUMN latest_content_delivered INTEGER NOT NULL DEFAULT 0
+       ;
+       CREATE INDEX subscription_latest_content_delivered_idx ON subscription(latest_content_delivered);
+       -- migrate existing values
+       UPDATE subscription SET latest_content_delivered = content_delivered;
+       -- no need for this index
+       DROP INDEX subscription_content_delivered_idx;
+       -- update the view to use latest_cotnent_delivered
+       DROP VIEW subscription_delivery_needed;
+       CREATE VIEW subscription_delivery_needed AS
+               SELECT s.*
+               FROM subscription s JOIN topic t ON s.topic_id = t.id
+               WHERE
+                       s.expires > strftime('%s', 'now')
+               AND
+                       s.latest_content_delivered < t.content_updated
+               AND
+                       s.delivery_next_attempt < strftime('%s', 'now')
+               AND
+                       s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+       ;
+
+       INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 2);
+COMMIT;
diff --git a/src/db/sqlite/sql/schema/1.0.2/revert.sql b/src/db/sqlite/sql/schema/1.0.2/revert.sql
new file mode 100644 (file)
index 0000000..45d3ef1
--- /dev/null
@@ -0,0 +1,21 @@
+BEGIN;
+       DROP INDEX subscription_latest_content_delivered_idx;
+       DROP VIEW subscription_delivery_needed;
+       ALTER TABLE subscription
+               DROP COLUMN latest_content_delivered
+       ;
+       CREATE INDEX subscription_content_delivered_idx ON subscription(content_delivered);
+       CREATE VIEW subscription_delivery_needed AS
+               SELECT s.*
+               FROM subscription s JOIN topic t ON s.topic_id = t.id
+               WHERE
+                       s.expires > strftime('%s', 'now')
+               AND
+                       s.content_delivered < t.content_updated
+               AND
+                       s.delivery_next_attempt < strftime('%s', 'now')
+               AND
+                       s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+       ;
+       DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 2;
+COMMIT;
diff --git a/src/db/sqlite/sql/schema/1.0.3/apply.sql b/src/db/sqlite/sql/schema/1.0.3/apply.sql
new file mode 100644 (file)
index 0000000..0a642c2
--- /dev/null
@@ -0,0 +1,13 @@
+BEGIN;
+       -- Track all content updates over time.
+       CREATE TABLE topic_content_history (
+               topic_id INTEGER NOT NULL REFERENCES topic(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+               content_updated INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
+               content_size INTEGER NOT NULL,
+               content_hash TEXT NOT NULL
+       );
+       CREATE INDEX topic_content_history_topic_id_idx ON topic_content_history(topic_id);
+       CREATE INDEX topic_content_history_content_updated_idx ON topic_content_history(content_updated);
+
+       INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 3);
+COMMIT;
diff --git a/src/db/sqlite/sql/schema/1.0.3/revert.sql b/src/db/sqlite/sql/schema/1.0.3/revert.sql
new file mode 100644 (file)
index 0000000..a838ade
--- /dev/null
@@ -0,0 +1,7 @@
+BEGIN;
+       DROP INDEX topic_content_history_topic_id_idx;
+       DROP INDEX topic_content_history_content_updated_idx;
+       DROP TABLE topic_content_history;
+
+       DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 3;
+COMMIT;
index 02b89e87e65328d25d1d2d83d9b743e36e4f1ebc..dba26322081e39a3c6e3d5035efe0cf5e2191f85 100644 (file)
@@ -1,6 +1,7 @@
 --
 UPDATE subscription SET
        content_delivered = strftime('%s', 'now'),
+       latest_content_delivered = :topicContentUpdated,
        delivery_attempts_since_success = 0,
        delivery_next_attempt = 0
 WHERE
diff --git a/src/db/sqlite/sql/topic-set-content-history.sql b/src/db/sqlite/sql/topic-set-content-history.sql
new file mode 100644 (file)
index 0000000..8afe3a4
--- /dev/null
@@ -0,0 +1,5 @@
+--
+INSERT INTO topic_content_history
+       (topic_id, content_size, content_hash)
+VALUES
+       (:topicId, :contentSize, :contentHash)
index d493f8affdf83f3585973ad380c3f9a5ab8caa48..17fa134d738447deb29aa5e889aaf806cb1443f8 100644 (file)
@@ -68,6 +68,24 @@ class Service extends Dingus {
 
   }
 
+  /**
+   * Wrap the Dingus head handler, to remove the response body from the context,
+   * lest it be logged.
+   * @param {http.ClientRequest} req
+   * @param {http.ServerResponse} res
+   * @param {object} ctx
+   */
+  static setHeadHandler(req, res, ctx) {
+    if (req.method === 'HEAD') {
+      Dingus.setHeadHandler(req, res, ctx);
+      const origEnd = res.end.bind(res);
+      res.end = function (data, encoding, ...rest) {
+        const origResult = origEnd(data, encoding, ...rest);
+        delete ctx.responseBody;
+        return origResult;
+      };
+    }
+  }
 
   /**
    * @param {http.ClientRequest} req
@@ -97,7 +115,7 @@ class Service extends Dingus {
     ];
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Dingus.setHeadHandler(req, res, ctx);
+    Service.setHeadHandler(req, res, ctx);
 
     this.setResponseType(responseTypes, req, res, ctx);
 
@@ -116,7 +134,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetHealthcheck');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
   
-    Dingus.setHeadHandler(req, res, ctx);
+    Service.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
@@ -135,7 +153,7 @@ class Service extends Dingus {
 
     const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
 
-    Dingus.setHeadHandler(req, res, ctx);
+    Service.setHeadHandler(req, res, ctx);
 
     this.setResponseType(responseTypes, req, res, ctx);
 
@@ -152,7 +170,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetAdminOverview');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Dingus.setHeadHandler(req, res, ctx);
+    Service.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
@@ -171,7 +189,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetAdminTopicDetails');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Dingus.setHeadHandler(req, res, ctx);
+    Service.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
@@ -264,7 +282,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetAdminLogin');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Dingus.setHeadHandler(req, res, ctx);
+    Service.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
index 14b9d4773c7e8dfcb5e5659a1b6b2cf6ef471b73..6771fdd91b8103e6b667d4eef52daccbf8559b31 100644 (file)
@@ -3,7 +3,7 @@
 const th = require('./template-helper');
 
 function hAppSection(pageTitle, logoUrl) {
-  return `      <section class="h-app hidden">
+  return `      <section hidden class="h-app">
         <h2>h-app Information for IndieAuth Logins</h2>
         <img src="${logoUrl}" class="u-logo">
         <a href="" class="u-url p-name">${pageTitle}</a>
@@ -166,5 +166,5 @@ module.exports = (ctx, options) => {
     contactSection(contactHTML),
     hAppSection(pageTitle, options.manager.logoUrl),
   ];
-  return th.htmlPage(1, ctx, htmlOptions, content);
+  return th.htmlPage(0, ctx, htmlOptions, content);
 };
\ No newline at end of file
index 8d5b61553d24f3b72e4ec6d39fd73534de7617cd..83feac761222a620793cb6963b9a2604e3fb60f5 100644 (file)
@@ -328,7 +328,8 @@ describe('Database Integration', function () {
         step('complete subscription', async function () {
           const { callback } = testData.subscriptionUpsert;
           await db.context(async (dbCtx) => {
-            await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+            const topic = await db.topicGetById(dbCtx, topicId);
+            await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topic.contentUpdated);
             const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
             assert.strictEqual(Number(subscription.deliveryAttemptsSinceSuccess), 0);
           });
index 0f037d6fae91b6dd4eed3098626317c9797f9d79..da071f2ab28566b53b73ba3ff25858c7e96e0355 100644 (file)
@@ -227,11 +227,24 @@ describe('DatabasePostgres', function () {
     });
     it('covers migration', async function() {
       sinon.stub(db.db, 'oneOrNone').resolves({});
-      sinon.stub(db.db, 'multiResult');
-      sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.max);
+      sinon.stub(db.db, 'multiResult').resolves({});
+      sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.min);
       sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
       await db.initialize();
     });
+    it('covers migration failure', async function() {
+      const expected = new Error('oh no');
+      sinon.stub(db.db, 'oneOrNone').resolves({});
+      sinon.stub(db.db, 'multiResult').rejects(expected);
+      sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.min);
+      sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
+      try {
+        await db.initialize();
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
     it('covers listener', async function() {
       db.listener = {
         start: sinon.stub(),
@@ -659,12 +672,16 @@ describe('DatabasePostgres', function () {
   }); // subscriptionDeliveryClaimById
 
   describe('subscriptionDeliveryComplete', function () {
+    let topicContentUpdated;
+    before(function () {
+      topicContentUpdated = new Date();
+    });
     it('success', async function() {
       const dbResult = {
         rowCount: 1,
       };
       sinon.stub(db.db, 'result').resolves(dbResult);
-      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
     });
     it('failure', async function () {
       const dbResult = {
@@ -672,7 +689,7 @@ describe('DatabasePostgres', function () {
       };
       sinon.stub(db.db, 'result').onCall(0).resolves(dbResult);
       try {
-        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -687,7 +704,7 @@ describe('DatabasePostgres', function () {
       };
       sinon.stub(db.db, 'result').onCall(0).resolves(dbResult0).onCall(1).resolves(dbResult1);
       try {
-        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -1375,6 +1392,7 @@ describe('DatabasePostgres', function () {
         contentType: 'text/plain',
         contentHash: 'abc123',
       };
+      sinon.stub(db.db, 'result');
     });
     it('success', async function() {
       const dbResult = {
@@ -1387,7 +1405,7 @@ describe('DatabasePostgres', function () {
         lastInsertRowid: undefined,
         duration: 10,
       };
-      sinon.stub(db.db, 'result').resolves(dbResult);
+      db.db.result.resolves(dbResult);
       const result = await db.topicSetContent(dbCtx, data);
       assert.deepStrictEqual(result, expected);
     });
@@ -1397,7 +1415,28 @@ describe('DatabasePostgres', function () {
         rows: [],
         duration: 10,
       };
-      sinon.stub(db.db, 'result').resolves(dbResult);
+      db.db.result.resolves(dbResult);
+      try {
+        await db.topicSetContent(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('failure 2', async function () {
+      const dbResultSuccess = {
+        rowCount: 1,
+        rows: [],
+        duration: 10,
+      };
+      const dbResultFail = {
+        rowCount: 0,
+        rows: [],
+        duration: 10,
+      };
+      db.db.result
+        .onCall(0).resolves(dbResultSuccess)
+        .onCall(1).resolves(dbResultFail);
       try {
         await db.topicSetContent(dbCtx, data);
         assert.fail(noExpectedException);
index 2af1262724a5269165ae07e18e5209b85e94a343..be6399445bcb01279ca2c2b9392c58cbf94e934c 100644 (file)
@@ -66,6 +66,38 @@ describe('DatabaseSQLite', function () {
     });
   }); // Implementation
 
+  describe('_initTables', function () {
+    let preparedGet;
+    beforeEach(function () {
+      preparedGet = sinon.stub();
+      sinon.stub(db.db, 'prepare').returns({
+        pluck: () => ({
+          bind: () => ({
+            get: preparedGet,
+          }),
+        }),
+      });
+      sinon.stub(db, '_currentSchema').returns(db.schemaVersionsSupported.min);
+      sinon.stub(db.db, 'exec');
+    });
+    it('covers migration', async function() {
+      preparedGet.returns({});
+      await db._initTables();
+      assert(db.db.exec.called);
+    });
+    it('covers migration failure', async function() {
+      const expected = new Error('oh no');
+      preparedGet.returns({});
+      db.db.exec.throws(expected);
+      try {
+        await db._initTables();
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // _initTables
+
   describe('_currentSchema', function () {
     it('covers', async function () {
       const version = { major: 1, minor: 0, patch: 0 };
@@ -494,13 +526,17 @@ describe('DatabaseSQLite', function () {
   }); // subscriptionDeliveryClaimById
 
   describe('subscriptionDeliveryComplete', function () {
+    let topicContentUpdated;
+    before(function () {
+      topicContentUpdated = new Date();
+    });
     it('success', async function() {
       const dbResult = {
         changes: 1,
       };
       sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
       sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
-      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
     });
     it('failure', async function () {
       const dbResult = {
@@ -509,7 +545,7 @@ describe('DatabaseSQLite', function () {
       sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
       sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
       try {
-        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -525,7 +561,7 @@ describe('DatabaseSQLite', function () {
       sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult0);
       sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult1);
       try {
-        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -1184,6 +1220,8 @@ describe('DatabaseSQLite', function () {
         contentType: 'text/plain',
         contentHash: 'abc123',
       };
+      sinon.stub(db.statement.topicSetContent, 'run');
+      sinon.stub(db.statement.topicSetContentHistory, 'run');
     });
     it('success', async function() {
       const dbResult = {
@@ -1194,7 +1232,8 @@ describe('DatabaseSQLite', function () {
         changes: 1,
         lastInsertRowid: undefined,
       };
-      sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+      db.statement.topicSetContent.run.returns(dbResult);
+      db.statement.topicSetContentHistory.run.returns(dbResult);
       const result = await db.topicSetContent(dbCtx, data);
       assert.deepStrictEqual(result, expected);
     });
@@ -1203,7 +1242,25 @@ describe('DatabaseSQLite', function () {
         changes: 0,
         lastInsertRowid: undefined,
       };
-      sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+      db.statement.topicSetContent.run.returns(dbResult);
+      try {
+        await db.topicSetContent(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('failure 2', async function () {
+      const dbResultSuccess = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResultFail = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      db.statement.topicSetContent.run.returns(dbResultSuccess);
+      db.statement.topicSetContentHistory.run.returns(dbResultFail);
       try {
         await db.topicSetContent(dbCtx, data);
         assert.fail(noExpectedException);
index 23b50e15eb0a970e30a7f8bc00bd558d3d64e150..cafe1d16922ff9e4e0f48b19921a572e39d7ea2f 100644 (file)
@@ -61,6 +61,20 @@ describe('Service', function () {
     });
   }); // maybeIngestBody
 
+  describe('setHeadHandler', function () {
+    it('covers', function () {
+      const origEnd = res.end;
+      sinon.stub(Service.__proto__, 'setHeadHandler');
+      ctx.responseBody = 'data';
+      req.method = 'HEAD';
+      Service.setHeadHandler(req, res, ctx);
+      res.end('foop');
+      assert(Service.__proto__.setHeadHandler.called);
+      assert(origEnd.called);
+      assert(!('responseBody' in ctx));
+    });
+  }); // setHeadHandler
+
   describe('handlerPostRoot', function () {
     it('covers public mode', async function () {
       await service.handlerPostRoot(req, res, ctx);