{"id":13615025,"url":"https://github.com/immers-space/activitypub-express","last_synced_at":"2025-04-06T12:11:37.371Z","repository":{"id":38747589,"uuid":"226694294","full_name":"immers-space/activitypub-express","owner":"immers-space","description":"Modular ActivityPub implementation as Express JS middleware to easily add decentralization and federation to Node apps","archived":false,"fork":false,"pushed_at":"2024-02-20T21:05:41.000Z","size":1391,"stargazers_count":302,"open_issues_count":27,"forks_count":24,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-31T21:15:36.235Z","etag":null,"topics":["activitypub","express","mongodb","nodejs"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/immers-space.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2019-12-08T16:08:42.000Z","updated_at":"2025-03-31T03:16:47.000Z","dependencies_parsed_at":"2023-12-21T16:44:36.489Z","dependency_job_id":"46649894-fc64-4383-b4cf-f8e16dbb226e","html_url":"https://github.com/immers-space/activitypub-express","commit_stats":{"total_commits":301,"total_committers":6,"mean_commits":"50.166666666666664","dds":0.03322259136212624,"last_synced_commit":"37b711906b4fc545184d5ad6e3d38110aa314a80"},"previous_names":["wmurphyrd/activitypub-express"],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/immers-space%2Factivitypub-express","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/immers-space%2Factivitypub-express/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/immers-space%2Factivitypub-express/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/immers-space%2Factivitypub-express/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/immers-space","download_url":"https://codeload.github.com/immers-space/activitypub-express/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247423497,"owners_count":20936622,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activitypub","express","mongodb","nodejs"],"created_at":"2024-08-01T20:01:08.341Z","updated_at":"2025-04-06T12:11:37.347Z","avatar_url":"https://github.com/immers-space.png","language":"JavaScript","funding_links":["https://opencollective.com/immers-space"],"categories":["JavaScript"],"sub_categories":[],"readme":"# activitypub-express (apex)\n\n[![npm](https://img.shields.io/npm/v/activitypub-express)](https://npmjs.com/activitypub-express)\n[![npm](https://img.shields.io/npm/dw/activitypub-express)](https://npmjs.com/activitypub-express)\n[![Build Status](https://app.travis-ci.com/immers-space/activitypub-express.svg?branch=master)](https://travis-ci.com/github/immers-space/activitypub-express)\n[![Coverage Status](https://coveralls.io/repos/github/immers-space/activitypub-express/badge.svg)](https://coveralls.io/github/immers-space/activitypub-express)\n[![Matrix](https://img.shields.io/matrix/immers-space:matrix.org?label=Matrix%20chat)](https://matrix.to/#/#immers-space:matrix.org)\n[![Open Collective](https://opencollective.com/immers-space/tiers/badge.svg)](https://opencollective.com/immers-space)\n\n\nModular implementation of the ActivityPub decentralized social networking protocol,\nwritten for NodeJS as ExpressJS middleware.\nIncludes a interchangeable storage interface with a default MongoDB implementation.\n\n## Installation\n\nIn order for http request signing to function correctly, a patched version of the `http-signature`\nlibrary is required. To ensure that the `request` library is using the correct version for its sub-dependency,\nyou may need to dedupe after installation.\n\n```\nnpm install --save activitypub-express\nnpm dedupe\n```\n\n## Usage\n\n```js\nconst express = require('express')\nconst { MongoClient } = require('mongodb')\nconst ActivitypubExpress = require('activitypub-express')\n\nconst port = 8080\nconst app = express()\nconst routes = {\n  actor: '/u/:actor',\n  object: '/o/:id',\n  activity: '/s/:id',\n  inbox: '/u/:actor/inbox',\n  outbox: '/u/:actor/outbox',\n  followers: '/u/:actor/followers',\n  following: '/u/:actor/following',\n  liked: '/u/:actor/liked',\n  collections: '/u/:actor/c/:id',\n  blocked: '/u/:actor/blocked',\n  rejections: '/u/:actor/rejections',\n  rejected: '/u/:actor/rejected',\n  shares: '/s/:id/shares',\n  likes: '/s/:id/likes'\n}\nconst apex = ActivitypubExpress({\n  name: 'Apex Example',\n  version: '1.0.0',\n  domain: 'localhost',\n  actorParam: 'actor',\n  objectParam: 'id',\n  activityParam: 'id',\n  routes,\n  endpoints: {\n    proxyUrl: 'https://localhost/proxy'\n  }\n})\nconst client = new MongoClient('mongodb://localhost:27017')\n\napp.use(\n  express.json({ type: apex.consts.jsonldTypes }),\n  express.urlencoded({ extended: true }),\n  apex\n)\n// define routes using prepacakged middleware collections\napp.route(routes.inbox)\n  .get(apex.net.inbox.get)\n  .post(apex.net.inbox.post)\napp.route(routes.outbox)\n  .get(apex.net.outbox.get)\n  .post(apex.net.outbox.post)\napp.get(routes.actor, apex.net.actor.get)\napp.get(routes.followers, apex.net.followers.get)\napp.get(routes.following, apex.net.following.get)\napp.get(routes.liked, apex.net.liked.get)\napp.get(routes.object, apex.net.object.get)\napp.get(routes.activity, apex.net.activityStream.get)\napp.get(routes.shares, apex.net.shares.get)\napp.get(routes.likes, apex.net.likes.get)\napp.get('/.well-known/webfinger', apex.net.webfinger.get)\napp.get('/.well-known/nodeinfo', apex.net.nodeInfoLocation.get)\napp.get('/nodeinfo/:version', apex.net.nodeInfo.get)\napp.post('/proxy', apex.net.proxy.post)\n// custom side-effects for your app\napp.on('apex-outbox', msg =\u003e {\n  if (msg.activity.type === 'Create') {\n    console.log(`New ${msg.object.type} from ${msg.actor}`)\n  }\n})\napp.on('apex-inbox', msg =\u003e {\n  if (msg.activity.type === 'Create') {\n    console.log(`New ${msg.object.type} from ${msg.actor} to ${msg.recipient}`)\n  }\n})\n\nclient.connect()\n  .then(() =\u003e {\n    apex.store.db = client.db('DB_NAME')\n    return apex.store.setup()\n  })\n  .then(() =\u003e {\n    app.listen(port, () =\u003e console.log(`apex app listening on port ${port}`))\n  })\n```\n\n### Next Steps and Examples\n\nOkay so you've got an ActivityPub server, now what?\n\n**Server-to-server apps**: For an app that people interact with by sending\nmessages from another app (e.g. Mastodon), you'll want to define custom side-effects\nusing `app.on('apex-inbox', ({ actor, activity, recipient, object }) =\u003e {...})`, which\nis fired for each incoming message.\n\nFor an example of a server-to-server app build with activitypub-expresss,\ncheck out [Guppe Groups](https://a.gup.pe) - the federated social groups\napp server built with just [250 lines of code](https://github.com/immers-space/guppe/blob/main/index.js).\n\n**Full platform**: For a full-fledged app with its own users, the next thing\nyou'll probably want is user authentication and authorization. Apex integrates\nwell with Passport.js, but can work with anything.\n\nFor an example of a full social platform built with activitypub-express,\ncheck out [Immers Space](https://github.com/immers-space/immers) - the\nfederated social platform for virtual worlds.\n\n## API\n\n### ActivitypubExpress initializer\n\nConfigures and returns an express middleware function that must be added to the route\nbefore any other apex middleware. It needs to be configured with the routes you will use\nin order to correctly generate IRIs and actor profiles\n\n```\nconst ActivitypubExpress = require('activitypub-express')\nconst apex = ActivitypubExpress(options)\napp.use(apex)\n```\n\nOption | Description\n--- | ---\n**Required** |\nname | String. Name of your app to list in nodeinfo\nversion | String. Version of your app to list in nodeinfo\nbaseUrl | String. Server URL origin. Also used as URI prefix. Should be the public-facing URL when using a reverse proxy. Overrides `domain` if set\ndomain | String. Hostname for your app\nactorParam | String. Express route parameter used for actor name\nobjectParam | String. Express route parameter used for object id\nroutes | Object. The routes your app uses for ActivityPub endpoints (including parameter). Details below\nroutes.actor | Actor profile route \u0026 IRI pattern\nroutes.object | Object retrieval route \u0026 IRI pattern\nroutes.activity | Activity retrieval route \u0026 IRI pattern\nroutes.inbox | Actor inbox route\nroutes.outbox | Actor outbox route\nroutes.following | Actor following collection route\nroutes.followers | Actor followers collection route\nroutes.liked | Actor liked collection route\nroutes.blocked | Actor's blocklist\nroutes.rejected | Activities rejected by actor\nroutes.rejections | Actor's activities that were rejected by recipient\nroutes.shares | Activity shares collection route\nroutes.likes | Activity likes collection route\nroutes.collections | Actors' miscellaneous collections route (must include `actorParam` and `collectionParam`)\n**Optional** |\nactivityParam | String. Express route parameter used for activity id (defaults to `objectParam`)\ncollectionParam | String. Express route parameter used for collection id (defaults to `objectParam`)\npageParam | String. Query parameter used for collection page identifier (defaults to `page`)\nitemsPerPage | Number. Count of items in each collection page (default `20`)\ncontext | String, Object, Array. JSON-LD context(s) to use with your app in addition to the base AcivityStreams + Security vocabs\nendpoints | Object. Optional system-wide api endpoint URLs included in [actor objects](https://www.w3.org/TR/activitypub/#actor-objects): `proxyUrl`, `oauthAuthorizationEndpoint`, `oauthTokenEndpoint`, `provideClientKey`, `signClientKey`, `sharedInbox`, `uploadMedia`\nlogger | Object with `info`, `warn`, `error` methods to replace `console`\nstore | Replace the default storage model \u0026 database backend with your own (see `store/interface.js` for API)\nthreadDepth | Controls how far up apex will follow links in incoming activities in order to display the conversation thread \u0026 check for inbox forwarding needs  (default 10)\nsystemUser | Actor object representing system and used for signing GETs (see below)\nofflineMode | Disable delivery. Useful for running migrations and queueing deliveries to be sent when app is running\nrequestTimeout | Timeout for requests to other servers, ms (default 5000)\nopenRegistrations | Advertise via nodeinfo if an instance allows instant registration (default false)\nnodeInfoMetadata | Object of additional data to provde in nodeinfo reponses\n\nBlocked, rejections, and rejected: these routes must be defined in order to track\nthese items internally for each actor, but they do not need to be exposed endpoints\n(and probably should not be public even then)\n\n### System User / GET authentication\n\nSome federated apps may require http signature authentication on GET requests.\nTo enable this functionality, set the `systemUser` property on your apex instance\nequal to an actor created with `createActor` (generally of type 'Service')\nand saved to your object store.\nIts keys will be used to sign all federated object retrieval requests.\nThis property can be set after initializing your apex instance, as\nyou will need access to the `createActor` method and a database connection.\n\n```\nconst ActivitypubExpress = require('activitypub-express')\nconst apex = ActivitypubExpress(options)\n// ... connect to db\napex.createActor('system-user', 'System user', '', null, 'Service')\n  .then(async actor =\u003e {\n    await apex.store.saveObject(actor)\n    apex.systemUser = actor\n  })\n```\n\n## FAQ\n\nQ: How do I resolve this error seen when receiving/delivering activities or running the federation tests: `Uncaught exception: InvalidHeaderError: bad param format`\n\nA: Run `npm dedupe` to ensure that the `request` library is using the patched version of the `http-signature` library.\n\n## Implementation status\n\n* [ ] Shared server- \u0026 client-to-server\n  * [x] Inbox GET\n  * [x] Outbox GET\n  * [ ] Shared inbox GET\n  * [x] Resource GET\n    * [x] Object\n    * [x] Actor\n    * [x] Activity\n    * [x] Collections\n      * [x] Special collections\n        * [x] Inbox\n        * [x] Outbox\n        * [x] Followers\n        * [x] Following\n        * [x] Liked\n        * [x] Likes\n        * [x] Shares\n      * [x] Misc collections (of activities)\n      * [x] Pagination\n    * [x] Relay requests for remote objects\n    * [x] Response code 410 for Tombstones\n  * [x] Security\n    * [x] Permission-based filtering\n* [ ] Server-to-server\n  * [x] Inbox POST\n    * [x] Activity side-effects\n      * [x] Create\n      * [x] Update\n      * [x] Delete\n      * [x] Follow\n      * [x] Accept\n      * [x] Reject[*](#implementation-notes)\n      * [x] Add[*](#implementation-notes)\n      * [x] Remove[*](#implementation-notes)\n      * [x] Like\n      * [x] Announce\n      * [x] Undo\n      * [x] Other acivity types\n    * [x] Security\n      * [x] Signature validation\n      * [x] Honor recipient blocklist\n    * [x] Recursive resolution of related objects\n    * [x] Forwarding from inbox\n  * [ ] Shared inbox POST\n    * [ ] Delivery to targeted local inboxes\n  * [x] Delivery\n    * [x] Request signing\n    * [x] Addressing\n      * [x] Shared inbox optmization\n      * [ ] Direct delivery to local inboxes\n    * [x] Redelivery attempts\n* [ ] Client-to-server\n  * [x] Outbox POST\n    * [x] Auto-Create for bare objects\n    * [x] Activity side-effects\n      * [x] Create\n      * [x] Update\n      * [x] Delete\n      * [x] Follow\n      * [x] Accept\n      * [x] Reject\n      * [x] Add\n      * [x] Remove\n      * [x] Like\n      * [x] Block[*](#implementation-notes)\n      * [x] Undo\n      * [x] Other acivity types\n  * [ ] Media upload\n* [ ] Other\n  * [x] Actor creation\n    * [x] Key generation\n  * [x] Security\n    * [x] Verification\n    * [x] localhost block\n    * [x] Recursive object resolution depth limit\n  * [ ] Related standards\n    * [x] http-signature\n    * [x] webfinger\n    * [x] json-ld\n      * [x] Context cache\n    * [x] nodeinfo\n    * [ ] Linked data signatures\n  * [x] Storage model (denormalized MongoDB)\n    * [ ] Index coverage for all queries\n    * [ ] Fully interchangeable with documented API\n\n### Implementation notes\n\n* `actor.streams` miscellaneous collections: Add/Remove activities create custom collections\non the fly using the id scheme in `routes.collections`, but these are not publicized by default.\nTo make a custom collection public, you'll need to publish Updates to the collection object with each Add/Remove. However, for those updates to be verified, the actor must demonstrate ownership\nby adding the collection id as a property value in `actor.streams` (and publishing the actor object update)\n\n* Addressing collections: in addition to followers, apex can also address to miscellaneous\ncollections. It will add actors from the actor and/or object fields of each activity in the\ncollection to the audience.\n\n* Inbox Add/Remove: I don't see a general purpose\n(i.e. a remote actor being able to modify local collections);\nspecific uses can be added in the implementation via the event handler.\n\n* Reject: Activity is added to the actor's rejected (outbox) or rejection (inbox) collection.\nIf the object is a Follow that was previously accepted, this will also remove it from\nthe followers (outbox) or following (inbox) collection.\n\n* Block: Activity is added to the actor's blocked collection.\nPer spec, future activities from blocked actors will be silently ignored.\nAdditionally, past activities will be filtered from display in the inbox and followers\ncollections, but they are not permanently deleted, so they would re-appear after undo of block.\n\n* Rate limits: not included in `activitpub-express`; should be handled in the specific implementation\n\n* Content sanitization: the apex default store will sanitize for storage in MongoDB,\nbut display sanitization is not included in `activitpub-express`.\nThis should be handled in the specific implementation\n\n* Authorization: the prepacked GET middlewares will only return items that are\npublicly addressed unless the request is authorized.\n**Determining the requesting user**: By default, apex will check for\n[PassportJS](http://www.passportjs.org/)-style authentication,\nwhere `request.user.username` has the `preferredUsername` of the authorized actor.\nOverride this by setting `response.locals.apex.authorizedUserId` to an actor IRI.\n**Determining authorization**: By default, a request is considered authorized\nif the `authorizedUserId` is the item's owner.\nOverride this by setting `response.locals.apex.authorized` to `true` (allow) or `false` (deny)\n\n* Client-to-server unfollow and unblock:\nto remove an actor from followers, following, or blocked, an \"Undo\" must be sent with\nthe object being the prior outgoing Follow/Accept/Block. As these past activity ids are\nnot readily available to clients, the client may send the undo (unfollow/unblock) or\nreject (remove from followers) with an Actor IRI as\nthe object, and the server will find the related Follow/Accept/Block and substitute it before publishing.\n\n### Federation notes\n\n* **http signatures**\n  * In production mode, incoming POST requests without valid http signatures will be\n  rejected (401 if missing, 403 if invalid)\n  * Outoing POST requests are signed ('(request-target)', 'host', 'date', 'digest')\n  with the actor's keypair using the `Signature` header\n  * When using the `systemUser` config option, outgoing GET requests are signed\n  ('(request-target)', 'host', 'date') with the system user's keypair using the\n  `Signature` header\n* **Synchronizing collections**\n  * An apex server does not modify collections that belong to other servers\n   and does not expect other servers to maintain the state of its collections.\n  * Instead, an Update activity is published whenever the content of\n  a collection changes.\n  * If it is an actor collection (e.g. followers), the Update\n  object will be the `OrderedCollection` itself. Other servers can verify ownership\n  by checking that the `Actor` object of the sender contains a reference to\n  the collection.\n  * If it is an activity collection (likes/shares), the Update object\n  will be the activity itself with the collection objects embedded.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimmers-space%2Factivitypub-express","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fimmers-space%2Factivitypub-express","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimmers-space%2Factivitypub-express/lists"}