Releases and notable changes to this project are documented here.
+## [v1.3.6] - TBD
+
+### Added
+
+- Recent history of topic publish updates are now displayed on topic details page.
+
+### Fixed
+
+- Depdency updates.
+
## [v1.3.5] - 2022-02-23
### Added
---
-[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.5
+[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.6
+[v1.3.6]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.6;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
Any tasks in progress (notably: fetching new topic content, distributing that content to subscribers, or confirming pending verifications) are doled out and managed by a cooperative advisory locking mechanism. The task queue is wrangled in the database within the `*_in_progress` tables.
+![Entity relationship diagram for Postgres engine](./documentation/media/postgres-er.svg)
+
A Hub node will periodically check for more tasks to perform, executing them up to a set concurrency limit.
### Quirks
'<a href="https://git.squeep.com/?p=websub-hub;a=tree">Development Repository</a> / <a href="https://github.com/thylacine/websub-hub/">GitHub mirror</a>',
'<span class="copyright">©<time datetime="2022">ⅯⅯⅩⅩⅠⅠ</time></span>',
],
+ publishHistoryDays: 60, // Number of days of update history to show on topic details page
strictSecrets: false, // If true, reject requests with secrets but not over https
publicHub: true, // Accept publish requests as new topics.
processImmediately: true, // If true, immediately attempt to process requests when accepted.
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.49.0 (20210828.1703)
+ -->
+<!-- Title: WebsubHubERD Pages: 1 -->
+<svg width="922pt" height="1084pt"
+ viewBox="0.00 0.00 922.00 1083.63" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1079.63)">
+<title>WebsubHubERD</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
+<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity-Relations</text>
+<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">Postgres</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.3</text>
+<!-- topic -->
+<g id="node1" class="node">
+<title>topic</title>
+<polygon fill="lightblue" stroke="transparent" points="0,-665.13 0,-688.13 280,-688.13 280,-665.13 0,-665.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="1,-666.13 1,-687.13 279,-687.13 279,-666.13 1,-666.13"/>
+<text text-anchor="start" x="117" y="-672.93" font-family="Times,serif" font-size="14.00">TOPIC</text>
+<polygon fill="none" stroke="black" points="0,-644.13 0,-665.13 280,-665.13 280,-644.13 0,-644.13"/>
+<text text-anchor="start" x="133" y="-650.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="0,-623.13 0,-644.13 280,-644.13 280,-623.13 0,-623.13"/>
+<text text-anchor="start" x="112.5" y="-629.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="0,-602.13 0,-623.13 280,-623.13 280,-602.13 0,-602.13"/>
+<text text-anchor="start" x="129.5" y="-608.93" font-family="Times,serif" font-size="14.00">url</text>
+<polygon fill="none" stroke="black" points="0,-581.13 0,-602.13 280,-602.13 280,-581.13 0,-581.13"/>
+<text text-anchor="start" x="52" y="-587.93" font-family="Times,serif" font-size="14.00">lease_seconds_preferred</text>
+<polygon fill="none" stroke="black" points="0,-560.13 0,-581.13 280,-581.13 280,-560.13 0,-560.13"/>
+<text text-anchor="start" x="72.5" y="-566.93" font-family="Times,serif" font-size="14.00">lease_seconds_min</text>
+<polygon fill="none" stroke="black" points="0,-539.13 0,-560.13 280,-560.13 280,-539.13 0,-539.13"/>
+<text text-anchor="start" x="71" y="-545.93" font-family="Times,serif" font-size="14.00">lease_seconds_max</text>
+<polygon fill="none" stroke="black" points="0,-518.13 0,-539.13 280,-539.13 280,-518.13 0,-518.13"/>
+<text text-anchor="start" x="54" y="-524.93" font-family="Times,serif" font-size="14.00">publisher_validation_url</text>
+<polygon fill="none" stroke="black" points="0,-497.13 0,-518.13 280,-518.13 280,-497.13 0,-497.13"/>
+<text text-anchor="start" x="54" y="-503.93" font-family="Times,serif" font-size="14.00">content_hash_algorithm</text>
+<polygon fill="none" stroke="black" points="0,-476.13 0,-497.13 280,-497.13 280,-476.13 0,-476.13"/>
+<text text-anchor="start" x="109" y="-482.93" font-family="Times,serif" font-size="14.00">is_active</text>
+<polygon fill="none" stroke="black" points="0,-455.13 0,-476.13 280,-476.13 280,-455.13 0,-455.13"/>
+<text text-anchor="start" x="103.5" y="-461.93" font-family="Times,serif" font-size="14.00">is_deleted</text>
+<polygon fill="none" stroke="black" points="0,-434.13 0,-455.13 280,-455.13 280,-434.13 0,-434.13"/>
+<text text-anchor="start" x="97" y="-440.93" font-family="Times,serif" font-size="14.00">last_publish</text>
+<polygon fill="none" stroke="black" points="0,-413.13 0,-434.13 280,-434.13 280,-413.13 0,-413.13"/>
+<text text-anchor="start" x="40.5" y="-419.93" font-family="Times,serif" font-size="14.00">content_fetch_next_attempt</text>
+<polygon fill="none" stroke="black" points="0,-392.13 0,-413.13 280,-413.13 280,-392.13 0,-392.13"/>
+<text text-anchor="start" x="3" y="-398.93" font-family="Times,serif" font-size="14.00">content_fetch_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="0,-371.13 0,-392.13 280,-392.13 280,-371.13 0,-371.13"/>
+<text text-anchor="start" x="80" y="-377.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="0,-350.13 0,-371.13 280,-371.13 280,-350.13 0,-350.13"/>
+<text text-anchor="start" x="112.5" y="-356.93" font-family="Times,serif" font-size="14.00">content</text>
+<polygon fill="none" stroke="black" points="0,-329.13 0,-350.13 280,-350.13 280,-329.13 0,-329.13"/>
+<text text-anchor="start" x="92.5" y="-335.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+<polygon fill="none" stroke="black" points="0,-308.13 0,-329.13 280,-329.13 280,-308.13 0,-308.13"/>
+<text text-anchor="start" x="93.5" y="-314.93" font-family="Times,serif" font-size="14.00">content_type</text>
+</g>
+<!-- topic_fetch_in_progress -->
+<g id="node2" class="node">
+<title>topic_fetch_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="323.5,-957.13 323.5,-980.13 550.5,-980.13 550.5,-957.13 323.5,-957.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="324.5,-958.13 324.5,-979.13 549.5,-979.13 549.5,-958.13 324.5,-958.13"/>
+<text text-anchor="start" x="327.5" y="-964.93" font-family="Times,serif" font-size="14.00">TOPIC_FETCH_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="323.5,-936.13 323.5,-957.13 550.5,-957.13 550.5,-936.13 323.5,-936.13"/>
+<text text-anchor="start" x="430" y="-942.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="323.5,-915.13 323.5,-936.13 550.5,-936.13 550.5,-915.13 323.5,-915.13"/>
+<text text-anchor="start" x="406" y="-921.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="323.5,-894.13 323.5,-915.13 550.5,-915.13 550.5,-894.13 323.5,-894.13"/>
+<text text-anchor="start" x="409" y="-900.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="323.5,-873.13 323.5,-894.13 550.5,-894.13 550.5,-873.13 323.5,-873.13"/>
+<text text-anchor="start" x="388" y="-879.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic->topic_fetch_in_progress -->
+<g id="edge1" class="edge">
+<title>topic:pk_id->topic_fetch_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C341.86,-655.13 266.47,-914.99 312.79,-944.43"/>
+<polygon fill="black" stroke="black" points="312.87,-944.45 321.29,-951.47 317.68,-945.79 322.5,-947.13 322.5,-947.13 322.5,-947.13 317.68,-945.79 323.71,-942.8 312.87,-944.45 312.87,-944.45"/>
+</g>
+<!-- topic_content_history -->
+<g id="node3" class="node">
+<title>topic_content_history</title>
+<polygon fill="lightblue" stroke="transparent" points="332.5,-832.13 332.5,-855.13 541.5,-855.13 541.5,-832.13 332.5,-832.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="333.5,-833.13 333.5,-854.13 540.5,-854.13 540.5,-833.13 333.5,-833.13"/>
+<text text-anchor="start" x="336.5" y="-839.93" font-family="Times,serif" font-size="14.00">TOPIC_CONTENT_HISTORY</text>
+<polygon fill="none" stroke="black" points="332.5,-811.13 332.5,-832.13 541.5,-832.13 541.5,-811.13 332.5,-811.13"/>
+<text text-anchor="start" x="409" y="-817.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="332.5,-790.13 332.5,-811.13 541.5,-811.13 541.5,-790.13 332.5,-790.13"/>
+<text text-anchor="start" x="377" y="-796.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="332.5,-769.13 332.5,-790.13 541.5,-790.13 541.5,-769.13 332.5,-769.13"/>
+<text text-anchor="start" x="392.5" y="-775.93" font-family="Times,serif" font-size="14.00">content_size</text>
+<polygon fill="none" stroke="black" points="332.5,-748.13 332.5,-769.13 541.5,-769.13 541.5,-748.13 332.5,-748.13"/>
+<text text-anchor="start" x="389.5" y="-754.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+</g>
+<!-- topic->topic_content_history -->
+<g id="edge2" class="edge">
+<title>topic:pk_id->topic_content_history:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C353.88,-655.13 263.54,-806.22 321.38,-820.98"/>
+<polygon fill="black" stroke="black" points="321.56,-821 330.99,-826.6 326.53,-821.56 331.5,-822.13 331.5,-822.13 331.5,-822.13 326.53,-821.56 332.01,-817.66 321.56,-821 321.56,-821"/>
+</g>
+<!-- subscription -->
+<g id="node4" class="node">
+<title>subscription</title>
+<polygon fill="lightblue" stroke="transparent" points="316.5,-707.13 316.5,-730.13 557.5,-730.13 557.5,-707.13 316.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="317.5,-708.13 317.5,-729.13 556.5,-729.13 556.5,-708.13 317.5,-708.13"/>
+<text text-anchor="start" x="379" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION</text>
+<polygon fill="none" stroke="black" points="316.5,-686.13 316.5,-707.13 557.5,-707.13 557.5,-686.13 316.5,-686.13"/>
+<text text-anchor="start" x="430" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="316.5,-665.13 316.5,-686.13 557.5,-686.13 557.5,-665.13 316.5,-665.13"/>
+<text text-anchor="start" x="409.5" y="-671.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="316.5,-644.13 316.5,-665.13 557.5,-665.13 557.5,-644.13 316.5,-644.13"/>
+<text text-anchor="start" x="409" y="-650.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="316.5,-623.13 316.5,-644.13 557.5,-644.13 557.5,-623.13 316.5,-623.13"/>
+<text text-anchor="start" x="408" y="-629.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="316.5,-602.13 316.5,-623.13 557.5,-623.13 557.5,-602.13 316.5,-602.13"/>
+<text text-anchor="start" x="409.5" y="-608.93" font-family="Times,serif" font-size="14.00">verified</text>
+<polygon fill="none" stroke="black" points="316.5,-581.13 316.5,-602.13 557.5,-602.13 557.5,-581.13 316.5,-581.13"/>
+<text text-anchor="start" x="410.5" y="-587.93" font-family="Times,serif" font-size="14.00">expires</text>
+<polygon fill="none" stroke="black" points="316.5,-560.13 316.5,-581.13 557.5,-581.13 557.5,-560.13 316.5,-560.13"/>
+<text text-anchor="start" x="414.5" y="-566.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="316.5,-539.13 316.5,-560.13 557.5,-560.13 557.5,-539.13 316.5,-539.13"/>
+<text text-anchor="start" x="364.5" y="-545.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="316.5,-518.13 316.5,-539.13 557.5,-539.13 557.5,-518.13 316.5,-518.13"/>
+<text text-anchor="start" x="373" y="-524.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="316.5,-497.13 316.5,-518.13 557.5,-518.13 557.5,-497.13 316.5,-497.13"/>
+<text text-anchor="start" x="401.5" y="-503.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="316.5,-476.13 316.5,-497.13 557.5,-497.13 557.5,-476.13 316.5,-476.13"/>
+<text text-anchor="start" x="373" y="-482.93" font-family="Times,serif" font-size="14.00">content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-455.13 316.5,-476.13 557.5,-476.13 557.5,-455.13 316.5,-455.13"/>
+<text text-anchor="start" x="349.5" y="-461.93" font-family="Times,serif" font-size="14.00">latest_content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-434.13 316.5,-455.13 557.5,-455.13 557.5,-434.13 316.5,-434.13"/>
+<text text-anchor="start" x="319.5" y="-440.93" font-family="Times,serif" font-size="14.00">delivery_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="316.5,-413.13 316.5,-434.13 557.5,-434.13 557.5,-413.13 316.5,-413.13"/>
+<text text-anchor="start" x="357" y="-419.93" font-family="Times,serif" font-size="14.00">delivery_next_attempt</text>
+</g>
+<!-- topic->subscription -->
+<g id="edge3" class="edge">
+<title>topic:pk_id->subscription:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C292,-655.13 297.25,-655.13 305.88,-655.13"/>
+<polygon fill="black" stroke="black" points="306,-655.13 316,-659.63 311,-655.13 316,-655.13 316,-655.13 316,-655.13 311,-655.13 316,-650.63 306,-655.13 306,-655.13"/>
+</g>
+<!-- verification -->
+<g id="node6" class="node">
+<title>verification</title>
+<polygon fill="lightblue" stroke="transparent" points="353.5,-372.13 353.5,-395.13 519.5,-395.13 519.5,-372.13 353.5,-372.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="354.5,-373.13 354.5,-394.13 518.5,-394.13 518.5,-373.13 354.5,-373.13"/>
+<text text-anchor="start" x="382" y="-379.93" font-family="Times,serif" font-size="14.00">VERIFICATION</text>
+<polygon fill="none" stroke="black" points="353.5,-351.13 353.5,-372.13 519.5,-372.13 519.5,-351.13 353.5,-351.13"/>
+<text text-anchor="start" x="429.5" y="-357.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="353.5,-330.13 353.5,-351.13 519.5,-351.13 519.5,-330.13 353.5,-330.13"/>
+<text text-anchor="start" x="409" y="-336.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="353.5,-309.13 353.5,-330.13 519.5,-330.13 519.5,-309.13 353.5,-309.13"/>
+<text text-anchor="start" x="408.5" y="-315.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="353.5,-288.13 353.5,-309.13 519.5,-309.13 519.5,-288.13 353.5,-288.13"/>
+<text text-anchor="start" x="407.5" y="-294.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="353.5,-267.13 353.5,-288.13 519.5,-288.13 519.5,-267.13 353.5,-267.13"/>
+<text text-anchor="start" x="414" y="-273.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="353.5,-246.13 353.5,-267.13 519.5,-267.13 519.5,-246.13 353.5,-246.13"/>
+<text text-anchor="start" x="364" y="-252.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="353.5,-225.13 353.5,-246.13 519.5,-246.13 519.5,-225.13 353.5,-225.13"/>
+<text text-anchor="start" x="372.5" y="-231.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="353.5,-204.13 353.5,-225.13 519.5,-225.13 519.5,-204.13 353.5,-204.13"/>
+<text text-anchor="start" x="401" y="-210.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="353.5,-183.13 353.5,-204.13 519.5,-204.13 519.5,-183.13 353.5,-183.13"/>
+<text text-anchor="start" x="416.5" y="-189.93" font-family="Times,serif" font-size="14.00">mode</text>
+<polygon fill="none" stroke="black" points="353.5,-162.13 353.5,-183.13 519.5,-183.13 519.5,-162.13 353.5,-162.13"/>
+<text text-anchor="start" x="412" y="-168.93" font-family="Times,serif" font-size="14.00">reason</text>
+<polygon fill="none" stroke="black" points="353.5,-141.13 353.5,-162.13 519.5,-162.13 519.5,-141.13 353.5,-141.13"/>
+<text text-anchor="start" x="386" y="-147.93" font-family="Times,serif" font-size="14.00">lease_seconds</text>
+<polygon fill="none" stroke="black" points="353.5,-120.13 353.5,-141.13 519.5,-141.13 519.5,-120.13 353.5,-120.13"/>
+<text text-anchor="start" x="356.5" y="-126.93" font-family="Times,serif" font-size="14.00">is_publisher_validated</text>
+<polygon fill="none" stroke="black" points="353.5,-99.13 353.5,-120.13 519.5,-120.13 519.5,-99.13 353.5,-99.13"/>
+<text text-anchor="start" x="398.5" y="-105.93" font-family="Times,serif" font-size="14.00">request_id</text>
+<polygon fill="none" stroke="black" points="353.5,-78.13 353.5,-99.13 519.5,-99.13 519.5,-78.13 353.5,-78.13"/>
+<text text-anchor="start" x="404" y="-84.93" font-family="Times,serif" font-size="14.00">attempts</text>
+<polygon fill="none" stroke="black" points="353.5,-57.13 353.5,-78.13 519.5,-78.13 519.5,-57.13 353.5,-57.13"/>
+<text text-anchor="start" x="388.5" y="-63.93" font-family="Times,serif" font-size="14.00">next_attempt</text>
+</g>
+<!-- topic->verification -->
+<g id="edge5" class="edge">
+<title>topic:pk_id->verification:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C336.13,-655.13 300.8,-459.16 316,-405.13 326.09,-369.27 314.1,-328.6 342.37,-321.29"/>
+<polygon fill="black" stroke="black" points="342.56,-321.26 353.01,-324.6 347.53,-320.7 352.5,-320.13 352.5,-320.13 352.5,-320.13 347.53,-320.7 351.99,-315.66 342.56,-321.26 342.56,-321.26"/>
+</g>
+<!-- verification_in_progress -->
+<g id="node7" class="node">
+<title>verification_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="638.5,-216.13 638.5,-239.13 869.5,-239.13 869.5,-216.13 638.5,-216.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="639.5,-217.13 639.5,-238.13 868.5,-238.13 868.5,-217.13 639.5,-217.13"/>
+<text text-anchor="start" x="642.5" y="-223.93" font-family="Times,serif" font-size="14.00">VERIFICATION_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="638.5,-195.13 638.5,-216.13 869.5,-216.13 869.5,-195.13 638.5,-195.13"/>
+<text text-anchor="start" x="747" y="-201.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="638.5,-174.13 638.5,-195.13 869.5,-195.13 869.5,-174.13 638.5,-174.13"/>
+<text text-anchor="start" x="726" y="-180.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="638.5,-153.13 638.5,-174.13 869.5,-174.13 869.5,-153.13 638.5,-153.13"/>
+<text text-anchor="start" x="725" y="-159.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="638.5,-132.13 638.5,-153.13 869.5,-153.13 869.5,-132.13 638.5,-132.13"/>
+<text text-anchor="start" x="723" y="-138.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="638.5,-111.13 638.5,-132.13 869.5,-132.13 869.5,-111.13 638.5,-111.13"/>
+<text text-anchor="start" x="726" y="-117.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="638.5,-90.13 638.5,-111.13 869.5,-111.13 869.5,-90.13 638.5,-90.13"/>
+<text text-anchor="start" x="705" y="-96.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic->verification_in_progress -->
+<g id="edge7" class="edge">
+<title>topic:pk_id->verification_in_progress:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C347.56,-655.13 266.83,-94.47 316,-48.13 393.95,25.33 464.03,5.07 557,-48.13 615.06,-81.36 571.99,-174.57 627.31,-184.31"/>
+<polygon fill="black" stroke="black" points="627.53,-184.32 637.14,-189.62 632.52,-184.73 637.5,-185.13 637.5,-185.13 637.5,-185.13 632.52,-184.73 637.86,-180.65 627.53,-184.32 627.53,-184.32"/>
+</g>
+<!-- subscription_delivery_in_progress -->
+<g id="node5" class="node">
+<title>subscription_delivery_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="593.5,-707.13 593.5,-730.13 914.5,-730.13 914.5,-707.13 593.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="594.5,-708.13 594.5,-729.13 913.5,-729.13 913.5,-708.13 594.5,-708.13"/>
+<text text-anchor="start" x="597.5" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION_DELIVERY_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="593.5,-686.13 593.5,-707.13 914.5,-707.13 914.5,-686.13 593.5,-686.13"/>
+<text text-anchor="start" x="747" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="593.5,-665.13 593.5,-686.13 914.5,-686.13 914.5,-665.13 593.5,-665.13"/>
+<text text-anchor="start" x="723" y="-671.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="593.5,-644.13 593.5,-665.13 914.5,-665.13 914.5,-644.13 593.5,-644.13"/>
+<text text-anchor="start" x="726" y="-650.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="593.5,-623.13 593.5,-644.13 914.5,-644.13 914.5,-623.13 593.5,-623.13"/>
+<text text-anchor="start" x="705" y="-629.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- subscription->subscription_delivery_in_progress -->
+<g id="edge4" class="edge">
+<title>subscription:pk_id->subscription_delivery_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M557,-697.13C569,-697.13 574.25,-697.13 582.88,-697.13"/>
+<polygon fill="black" stroke="black" points="583,-697.13 593,-701.63 588,-697.13 593,-697.13 593,-697.13 593,-697.13 588,-697.13 593,-692.63 583,-697.13 583,-697.13"/>
+</g>
+<!-- verification->verification_in_progress -->
+<g id="edge6" class="edge">
+<title>verification:pk_id->verification_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M520.5,-362.13C603.44,-362.13 555.41,-219.25 627.1,-206.97"/>
+<polygon fill="black" stroke="black" points="627.53,-206.94 637.86,-210.62 632.52,-206.53 637.5,-206.13 637.5,-206.13 637.5,-206.13 632.52,-206.53 637.14,-201.65 627.53,-206.94 627.53,-206.94"/>
+</g>
+<!-- authentication -->
+<g id="node8" class="node">
+<title>authentication</title>
+<polygon fill="lightblue" stroke="transparent" points="67,-790.13 67,-813.13 213,-813.13 213,-790.13 67,-790.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="68,-791.13 68,-812.13 212,-812.13 212,-791.13 68,-791.13"/>
+<text text-anchor="start" x="71" y="-797.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="none" stroke="black" points="67,-769.13 67,-790.13 213,-790.13 213,-769.13 67,-769.13"/>
+<text text-anchor="start" x="112.5" y="-775.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="67,-748.13 67,-769.13 213,-769.13 213,-748.13 67,-748.13"/>
+<text text-anchor="start" x="73.5" y="-754.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
+<polygon fill="none" stroke="black" points="67,-727.13 67,-748.13 213,-748.13 213,-727.13 67,-727.13"/>
+<text text-anchor="start" x="106.5" y="-733.93" font-family="Times,serif" font-size="14.00">identifier</text>
+<polygon fill="none" stroke="black" points="67,-706.13 67,-727.13 213,-727.13 213,-706.13 67,-706.13"/>
+<text text-anchor="start" x="103.5" y="-712.93" font-family="Times,serif" font-size="14.00">credential</text>
+</g>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.49.0 (20210828.1703)
+ -->
+<!-- Title: WebsubHubERD Pages: 1 -->
+<svg width="922pt" height="1084pt"
+ viewBox="0.00 0.00 922.00 1083.63" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1079.63)">
+<title>WebsubHubERD</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
+<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity-Relations</text>
+<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">SQLite</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.3</text>
+<!-- topic -->
+<g id="node1" class="node">
+<title>topic</title>
+<polygon fill="lightblue" stroke="transparent" points="0,-665.13 0,-688.13 280,-688.13 280,-665.13 0,-665.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="1,-666.13 1,-687.13 279,-687.13 279,-666.13 1,-666.13"/>
+<text text-anchor="start" x="117" y="-672.93" font-family="Times,serif" font-size="14.00">TOPIC</text>
+<polygon fill="none" stroke="black" points="0,-644.13 0,-665.13 280,-665.13 280,-644.13 0,-644.13"/>
+<text text-anchor="start" x="133" y="-650.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="0,-623.13 0,-644.13 280,-644.13 280,-623.13 0,-623.13"/>
+<text text-anchor="start" x="112.5" y="-629.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="0,-602.13 0,-623.13 280,-623.13 280,-602.13 0,-602.13"/>
+<text text-anchor="start" x="129.5" y="-608.93" font-family="Times,serif" font-size="14.00">url</text>
+<polygon fill="none" stroke="black" points="0,-581.13 0,-602.13 280,-602.13 280,-581.13 0,-581.13"/>
+<text text-anchor="start" x="52" y="-587.93" font-family="Times,serif" font-size="14.00">lease_seconds_preferred</text>
+<polygon fill="none" stroke="black" points="0,-560.13 0,-581.13 280,-581.13 280,-560.13 0,-560.13"/>
+<text text-anchor="start" x="72.5" y="-566.93" font-family="Times,serif" font-size="14.00">lease_seconds_min</text>
+<polygon fill="none" stroke="black" points="0,-539.13 0,-560.13 280,-560.13 280,-539.13 0,-539.13"/>
+<text text-anchor="start" x="71" y="-545.93" font-family="Times,serif" font-size="14.00">lease_seconds_max</text>
+<polygon fill="none" stroke="black" points="0,-518.13 0,-539.13 280,-539.13 280,-518.13 0,-518.13"/>
+<text text-anchor="start" x="54" y="-524.93" font-family="Times,serif" font-size="14.00">publisher_validation_url</text>
+<polygon fill="none" stroke="black" points="0,-497.13 0,-518.13 280,-518.13 280,-497.13 0,-497.13"/>
+<text text-anchor="start" x="54" y="-503.93" font-family="Times,serif" font-size="14.00">content_hash_algorithm</text>
+<polygon fill="none" stroke="black" points="0,-476.13 0,-497.13 280,-497.13 280,-476.13 0,-476.13"/>
+<text text-anchor="start" x="109" y="-482.93" font-family="Times,serif" font-size="14.00">is_active</text>
+<polygon fill="none" stroke="black" points="0,-455.13 0,-476.13 280,-476.13 280,-455.13 0,-455.13"/>
+<text text-anchor="start" x="103.5" y="-461.93" font-family="Times,serif" font-size="14.00">is_deleted</text>
+<polygon fill="none" stroke="black" points="0,-434.13 0,-455.13 280,-455.13 280,-434.13 0,-434.13"/>
+<text text-anchor="start" x="97" y="-440.93" font-family="Times,serif" font-size="14.00">last_publish</text>
+<polygon fill="none" stroke="black" points="0,-413.13 0,-434.13 280,-434.13 280,-413.13 0,-413.13"/>
+<text text-anchor="start" x="40.5" y="-419.93" font-family="Times,serif" font-size="14.00">content_fetch_next_attempt</text>
+<polygon fill="none" stroke="black" points="0,-392.13 0,-413.13 280,-413.13 280,-392.13 0,-392.13"/>
+<text text-anchor="start" x="3" y="-398.93" font-family="Times,serif" font-size="14.00">content_fetch_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="0,-371.13 0,-392.13 280,-392.13 280,-371.13 0,-371.13"/>
+<text text-anchor="start" x="80" y="-377.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="0,-350.13 0,-371.13 280,-371.13 280,-350.13 0,-350.13"/>
+<text text-anchor="start" x="112.5" y="-356.93" font-family="Times,serif" font-size="14.00">content</text>
+<polygon fill="none" stroke="black" points="0,-329.13 0,-350.13 280,-350.13 280,-329.13 0,-329.13"/>
+<text text-anchor="start" x="92.5" y="-335.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+<polygon fill="none" stroke="black" points="0,-308.13 0,-329.13 280,-329.13 280,-308.13 0,-308.13"/>
+<text text-anchor="start" x="93.5" y="-314.93" font-family="Times,serif" font-size="14.00">content_type</text>
+</g>
+<!-- topic_fetch_in_progress -->
+<g id="node2" class="node">
+<title>topic_fetch_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="323.5,-957.13 323.5,-980.13 550.5,-980.13 550.5,-957.13 323.5,-957.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="324.5,-958.13 324.5,-979.13 549.5,-979.13 549.5,-958.13 324.5,-958.13"/>
+<text text-anchor="start" x="327.5" y="-964.93" font-family="Times,serif" font-size="14.00">TOPIC_FETCH_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="323.5,-936.13 323.5,-957.13 550.5,-957.13 550.5,-936.13 323.5,-936.13"/>
+<text text-anchor="start" x="430" y="-942.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="323.5,-915.13 323.5,-936.13 550.5,-936.13 550.5,-915.13 323.5,-915.13"/>
+<text text-anchor="start" x="406" y="-921.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="323.5,-894.13 323.5,-915.13 550.5,-915.13 550.5,-894.13 323.5,-894.13"/>
+<text text-anchor="start" x="409" y="-900.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="323.5,-873.13 323.5,-894.13 550.5,-894.13 550.5,-873.13 323.5,-873.13"/>
+<text text-anchor="start" x="388" y="-879.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic->topic_fetch_in_progress -->
+<g id="edge1" class="edge">
+<title>topic:pk_id->topic_fetch_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C341.86,-655.13 266.47,-914.99 312.79,-944.43"/>
+<polygon fill="black" stroke="black" points="312.87,-944.45 321.29,-951.47 317.68,-945.79 322.5,-947.13 322.5,-947.13 322.5,-947.13 317.68,-945.79 323.71,-942.8 312.87,-944.45 312.87,-944.45"/>
+</g>
+<!-- topic_content_history -->
+<g id="node3" class="node">
+<title>topic_content_history</title>
+<polygon fill="lightblue" stroke="transparent" points="332.5,-832.13 332.5,-855.13 541.5,-855.13 541.5,-832.13 332.5,-832.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="333.5,-833.13 333.5,-854.13 540.5,-854.13 540.5,-833.13 333.5,-833.13"/>
+<text text-anchor="start" x="336.5" y="-839.93" font-family="Times,serif" font-size="14.00">TOPIC_CONTENT_HISTORY</text>
+<polygon fill="none" stroke="black" points="332.5,-811.13 332.5,-832.13 541.5,-832.13 541.5,-811.13 332.5,-811.13"/>
+<text text-anchor="start" x="409" y="-817.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="332.5,-790.13 332.5,-811.13 541.5,-811.13 541.5,-790.13 332.5,-790.13"/>
+<text text-anchor="start" x="377" y="-796.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="332.5,-769.13 332.5,-790.13 541.5,-790.13 541.5,-769.13 332.5,-769.13"/>
+<text text-anchor="start" x="392.5" y="-775.93" font-family="Times,serif" font-size="14.00">content_size</text>
+<polygon fill="none" stroke="black" points="332.5,-748.13 332.5,-769.13 541.5,-769.13 541.5,-748.13 332.5,-748.13"/>
+<text text-anchor="start" x="389.5" y="-754.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+</g>
+<!-- topic->topic_content_history -->
+<g id="edge2" class="edge">
+<title>topic:pk_id->topic_content_history:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C353.88,-655.13 263.54,-806.22 321.38,-820.98"/>
+<polygon fill="black" stroke="black" points="321.56,-821 330.99,-826.6 326.53,-821.56 331.5,-822.13 331.5,-822.13 331.5,-822.13 326.53,-821.56 332.01,-817.66 321.56,-821 321.56,-821"/>
+</g>
+<!-- subscription -->
+<g id="node4" class="node">
+<title>subscription</title>
+<polygon fill="lightblue" stroke="transparent" points="316.5,-707.13 316.5,-730.13 557.5,-730.13 557.5,-707.13 316.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="317.5,-708.13 317.5,-729.13 556.5,-729.13 556.5,-708.13 317.5,-708.13"/>
+<text text-anchor="start" x="379" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION</text>
+<polygon fill="none" stroke="black" points="316.5,-686.13 316.5,-707.13 557.5,-707.13 557.5,-686.13 316.5,-686.13"/>
+<text text-anchor="start" x="430" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="316.5,-665.13 316.5,-686.13 557.5,-686.13 557.5,-665.13 316.5,-665.13"/>
+<text text-anchor="start" x="409.5" y="-671.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="316.5,-644.13 316.5,-665.13 557.5,-665.13 557.5,-644.13 316.5,-644.13"/>
+<text text-anchor="start" x="409" y="-650.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="316.5,-623.13 316.5,-644.13 557.5,-644.13 557.5,-623.13 316.5,-623.13"/>
+<text text-anchor="start" x="408" y="-629.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="316.5,-602.13 316.5,-623.13 557.5,-623.13 557.5,-602.13 316.5,-602.13"/>
+<text text-anchor="start" x="409.5" y="-608.93" font-family="Times,serif" font-size="14.00">verified</text>
+<polygon fill="none" stroke="black" points="316.5,-581.13 316.5,-602.13 557.5,-602.13 557.5,-581.13 316.5,-581.13"/>
+<text text-anchor="start" x="410.5" y="-587.93" font-family="Times,serif" font-size="14.00">expires</text>
+<polygon fill="none" stroke="black" points="316.5,-560.13 316.5,-581.13 557.5,-581.13 557.5,-560.13 316.5,-560.13"/>
+<text text-anchor="start" x="414.5" y="-566.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="316.5,-539.13 316.5,-560.13 557.5,-560.13 557.5,-539.13 316.5,-539.13"/>
+<text text-anchor="start" x="364.5" y="-545.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="316.5,-518.13 316.5,-539.13 557.5,-539.13 557.5,-518.13 316.5,-518.13"/>
+<text text-anchor="start" x="373" y="-524.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="316.5,-497.13 316.5,-518.13 557.5,-518.13 557.5,-497.13 316.5,-497.13"/>
+<text text-anchor="start" x="401.5" y="-503.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="316.5,-476.13 316.5,-497.13 557.5,-497.13 557.5,-476.13 316.5,-476.13"/>
+<text text-anchor="start" x="373" y="-482.93" font-family="Times,serif" font-size="14.00">content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-455.13 316.5,-476.13 557.5,-476.13 557.5,-455.13 316.5,-455.13"/>
+<text text-anchor="start" x="349.5" y="-461.93" font-family="Times,serif" font-size="14.00">latest_content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-434.13 316.5,-455.13 557.5,-455.13 557.5,-434.13 316.5,-434.13"/>
+<text text-anchor="start" x="319.5" y="-440.93" font-family="Times,serif" font-size="14.00">delivery_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="316.5,-413.13 316.5,-434.13 557.5,-434.13 557.5,-413.13 316.5,-413.13"/>
+<text text-anchor="start" x="357" y="-419.93" font-family="Times,serif" font-size="14.00">delivery_next_attempt</text>
+</g>
+<!-- topic->subscription -->
+<g id="edge3" class="edge">
+<title>topic:pk_id->subscription:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C292,-655.13 297.25,-655.13 305.88,-655.13"/>
+<polygon fill="black" stroke="black" points="306,-655.13 316,-659.63 311,-655.13 316,-655.13 316,-655.13 316,-655.13 311,-655.13 316,-650.63 306,-655.13 306,-655.13"/>
+</g>
+<!-- verification -->
+<g id="node6" class="node">
+<title>verification</title>
+<polygon fill="lightblue" stroke="transparent" points="353.5,-372.13 353.5,-395.13 519.5,-395.13 519.5,-372.13 353.5,-372.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="354.5,-373.13 354.5,-394.13 518.5,-394.13 518.5,-373.13 354.5,-373.13"/>
+<text text-anchor="start" x="382" y="-379.93" font-family="Times,serif" font-size="14.00">VERIFICATION</text>
+<polygon fill="none" stroke="black" points="353.5,-351.13 353.5,-372.13 519.5,-372.13 519.5,-351.13 353.5,-351.13"/>
+<text text-anchor="start" x="429.5" y="-357.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="353.5,-330.13 353.5,-351.13 519.5,-351.13 519.5,-330.13 353.5,-330.13"/>
+<text text-anchor="start" x="409" y="-336.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="353.5,-309.13 353.5,-330.13 519.5,-330.13 519.5,-309.13 353.5,-309.13"/>
+<text text-anchor="start" x="408.5" y="-315.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="353.5,-288.13 353.5,-309.13 519.5,-309.13 519.5,-288.13 353.5,-288.13"/>
+<text text-anchor="start" x="407.5" y="-294.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="353.5,-267.13 353.5,-288.13 519.5,-288.13 519.5,-267.13 353.5,-267.13"/>
+<text text-anchor="start" x="414" y="-273.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="353.5,-246.13 353.5,-267.13 519.5,-267.13 519.5,-246.13 353.5,-246.13"/>
+<text text-anchor="start" x="364" y="-252.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="353.5,-225.13 353.5,-246.13 519.5,-246.13 519.5,-225.13 353.5,-225.13"/>
+<text text-anchor="start" x="372.5" y="-231.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="353.5,-204.13 353.5,-225.13 519.5,-225.13 519.5,-204.13 353.5,-204.13"/>
+<text text-anchor="start" x="401" y="-210.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="353.5,-183.13 353.5,-204.13 519.5,-204.13 519.5,-183.13 353.5,-183.13"/>
+<text text-anchor="start" x="416.5" y="-189.93" font-family="Times,serif" font-size="14.00">mode</text>
+<polygon fill="none" stroke="black" points="353.5,-162.13 353.5,-183.13 519.5,-183.13 519.5,-162.13 353.5,-162.13"/>
+<text text-anchor="start" x="412" y="-168.93" font-family="Times,serif" font-size="14.00">reason</text>
+<polygon fill="none" stroke="black" points="353.5,-141.13 353.5,-162.13 519.5,-162.13 519.5,-141.13 353.5,-141.13"/>
+<text text-anchor="start" x="386" y="-147.93" font-family="Times,serif" font-size="14.00">lease_seconds</text>
+<polygon fill="none" stroke="black" points="353.5,-120.13 353.5,-141.13 519.5,-141.13 519.5,-120.13 353.5,-120.13"/>
+<text text-anchor="start" x="356.5" y="-126.93" font-family="Times,serif" font-size="14.00">is_publisher_validated</text>
+<polygon fill="none" stroke="black" points="353.5,-99.13 353.5,-120.13 519.5,-120.13 519.5,-99.13 353.5,-99.13"/>
+<text text-anchor="start" x="398.5" y="-105.93" font-family="Times,serif" font-size="14.00">request_id</text>
+<polygon fill="none" stroke="black" points="353.5,-78.13 353.5,-99.13 519.5,-99.13 519.5,-78.13 353.5,-78.13"/>
+<text text-anchor="start" x="404" y="-84.93" font-family="Times,serif" font-size="14.00">attempts</text>
+<polygon fill="none" stroke="black" points="353.5,-57.13 353.5,-78.13 519.5,-78.13 519.5,-57.13 353.5,-57.13"/>
+<text text-anchor="start" x="388.5" y="-63.93" font-family="Times,serif" font-size="14.00">next_attempt</text>
+</g>
+<!-- topic->verification -->
+<g id="edge5" class="edge">
+<title>topic:pk_id->verification:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C336.13,-655.13 300.8,-459.16 316,-405.13 326.09,-369.27 314.1,-328.6 342.37,-321.29"/>
+<polygon fill="black" stroke="black" points="342.56,-321.26 353.01,-324.6 347.53,-320.7 352.5,-320.13 352.5,-320.13 352.5,-320.13 347.53,-320.7 351.99,-315.66 342.56,-321.26 342.56,-321.26"/>
+</g>
+<!-- verification_in_progress -->
+<g id="node7" class="node">
+<title>verification_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="638.5,-216.13 638.5,-239.13 869.5,-239.13 869.5,-216.13 638.5,-216.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="639.5,-217.13 639.5,-238.13 868.5,-238.13 868.5,-217.13 639.5,-217.13"/>
+<text text-anchor="start" x="642.5" y="-223.93" font-family="Times,serif" font-size="14.00">VERIFICATION_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="638.5,-195.13 638.5,-216.13 869.5,-216.13 869.5,-195.13 638.5,-195.13"/>
+<text text-anchor="start" x="747" y="-201.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="638.5,-174.13 638.5,-195.13 869.5,-195.13 869.5,-174.13 638.5,-174.13"/>
+<text text-anchor="start" x="726" y="-180.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="638.5,-153.13 638.5,-174.13 869.5,-174.13 869.5,-153.13 638.5,-153.13"/>
+<text text-anchor="start" x="725" y="-159.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="638.5,-132.13 638.5,-153.13 869.5,-153.13 869.5,-132.13 638.5,-132.13"/>
+<text text-anchor="start" x="723" y="-138.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="638.5,-111.13 638.5,-132.13 869.5,-132.13 869.5,-111.13 638.5,-111.13"/>
+<text text-anchor="start" x="726" y="-117.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="638.5,-90.13 638.5,-111.13 869.5,-111.13 869.5,-90.13 638.5,-90.13"/>
+<text text-anchor="start" x="705" y="-96.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic->verification_in_progress -->
+<g id="edge7" class="edge">
+<title>topic:pk_id->verification_in_progress:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C347.56,-655.13 266.83,-94.47 316,-48.13 393.95,25.33 464.03,5.07 557,-48.13 615.06,-81.36 571.99,-174.57 627.31,-184.31"/>
+<polygon fill="black" stroke="black" points="627.53,-184.32 637.14,-189.62 632.52,-184.73 637.5,-185.13 637.5,-185.13 637.5,-185.13 632.52,-184.73 637.86,-180.65 627.53,-184.32 627.53,-184.32"/>
+</g>
+<!-- subscription_delivery_in_progress -->
+<g id="node5" class="node">
+<title>subscription_delivery_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="593.5,-707.13 593.5,-730.13 914.5,-730.13 914.5,-707.13 593.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="594.5,-708.13 594.5,-729.13 913.5,-729.13 913.5,-708.13 594.5,-708.13"/>
+<text text-anchor="start" x="597.5" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION_DELIVERY_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="593.5,-686.13 593.5,-707.13 914.5,-707.13 914.5,-686.13 593.5,-686.13"/>
+<text text-anchor="start" x="747" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="593.5,-665.13 593.5,-686.13 914.5,-686.13 914.5,-665.13 593.5,-665.13"/>
+<text text-anchor="start" x="723" y="-671.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="593.5,-644.13 593.5,-665.13 914.5,-665.13 914.5,-644.13 593.5,-644.13"/>
+<text text-anchor="start" x="726" y="-650.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="593.5,-623.13 593.5,-644.13 914.5,-644.13 914.5,-623.13 593.5,-623.13"/>
+<text text-anchor="start" x="705" y="-629.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- subscription->subscription_delivery_in_progress -->
+<g id="edge4" class="edge">
+<title>subscription:pk_id->subscription_delivery_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M557,-697.13C569,-697.13 574.25,-697.13 582.88,-697.13"/>
+<polygon fill="black" stroke="black" points="583,-697.13 593,-701.63 588,-697.13 593,-697.13 593,-697.13 593,-697.13 588,-697.13 593,-692.63 583,-697.13 583,-697.13"/>
+</g>
+<!-- verification->verification_in_progress -->
+<g id="edge6" class="edge">
+<title>verification:pk_id->verification_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M520.5,-362.13C603.44,-362.13 555.41,-219.25 627.1,-206.97"/>
+<polygon fill="black" stroke="black" points="627.53,-206.94 637.86,-210.62 632.52,-206.53 637.5,-206.13 637.5,-206.13 637.5,-206.13 632.52,-206.53 637.14,-201.65 627.53,-206.94 627.53,-206.94"/>
+</g>
+<!-- authentication -->
+<g id="node8" class="node">
+<title>authentication</title>
+<polygon fill="lightblue" stroke="transparent" points="67,-790.13 67,-813.13 213,-813.13 213,-790.13 67,-790.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="68,-791.13 68,-812.13 212,-812.13 212,-791.13 68,-791.13"/>
+<text text-anchor="start" x="71" y="-797.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="none" stroke="black" points="67,-769.13 67,-790.13 213,-790.13 213,-769.13 67,-769.13"/>
+<text text-anchor="start" x="112.5" y="-775.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="67,-748.13 67,-769.13 213,-769.13 213,-748.13 67,-748.13"/>
+<text text-anchor="start" x="73.5" y="-754.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
+<polygon fill="none" stroke="black" points="67,-727.13 67,-748.13 213,-748.13 213,-727.13 67,-727.13"/>
+<text text-anchor="start" x="106.5" y="-733.93" font-family="Times,serif" font-size="14.00">identifier</text>
+<polygon fill="none" stroke="black" points="67,-706.13 67,-727.13 213,-727.13 213,-706.13 67,-706.13"/>
+<text text-anchor="start" x="103.5" y="-712.93" font-family="Times,serif" font-size="14.00">credential</text>
+</g>
+</g>
+</svg>
{
"name": "@squeep/websub-hub",
- "version": "1.3.5",
+ "version": "1.3.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
}
},
"@eslint/eslintrc": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz",
- "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz",
+ "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.3.1",
"globals": "^13.9.0",
- "ignore": "^4.0.6",
+ "ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.0.4",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
- "ignore": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
- "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
- "dev": true
- },
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
}
},
"@humanwhocodes/config-array": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz",
- "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
+ "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"dev": true
},
"@squeep/api-dingus": {
- "version": "git+https://git.squeep.com/squeep-api-dingus/#0ebfbe2181ce363e13d8b6fd10c9703897d5306f",
- "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
+ "version": "git+https://git.squeep.com/squeep-api-dingus/#12568946a94e853c3c16974d57dd34de1ad3877c",
+ "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.5",
"requires": {
"mime-db": "^1.51.0",
"uuid": "^8.3.2"
}
},
"@squeep/authentication-module": {
- "version": "git+https://git.squeep.com/squeep-authentication-module/#92658b114da01ab5537c53dee7ff5ad6385fe179",
- "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
+ "version": "git+https://git.squeep.com/squeep-authentication-module/#017c7190ecb48afa9b9cd164c8d9cd3b62fb29df",
+ "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.1",
"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.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",
+ "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.5",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.4",
+ "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.1",
+ "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.4",
"argon2": "^0.28.4",
"node-linux-pam": "^0.2.1"
}
},
"@squeep/html-template-helper": {
- "version": "git+https://git.squeep.com/squeep-html-template-helper#5506e8de3b9c93e2ec2d37d71134eee8ee5fd27c",
- "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2"
+ "version": "git+https://git.squeep.com/squeep-html-template-helper#f8d8a0142eaf713a9258c4f5738cdcb6b6e5c7bd",
+ "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.4"
},
"@squeep/indieauth-helper": {
- "version": "git+https://git.squeep.com/squeep-indieauth-helper/#7ece3489799b5349e22e95e3bd9fe7a30a985ebf",
- "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
+ "version": "git+https://git.squeep.com/squeep-indieauth-helper/#f26fc3563484621523dd4d8a8efdc0e020ab6943",
+ "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.1",
"requires": {
- "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
- "axios": "^0.26.0",
+ "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.4",
+ "axios": "^0.26.1",
"iconv": "^3.0.1",
"microformats-parser": "^1.4.1"
}
},
"@squeep/mystery-box": {
- "version": "git+https://git.squeep.com/squeep-mystery-box/#22091e8c1fd175c17031c6237e6a166271edffd1",
- "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.3"
+ "version": "git+https://git.squeep.com/squeep-mystery-box/#c47d2327a3a642792e9521677cd5f551e9aa7bfb",
+ "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.4"
},
"@squeep/web-linking": {
- "version": "git+https://git.squeep.com/squeep-web-linking/#a09659d26192d35e11461a7776f4f76c2e211576",
- "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3"
+ "version": "git+https://git.squeep.com/squeep-web-linking/#9a786b67a16416996df95b63c1a295dae98cf663",
+ "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.4"
},
"@ungap/promise-all-settled": {
"version": "1.1.2",
}
},
"argon2": {
- "version": "0.28.4",
- "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.4.tgz",
- "integrity": "sha512-WsfqiDp/tf5+eieLc1+S7RtO7Y3cAiZQ1F6GIaskENoJy/6xuCN5WGBIc8dG7QVPDavy6jUSads8zwZTtrHVag==",
+ "version": "0.28.5",
+ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.5.tgz",
+ "integrity": "sha512-kGFCctzc3VWmR1aCOYjNgvoTmVF5uVBUtWlXCKKO54d1K+31zRz45KAcDIqMo2746ozv/52d25nfEekitaXP0w==",
"optional": true,
"requires": {
"@mapbox/node-pre-gyp": "^1.0.8",
"@phc/format": "^1.0.0",
- "node-addon-api": "^4.3.0",
- "opencollective-postinstall": "^2.0.3"
+ "node-addon-api": "^4.3.0"
}
},
"argparse": {
"optional": true
},
"axios": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
- "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
+ "version": "0.26.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+ "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"requires": {
"follow-redirects": "^1.14.8"
}
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001243",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
- "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
+ "version": "1.0.30001315",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001315.tgz",
+ "integrity": "sha512-5v7LFQU4Sb/qvkz7JcZkvtSH1Ko+1x2kgo3ocdBeMGZSOFpuE1kkm0kpTwLtWeFrw5qw08ulLxJjVIXIS8MkiQ==",
"dev": true
},
"chalk": {
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz",
- "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==",
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz",
+ "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==",
"dev": true,
"requires": {
- "@eslint/eslintrc": "^1.1.0",
+ "@eslint/eslintrc": "^1.2.1",
"@humanwhocodes/config-array": "^0.9.2",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"dev": true
},
"follow-redirects": {
- "version": "1.14.8",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
- "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
+ "version": "1.14.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+ "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
},
"foreground-child": {
"version": "2.0.0",
}
},
"mime-db": {
- "version": "1.51.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
- "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mimic-response": {
"version": "3.1.0",
"optional": true
},
"mocha": {
- "version": "9.2.1",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.1.tgz",
- "integrity": "sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ==",
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz",
+ "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==",
"dev": true,
"requires": {
"@ungap/promise-all-settled": "1.1.2",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
- "minimatch": "3.0.4",
+ "minimatch": "4.2.1",
"ms": "2.1.3",
- "nanoid": "3.2.0",
+ "nanoid": "3.3.1",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
+ },
+ "dependencies": {
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ }
}
},
"has-flag": {
"argparse": "^2.0.1"
}
},
+ "minimatch": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz",
+ "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nanoid": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
- "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
+ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"dev": true
},
"napi-build-utils": {
"wrappy": "1"
}
},
- "opencollective-postinstall": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
- "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
- "optional": true
- },
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
{
"name": "@squeep/websub-hub",
- "version": "1.3.5",
+ "version": "1.3.6",
"description": "A WebSub Hub server implementation.",
"main": "server.js",
"scripts": {
"coverage-check"
],
"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.2.0",
- "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
- "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
- "axios": "^0.26.0",
+ "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.5",
+ "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.1",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.4",
+ "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.4",
+ "axios": "^0.26.1",
"feedparser": "^2.2.10",
"htmlparser2": "^7.2.0",
"iconv": "^3.0.1"
"pg-promise": "^10.11.1"
},
"devDependencies": {
- "eslint": "^8.9.0",
+ "eslint": "^8.11.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-sonarjs": "^0.12.0",
"html-minifier-lint": "^2.0.0",
- "mocha": "^9.2.1",
+ "mocha": "^9.2.2",
"mocha-steps": "^1.3.0",
"nyc": "^15.1.0",
"pre-commit": "^1.2.2",
}
+ /**
+ * Return an array of the counts of the last #days of topic updates.
+ * @param {*} dbCtx
+ * @param {*} topicId
+ * @param {Number} days
+ * @returns {Number[]}
+ */
+ async topicPublishHistory(dbCtx, topicId, days) {
+ this._notImplemented('topicPublishHistory', arguments);
+ }
+
+
/**
* Create or update the basic parameters of a topic.
* @param {*} dbCtx
/**
* Updates a topic's content data and content update timestamp.
* @param {Object} data
- * @param {Integer} data.topicId
+ * @param {*} data.topicId
* @param {String} data.content
* @param {String} data.contentHash
* @param {String=} data.contentType
}
+ async topicPublishHistory(dbCtx, topicId, days) {
+ const _scope = _fileScope('topicPublishHistory');
+ this.logger.debug(_scope, 'called', { topicId, days });
+
+ const events = await dbCtx.manyOrNone(this.statement.topicPublishHistory, { topicIds: [topicId], daysAgo: days });
+ const history = Array.from({ length: days }, () => 0);
+ events.forEach(({ daysAgo, contentUpdates }) => history[daysAgo] = Number(contentUpdates));
+
+ return history;
+ }
+
+
async topicSet(dbCtx, data) {
const _scope = _fileScope('topicSet');
this.logger.debug(_scope, 'called', data);
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.0",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.1",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.2",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.3",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ topic_content_history [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content_size</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_content_history:fk_topic_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+--
+SELECT
+ EXTRACT(DAY FROM now() - content_updated::date) AS days_ago,
+ count(*) AS content_updates
+FROM topic_content_history
+WHERE
+ (now() - content_updated::date) < ($(daysAgo) * INTERVAL '1 day')
+AND
+ ($(topicIds) IS NULL OR topic_id = ANY($(topicIds)::uuid[]))
+GROUP BY content_updated::date
+ORDER BY content_updated::date
+;
}
+ topicPublishHistory(dbCtx, topicId, days) {
+ const _scope = _fileScope('topicPublishHistory');
+ this.logger.debug(_scope, 'called', { topicId, days })
+
+ const events = this.statement.topicPublishHistory.all({ topicId, daysAgo: days });
+ const history = Array.from({ length: days }, () => 0);
+ events.forEach(({ daysAgo, contentUpdates }) => history[daysAgo] = Number(contentUpdates));
+
+ return history;
+ }
+
+
topicSet(dbCtx, data) {
const _scope = _fileScope('topicSet');
this.logger.debug(_scope, 'called', data);
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.0",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id [ arrowhead = crow ];
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\SQLite\nSchema 1.0.1",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.2",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.3",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ topic_content_history [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content_size</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_content_history:fk_topic_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+--
+SELECT
+ (strftime('%s', 'now') - content_updated) / 86400 AS days_ago,
+ count(*) AS content_updates
+FROM topic_content_history
+WHERE
+ days_ago <= :daysAgo
+AND
+ (:topicId IS NULL OR topic_id = :topicId)
+GROUP BY days_ago
+ORDER BY days_ago
+;
});
res.end(this.infoContent(ctx));
- this.logger.info(_scope, 'finished', { ...ctx });
+ this.logger.info(_scope, 'finished', { ctx });
}
+ /**
+ * label the bars of the topic update history graph
+ * @param {Number} index
+ * @param {Number} value
+ * @returns {String}
+ */
+ static _historyBarCaption(index, value) {
+ let when;
+ switch (index) {
+ case 0:
+ when ='today';
+ break;
+ case 1:
+ when = 'yesterday';
+ break;
+ default:
+ when = `${index} days ago`;
+ }
+ return `${when}, ${value ? value : 'no'} update${value === 1 ? '': 's'}`;
+ }
+
+
+ /**
+ * GET SVG chart of topic update history
+ * @param {http.ServerResponse} res
+ * @param {object} ctx
+ */
+ async getHistorySVG(res, ctx) {
+ const _scope = _fileScope('getHist');
+ this.logger.debug(_scope, 'called', { ctx });
+
+ const days = Math.min(parseInt(ctx.queryParams.days) || this.options.manager.publishHistoryDays, 365);
+ const histOptions = {
+ title: 'Topic Publish History',
+ description: 'Updates per Day',
+ labelZero: '^ Today',
+ labelX: 'Days Ago',
+ maxItems: days,
+ minItems: days,
+ tickEvery: 7,
+ barWidth: 25,
+ barHeight: 40,
+ labelHeight: 12,
+ barCaptionFn: Manager._historyBarCaption,
+ };
+
+ let publishHistory;
+ await this.db.context(async (dbCtx) => {
+ publishHistory = await this.db.topicPublishHistory(dbCtx, ctx.params.topicId, days);
+ });
+
+ res.end(Template.histogramSVG(publishHistory, histOptions));
+ this.logger.info(_scope, 'finished', { ctx });
+ }
+
/**
* GET request for authorized /admin information.
* @param {http.ServerResponse} res
}
res.end(Template.adminOverviewHTML(ctx, this.options));
- this.logger.info(_scope, 'finished', { ...ctx, topics: ctx.topics.length })
+ this.logger.info(_scope, 'finished', { ctx, topics: ctx.topics.length });
}
const _scope = _fileScope('getTopicDetails');
this.logger.debug(_scope, 'called', { ctx });
+
+ ctx.publishSpan = 60;
const topicId = ctx.params.topicId;
+ let publishHistory;
await this.db.context(async (dbCtx) => {
ctx.topic = await this.db.topicGetById(dbCtx, topicId);
ctx.subscriptions = await this.db.subscriptionsByTopicId(dbCtx, topicId);
+ publishHistory = await this.db.topicPublishHistory(dbCtx, topicId, ctx.publishSpan);
});
- this.logger.debug(_scope, 'got topic details', { topic: ctx.topic, subscriptions: ctx.subscriptions });
+ ctx.publishCount = publishHistory.reduce((a, b) => a + b, 0);
+ this.logger.debug(_scope, 'got topic details', { topic: ctx.topic, subscriptions: ctx.subscriptions, updates: ctx.publishCount });
// Profile users can only see related topics.
if (ctx.session && ctx.session.authenticatedProfile) {
}
res.end(Template.adminTopicDetailsHTML(ctx, this.options));
- this.logger.info(_scope, 'finished', { ...ctx, subscriptions: ctx.subscriptions.length, topic: ctx.topic && ctx.topic.id || ctx.topic });
+ this.logger.info(_scope, 'finished', { ctx, subscriptions: ctx.subscriptions.length, topic: ctx.topic && ctx.topic.id || ctx.topic });
}
this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this));
this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
+ this.on(['GET', 'HEAD'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this));
// Private data-editing endpoints
this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this));
}
- /**
- * 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
];
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Service.setHeadHandler(req, res, ctx);
+ Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetHealthcheck');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Service.setHeadHandler(req, res, ctx);
+ Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
- Service.setHeadHandler(req, res, ctx);
+ Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(responseTypes, req, res, ctx);
}
+ async handlerGetHistorySVG(req, res, ctx) {
+ const _scope = _fileScope('handlerGetHist');
+ this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
+
+ const responseTypes = [Enum.ContentType.ImageSVG];
+
+ Dingus.setHeadHandler(req, res, ctx);
+
+ this.setResponseType(responseTypes, req, res, ctx);
+
+ await this.manager.getHistorySVG(res, ctx);
+ }
+
+
/**
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
const _scope = _fileScope('handlerGetAdminOverview');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Service.setHeadHandler(req, res, ctx);
+ Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetAdminTopicDetails');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Service.setHeadHandler(req, res, ctx);
+ Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
/**
- * Similar to super.ingestBody, but if no body was sent, do not parse (and
- * thus avoid possible unsupported media type error).
- * Also removes raw body from context, to simplify scrubbing sensitive data from logs.
+ * If no body was sent, do not parse (and thus avoid possible unsupported media type error).
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {Object} ctx
*/
async maybeIngestBody(req, res, ctx) {
- ctx.rawBody = await this.bodyData(req);
- const contentType = Dingus.getRequestContentType(req);
- if (ctx.rawBody) {
- this.parseBody(contentType, ctx);
- delete ctx.rawBody;
- }
+ return super.ingestBody(req, res, ctx, {
+ parseEmptyBody: false,
+ });
}
const _scope = _fileScope('handlerGetAdminLogin');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Service.setHeadHandler(req, res, ctx);
+ Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
};
const content = [
- '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
` <section class="topics">
<p>${ctx.topics.length ? ctx.topics.length : 'no'} topic${(ctx.topics.length === 1) ? '' : 's'}</p>
<table>
};
const content = [
- '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
` <section class="topics">
<table>
<thead>`,
` </tbody>
</table>
</section>`,
+ ` <section class="history">
+ <p>Topic Publish History — ${ctx.publishCount} updates in the last ${ctx.publishSpan} days</p>
+ <img title="Topic Publish History" src="${ctx.params.topicId}/history.svg">
+ </section>`,
` <section class="subscriptions">
<p>${ctx.subscriptions.length ? ctx.subscriptions.length : 'no'} subscription${(ctx.subscriptions.length === 1) ? '' : 's'}</p>
<table>
'use strict';
-function escapeXml(s) {
- if (typeof s === 'number') {
- return s;
- } else if (typeof s !== 'string') {
- return undefined;
- } else {
- return s
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- }
-}
+const th = require('./template-helper');
const ctxDefaults = {
* renderer from https://github.com/badges/shields/tree/master/badge-maker which
* is under the http://creativecommons.org/publicdomain/zero/1.0/ license.
*/
- return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${ctx.width}" height="${ctx.height}" role="img" aria-label="${escapeXml(ctx.accessibleText)}">
- <title>${escapeXml(ctx.accessibleText)}</title>
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${ctx.width}" height="${ctx.height}" role="img" aria-label="${th.xmlEscape(ctx.accessibleText)}">
+ <title>${th.xmlEscape(ctx.accessibleText)}</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#fff" stop-opacity=".7"/>
<stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
<rect width="${ctx.width}" height="${ctx.height}" fill="url(#s)"/>
</g>
<g fill="${ctx.color}" text-anchor="left" font-family="${ctx.fontFamily}" text-rendering="geometricPrecision" font-size="11" font-weight="bold">
- <text x="${ctx.halfCharWidth}" y="${ctx.verticalMargin}">${escapeXml(ctx.label)}</text>
- <text x="${fixedRound(ctx.halfCharWidth + ctx.labelWidth)}" y="${ctx.verticalMargin}">${escapeXml(ctx.message)}</text>
+ <text x="${ctx.halfCharWidth}" y="${ctx.verticalMargin}">${th.xmlEscape(ctx.label)}</text>
+ <text x="${fixedRound(ctx.halfCharWidth + ctx.labelWidth)}" y="${ctx.verticalMargin}">${th.xmlEscape(ctx.message)}</text>
</g>
</svg>`;
};
--- /dev/null
+'use strict';
+
+const th = require('./template-helper');
+
+const optionsDefaults = {
+ barWidth: 20,
+ barHeight: 100,
+ scaleBars: true,
+ barCaptionFn: () => '',
+ labelZero: undefined,
+ labelX: undefined,
+ labelHeight: 8,
+ fontFamily: 'DejaVu Sans,Verdana,Geneva,sans-serif',
+ frameColor: 'gray',
+ tickEvery: undefined,
+ tickHeight: 4,
+ tickColor: 'gray',
+ minItems: 0,
+ maxItems: undefined,
+};
+
+function grey(percent) {
+ const value = Math.round(95 * (1.0 - percent));
+ return `rgb(${value}%, ${value}%, ${value}%)`;
+}
+
+function svgHeader(options, width, height) {
+ return [
+ `<svg
+version="1.1"
+baseProfile="full"
+xmlns="http://www.w3.org/2000/svg"
+xmlns:xlink="http://www.w3.org/1999/xlink"
+xmlns:ev="http://www.w3.org/2001/xml-events"
+width="${width}px" height="${height}px"
+viewBox="0 0 ${width} ${height}"
+preserveAspectRatio="xMinYMin meet"`,
+ options.accessibleText ? ` role="img" aria-label="${th.xmlEscape(options.accessibleText)}"` : '',
+ '>',
+ options.title ? `<title>${th.xmlEscape(options.title)}</title>` : '',
+ options.description ? `<desc>${th.xmlEscape(options.description)}</desc>` : '',
+ '\n',
+ ];
+}
+
+function svgFrame(options, width) {
+ if (!options.frameColor) {
+ return '';
+ }
+ return `\t<g id="frame">
+\t\t<rect x="0.5px" y="0.5px" width="${width - 1}px" height="${options.barHeight - 1}px" fill="none" stroke="${options.frameColor}" />
+\t</g>\n`;
+}
+
+function svgTicks(options, width) {
+ if (!options.tickEvery) {
+ return '';
+ }
+ const tickSpacing = options.tickEvery * options.barWidth;
+ const tickOffset = options.barWidth / 2;
+ const numTicks = Math.ceil(width / tickSpacing);
+ const ticks = Array.from({ length: numTicks }, (_, i) => i)
+ .map((index) => `M ${(index * tickSpacing) + tickOffset} ${options.barHeight - 2} v ${options.tickHeight}`)
+ .join(' ');
+
+ return `\t<g id="ticks">
+\t<path d="${ticks}" stroke="${options.tickColor}" fill="none" stroke-width="0.5px" />
+\t</g>\n`;
+}
+
+function svgLabels(options, width, height) {
+ const labels = [];
+ if (!options.labelHeight) {
+ return labels;
+ }
+ labels.push(`\t<g font-size="${options.labelHeight}px" font-family="${options.fontFamily}" font-variant="small-caps">\n`);
+ const y = height - (options.labelHeight / 2) + 2;
+ if (options.labelZero) {
+ labels.push(`\t\t<text font-size="${options.labelHeight}px" text-anchor="start" x="0" y="${y}">${options.labelZero}</text>\n`);
+ }
+ if (options.labelX) {
+ labels.push(`\t\t<text text-anchor="middle" x="${width / 2}" y="${y}">${options.labelX}</text>\n`);
+ }
+ labels.push('\t</g>\n');
+ return labels;
+}
+
+function svgFooter() {
+ return '</svg>';
+}
+
+function svgBar(options, value, index, maxValue) {
+ const id = `i${index}`;
+ const x = options.barWidth * index;
+ const width = options.barWidth;
+ const height = options.barHeight;
+ const scale = value / Math.max(1, maxValue);
+ const scaleHeight = options.scaleBars ? height * scale : height;
+ const yOffset = height - scaleHeight;
+ const fill = grey(scale);
+ const emptyFill = grey(0);
+ const title = th.xmlEscape(options.barCaptionFn(index, value));
+ return [
+ ...(options.scaleBars && [
+ `\t<rect id="${id}" x="${x}" y="0" width="${width}" height="${height}" fill="${emptyFill}">`,
+ ...(title && `<title>${title}</title>`),
+ '\t</rect>\n',
+ ]),
+ `\t<rect id="${id}" x="${x}" y="${yOffset}" width="${width}" height="${scaleHeight}" fill="${fill}">`,
+ ...(title && `<title>${title}</title>`),
+ '\t</rect>\n',
+ ].join('');
+}
+
+module.exports = (items, options = {}) => {
+ options = Object.assign({}, optionsDefaults, options);
+
+ const maxValue = items.reduce((a, b) => Math.max(a, b), 0);
+ const hasLabel = !!options.labelX || !!options.labelZero;
+ const height = options.barHeight + 2 + (hasLabel ? options.labelHeight + 2 : 0);
+ const width = Math.max(items.length, options.minItems) * options.barWidth;
+
+ return [
+ ...svgHeader(options, width, height),
+ ...items.slice(0, options.maxItems).map((value, index) => svgBar(options, value, index, maxValue)),
+ svgFrame(options, width),
+ svgTicks(options, width),
+ ...svgLabels(options, width, height),
+ svgFooter(),
+ ].join('');
+};
\ No newline at end of file
adminOverviewHTML: require('./admin-overview-html'),
adminTopicDetailsHTML: require('./admin-topic-details-html'),
badgeSVG: require('./badge-svg'),
+ histogramSVG: require('./histogram-svg'),
rootHTML: require('./root-html'),
};
\ No newline at end of file
navLinks,
};
const content = [
- '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
aboutSection(),
usageSection(isPublicHub, hubURL),
contactSection(contactHTML),
const { TemplateHelper } = require('@squeep/html-template-helper');
-
/**
* Render a topic as a row of details.
* @param {Object} topic
}
+/**
+ * Escape some xml things in strings.
+ * @param {String} string
+ */
+function xmlEscape(string) {
+ if (typeof string === 'number') {
+ return string;
+ }
+ if (typeof string !== 'string') {
+ return undefined;
+ }
+ // eslint-disable-next-line security/detect-object-injection
+ return string.replace(/[<>&'"]/, (c) => ({
+ '<': '<',
+ '>': '>',
+ '&': '&',
+ '\'': ''',
+ '"': '"',
+ }[c]));
+}
+
module.exports = Object.assign(Object.create(TemplateHelper), {
+ xmlEscape,
renderTopicRowHeader,
renderTopicRow,
renderSubscriptionRowHeader,
}
.topics {}
.subscriptions {}
+.history img {
+ width: 100%;
+}
.external {
background-image: url("external-link.svg");
background-position: right center;
db._topicSetDataValidate(data);
});
it('covers invalid value', function () {
- data.leaseSecondsPreferred = -100;
- try {
- db._topicSetDataValidate(data);
- assert.fail('did not get expected exception');
- } catch (e) {
- assert(e instanceof DBErrors.DataValidation);
- }
+ data.leaseSecondsPreferred = -100;
+ try {
+ db._topicSetDataValidate(data);
+ assert.fail('did not get expected exception');
+ } catch (e) {
+ assert(e instanceof DBErrors.DataValidation);
+ }
});
it('covers invalid range', function () {
data.leaseSecondsPreferred = 10000;
assert(db);
});
+ it('is healthy', async function () {
+ const result = await db.healthCheck();
+ assert(result);
+ });
+
describe('Authentication', function () {
let identifier, credential;
beforeEach(function () {
assert.strictEqual(Number(topic.contentFetchAttemptsSinceSuccess), 0);
});
});
+ step('gets publish history', async function () {
+ await db.context(async (dbCtx) => {
+ const result = (await db.topicPublishHistory(dbCtx, topicId, 7))
+ .map((x) => Number(x));
+ const expected = [1, 0, 0, 0, 0, 0, 0];
+ assert.deepStrictEqual(result, expected);
+ });
+ });
step('deletes a topic', async function () {
await db.context(async (dbCtx) => {
const result = await db.topicSet(dbCtx, testData.anotherTopicSet);
});
});
+ describe('topicPublishHistory', function () {
+ beforeEach(function () {
+ sinon.stub(db.db, 'manyOrNone');
+ });
+ it('success', async function () {
+ db.db.manyOrNone.returns([
+ { daysAgo: 1, contentUpdates: 1 },
+ { daysAgo: 3, contentUpdates: 2 },
+ ]);
+ const result = await db.topicPublishHistory(dbCtx, topicId, 7);
+ const expected = [0, 1, 0, 2, 0, 0, 0];
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // topicPublishHistory
+
describe('topicSet', function () {
let data;
beforeEach(function () {
assert.deepStrictEqual(e, expected);
}
});
- });
+ }); // subscriptionDeleteExpired
describe('subscriptionDeliveryClaim', function () {
it('success', async function () {
}
assert(db.statement.topicDeleteById.run.called);
});
- });
+ }); // topicPendingDelete
+
+ describe('topicPublishHistory', function () {
+ beforeEach(function () {
+ sinon.stub(db.statement.topicPublishHistory, 'all');
+ });
+ it('success', function () {
+ db.statement.topicPublishHistory.all.returns([
+ { daysAgo: 1, contentUpdates: 1 },
+ { daysAgo: 3, contentUpdates: 2 },
+ ]);
+ const result = db.topicPublishHistory(dbCtx, topicId, 7);
+ const expected = [0, 1, 0, 2, 0, 0, 0];
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // topicPublishHistory
describe('topicSet', function () {
let data;
};
ctx = {
params: {},
+ queryParams: {},
};
manager = new Manager(stubLogger, stubDb, options);
sinon.stub(manager.communication, 'verificationProcess');
});
}); // getInfo
+ describe('_historyBarCaption', function () {
+ it('covers today, none', function () {
+ const result = Manager._historyBarCaption(0, 0);
+ assert.strictEqual(result, 'today, no updates');
+ });
+ it('covers yesterday, singular', function () {
+ const result = Manager._historyBarCaption(1, 1);
+ assert.strictEqual(result, 'yesterday, 1 update');
+ });
+ it('covers older, plural', function () {
+ const result = Manager._historyBarCaption(7, 3);
+ assert.strictEqual(result, '7 days ago, 3 updates');
+ });
+ }); // _historyBarCaption
+
+ describe('getHistorySVG', function () {
+ beforeEach(function () {
+ manager.db.topicPublishHistory.resolves([0, 1, 2, 1, 0, 1, 2, 0, 1]);
+ });
+ it('covers', async function () {
+ await manager.getHistorySVG(res, ctx);
+ assert(res.end.called);
+ });
+ }); // getHistorySVG
+
describe('getAdminOverview', function () {
beforeEach(function () {
manager.db.topicGetAll.resolves([
deliveryAttemptsSinceSuccess: 0,
deliveryNextAttempt: new Date(-Infinity),
}]);
+ manager.db.topicPublishHistory.resolves([0, 1, 0, 1, 0]);
});
it('covers', async function() {
await manager.getTopicDetails(res, ctx);
});
}); // 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);
}); // handlerGetHealthcheck
describe('handlerGetInfo', function () {
- it('covers', async function() {
+ it('covers', async function () {
await service.handlerGetInfo(req, res, ctx);
assert(service.manager.getInfo.called);
});
}); // handlerGetInfo
+ describe('handlerGetHistorySVG', function () {
+ it('covers', async function () {
+ await service.handlerGetHistorySVG(req, res, ctx);
+ assert(service.manager.getHistorySVG.called);
+ });
+ }); // handlerGetHistorySVG
+
describe('handlerGetAdminOverview', function () {
it('covers', async function () {
await service.handlerGetAdminOverview(req, res, ctx);
beforeEach(function () {
ctx = {
+ params: {
+ topicId: '97dd5488-a303-11ec-97ab-0025905f714a',
+ },
topic: {},
subscriptions: [
{},
--- /dev/null
+/* eslint-env mocha */
+'use strict';
+
+const assert = require('assert');
+const template = require('../../../src/template/histogram-svg');
+
+describe('Histogram SVG Template', function () {
+ let items, options;
+ beforeEach(function () {
+ items = [];
+ options = {};
+ });
+ it('covers defaults', function () {
+ const result = template(items, options);
+ assert(result);
+ });
+ it('covers options', function () {
+ items = [0, 1, 2];
+ options = {
+ labelX: 'Days Ago',
+ labelZero: 'Today',
+ tickEvery: 2,
+ frameColor: undefined,
+ labelHeight: 0,
+ };
+ const result = template(items, options);
+ assert(result);
+ });
+});
\ No newline at end of file
});
}); // renderSubscriptionRowHeader
+ describe('xmlEscape', function () {
+ it('ignores numbers', function () {
+ const result = th.xmlEscape(3);
+ assert.strictEqual(result, 3);
+ });
+ it('ignores objects', function () {
+ const result = th.xmlEscape({});
+ assert.strictEqual(result, undefined);
+ });
+ it('escapes a thing', function () {
+ const result = th.xmlEscape('&');
+ assert.strictEqual(result, '&');
+ });
+ }); // xmlEscape
+
});
'topicGetByUrl',
'topicGetContentById',
'topicPendingDelete',
+ 'topicPublishHistory',
'topicSet',
'topicSetContent',
'topicUpdate',