Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
970f5e6
feat(endpoint-github): add GitHub activity endpoint plugin
rmdes Jan 18, 2026
71e6198
feat(endpoint-github): add /api/featured endpoint
rmdes Jan 18, 2026
ba349df
fix(endpoint-github): add fallback for commits when Events API lacks …
rmdes Jan 24, 2026
6782449
chore: remove endpoint-github from monorepo
rmdes Jan 24, 2026
19376f9
feat(endpoint-microsub): implement Phase 1 - core Microsub plugin str…
rmdes Jan 25, 2026
9de2fff
feat(endpoint-microsub): implement Phase 2 - feed ingestion pipeline
rmdes Jan 25, 2026
6a4de2b
feat(endpoint-microsub): implement Phase 3 - search and preview
rmdes Jan 25, 2026
651732f
feat(endpoint-microsub): implement Phase 4 - filtering system
rmdes Jan 25, 2026
998bffe
feat(endpoint-microsub): implement Phase 5 - real-time events
rmdes Jan 25, 2026
adfc322
feat(endpoint-microsub): implement Phase 6 - webmention receiving
rmdes Jan 25, 2026
c2e7889
feat(endpoint-microsub): implement Phase 7 - built-in reader UI
rmdes Jan 25, 2026
fa45ee8
chore(endpoint-microsub): update package for @rmdes npm scope
rmdes Jan 25, 2026
7fa3a85
fix(endpoint-microsub): use .sort() instead of .toSorted() for MongoD…
rmdes Jan 25, 2026
0982bdf
chore(endpoint-microsub): bump version to 1.0.0-beta.2
rmdes Jan 25, 2026
9ae98b5
fix(endpoint-microsub): redirect to reader UI when no action paramete…
rmdes Jan 25, 2026
b353f89
chore(endpoint-microsub): bump version to 1.0.0-beta.3
rmdes Jan 25, 2026
11fb55f
fix(endpoint-microsub): pass baseUrl to templates correctly
rmdes Jan 25, 2026
a43b421
fix(endpoint-microsub): use correct Indiekit frontend macros
rmdes Jan 26, 2026
e87bd6f
fix(endpoint-microsub): use valid Indiekit icon names and fix templat…
rmdes Jan 26, 2026
7afcb7d
feat(endpoint-microsub): add feeds management UI and fix locale inter…
rmdes Jan 26, 2026
3701356
feat(endpoint-microsub): add search/subscribe web UI for feed discovery
rmdes Jan 26, 2026
dbfaa89
fix(endpoint-microsub): add startup logging to diagnose scheduler issues
rmdes Jan 26, 2026
30d5831
feat(endpoint-microsub): add Redis connection from config URL
rmdes Jan 26, 2026
e8c50b2
fix(endpoint-microsub): cascade delete items when feed is removed
rmdes Jan 26, 2026
4cf23f5
fix: ensure name is a string before calling replace in getError
rmdes Jan 26, 2026
0d47d6a
feat(endpoint-microsub): release v1.0.0 with full Microsub spec compl…
rmdes Jan 26, 2026
b2727da
fix(endpoint-microsub): handle non-standard dates and ActivityPub sites
rmdes Jan 28, 2026
fc5f9d9
fix(endpoint-microsub): fix reader UI rendering issues
rmdes Jan 28, 2026
796bbf1
fix: improve service worker error handling
rmdes Jan 28, 2026
2b084bf
feat(endpoint-microsub): enhance reader UI with Aperture-inspired pat…
rmdes Jan 28, 2026
00c3e4d
fix(endpoint-microsub): replace non-existent min filter with conditional
rmdes Jan 28, 2026
1a7d616
fix(endpoint-microsub): support short-form query params in compose
rmdes Jan 28, 2026
4a72dee
chore(endpoint-microsub): add debug logging to compose submission
rmdes Jan 28, 2026
c2f476b
feat(endpoint-auth): add profile scope support for IndieAuth
rmdes Jan 28, 2026
78fef7d
fix(endpoint-microsub): handle object URLs in interaction properties
rmdes Jan 29, 2026
d626191
chore(endpoint-microsub): bump version to 1.0.7
rmdes Jan 29, 2026
fe80c19
fix(endpoint-microsub): fix error template rendering in compose submi…
rmdes Jan 29, 2026
fa6e329
chore(endpoint-microsub): bump version to 1.0.8
rmdes Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,546 changes: 2,018 additions & 1,528 deletions package-lock.json

Large diffs are not rendered by default.

23 changes: 21 additions & 2 deletions packages/endpoint-auth/lib/controllers/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IndiekitError } from "@indiekit/error";
import { getCanonicalUrl } from "@indiekit/util";

