@dwk/micropub
v1.0.0-beta.3
Published
Micropub create/update/delete endpoint with R2 media endpoint. Consumes IndieAuth tokens.
Downloads
403
Maintainers
Readme
@dwk/micropub
Micropub create/update/delete endpoint with R2 media endpoint. Consumes IndieAuth tokens.
Part of the @dwk IndieWeb + Solid cohort. See the
package specification for the full requirements.
A Micropub server that runs as a
Cloudflare Worker. It accepts both JSON and form-encoded requests, authorizes
every request with a DPoP-bound IndieAuth access token (issued by
@dwk/indieauth), stores published posts as microformats2 source
in D1, and backs its media endpoint with R2.
Usage
import { createMicropub, createMicropubContactStore } from "@dwk/micropub";
const micropub = createMicropub({
baseUrl: "https://example.com",
// the site owner's IndieAuth profile URL; tokens minted for any other `me`
// are rejected even if they carry the right scope
me: "https://example.com/",
// optional: defaults are `${origin}/micropub` and `${origin}/media`
micropubEndpoint: "https://example.com/micropub",
mediaEndpoint: "https://example.com/media",
syndicateTo: [{ uid: "https://news.example/@me", name: "Example News" }],
// Proposed extensions stay opt-in. These are metadata for the site's
// serving/access-control layer; Micropub itself does not enforce them.
extensions: { proposed: true },
audiences: [{ uid: "family", name: "Family" }],
contacts: createMicropubContactStore,
});
export default {
fetch(request, env, ctx) {
return micropub(request, env, ctx);
},
};Bindings (declared Env fragment)
The handler fails loudly at startup if any of these are missing:
MEDIA— R2 bucket backing the media endpoint.MICROPUB_DB— D1 database holding published post records.AUTH_DB— the@dwk/indieauthissued-token store, consulted for revocation.TOKEN_SIGNING_KEY— the secret the IndieAuth token endpoint signs tokens with.
What it implements
- Create (
h-entryetc.) from JSON, form-encoded, andmultipart/form-databodies — the latter folds uploaded files (e.g.photo) into the post. - Update (JSON
replace/add/delete), delete, and undelete (soft, reversible). - Media endpoint: streams uploads to R2 and serves them back.
- Queries:
q=config,q=source(with aproperties[]filter), andq=syndicate-to. - Opt-in Contacts (
q=contact): a private h-card address book with filtered, paginated listing and create/update/delete lifecycle actions. - Opt-in proposed metadata: named private-post
audiencevalues andlocation-visibility(public,private, or textual-onlytext). They are persisted and returned byq=source; the site or WAC layer enforces access control and redaction. - Opt-in source-list filters: proposed deployments can filter a
q=sourcelist by creation bounds, type, status, visibility, or exact mf2 properties; filtered lists use deterministic keyset cursors. - Opt-in Location/Venue (
q=geo): a read-only proximity search over an injected venue store, independent from post storage. See below. - Opt-in media-endpoint extensions: with
extensions.proposedon, the media endpoint gains aq=sourcelisting (newest-first,mediascope required) and by-URL lookup, a{ "url": ... }JSON body on upload, and recoverableaction=delete/action=undelete(requiring both the action scope andmedia). Deleted blobs move to an R2.trash/prefix retained formediaTrashRetentionDays(default 30); configure an R2 lifecycle rule on that prefix to purge the bytes.
Location/Venue (q=geo) extension
The q=geo extension is implemented for the proposed Location/Venue feature.
It remains disabled by default (extensions.proposed: false); clients must
enable the proposed group and configure a venues store to use it.
A GET ?q=geo&uri=geo:lat,lon;u=radius or GET ?q=geo&lat=...&lon=...&u=... query
returns a geo suggestion and nearby venues ordered by distance. geo is not
a real reverse-geocoding lookup — this first implementation echoes the query
coordinates back as geo.label; wiring in an actual place-name service is
future work. Each venue has name, latitude, longitude, and a canonical
url (populated by whatever writes venue rows — venue create/update/delete is
out of scope for this read-only query). Clients reference a venue via the
post's location property (either plain text or an h-card with url). The
store is independent of post storage — querying q=geo never reads post data.
import { createMicropub, createMicropubVenueStore } from "@dwk/micropub";
const micropub = createMicropub({
baseUrl: "https://example.com",
me: "https://example.com/",
extensions: { proposed: true },
venues: createMicropubVenueStore(env),
});See the package specification.
Every request is authorized by an IndieAuth access token whose scope gates the
action (create, update, delete, media), with the DPoP proof-of-possession
binding completed via @dwk/dpop and revocation checked against the
strongly-consistent token store.
