max / makenotwork
7 files changed,
+214 insertions,
-2 deletions
| @@ -4402,6 +4402,7 @@ dependencies = [ | |||
| 4402 | 4402 | "urlencoding", | |
| 4403 | 4403 | "utoipa", | |
| 4404 | 4404 | "utoipa-axum", | |
| 4405 | + | "utoipa-swagger-ui", | |
| 4405 | 4406 | "uuid", | |
| 4406 | 4407 | "webauthn-authenticator-rs", | |
| 4407 | 4408 | "webauthn-rs", | |
| @@ -6069,6 +6070,41 @@ dependencies = [ | |||
| 6069 | 6070 | ] | |
| 6070 | 6071 | ||
| 6071 | 6072 | [[package]] | |
| 6073 | + | name = "rust-embed" | |
| 6074 | + | version = "8.12.0" | |
| 6075 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6076 | + | checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" | |
| 6077 | + | dependencies = [ | |
| 6078 | + | "rust-embed-impl", | |
| 6079 | + | "rust-embed-utils", | |
| 6080 | + | "walkdir", | |
| 6081 | + | ] | |
| 6082 | + | ||
| 6083 | + | [[package]] | |
| 6084 | + | name = "rust-embed-impl" | |
| 6085 | + | version = "8.12.0" | |
| 6086 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6087 | + | checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" | |
| 6088 | + | dependencies = [ | |
| 6089 | + | "mime_guess", | |
| 6090 | + | "proc-macro2", | |
| 6091 | + | "quote", | |
| 6092 | + | "rust-embed-utils", | |
| 6093 | + | "syn 2.0.118", | |
| 6094 | + | "walkdir", | |
| 6095 | + | ] | |
| 6096 | + | ||
| 6097 | + | [[package]] | |
| 6098 | + | name = "rust-embed-utils" | |
| 6099 | + | version = "8.12.0" | |
| 6100 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6101 | + | checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" | |
| 6102 | + | dependencies = [ | |
| 6103 | + | "sha2 0.11.0", | |
| 6104 | + | "walkdir", | |
| 6105 | + | ] | |
| 6106 | + | ||
| 6107 | + | [[package]] | |
| 6072 | 6108 | name = "rustc-hash" | |
| 6073 | 6109 | version = "1.1.0" | |
| 6074 | 6110 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -7949,6 +7985,24 @@ dependencies = [ | |||
| 7949 | 7985 | ] | |
| 7950 | 7986 | ||
| 7951 | 7987 | [[package]] | |
| 7988 | + | name = "utoipa-swagger-ui" | |
| 7989 | + | version = "9.0.2" | |
| 7990 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 7991 | + | checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" | |
| 7992 | + | dependencies = [ | |
| 7993 | + | "axum", | |
| 7994 | + | "base64 0.22.1", | |
| 7995 | + | "mime_guess", | |
| 7996 | + | "regex", | |
| 7997 | + | "rust-embed", | |
| 7998 | + | "serde", | |
| 7999 | + | "serde_json", | |
| 8000 | + | "url", | |
| 8001 | + | "utoipa", | |
| 8002 | + | "zip 3.0.0", | |
| 8003 | + | ] | |
| 8004 | + | ||
| 8005 | + | [[package]] | |
| 7952 | 8006 | name = "uuid" | |
| 7953 | 8007 | version = "1.23.4" | |
| 7954 | 8008 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -9232,6 +9286,20 @@ dependencies = [ | |||
| 9232 | 9286 | ||
| 9233 | 9287 | [[package]] | |
| 9234 | 9288 | name = "zip" | |
| 9289 | + | version = "3.0.0" | |
| 9290 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9291 | + | checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" | |
| 9292 | + | dependencies = [ | |
| 9293 | + | "arbitrary", | |
| 9294 | + | "crc32fast", | |
| 9295 | + | "flate2", | |
| 9296 | + | "indexmap", | |
| 9297 | + | "memchr", | |
| 9298 | + | "zopfli", | |
| 9299 | + | ] | |
| 9300 | + | ||
| 9301 | + | [[package]] | |
| 9302 | + | name = "zip" | |
| 9235 | 9303 | version = "8.6.0" | |
| 9236 | 9304 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9237 | 9305 | checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" |
| @@ -21,6 +21,7 @@ axum-extra = { version = "0.10.3", features = ["cookie", "form", "typed-header"] | |||
| 21 | 21 | # OpenAPI spec generation | |
| 22 | 22 | utoipa = { version = "5", features = ["axum_extras", "chrono", "uuid"] } | |
| 23 | 23 | utoipa-axum = "0.2" | |
| 24 | + | utoipa-swagger-ui = { version = "9", features = ["axum"] } | |
| 24 | 25 | serde = { version = "1.0.228", features = ["derive"] } | |
| 25 | 26 | serde_json = "1.0.149" | |
| 26 | 27 | toml = "0.8" |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # Developer API Overview | |
| 2 | 2 | ||
| 3 | - | Makenot.work provides four APIs for building integrations, desktop applications, and developer tools: session-based (web dashboard), SyncKit JWT (cloud sync and OTA), OAuth2 PKCE (third-party apps), and a public License Key API. This page covers authentication methods, error handling, and rate limits shared across all endpoints. | |
| 3 | + | Makenot.work provides four APIs for building integrations, desktop applications, and developer tools: session-based (web dashboard), SyncKit JWT (cloud sync and OTA), OAuth2 PKCE (third-party apps), and a public License Key API. Public [RSS feeds](./feeds.md) are documented separately. This page covers authentication methods, error handling, and rate limits shared across all endpoints. | |
| 4 | 4 | ||
| 5 | 5 | ## Authentication Methods | |
| 6 | 6 | ||
| @@ -20,6 +20,7 @@ Public endpoints that require no auth: | |||
| 20 | 20 | - [License Key API](./license-keys.md): key validation, activation, deactivation | |
| 21 | 21 | - [OTA update check](./ota.md): Tauri-compatible update endpoint | |
| 22 | 22 | - [OAuth authorize](./oauth.md): authorization page and code exchange | |
| 23 | + | - [Public Feeds](./feeds.md): RSS feeds for creators, projects, and blog posts | |
| 23 | 24 | ||
| 24 | 25 | ## Error Format | |
| 25 | 26 | ||
| @@ -65,10 +66,25 @@ All rate limits are per IP. Check response headers (`X-RateLimit-Limit`, `X-Rate | |||
| 65 | 66 | ||
| 66 | 67 | Exceeding a limit returns HTTP 429. Implement exponential backoff in your client. | |
| 67 | 68 | ||
| 69 | + | ## OpenAPI Spec | |
| 70 | + | ||
| 71 | + | A machine-readable OpenAPI 3 spec is served at: | |
| 72 | + | ||
| 73 | + | ``` | |
| 74 | + | GET /api/openapi.json | |
| 75 | + | ``` | |
| 76 | + | ||
| 77 | + | An interactive Swagger UI viewer for the same spec is available at [`/api/docs`](/api/docs) — send test requests, browse schemas, and copy example payloads without leaving the browser. | |
| 78 | + | ||
| 79 | + | The spec covers the SyncKit and License Key endpoints (the surfaces with frozen request/response shapes). Point any OpenAPI-aware client generator, Postman-style tool, or IDE plugin at the JSON URL. The spec's `version` matches the server version, so a hard-refresh after a deploy picks up any schema changes. | |
| 80 | + | ||
| 81 | + | OAuth, OTA, and RSS feeds are documented on their own pages rather than in the spec — OAuth is fully described by [discovery metadata](./oauth.md#discovery-metadata), OTA follows Tauri's protocol, and RSS is an XML surface. | |
| 82 | + | ||
| 68 | 83 | ## API Reference | |
| 69 | 84 | ||
| 70 | 85 | - [SyncKit Cloud Sync](./synckit.md): push/pull encrypted data, device management, blob storage | |
| 71 | 86 | - [OTA Updates](./ota.md): app auto-update server (Tauri-compatible protocol) | |
| 72 | 87 | - [OAuth2 PKCE](./oauth.md): "Log in with Makenot.work" | |
| 73 | 88 | - [License Key API](./license-keys.md): validate, activate, and deactivate license keys | |
| 89 | + | - [Public Feeds](./feeds.md): RSS feeds for creators, projects, blogs, and the changelog | |
| 74 | 90 | - [Rustdoc API Reference](/rustdoc/synckit_client/): SyncKit client SDK documentation |
| @@ -0,0 +1,113 @@ | |||
| 1 | + | # Public Feeds API | |
| 2 | + | ||
| 3 | + | Makenot.work publishes creator and project content as RSS 2.0 feeds. The feeds are unauthenticated, uncapped by application, and stable — aggregators can rely on them without registering an app or requesting a key. | |
| 4 | + | ||
| 5 | + | The fan-facing walkthrough lives at [RSS Feeds](../guide/rss.md). This page is for aggregators, feed readers, and anyone building on top of MNW content. | |
| 6 | + | ||
| 7 | + | ## Open by Policy | |
| 8 | + | ||
| 9 | + | These feeds are a permanent, first-class integration surface. Any aggregator — including ones that compete with Balanced Breakfast (our own aggregator) — can consume them on equal footing. There is no privileged internal path; BB reads the same URLs as everyone else. | |
| 10 | + | ||
| 11 | + | Concretely, this means: | |
| 12 | + | ||
| 13 | + | - The URL shapes below will not be broken. Additions are fine; removals require a migration path and an entry in the [changelog feed](https://makenot.work/changelog/feed.xml). | |
| 14 | + | - No API key, contract, or business relationship is required to poll them. | |
| 15 | + | - We will not rate-limit a well-behaved aggregator more aggressively than a browser polling the same URL. | |
| 16 | + | ||
| 17 | + | ## Endpoints | |
| 18 | + | ||
| 19 | + | All responses are `application/rss+xml; charset=utf-8`, RSS 2.0. | |
| 20 | + | ||
| 21 | + | | Endpoint | Contents | | |
| 22 | + | |----------|----------| | |
| 23 | + | | `GET /u/{username}/rss` | A creator's public items across all projects | | |
| 24 | + | | `GET /p/{slug}/rss` | A project's public items | | |
| 25 | + | | `GET /p/{slug}/blog/feed.xml` | A project's published blog posts | | |
| 26 | + | | `GET /changelog/feed.xml` | Platform changelog (canonical alias for the `changelog` project blog) | | |
| 27 | + | | `GET /feed/{user_id}?sig={hmac}&v={version}` | Personal feed of items from followed creators and projects | | |
| 28 | + | ||
| 29 | + | Sandbox accounts are excluded from all feeds; requests against a sandbox username or slug return `404`. | |
| 30 | + | ||
| 31 | + | ### Item Feeds (`/u/*/rss`, `/p/*/rss`) | |
| 32 | + | ||
| 33 | + | Each `<item>` element carries: | |
| 34 | + | ||
| 35 | + | | Field | Source | | |
| 36 | + | |-------|--------| | |
| 37 | + | | `<title>` | Item title | | |
| 38 | + | | `<link>` | `{host}/i/{item_id}` — the canonical item page | | |
| 39 | + | | `<description>` | Item description (empty string if unset) | | |
| 40 | + | | `<pubDate>` | Item `created_at` | | |
| 41 | + | | `<guid>` | Item ID (UUID). Stable for the life of the item. | | |
| 42 | + | ||
| 43 | + | The GUID is the item's UUID, not its URL, so aggregators can detect a renamed or moved item as the same entry. | |
| 44 | + | ||
| 45 | + | ### Blog Feeds (`/p/*/blog/feed.xml`, `/changelog/feed.xml`) | |
| 46 | + | ||
| 47 | + | Blog `<item>` elements carry: | |
| 48 | + | ||
| 49 | + | | Field | Source | | |
| 50 | + | |-------|--------| | |
| 51 | + | | `<title>` | Post title | | |
| 52 | + | | `<link>` | `{host}/p/{project_slug}/blog/{post_slug}` (or `{host}/changelog/{post_slug}` for the changelog alias) | | |
| 53 | + | | `<description>` | First 300 characters of the post body (Markdown, not rendered) | | |
| 54 | + | | `<pubDate>` | `published_at`, falling back to `created_at` if unset | | |
| 55 | + | | `<guid>` | Post ID (UUID) | | |
| 56 | + | ||
| 57 | + | Only published posts appear. Drafts and scheduled-future posts are omitted. | |
| 58 | + | ||
| 59 | + | ### Personal Feed (`/feed/{user_id}`) | |
| 60 | + | ||
| 61 | + | The personal feed aggregates new items from creators and projects the user follows. Unlike the other endpoints, it is per-user and private, so it is authenticated by an HMAC signature in the query string — no cookie or bearer token, which lets any RSS reader fetch it. | |
| 62 | + | ||
| 63 | + | The signed URL is minted for the user in the dashboard. Users can rotate it (the URL carries a version `v`; when the user regenerates, the old version's signatures are rejected). Aggregators should treat a `403` on this endpoint as a signal that the user needs to paste in a fresh URL. | |
| 64 | + | ||
| 65 | + | ## URL Stability | |
| 66 | + | ||
| 67 | + | The paths above are load-bearing. In particular: | |
| 68 | + | ||
| 69 | + | - `/u/{username}/rss` and `/p/{slug}/rss` will remain valid for as long as the username or slug exists. | |
| 70 | + | - If a creator renames their account or a project changes slug, the old feed URL stops working. There is no automatic redirect. GUIDs remain stable across renames, so an aggregator that already ingested items will not lose de-duplication. | |
| 71 | + | - `/changelog/feed.xml` is a canonical alias; it will follow the changelog wherever it moves. | |
| 72 | + | ||
| 73 | + | New endpoints may be added under `/feed/`, `/u/`, or `/p/`. Existing ones will not be removed without a deprecation window announced on the changelog feed. | |
| 74 | + | ||
| 75 | + | ## Discovery | |
| 76 | + | ||
| 77 | + | There is no JSON directory endpoint yet, but three existing mechanisms cover the common cases: | |
| 78 | + | ||
| 79 | + | - **Sitemap.** `GET /sitemap.xml` enumerates every active public creator (`/u/{username}`) and every public item (`/i/{item_id}`). An aggregator can walk the sitemap and construct `/u/{username}/rss` from each creator URL. The sitemap is cached and rate-friendly to crawl. | |
| 80 | + | - **Autodiscovery `<link>` tags.** Three page types advertise their feed via `<link rel="alternate" type="application/rss+xml">` in the HTML `<head>`: | |
| 81 | + | - `/u/{username}` — the creator's item feed | |
| 82 | + | - `/p/{slug}` — the project's item feed | |
| 83 | + | - `/p/{slug}/blog` — the project's blog feed | |
| 84 | + | Standard feed-reader auto-detection picks these up. | |
| 85 | + | - **Direct construction.** If you have a username or project slug already, build the URL from the endpoint table above. | |
| 86 | + | ||
| 87 | + | A JSON discovery endpoint (creators and projects opting into aggregation, with metadata) is a planned addition; it will land here when it ships. | |
| 88 | + | ||
| 89 | + | ## Polling | |
| 90 | + | ||
| 91 | + | Feeds are cheap to generate but not free. Recommended cadence: | |
| 92 | + | ||
| 93 | + | - **Item and blog feeds**: poll no more than every 15 minutes per URL. | |
| 94 | + | - **Changelog feed**: hourly is plenty. | |
| 95 | + | - **Personal feed**: poll on the same cadence the user's reader would — typically 15–60 minutes. | |
| 96 | + | ||
| 97 | + | There is no per-endpoint rate limit distinct from the platform baseline described in [API Overview](./api-overview.md). Feeds share the general read-path budget. If you are polling thousands of URLs, stagger requests rather than firing them in a burst. | |
| 98 | + | ||
| 99 | + | Conditional requests (`If-Modified-Since`, `ETag`) are not currently honored — every request re-renders the feed. Assume you will download the full body each poll and de-duplicate on GUID. This may change; watch the changelog feed. | |
| 100 | + | ||
| 101 | + | ## Content and Licensing | |
| 102 | + | ||
| 103 | + | The feeds carry titles, descriptions, publication dates, and canonical links back to makenot.work. They do not carry the underlying media (audio, video, downloads, or paid content), which remains gated behind the item page. | |
| 104 | + | ||
| 105 | + | Redistribution of feed metadata (titles, descriptions, links) for the purpose of aggregation, indexing, or notification is expressly permitted. Attribution back to the creator's makenot.work page via the `<link>` element is required and is included in every entry by default — do not strip it. | |
| 106 | + | ||
| 107 | + | Republishing feed content as if it were your own, or scraping full media through the feed's link elements, is not permitted and is not what these feeds are for. When in doubt, link back. | |
| 108 | + | ||
| 109 | + | ## See Also | |
| 110 | + | ||
| 111 | + | - [RSS Feeds (fan guide)](../guide/rss.md): subscribing in a reader, podcast setup | |
| 112 | + | - [API Overview](./api-overview.md): shared error format and rate-limit policy | |
| 113 | + | - [Changelog](https://makenot.work/changelog): platform changes, including feed additions and deprecations |
| @@ -2,6 +2,16 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Makenot.work supports OAuth2 Authorization Code with PKCE for "Log in with Makenot.work" flows. This lets third-party applications authenticate users without handling their passwords directly. | |
| 4 | 4 | ||
| 5 | + | ## Discovery Metadata | |
| 6 | + | ||
| 7 | + | Endpoints, supported scopes, and grant types are published at the standard RFC 8414 discovery URL: | |
| 8 | + | ||
| 9 | + | ``` | |
| 10 | + | GET /.well-known/oauth-authorization-server | |
| 11 | + | ``` | |
| 12 | + | ||
| 13 | + | Most OAuth libraries auto-configure from this URL — pass the base host and they will fetch the metadata. Manual configuration values are documented in the sections below. | |
| 14 | + | ||
| 5 | 15 | ## Overview | |
| 6 | 16 | ||
| 7 | 17 | 1. Your app generates a PKCE code verifier and challenge |
| @@ -213,6 +213,10 @@ pub fn build_app( | |||
| 213 | 213 | .merge(git_routes()) | |
| 214 | 214 | .merge(routes::embed::embed_routes()) | |
| 215 | 215 | .route("/api/openapi.json", axum::routing::get(openapi::openapi_json)) | |
| 216 | + | .merge( | |
| 217 | + | utoipa_swagger_ui::SwaggerUi::new("/api/docs") | |
| 218 | + | .url("/api/openapi.json", <openapi::ApiDoc as utoipa::OpenApi>::openapi()), | |
| 219 | + | ) | |
| 216 | 220 | .nest_service( | |
| 217 | 221 | "/static", | |
| 218 | 222 | tower::ServiceBuilder::new() |
| @@ -119,7 +119,7 @@ use utoipa::OpenApi; | |||
| 119 | 119 | ("bearer" = []), | |
| 120 | 120 | ), | |
| 121 | 121 | )] | |
| 122 | - | struct ApiDoc; | |
| 122 | + | pub struct ApiDoc; | |
| 123 | 123 | ||
| 124 | 124 | /// Serve the OpenAPI spec as JSON. | |
| 125 | 125 | pub async fn openapi_json() -> impl IntoResponse { |