import { getClientInformation } from "../client.js";
import { getProfileInformation } from "../profile.js";
import { createRequestUri } from "../pushed-authorization-request.js";
import { validateRedirect } from "../redirect.js";

Expand Down Expand Up @@ -102,13 +103,31 @@ export const authorizationController = {
* @see {@link https://indieauth.spec.indieweb.org/#profile-url-response}
*/
async post(request, response) {
const profileToken = { me: request.verifiedToken.me };
const { me, scope } = request.verifiedToken;
const profileToken = { me };

// Include profile information if profile scope was requested
if (scope && scope.includes("profile")) {
const profile = await getProfileInformation(me);
if (profile) {
profileToken.profile = profile;
}
}

if (request.accepts("application/json")) {
response.json(profileToken);
} else {
response.set("content-type", "application/x-www-form-urlencoded");
response.send(new URLSearchParams(profileToken).toString());
// Flatten profile for form-urlencoded response
const parameters = new URLSearchParams();
for (const [key, value] of Object.entries(profileToken)) {
if (key === "profile" && typeof value === "object") {
parameters.set("profile", JSON.stringify(value));
} else {
parameters.set(key, String(value));
}
}
response.send(parameters.toString());
}
},
};
23 changes: 21 additions & 2 deletions packages/endpoint-auth/lib/controllers/token.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getProfileInformation } from "../profile.js";
import { signToken } from "../token.js";

