Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions at-intent/capabilities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"capabilities": [
{
"name": "Save to Skyreader",
"icon": "https://skyreader.app/icons/icon-512.png",
"description": "Save an article to your Skyreader reading list (read-it-later). Accepts any web URL. Not for following a source over time; use subscribe for that.",
"verb": "save",
"subject": [{ "kind": "uri" }],
"input": [{ "name": "title", "kind": "string", "required": false }],
"delivery": "service",
"endpoint": "https://api.skyreader.app/xrpc/app.skyreader.feed.save",
"serviceId": "skyreader_api"
},
{
"name": "Subscribe to feed in Skyreader",
"icon": "https://skyreader.app/icons/icon-512.png",
"description": "Subscribe to an RSS/Atom feed by URL so new items appear in your Skyreader reading list. For following an atproto account or publication, this maps to the account-follow capability instead.",
"verb": "subscribe",
"subject": [{ "kind": "uri" }],
"input": [{ "name": "category", "kind": "string", "required": false }],
"delivery": "service",
"endpoint": "https://api.skyreader.app/xrpc/app.skyreader.feed.subscribe",
"serviceId": "skyreader_api"
},
{
"name": "Follow in Skyreader",
"icon": "https://skyreader.app/icons/icon-512.png",
"description": "Follow an atproto account or standard.site publication so its new posts appear in Skyreader. Requires Atmospheric sync to be enabled on the account.",
"verb": "subscribe",
"subject": [
{ "kind": "did", "as": "account" },
{ "kind": "at-uri", "of": "site.standard.publication" }
],
"produces": ["site.standard.graph.subscription"],
"delivery": "repo",
"scope": "repo:site.standard.graph.subscription"
},
{
"name": "Share to Skyreader linkblog",
"icon": "https://skyreader.app/icons/icon-512.png",
"description": "Share a link to your public linkblog with an optional note. The post appears in your portable standard.site linkblog, visible across the Atmosphere.",
"verb": "share",
"subject": [{ "kind": "uri" }],
"input": [
{ "name": "title", "kind": "string", "required": false },
{ "name": "note", "kind": "string", "required": false },
{ "name": "tags", "kind": "string", "required": false }
],
"delivery": "service",
"endpoint": "https://api.skyreader.app/xrpc/app.skyreader.linkblog.share",
"serviceId": "skyreader_api"
}
]
}
38 changes: 38 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"format:check": "prettier --check ."
},
"dependencies": {
"@noble/curves": "^2.2.0",
"@scure/base": "^2.2.0",
"fast-xml-parser": "^5.2.0"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions backend/src/config/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Skyreader's own atproto identity (handle skyreader.app). This is the repo where
// Skyreader publishes its dev.at-intent.capability records, the `app` value written into
// users' dev.at-intent.usage footprints, AND the expected `aud` of an atproto service-auth
// JWT presented to Skyreader's XRPC endpoints. Resolve with:
// curl https://skyreader.app/.well-known/atproto-did
export const SKYREADER_APP_DID = 'did:plc:ra4jsemddo2ii4pn5jaf6x4v';
10 changes: 10 additions & 0 deletions backend/src/config/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,21 @@ export const LINKBLOG_SCOPES = ['repo:site.standard.publication', 'repo:site.sta
// check for sharing) so adding it doesn't retroactively over-restrict shares.
export const ATMOSPHERE_SCOPES = ['repo:site.standard.graph.subscription'];

// AT Intents discovery footprint — lets Skyreader write a dev.at-intent.usage record
// into the user's OWN repo so other Atmosphere apps/agents can discover that the user
// uses Skyreader and resolve the capabilities it publishes. Deliberately kept OUT of
// GRANULAR_SCOPES (it's not required for any core Skyreader action), so requesting it
// doesn't push existing users through a hasRequiredScopes re-auth. It's only added to
// ALL_POSSIBLE_SCOPES, which is what the login/callback flow actually requests, so new
// logins pick it up and the usage write is skipped for sessions that lack it.
export const AT_INTENT_SCOPES = ['repo:dev.at-intent.usage'];

// All possible scopes (base + all integrations) — used in client metadata
export const ALL_POSSIBLE_SCOPES = [
GRANULAR_SCOPES,
...SEMBLE_SCOPES,
...MARGIN_SCOPES,
...LINKBLOG_SCOPES,
...ATMOSPHERE_SCOPES,
...AT_INTENT_SCOPES,
].join(' ');
13 changes: 13 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
handleDeleteLabel,
handleBulkAddLabels,
} from './routes/labels';
import { handleXrpcSave, handleXrpcSubscribe, handleXrpcLinkblogShare } from './routes/xrpc';
import {
handleCreateSaved,
handleGetSaved,
Expand Down Expand Up @@ -527,6 +528,18 @@ export default {
response = await handleSyncStatus(request, env);
break;

// AT Intents service endpoints (XRPC procedures). No top-level session guard:
// the handlers return XRPC-shaped `{ error, message }` auth errors themselves.
case url.pathname === '/xrpc/app.skyreader.feed.save':
response = await handleXrpcSave(request, env, ctx);
break;
case url.pathname === '/xrpc/app.skyreader.feed.subscribe':
response = await handleXrpcSubscribe(request, env, ctx);
break;
case url.pathname === '/xrpc/app.skyreader.linkblog.share':
response = await handleXrpcLinkblogShare(request, env);
break;

default:
response = new Response(JSON.stringify({ error: 'Not found' }), {
status: 404,
Expand Down
5 changes: 5 additions & 0 deletions backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ALL_POSSIBLE_SCOPES,
} from '../config/scopes';
import { getUserTier } from '../services/user-tier';
import { writeUsageRecord } from '../services/at-intent-usage';
import { getLimitsForTier } from '../config/tier-limits';
import {
buildSetCookieHeader,
Expand Down Expand Up @@ -749,6 +750,10 @@ export async function handleAuthCallback(
// Now store session (after user exists in DB due to FK constraint)
await storeSession(env, sessionId, session);

// Write the AT Intents discovery footprint into the user's repo (best-effort,
// skipped if the usage scope wasn't granted). Never block the login redirect on it.
ctx.waitUntil(writeUsageRecord(session));

// Build the session cookie to set during redirect
const cookieDomain = getCookieDomain(env, request);
const isSecure = isSecureContext(request);
Expand Down
Loading
Loading