export const tokenController = {
Expand All @@ -9,7 +10,7 @@ export const tokenController = {
* @see {@link https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code}
* @see {@link https://indieauth.spec.indieweb.org/#access-token-response}
*/
post(request, response) {
async post(request, response) {
const { me, scope } = request.verifiedToken;

const tokenData = { me, ...(scope && { scope }) };
Expand All @@ -19,11 +20,29 @@ export const tokenController = {
...tokenData,
};

// Include profile information if profile scope was requested
if (scope && scope.includes("profile")) {
const profile = await getProfileInformation(me);
if (profile) {
accessToken.profile = profile;
}
}

if (request.accepts("application/json")) {
response.json(accessToken);
} else {
response.set("content-type", "application/x-www-form-urlencoded");
response.send(new URLSearchParams(accessToken).toString());
// Flatten profile for form-urlencoded response
const parameters = new URLSearchParams();
for (const [key, value] of Object.entries(accessToken)) {
if (key === "profile" && typeof value === "object") {
// Encode profile as JSON string for form-urlencoded
parameters.set("profile", JSON.stringify(value));
} else {
parameters.set(key, String(value));
}
}
response.send(parameters.toString());
}
},
};
100 changes: 100 additions & 0 deletions packages/endpoint-auth/lib/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { mf2 } from "microformats-parser";

/**
* Get profile information from h-card on user's site
* @param {string} me - User's profile URL
* @returns {Promise<object|undefined>} Profile information (name, url, photo)
* @see {@link https://indieauth.spec.indieweb.org/#profile-information}
*/
export const getProfileInformation = async (me) => {
try {
const response = await fetch(me);
if (!response.ok) {
return;
}

const body = await response.text();
const { items } = mf2(body, { baseUrl: me });

// Find the representative h-card
// Per spec: h-card with u-url matching the profile URL
for (const item of items) {
if (item.type?.includes("h-card")) {
const { properties } = item;

// Check if this h-card represents the user (url matches me)
const urls = properties.url || [];
const hasMatchingUrl = urls.some((url) => {
const urlString = typeof url === "object" ? url.value : url;
return urlString === me || urlString === me.replace(/\/$/, "");
});

if (hasMatchingUrl || urls.length === 0) {
const profile = {};

// Extract name
if (properties.name?.[0]) {
const name = properties.name[0];
profile.name = typeof name === "object" ? name.value : name;
}

// Extract url
if (properties.url?.[0]) {
const url = properties.url[0];
profile.url = typeof url === "object" ? url.value : url;
} else {
profile.url = me;
}

// Extract photo
if (properties.photo?.[0]) {
const photo = properties.photo[0];
profile.photo = typeof photo === "object" ? photo.value : photo;
}

// Only return if we have at least some profile data
if (profile.name || profile.photo) {
return profile;
}
}
}
}

// If no h-card found, try looking in nested items
for (const item of items) {
if (item.children) {
for (const child of item.children) {
if (child.type?.includes("h-card")) {
const { properties } = child;
const profile = {};

if (properties.name?.[0]) {
const name = properties.name[0];
profile.name = typeof name === "object" ? name.value : name;
}

if (properties.url?.[0]) {
const url = properties.url[0];
profile.url = typeof url === "object" ? url.value : url;
} else {
profile.url = me;
}

if (properties.photo?.[0]) {
const photo = properties.photo[0];
profile.photo = typeof photo === "object" ? photo.value : photo;
}

if (profile.name || profile.photo) {
return profile;
}
}
}
}
}

return;
} catch {
return;
}
};
2 changes: 1 addition & 1 deletion packages/endpoint-auth/lib/scope.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const scopes = {
// IndieAuth scopes
email: { supported: false },
profile: { supported: false },
profile: { supported: true },
// Micropub scopes
create: { supported: true },
draft: { supported: true },
Expand Down
12 changes: 12 additions & 0 deletions packages/endpoint-auth/test/unit/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { strict as assert } from "node:assert";
import { describe, it } from "node:test";

import { getProfileInformation } from "../../lib/profile.js";

describe("endpoint-auth/lib/profile", () => {
it("Returns undefined for non-existent URL", async () => {
const result = await getProfileInformation("https://nonexistent.example");

assert.equal(result, undefined);
});
});
111 changes: 111 additions & 0 deletions packages/endpoint-microsub/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# @indiekit/endpoint-microsub

Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the [Microsub protocol](https://indieweb.org/Microsub).

## Features

- **Full Microsub API** - Channels, timeline, follow/unfollow, search, preview, mute, block
- **Multiple feed formats** - RSS 1.0/2.0, Atom, JSON Feed, h-feed (Microformats)
- **External client support** - Works with Monocle, Together, IndiePass
- **Built-in reader UI** - Social reading experience in Indiekit admin
- **Real-time updates** - Server-Sent Events (SSE) and WebSub integration
- **Adaptive polling** - Tier-based feed fetching inspired by Ekster
- **Direct webmention receiving** - Notifications channel for mentions

## Installation

`npm install @indiekit/endpoint-microsub`

## Usage

Add `@indiekit/endpoint-microsub` to the list of plugins in your configuration:

```js
export default {
plugins: ["@indiekit/endpoint-microsub"],
};
```

## Options

| Option | Type | Description |
| :----------- | :------- | :--------------------------------------------------------------- |
| `mountPath` | `string` | Path to mount Microsub API. _Optional_, defaults to `/microsub`. |
| `readerPath` | `string` | Path to mount reader UI. _Optional_, defaults to `/reader`. |

## Endpoints

### Microsub API

The main Microsub endpoint is mounted at `/microsub` (configurable).

**Discovery**: Add this to your site's `<head>`:

```html
<link rel="microsub" href="https://yoursite.com/microsub" />
```

### Supported actions

| Action | GET | POST | Description |
| :--------- | :-- | :--- | :--------------------------------------------- |
| `channels` | ✓ | ✓ | List, create, update, delete, reorder channels |
| `timeline` | ✓ | ✓ | Get timeline, mark read/unread, remove items |
| `follow` | ✓ | ✓ | List followed feeds, subscribe to new feeds |
| `unfollow` | - | ✓ | Unsubscribe from feeds |
| `search` | ✓ | ✓ | Feed discovery and full-text search |
| `preview` | ✓ | ✓ | Preview feed before subscribing |
| `mute` | ✓ | ✓ | List muted URLs, mute/unmute |
| `block` | ✓ | ✓ | List blocked URLs, block/unblock |
| `events` | ✓ | - | Server-Sent Events stream |

### Reader UI

The built-in reader is mounted at `/reader` (configurable) and provides:

- Channel list with unread counts
- Timeline view with items
- Mark as read on scroll/click
- Like/reply/repost via Micropub
- Channel settings (filters)
- Compose modal

### WebSub callbacks

WebSub hub callbacks are handled at `/microsub/websub/:id`.

### Webmention receiving

Direct webmentions can be sent to `/microsub/webmention`.

## MongoDB Collections

This plugin creates the following collections:

- `microsub_channels` - User's feed channels
- `microsub_feeds` - Subscribed feeds
- `microsub_items` - Timeline entries
- `microsub_notifications` - Webmention notifications
- `microsub_muted` - Muted URLs
- `microsub_blocked` - Blocked URLs

## Dependencies

- **feedparser** - RSS/Atom parsing
- **microformats-parser** - h-feed parsing
- **ioredis** - Redis client (optional, for caching/pub-sub)
- **sanitize-html** - XSS prevention

## External Clients

This endpoint is compatible with:

- [Monocle](https://monocle.p3k.io/) - Web-based reader
- [Together](https://together.tpxl.io/) - Web-based reader
- [IndiePass](https://indiepass.app/) - Mobile/desktop app (archived)

## References

- [Microsub Specification](https://indieweb.org/Microsub-spec)
- [Ekster](https://github.com/pstuifzand/ekster) - Reference implementation in Go
- [Aperture](https://github.com/aaronpk/Aperture) - Popular Microsub server in PHP
Loading