@geekity/cms 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -9
- package/admin/pages/account/login.njk +3 -0
- package/admin/pages/documents/editor.njk +20 -0
- package/admin/pages/indieauth/consent.njk +51 -0
- package/admin/pages/indieauth/refused.njk +11 -0
- package/admin/pages/settings/general.njk +12 -2
- package/admin/pages/settings/reading.njk +93 -0
- package/admin/pages/users/apps.njk +74 -0
- package/admin/pages/users/edit.njk +12 -0
- package/admin/static/admin.css +27 -0
- package/dist/admin/accounts.d.ts +3 -0
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +5 -0
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/documents.d.ts +4 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +45 -5
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.js +2 -2
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/headers.d.ts +9 -2
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +16 -7
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +2 -1
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +48 -5
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/settings-general.d.ts.map +1 -1
- package/dist/admin/settings-general.js +28 -3
- package/dist/admin/settings-general.js.map +1 -1
- package/dist/admin/settings-reading.d.ts.map +1 -1
- package/dist/admin/settings-reading.js +15 -1
- package/dist/admin/settings-reading.js.map +1 -1
- package/dist/admin/settings.d.ts +83 -4
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +166 -4
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/templates.d.ts +3 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +3 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/users.d.ts +8 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +9 -0
- package/dist/admin/users.js.map +1 -1
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/content/pinned.d.ts +26 -0
- package/dist/content/pinned.d.ts.map +1 -0
- package/dist/content/pinned.js +42 -0
- package/dist/content/pinned.js.map +1 -0
- package/dist/content/store.d.ts +7 -0
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +20 -0
- package/dist/content/store.js.map +1 -1
- package/dist/env.d.ts +21 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/actor.d.ts +15 -0
- package/dist/federation/actor.d.ts.map +1 -1
- package/dist/federation/actor.js +94 -3
- package/dist/federation/actor.js.map +1 -1
- package/dist/federation/article.d.ts +13 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +45 -6
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +36 -4
- package/dist/federation/delivery.js.map +1 -1
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +24 -2
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/mount.js +2 -2
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/paths.d.ts +30 -0
- package/dist/federation/paths.d.ts.map +1 -1
- package/dist/federation/paths.js +36 -0
- package/dist/federation/paths.js.map +1 -1
- package/dist/images/icons.d.ts +44 -25
- package/dist/images/icons.d.ts.map +1 -1
- package/dist/images/icons.js +205 -60
- package/dist/images/icons.js.map +1 -1
- package/dist/images/index.d.ts +2 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +1 -1
- package/dist/images/index.js.map +1 -1
- package/dist/images/markup.d.ts +14 -0
- package/dist/images/markup.d.ts.map +1 -1
- package/dist/images/markup.js +16 -11
- package/dist/images/markup.js.map +1 -1
- package/dist/images/variants.d.ts +11 -0
- package/dist/images/variants.d.ts.map +1 -1
- package/dist/images/variants.js +24 -4
- package/dist/images/variants.js.map +1 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/dist/indexnow.d.ts +34 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +141 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/indieauth/bearer.d.ts +39 -0
- package/dist/indieauth/bearer.d.ts.map +1 -0
- package/dist/indieauth/bearer.js +63 -0
- package/dist/indieauth/bearer.js.map +1 -0
- package/dist/indieauth/client.d.ts +61 -0
- package/dist/indieauth/client.d.ts.map +1 -0
- package/dist/indieauth/client.js +139 -0
- package/dist/indieauth/client.js.map +1 -0
- package/dist/indieauth/connected-apps.d.ts +23 -0
- package/dist/indieauth/connected-apps.d.ts.map +1 -0
- package/dist/indieauth/connected-apps.js +73 -0
- package/dist/indieauth/connected-apps.js.map +1 -0
- package/dist/indieauth/consent.d.ts +28 -0
- package/dist/indieauth/consent.d.ts.map +1 -0
- package/dist/indieauth/consent.js +185 -0
- package/dist/indieauth/consent.js.map +1 -0
- package/dist/indieauth/discovery.d.ts +87 -0
- package/dist/indieauth/discovery.d.ts.map +1 -0
- package/dist/indieauth/discovery.js +127 -0
- package/dist/indieauth/discovery.js.map +1 -0
- package/dist/indieauth/grants.d.ts +57 -0
- package/dist/indieauth/grants.d.ts.map +1 -0
- package/dist/indieauth/grants.js +35 -0
- package/dist/indieauth/grants.js.map +1 -0
- package/dist/indieauth/identity.d.ts +36 -0
- package/dist/indieauth/identity.d.ts.map +1 -0
- package/dist/indieauth/identity.js +77 -0
- package/dist/indieauth/identity.js.map +1 -0
- package/dist/indieauth/redeem.d.ts +47 -0
- package/dist/indieauth/redeem.d.ts.map +1 -0
- package/dist/indieauth/redeem.js +69 -0
- package/dist/indieauth/redeem.js.map +1 -0
- package/dist/indieauth/request.d.ts +47 -0
- package/dist/indieauth/request.d.ts.map +1 -0
- package/dist/indieauth/request.js +104 -0
- package/dist/indieauth/request.js.map +1 -0
- package/dist/indieauth/token.d.ts +19 -0
- package/dist/indieauth/token.d.ts.map +1 -0
- package/dist/indieauth/token.js +110 -0
- package/dist/indieauth/token.js.map +1 -0
- package/dist/indieauth/tokens.d.ts +131 -0
- package/dist/indieauth/tokens.d.ts.map +1 -0
- package/dist/indieauth/tokens.js +205 -0
- package/dist/indieauth/tokens.js.map +1 -0
- package/dist/web/admin-bar.d.ts +3 -1
- package/dist/web/admin-bar.d.ts.map +1 -1
- package/dist/web/admin-bar.js +5 -1
- package/dist/web/admin-bar.js.map +1 -1
- package/dist/web/archive.d.ts +1 -1
- package/dist/web/archive.d.ts.map +1 -1
- package/dist/web/archive.js +3 -2
- package/dist/web/archive.js.map +1 -1
- package/dist/web/authors.d.ts +19 -0
- package/dist/web/authors.d.ts.map +1 -1
- package/dist/web/authors.js +25 -0
- package/dist/web/authors.js.map +1 -1
- package/dist/web/context.d.ts +25 -0
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +18 -0
- package/dist/web/context.js.map +1 -1
- package/dist/web/feed-atom.d.ts.map +1 -1
- package/dist/web/feed-atom.js +5 -2
- package/dist/web/feed-atom.js.map +1 -1
- package/dist/web/feed-item.d.ts +24 -6
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +16 -8
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/feed-json.d.ts +6 -2
- package/dist/web/feed-json.d.ts.map +1 -1
- package/dist/web/feed-json.js +6 -2
- package/dist/web/feed-json.js.map +1 -1
- package/dist/web/feed-rss.d.ts.map +1 -1
- package/dist/web/feed-rss.js +17 -4
- package/dist/web/feed-rss.js.map +1 -1
- package/dist/web/feed-source.d.ts +31 -0
- package/dist/web/feed-source.d.ts.map +1 -1
- package/dist/web/feed-source.js +31 -0
- package/dist/web/feed-source.js.map +1 -1
- package/dist/web/feed-xml.d.ts +2 -0
- package/dist/web/feed-xml.d.ts.map +1 -1
- package/dist/web/feed-xml.js +2 -0
- package/dist/web/feed-xml.js.map +1 -1
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +15 -3
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/index.d.ts +4 -2
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +3 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/indexnow.d.ts +9 -0
- package/dist/web/indexnow.d.ts.map +1 -0
- package/dist/web/indexnow.js +27 -0
- package/dist/web/indexnow.js.map +1 -0
- package/dist/web/llms.d.ts +58 -0
- package/dist/web/llms.d.ts.map +1 -0
- package/dist/web/llms.js +91 -0
- package/dist/web/llms.js.map +1 -0
- package/dist/web/locale.d.ts +41 -0
- package/dist/web/locale.d.ts.map +1 -0
- package/dist/web/locale.js +56 -0
- package/dist/web/locale.js.map +1 -0
- package/dist/web/manifest.d.ts +50 -0
- package/dist/web/manifest.d.ts.map +1 -0
- package/dist/web/manifest.js +56 -0
- package/dist/web/manifest.js.map +1 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +6 -0
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/oembed.d.ts +104 -0
- package/dist/web/oembed.d.ts.map +1 -0
- package/dist/web/oembed.js +290 -0
- package/dist/web/oembed.js.map +1 -0
- package/dist/web/opensearch.d.ts +35 -0
- package/dist/web/opensearch.d.ts.map +1 -0
- package/dist/web/opensearch.js +65 -0
- package/dist/web/opensearch.js.map +1 -0
- package/dist/web/render.d.ts +6 -1
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +40 -10
- package/dist/web/render.js.map +1 -1
- package/dist/web/robots.d.ts +89 -0
- package/dist/web/robots.d.ts.map +1 -0
- package/dist/web/robots.js +259 -0
- package/dist/web/robots.js.map +1 -0
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +179 -8
- package/dist/web/routes.js.map +1 -1
- package/dist/web/share-image.d.ts +51 -0
- package/dist/web/share-image.d.ts.map +1 -0
- package/dist/web/share-image.js +43 -0
- package/dist/web/share-image.js.map +1 -0
- package/dist/web/sitemap.d.ts +0 -26
- package/dist/web/sitemap.d.ts.map +1 -1
- package/dist/web/sitemap.js +0 -44
- package/dist/web/sitemap.js.map +1 -1
- package/dist/web/templates.d.ts +16 -9
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +88 -55
- package/dist/web/templates.js.map +1 -1
- package/dist/web/themes.d.ts +33 -0
- package/dist/web/themes.d.ts.map +1 -1
- package/dist/web/themes.js +57 -0
- package/dist/web/themes.js.map +1 -1
- package/dist/webmention/discovery.d.ts +4 -2
- package/dist/webmention/discovery.d.ts.map +1 -1
- package/dist/webmention/discovery.js +14 -8
- package/dist/webmention/discovery.js.map +1 -1
- package/dist/webmention/fetch-public.d.ts +2 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -1
- package/dist/webmention/fetch-public.js +1 -1
- package/dist/webmention/fetch-public.js.map +1 -1
- package/package.json +2 -1
- package/templates/site/content/_data/site.json +0 -1
- package/themes/default/README.md +174 -43
- package/themes/default/layouts/base.njk +116 -17
- package/themes/default/layouts/front-page.njk +7 -1
- package/themes/default/layouts/home.njk +11 -0
- package/themes/default/layouts/page.njk +4 -3
- package/themes/default/layouts/post.njk +5 -4
- package/themes/default/layouts/search.njk +4 -5
- package/themes/default/partials/archive.njk +3 -1
- package/themes/default/partials/bio.njk +16 -2
- package/themes/default/partials/breadcrumbs.njk +20 -0
- package/themes/default/partials/conversation.njk +4 -1
- package/themes/default/partials/jsonld.njk +49 -9
- package/themes/default/partials/kicker.njk +6 -4
- package/themes/default/partials/lang.njk +7 -0
- package/themes/default/partials/post-list.njk +5 -3
- package/themes/default/partials/reply-context.njk +4 -2
- package/themes/default/src/style.css +14 -0
- package/themes/default/static/style.css +57 -0
- package/themes/default/theme.json +2 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { findUserById, listUsers } from '../admin/accounts.js';
|
|
2
|
+
import { ADMIN_PREFIX } from '../admin/session.js';
|
|
3
|
+
import { effectiveBaseUrl, readSiteSettings } from '../admin/settings.js';
|
|
4
|
+
import { ADMIN_TEMPLATES } from '../admin/templates.js';
|
|
5
|
+
import { isPrivateHost } from '../webmention/public-address.js';
|
|
6
|
+
import { fetchClientInformation, fetchClientLogo } from './client.js';
|
|
7
|
+
import { AUTHORIZATION_PATH, authorizationServerMetadata } from './discovery.js';
|
|
8
|
+
import { meForSignIn } from './identity.js';
|
|
9
|
+
import { profileResponse, redeemCode, redemptionForm } from './redeem.js';
|
|
10
|
+
import { parseAuthorizationRequest } from './request.js';
|
|
11
|
+
/**
|
|
12
|
+
* Where the person approves or denies a sign-in. Under `/admin`, so the admin
|
|
13
|
+
* guard signs them in first and checks the form's CSRF token.
|
|
14
|
+
*/
|
|
15
|
+
export const CONSENT_PATH = `${ADMIN_PREFIX}/indieauth/consent`;
|
|
16
|
+
/** What each scope lets a client learn or do, as the consent and connected apps screens say it. */
|
|
17
|
+
export const SCOPE_LABELS = {
|
|
18
|
+
profile: 'Your name, URL and photo',
|
|
19
|
+
email: 'Your email address',
|
|
20
|
+
create: 'Create posts as you',
|
|
21
|
+
update: 'Edit your posts',
|
|
22
|
+
delete: 'Delete your posts',
|
|
23
|
+
media: 'Upload media to your site',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The authorization endpoint (TASK-158, TASK-159).
|
|
27
|
+
*
|
|
28
|
+
* A GET is the person arriving from a client, and is handed, query and all,
|
|
29
|
+
* to the consent screen. The screen is under `/admin` so that the admin's
|
|
30
|
+
* guard, login and headers apply to it; the endpoint is not, because a POST
|
|
31
|
+
* here is a client redeeming its code for the profile, with no admin session.
|
|
32
|
+
* No access token is issued on this path; that is the token endpoint's job.
|
|
33
|
+
* Not exempt from maintenance mode: the identity URLs that advertise it
|
|
34
|
+
* answer 503 then too.
|
|
35
|
+
*/
|
|
36
|
+
export function mountAuthorizationEndpoint(app) {
|
|
37
|
+
app.get(AUTHORIZATION_PATH, (c) => c.redirect(`${CONSENT_PATH}${new URL(c.req.url).search}`, 302));
|
|
38
|
+
app.post(AUTHORIZATION_PATH, async (c) => {
|
|
39
|
+
c.header('cache-control', 'no-store');
|
|
40
|
+
const { config } = c.var;
|
|
41
|
+
const form = redemptionForm(await c.req.parseBody());
|
|
42
|
+
const redeemed = redeemCode(c.var.indieauth.codes, form);
|
|
43
|
+
if (!redeemed.ok) {
|
|
44
|
+
return c.json({ error: redeemed.error, error_description: redeemed.description }, 400);
|
|
45
|
+
}
|
|
46
|
+
const { grant } = redeemed;
|
|
47
|
+
const user = findUserById(config.dataDir, grant.userId);
|
|
48
|
+
if (user === undefined) {
|
|
49
|
+
return c.json({ error: 'invalid_grant', error_description: 'The person who approved this is gone.' }, 400);
|
|
50
|
+
}
|
|
51
|
+
const baseUrl = effectiveBaseUrl(config, readSiteSettings(config.contentDir));
|
|
52
|
+
return c.json(profileResponse(grant, user, baseUrl));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Register the consent screen and the Approve and Deny buttons behind it. */
|
|
56
|
+
export function mountConsentScreen(app, options) {
|
|
57
|
+
const { render } = options;
|
|
58
|
+
function refuse(c, message) {
|
|
59
|
+
c.status(400);
|
|
60
|
+
return render(c, ADMIN_TEMPLATES.indieauthRefused, { message });
|
|
61
|
+
}
|
|
62
|
+
app.get(CONSENT_PATH, async (c) => {
|
|
63
|
+
const { config } = c.var;
|
|
64
|
+
const user = signedInUser(c);
|
|
65
|
+
if (user === undefined)
|
|
66
|
+
return c.redirect(ADMIN_PREFIX, 302);
|
|
67
|
+
const settings = readSiteSettings(config.contentDir);
|
|
68
|
+
const baseUrl = effectiveBaseUrl(config, settings);
|
|
69
|
+
const parsed = parseAuthorizationRequest(new URL(c.req.url).searchParams, baseUrl);
|
|
70
|
+
if (parsed.kind === 'unredirectable')
|
|
71
|
+
return refuse(c, parsed.message);
|
|
72
|
+
const { clientId, redirectUri } = parsed.kind === 'valid' ? parsed.request : parsed;
|
|
73
|
+
const client = await clientInformation(c, clientId);
|
|
74
|
+
if (!mayRedirect(clientId, redirectUri, client)) {
|
|
75
|
+
return refuse(c, 'The app asked to send you back to an address it has not published as its own.');
|
|
76
|
+
}
|
|
77
|
+
if (parsed.kind === 'refused') {
|
|
78
|
+
return c.redirect(answer(redirectUri, baseUrl, {
|
|
79
|
+
error: parsed.error,
|
|
80
|
+
error_description: parsed.description,
|
|
81
|
+
state: parsed.state,
|
|
82
|
+
}), 302);
|
|
83
|
+
}
|
|
84
|
+
const { request } = parsed;
|
|
85
|
+
const me = meForSignIn(request.me, user, {
|
|
86
|
+
baseUrl,
|
|
87
|
+
users: listUsers(config.dataDir),
|
|
88
|
+
settings,
|
|
89
|
+
});
|
|
90
|
+
const pending = c.var.indieauth.consents.put({
|
|
91
|
+
request,
|
|
92
|
+
userId: user.id,
|
|
93
|
+
me,
|
|
94
|
+
...(client.name === undefined ? {} : { clientName: client.name }),
|
|
95
|
+
});
|
|
96
|
+
const redirect = new URL(redirectUri);
|
|
97
|
+
const logo = client.logo === undefined
|
|
98
|
+
? undefined
|
|
99
|
+
: await fetchClientLogo(client.logo, { lookup: config.hostLookup });
|
|
100
|
+
c.set('cspFormAction', formActionSource(redirect));
|
|
101
|
+
return render(c, ADMIN_TEMPLATES.indieauthConsent, {
|
|
102
|
+
consentUrl: CONSENT_PATH,
|
|
103
|
+
pending,
|
|
104
|
+
client: {
|
|
105
|
+
name: client.name ?? clientId,
|
|
106
|
+
id: clientId,
|
|
107
|
+
named: client.name !== undefined,
|
|
108
|
+
logo,
|
|
109
|
+
},
|
|
110
|
+
redirectHost: redirect.host === '' ? redirect.protocol : redirect.host,
|
|
111
|
+
loopback: redirect.hostname !== '' && isPrivateHost(redirect.hostname),
|
|
112
|
+
identity: me,
|
|
113
|
+
resource: request.resource,
|
|
114
|
+
scopes: request.scopes.map((scope) => ({ name: scope, label: SCOPE_LABELS[scope] })),
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
app.post(CONSENT_PATH, async (c) => {
|
|
118
|
+
const { config } = c.var;
|
|
119
|
+
const user = signedInUser(c);
|
|
120
|
+
if (user === undefined)
|
|
121
|
+
return c.redirect(ADMIN_PREFIX, 302);
|
|
122
|
+
const body = await c.req.parseBody({ all: true });
|
|
123
|
+
const pending = c.var.indieauth.consents.take(text(body['request']));
|
|
124
|
+
if (pending?.userId !== user.id) {
|
|
125
|
+
return refuse(c, 'This sign-in was already answered or has expired. Start again from the app.');
|
|
126
|
+
}
|
|
127
|
+
const { request } = pending;
|
|
128
|
+
const baseUrl = effectiveBaseUrl(config, readSiteSettings(config.contentDir));
|
|
129
|
+
if (text(body['decision']) !== 'approve') {
|
|
130
|
+
return c.redirect(answer(request.redirectUri, baseUrl, { error: 'access_denied', state: request.state }), 303);
|
|
131
|
+
}
|
|
132
|
+
const ticked = new Set([body['scope']].flat().map(text));
|
|
133
|
+
const code = c.var.indieauth.codes.put({
|
|
134
|
+
clientId: request.clientId,
|
|
135
|
+
...(pending.clientName === undefined ? {} : { clientName: pending.clientName }),
|
|
136
|
+
redirectUri: request.redirectUri,
|
|
137
|
+
codeChallenge: request.codeChallenge,
|
|
138
|
+
userId: user.id,
|
|
139
|
+
me: pending.me,
|
|
140
|
+
scopes: request.scopes.filter((scope) => ticked.has(scope)),
|
|
141
|
+
...(request.resource === undefined ? {} : { resource: request.resource }),
|
|
142
|
+
});
|
|
143
|
+
return c.redirect(answer(request.redirectUri, baseUrl, { code, state: request.state }), 303);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function signedInUser(c) {
|
|
147
|
+
const userId = c.var.session?.userId;
|
|
148
|
+
return userId == null ? undefined : findUserById(c.var.config.dataDir, userId);
|
|
149
|
+
}
|
|
150
|
+
/** What the client says about itself, or nothing when it could not be read. */
|
|
151
|
+
async function clientInformation(c, clientId) {
|
|
152
|
+
const fetched = await fetchClientInformation(clientId, { lookup: c.var.config.hostLookup });
|
|
153
|
+
if (fetched.ok)
|
|
154
|
+
return fetched.client;
|
|
155
|
+
console.warn(`Could not read IndieAuth client ${clientId}: ${fetched.reason}`);
|
|
156
|
+
return { redirectUris: [] };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Whether a client may be sent back to `redirectUri`: always on its own
|
|
160
|
+
* origin, and elsewhere only when its metadata lists that exact URL.
|
|
161
|
+
*/
|
|
162
|
+
function mayRedirect(clientId, redirectUri, client) {
|
|
163
|
+
const target = new URL(redirectUri);
|
|
164
|
+
const sameOrigin = (target.protocol === 'https:' || target.protocol === 'http:') &&
|
|
165
|
+
target.origin === new URL(clientId).origin;
|
|
166
|
+
return sameOrigin || client.redirectUris.includes(redirectUri);
|
|
167
|
+
}
|
|
168
|
+
/** `redirectUri` with the answer and the issuer added to its query. */
|
|
169
|
+
function answer(redirectUri, baseUrl, fields) {
|
|
170
|
+
const url = new URL(redirectUri);
|
|
171
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
172
|
+
if (value !== undefined)
|
|
173
|
+
url.searchParams.set(name, value);
|
|
174
|
+
}
|
|
175
|
+
url.searchParams.set('iss', authorizationServerMetadata(baseUrl).issuer);
|
|
176
|
+
return url.href;
|
|
177
|
+
}
|
|
178
|
+
/** The CSP source that lets a form's redirect reach `url`. */
|
|
179
|
+
function formActionSource(url) {
|
|
180
|
+
return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : url.protocol;
|
|
181
|
+
}
|
|
182
|
+
function text(value) {
|
|
183
|
+
return typeof value === 'string' ? value : '';
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.js","sourceRoot":"","sources":["../../src/indieauth/consent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Hono, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { GeekityEnv } from '../env.ts';
|
|
3
|
+
/** Where the authorization server metadata document is published. */
|
|
4
|
+
export declare const INDIEAUTH_METADATA_PATH = "/_geekity/indieauth/metadata";
|
|
5
|
+
/** RFC 8414's well-known location for the same document. */
|
|
6
|
+
export declare const OAUTH_METADATA_WELL_KNOWN_PATH = "/.well-known/oauth-authorization-server";
|
|
7
|
+
/** Where a client sends the person to approve a sign-in (TASK-158). */
|
|
8
|
+
export declare const AUTHORIZATION_PATH = "/_geekity/indieauth/auth";
|
|
9
|
+
/** Where a client redeems a code for an access token (TASK-160). */
|
|
10
|
+
export declare const TOKEN_PATH = "/_geekity/indieauth/token";
|
|
11
|
+
/** Where a resource server asks what a token is good for (TASK-161). */
|
|
12
|
+
export declare const INTROSPECTION_PATH = "/_geekity/indieauth/introspect";
|
|
13
|
+
/** Where a client hands a token back to end it (TASK-161). */
|
|
14
|
+
export declare const REVOCATION_PATH = "/_geekity/indieauth/revoke";
|
|
15
|
+
/** Where a client asks for the profile a token was approved to see (TASK-161). */
|
|
16
|
+
export declare const USERINFO_PATH = "/_geekity/indieauth/userinfo";
|
|
17
|
+
/**
|
|
18
|
+
* RFC 9728's well-known location for the protected resource metadata, which
|
|
19
|
+
* an MCP client reads to learn where to sign in (TASK-161).
|
|
20
|
+
*/
|
|
21
|
+
export declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
22
|
+
/**
|
|
23
|
+
* The scopes a client may ask for: the profile ones (TASK-158) and the
|
|
24
|
+
* Micropub ones a token is issued for (TASK-160).
|
|
25
|
+
*/
|
|
26
|
+
export declare const SCOPES: readonly ['profile', 'email', 'create', 'update', 'delete', 'media'];
|
|
27
|
+
/** RFC 8414 authorization server metadata, as the IndieAuth spec profiles it. */
|
|
28
|
+
export interface AuthorizationServerMetadata {
|
|
29
|
+
issuer: string;
|
|
30
|
+
authorization_endpoint: string;
|
|
31
|
+
token_endpoint: string;
|
|
32
|
+
introspection_endpoint: string;
|
|
33
|
+
revocation_endpoint: string;
|
|
34
|
+
userinfo_endpoint: string;
|
|
35
|
+
response_types_supported: readonly string[];
|
|
36
|
+
grant_types_supported: readonly string[];
|
|
37
|
+
code_challenge_methods_supported: readonly string[];
|
|
38
|
+
scopes_supported: readonly string[];
|
|
39
|
+
authorization_response_iss_parameter_supported: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* That a client may use the URL of a JSON client metadata document as its
|
|
42
|
+
* client_id, which the MCP authorization spec asks for (TASK-158).
|
|
43
|
+
*/
|
|
44
|
+
client_id_metadata_document_supported: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The metadata for a site at `baseUrl`.
|
|
48
|
+
*
|
|
49
|
+
* The issuer is the base URL with no trailing slash: RFC 8414 builds the
|
|
50
|
+
* well-known URL by putting `/.well-known/oauth-authorization-server` between
|
|
51
|
+
* the issuer's host and its path, so an issuer with no path is the one whose
|
|
52
|
+
* document lives at exactly that URL, and a client that checks the two agree
|
|
53
|
+
* finds they do.
|
|
54
|
+
*/
|
|
55
|
+
export declare function authorizationServerMetadata(baseUrl: string): AuthorizationServerMetadata;
|
|
56
|
+
/** RFC 9728 protected resource metadata. */
|
|
57
|
+
export interface ProtectedResourceMetadata {
|
|
58
|
+
resource: string;
|
|
59
|
+
authorization_servers: readonly string[];
|
|
60
|
+
scopes_supported: readonly string[];
|
|
61
|
+
bearer_methods_supported: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The protected resource metadata for a site at `baseUrl`: the site is the
|
|
65
|
+
* resource, and its own authorization server is the one to sign in at. A
|
|
66
|
+
* token may come in the header or, as Micropub allows, in a form body.
|
|
67
|
+
*/
|
|
68
|
+
export declare function protectedResourceMetadata(baseUrl: string): ProtectedResourceMetadata;
|
|
69
|
+
/** The site's base URL as every absolute URL it hands out is built from. */
|
|
70
|
+
export declare function siteBaseUrl(c: {
|
|
71
|
+
var: Pick<GeekityEnv['Variables'], 'config'>;
|
|
72
|
+
}): string;
|
|
73
|
+
/**
|
|
74
|
+
* Mount the authorization server metadata at its own path and at the
|
|
75
|
+
* well-known one, and the protected resource metadata at its well-known one.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mountIndieAuthDiscovery(app: Hono<GeekityEnv>): void;
|
|
78
|
+
/**
|
|
79
|
+
* Point every identity URL at the metadata, with a `Link` header and a
|
|
80
|
+
* `<link>` before `</head>` (TASK-157).
|
|
81
|
+
*
|
|
82
|
+
* Added to the response rather than left to the layouts so that every theme
|
|
83
|
+
* carries it, a custom one that replaces the packaged base layout included. A
|
|
84
|
+
* 404 for a username nobody has is left alone: it is nobody's identity.
|
|
85
|
+
*/
|
|
86
|
+
export declare const advertiseIndieAuthMetadata: MiddlewareHandler<GeekityEnv>;
|
|
87
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/indieauth/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAI5C,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,iCAAiC,CAAC;AAEtE,4DAA4D;AAC5D,eAAO,MAAM,8BAA8B,4CAA4C,CAAC;AAExF,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAE7D,oEAAoE;AACpE,eAAO,MAAM,UAAU,8BAA8B,CAAC;AAEtD,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,eAAe,+BAA+B,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,aAAa,iCAAiC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,gCAAgC,0CAA0C,CAAC;AAExF;;;GAGG;AACH,eAAO,MAAM,MAAM,YAAI,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAC;AAE3F,iFAAiF;AACjF,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,gCAAgC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpD,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8CAA8C,EAAE,OAAO,CAAC;IACxD;;;OAGG;IACH,qCAAqC,EAAE,OAAO,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,2BAA2B,CAexF;AAED,4CAA4C;AAC5C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,yBAAyB,CAOpF;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,CAAC,EAAE;IAAE,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAA;CAAE,GAAG,MAAM,CAGvF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAOnE;AAeD;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,iBAAiB,CAAC,UAAU,CAiBpE,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { effectiveBaseUrl, readSiteSettings } from '../admin/settings.js';
|
|
2
|
+
import { parseAuthorPath } from '../web/authors.js';
|
|
3
|
+
import { requestPath } from '../web/routes.js';
|
|
4
|
+
/** Where the authorization server metadata document is published. */
|
|
5
|
+
export const INDIEAUTH_METADATA_PATH = '/_geekity/indieauth/metadata';
|
|
6
|
+
/** RFC 8414's well-known location for the same document. */
|
|
7
|
+
export const OAUTH_METADATA_WELL_KNOWN_PATH = '/.well-known/oauth-authorization-server';
|
|
8
|
+
/** Where a client sends the person to approve a sign-in (TASK-158). */
|
|
9
|
+
export const AUTHORIZATION_PATH = '/_geekity/indieauth/auth';
|
|
10
|
+
/** Where a client redeems a code for an access token (TASK-160). */
|
|
11
|
+
export const TOKEN_PATH = '/_geekity/indieauth/token';
|
|
12
|
+
/** Where a resource server asks what a token is good for (TASK-161). */
|
|
13
|
+
export const INTROSPECTION_PATH = '/_geekity/indieauth/introspect';
|
|
14
|
+
/** Where a client hands a token back to end it (TASK-161). */
|
|
15
|
+
export const REVOCATION_PATH = '/_geekity/indieauth/revoke';
|
|
16
|
+
/** Where a client asks for the profile a token was approved to see (TASK-161). */
|
|
17
|
+
export const USERINFO_PATH = '/_geekity/indieauth/userinfo';
|
|
18
|
+
/**
|
|
19
|
+
* RFC 9728's well-known location for the protected resource metadata, which
|
|
20
|
+
* an MCP client reads to learn where to sign in (TASK-161).
|
|
21
|
+
*/
|
|
22
|
+
export const PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource';
|
|
23
|
+
/**
|
|
24
|
+
* The scopes a client may ask for: the profile ones (TASK-158) and the
|
|
25
|
+
* Micropub ones a token is issued for (TASK-160).
|
|
26
|
+
*/
|
|
27
|
+
export const SCOPES = ['profile', 'email', 'create', 'update', 'delete', 'media'];
|
|
28
|
+
/**
|
|
29
|
+
* The metadata for a site at `baseUrl`.
|
|
30
|
+
*
|
|
31
|
+
* The issuer is the base URL with no trailing slash: RFC 8414 builds the
|
|
32
|
+
* well-known URL by putting `/.well-known/oauth-authorization-server` between
|
|
33
|
+
* the issuer's host and its path, so an issuer with no path is the one whose
|
|
34
|
+
* document lives at exactly that URL, and a client that checks the two agree
|
|
35
|
+
* finds they do.
|
|
36
|
+
*/
|
|
37
|
+
export function authorizationServerMetadata(baseUrl) {
|
|
38
|
+
return {
|
|
39
|
+
issuer: baseUrl,
|
|
40
|
+
authorization_endpoint: `${baseUrl}${AUTHORIZATION_PATH}`,
|
|
41
|
+
token_endpoint: `${baseUrl}${TOKEN_PATH}`,
|
|
42
|
+
introspection_endpoint: `${baseUrl}${INTROSPECTION_PATH}`,
|
|
43
|
+
revocation_endpoint: `${baseUrl}${REVOCATION_PATH}`,
|
|
44
|
+
userinfo_endpoint: `${baseUrl}${USERINFO_PATH}`,
|
|
45
|
+
response_types_supported: ['code'],
|
|
46
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
47
|
+
code_challenge_methods_supported: ['S256'],
|
|
48
|
+
scopes_supported: SCOPES,
|
|
49
|
+
authorization_response_iss_parameter_supported: true,
|
|
50
|
+
client_id_metadata_document_supported: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The protected resource metadata for a site at `baseUrl`: the site is the
|
|
55
|
+
* resource, and its own authorization server is the one to sign in at. A
|
|
56
|
+
* token may come in the header or, as Micropub allows, in a form body.
|
|
57
|
+
*/
|
|
58
|
+
export function protectedResourceMetadata(baseUrl) {
|
|
59
|
+
return {
|
|
60
|
+
resource: baseUrl,
|
|
61
|
+
authorization_servers: [authorizationServerMetadata(baseUrl).issuer],
|
|
62
|
+
scopes_supported: SCOPES,
|
|
63
|
+
bearer_methods_supported: ['header', 'body'],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The site's base URL as every absolute URL it hands out is built from. */
|
|
67
|
+
export function siteBaseUrl(c) {
|
|
68
|
+
const { config } = c.var;
|
|
69
|
+
return effectiveBaseUrl(config, readSiteSettings(config.contentDir));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mount the authorization server metadata at its own path and at the
|
|
73
|
+
* well-known one, and the protected resource metadata at its well-known one.
|
|
74
|
+
*/
|
|
75
|
+
export function mountIndieAuthDiscovery(app) {
|
|
76
|
+
for (const pathname of [INDIEAUTH_METADATA_PATH, OAUTH_METADATA_WELL_KNOWN_PATH]) {
|
|
77
|
+
app.get(pathname, (c) => c.json(authorizationServerMetadata(siteBaseUrl(c))));
|
|
78
|
+
}
|
|
79
|
+
app.get(PROTECTED_RESOURCE_METADATA_PATH, (c) => c.json(protectedResourceMetadata(siteBaseUrl(c))));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a path is an identity URL a person may type into a sign-in form:
|
|
83
|
+
* the root, and every author archive's first page (decision-23).
|
|
84
|
+
*
|
|
85
|
+
* The root is advertised on a multi-author site too, where it names nobody: a
|
|
86
|
+
* person who types it still reaches this server, signs in as whoever they log
|
|
87
|
+
* in as, and the client is handed that user's author URL, which shares the
|
|
88
|
+
* host and advertises the same server.
|
|
89
|
+
*/
|
|
90
|
+
function isIdentityPath(pathname) {
|
|
91
|
+
return pathname === '/' || parseAuthorPath(pathname)?.pageNumber === 0;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Point every identity URL at the metadata, with a `Link` header and a
|
|
95
|
+
* `<link>` before `</head>` (TASK-157).
|
|
96
|
+
*
|
|
97
|
+
* Added to the response rather than left to the layouts so that every theme
|
|
98
|
+
* carries it, a custom one that replaces the packaged base layout included. A
|
|
99
|
+
* 404 for a username nobody has is left alone: it is nobody's identity.
|
|
100
|
+
*/
|
|
101
|
+
export const advertiseIndieAuthMetadata = async (c, next) => {
|
|
102
|
+
await next();
|
|
103
|
+
if (!isIdentityPath(requestPath(c)))
|
|
104
|
+
return;
|
|
105
|
+
const { status } = c.res;
|
|
106
|
+
if (status !== 304 && (status < 200 || status >= 300))
|
|
107
|
+
return;
|
|
108
|
+
const href = `${siteBaseUrl(c)}${INDIEAUTH_METADATA_PATH}`;
|
|
109
|
+
c.res.headers.append('link', `<${href}>; rel="indieauth-metadata"`);
|
|
110
|
+
if (status === 304 || !(c.res.headers.get('content-type') ?? '').startsWith('text/html'))
|
|
111
|
+
return;
|
|
112
|
+
const headers = new Headers(c.res.headers);
|
|
113
|
+
headers.delete('content-length');
|
|
114
|
+
const body = withHeadLink(await c.res.text(), `<link rel="indieauth-metadata" href="${href}">`);
|
|
115
|
+
// Cleared first, because assigning over a response copies its headers onto
|
|
116
|
+
// the new one.
|
|
117
|
+
c.res = undefined;
|
|
118
|
+
c.res = new Response(body, { status, headers });
|
|
119
|
+
};
|
|
120
|
+
/** `html` with `link` just before its `</head>`, or unchanged when it has none. */
|
|
121
|
+
function withHeadLink(html, link) {
|
|
122
|
+
const head = /<\/head\s*>/i.exec(html);
|
|
123
|
+
if (head === null)
|
|
124
|
+
return html;
|
|
125
|
+
return `${html.slice(0, head.index)}${link}${html.slice(head.index)}`;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/indieauth/discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Clock } from '../content/store.ts';
|
|
2
|
+
import type { AuthorizationRequest, Scope } from './request.ts';
|
|
3
|
+
/**
|
|
4
|
+
* What an authorization code stands for: who approved what, for which client,
|
|
5
|
+
* and the PKCE challenge the client must answer to redeem it (TASK-159,
|
|
6
|
+
* TASK-160).
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthorizationCode {
|
|
9
|
+
readonly clientId: string;
|
|
10
|
+
/** What the client called itself, for the connected apps screen. */
|
|
11
|
+
readonly clientName?: string;
|
|
12
|
+
readonly redirectUri: string;
|
|
13
|
+
readonly codeChallenge: string;
|
|
14
|
+
readonly userId: number;
|
|
15
|
+
/** The canonical me URL the client is handed back. */
|
|
16
|
+
readonly me: string;
|
|
17
|
+
/** The scopes the person left ticked. */
|
|
18
|
+
readonly scopes: readonly Scope[];
|
|
19
|
+
/** The RFC 8707 resource the token is to be issued for. */
|
|
20
|
+
readonly resource?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A request on the consent screen, waiting for the person to decide. */
|
|
23
|
+
export interface PendingConsent {
|
|
24
|
+
readonly request: AuthorizationRequest;
|
|
25
|
+
/** Who was shown the screen. Nobody else may answer it. */
|
|
26
|
+
readonly userId: number;
|
|
27
|
+
/** The me URL the screen named. */
|
|
28
|
+
readonly me: string;
|
|
29
|
+
/** What the client called itself on the screen, when it said. */
|
|
30
|
+
readonly clientName?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Values kept for a while under an unguessable key, each taken at most once. */
|
|
33
|
+
export interface ExpiringStore<T> {
|
|
34
|
+
/** Keep `value` and answer the key it is kept under. */
|
|
35
|
+
put(value: T): string;
|
|
36
|
+
/** The value under `key`, removed as it is read, or `undefined` once it has expired. */
|
|
37
|
+
take(key: string): T | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** How long a code may wait to be redeemed. The IndieAuth spec says at most ten minutes. */
|
|
40
|
+
export declare const CODE_LIFETIME_MS: number;
|
|
41
|
+
/** How long a consent screen may sit before its answer is refused. */
|
|
42
|
+
export declare const CONSENT_LIFETIME_MS: number;
|
|
43
|
+
/**
|
|
44
|
+
* The authorization server's short-lived state: codes and the consent screens
|
|
45
|
+
* they come from.
|
|
46
|
+
*
|
|
47
|
+
* Held in memory rather than in `data/`. Decision-9 asks files for state a
|
|
48
|
+
* restart must not lose, and these live for minutes: a restart costs whoever
|
|
49
|
+
* was mid-sign-in one more try, which is less than writing every sign-in to
|
|
50
|
+
* disk and sweeping it after.
|
|
51
|
+
*/
|
|
52
|
+
export interface IndieAuthState {
|
|
53
|
+
readonly codes: ExpiringStore<AuthorizationCode>;
|
|
54
|
+
readonly consents: ExpiringStore<PendingConsent>;
|
|
55
|
+
}
|
|
56
|
+
export declare function createIndieAuthState(now: Clock): IndieAuthState;
|
|
57
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../../src/indieauth/grants.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,wDAAwD;IACxD,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;IACtB,wFAAwF;IACxF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAClC;AAED,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAE9C,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CAClD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,KAAK,GAAG,cAAc,CAK/D"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
/** How long a code may wait to be redeemed. The IndieAuth spec says at most ten minutes. */
|
|
3
|
+
export const CODE_LIFETIME_MS = 5 * 60 * 1000;
|
|
4
|
+
/** How long a consent screen may sit before its answer is refused. */
|
|
5
|
+
export const CONSENT_LIFETIME_MS = 30 * 60 * 1000;
|
|
6
|
+
export function createIndieAuthState(now) {
|
|
7
|
+
return {
|
|
8
|
+
codes: expiringStore(CODE_LIFETIME_MS, now),
|
|
9
|
+
consents: expiringStore(CONSENT_LIFETIME_MS, now),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function expiringStore(lifetimeMs, now) {
|
|
13
|
+
const entries = new Map();
|
|
14
|
+
return {
|
|
15
|
+
put(value) {
|
|
16
|
+
const at = now().getTime();
|
|
17
|
+
// Swept on every put, so nothing outlives its lifetime by more than the
|
|
18
|
+
// gap to the next sign-in.
|
|
19
|
+
for (const [key, entry] of entries)
|
|
20
|
+
if (entry.expiresAt <= at)
|
|
21
|
+
entries.delete(key);
|
|
22
|
+
const key = randomBytes(32).toString('base64url');
|
|
23
|
+
entries.set(key, { value, expiresAt: at + lifetimeMs });
|
|
24
|
+
return key;
|
|
25
|
+
},
|
|
26
|
+
take(key) {
|
|
27
|
+
const entry = entries.get(key);
|
|
28
|
+
entries.delete(key);
|
|
29
|
+
if (entry === undefined || entry.expiresAt <= now().getTime())
|
|
30
|
+
return undefined;
|
|
31
|
+
return entry.value;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=grants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../../src/indieauth/grants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA4C1C,4FAA4F;AAC5F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAgBlD,MAAM,UAAU,oBAAoB,CAAC,GAAU;IAC7C,OAAO;QACL,KAAK,EAAE,aAAa,CAAC,gBAAgB,EAAE,GAAG,CAAC;QAC3C,QAAQ,EAAE,aAAa,CAAC,mBAAmB,EAAE,GAAG,CAAC;KAClD,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAI,UAAkB,EAAE,GAAU;IACtD,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2C,CAAC;IAEnE,OAAO;QACL,GAAG,CAAC,KAAK;YACP,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;YAC3B,wEAAwE;YACxE,2BAA2B;YAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO;gBAAE,IAAI,KAAK,CAAC,SAAS,IAAI,EAAE;oBAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnF,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;YACxD,OAAO,GAAG,CAAC;QACb,CAAC;QACD,IAAI,CAAC,GAAG;YACN,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC,OAAO,EAAE;gBAAE,OAAO,SAAS,CAAC;YAChF,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { User } from '../admin/accounts.ts';
|
|
2
|
+
import type { SiteSettings } from '../admin/settings.ts';
|
|
3
|
+
/** What resolving a me URL needs to know about the site. */
|
|
4
|
+
export interface IdentitySite {
|
|
5
|
+
/** The site's base URL, which every identity URL is under. */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
users: readonly User[];
|
|
8
|
+
settings: Pick<SiteSettings, 'author'>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The one user a me URL names, or `undefined` when it names nobody
|
|
12
|
+
* (decision-23).
|
|
13
|
+
*
|
|
14
|
+
* `/author/{username}/` names that user. The root names the user the author
|
|
15
|
+
* setting names, which makes it a solo author site (TASK-192); on a site with
|
|
16
|
+
* several authors it is nobody's. Anything else names nobody, a query or a fragment
|
|
17
|
+
* included.
|
|
18
|
+
*
|
|
19
|
+
* A URL that differs from the site's only in http or https, a `www.` prefix
|
|
20
|
+
* on either side, the case of its scheme and host, or a missing trailing slash
|
|
21
|
+
* is the same URL: those are the spellings a person types and a client's
|
|
22
|
+
* redirects pass through, and the me handed back is always the canonical one.
|
|
23
|
+
*/
|
|
24
|
+
export declare function userForMe(me: string, site: IdentitySite): User | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The me a sign-in hands back for `user`, who has just approved it
|
|
27
|
+
* (decision-23).
|
|
28
|
+
*
|
|
29
|
+
* The URL the person typed when it names them, spelled canonically: the root
|
|
30
|
+
* for the solo author who typed it, their author URL otherwise. A typed URL
|
|
31
|
+
* that names somebody else, or nobody, is ignored, and the answer is the
|
|
32
|
+
* signed-in user's own author URL: a person can only ever sign in as
|
|
33
|
+
* themselves.
|
|
34
|
+
*/
|
|
35
|
+
export declare function meForSignIn(typed: string | undefined, user: User, site: IdentitySite): string;
|
|
36
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/indieauth/identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;CACxC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,CAE1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAK7F"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { authorHref, parseAuthorPath, userForAuthor } from '../web/authors.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one user a me URL names, or `undefined` when it names nobody
|
|
4
|
+
* (decision-23).
|
|
5
|
+
*
|
|
6
|
+
* `/author/{username}/` names that user. The root names the user the author
|
|
7
|
+
* setting names, which makes it a solo author site (TASK-192); on a site with
|
|
8
|
+
* several authors it is nobody's. Anything else names nobody, a query or a fragment
|
|
9
|
+
* included.
|
|
10
|
+
*
|
|
11
|
+
* A URL that differs from the site's only in http or https, a `www.` prefix
|
|
12
|
+
* on either side, the case of its scheme and host, or a missing trailing slash
|
|
13
|
+
* is the same URL: those are the spellings a person types and a client's
|
|
14
|
+
* redirects pass through, and the me handed back is always the canonical one.
|
|
15
|
+
*/
|
|
16
|
+
export function userForMe(me, site) {
|
|
17
|
+
return identityNamed(me, site)?.user;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The me a sign-in hands back for `user`, who has just approved it
|
|
21
|
+
* (decision-23).
|
|
22
|
+
*
|
|
23
|
+
* The URL the person typed when it names them, spelled canonically: the root
|
|
24
|
+
* for the solo author who typed it, their author URL otherwise. A typed URL
|
|
25
|
+
* that names somebody else, or nobody, is ignored, and the answer is the
|
|
26
|
+
* signed-in user's own author URL: a person can only ever sign in as
|
|
27
|
+
* themselves.
|
|
28
|
+
*/
|
|
29
|
+
export function meForSignIn(typed, user, site) {
|
|
30
|
+
const named = typed === undefined ? undefined : identityNamed(typed, site);
|
|
31
|
+
const base = site.baseUrl.replace(/\/$/, '');
|
|
32
|
+
if (named?.user.id === user.id && named.root)
|
|
33
|
+
return `${base}/`;
|
|
34
|
+
return `${base}${authorHref(user.username)}`;
|
|
35
|
+
}
|
|
36
|
+
/** The user a me URL names, and whether it named them by the site root. */
|
|
37
|
+
function identityNamed(me, site) {
|
|
38
|
+
const typed = URL.parse(me);
|
|
39
|
+
const base = URL.parse(site.baseUrl);
|
|
40
|
+
if (typed === null || base === null)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typed.protocol !== 'https:' && typed.protocol !== 'http:')
|
|
43
|
+
return undefined;
|
|
44
|
+
if (typed.username !== '' || typed.password !== '')
|
|
45
|
+
return undefined;
|
|
46
|
+
if (typed.search !== '' || typed.hash !== '' || me.includes('#') || me.includes('?')) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (withoutWww(typed.hostname) !== withoutWww(base.hostname))
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typed.port !== base.port)
|
|
52
|
+
return undefined;
|
|
53
|
+
const root = withSlash(base.pathname);
|
|
54
|
+
const pathname = withSlash(typed.pathname);
|
|
55
|
+
if (!pathname.startsWith(root))
|
|
56
|
+
return undefined;
|
|
57
|
+
const local = `/${pathname.slice(root.length)}`;
|
|
58
|
+
// The author setting holds a username, or in a file written before the
|
|
59
|
+
// Site author select a display name, which the homepage's bio reads the same
|
|
60
|
+
// way; an author path holds a username, matched exactly.
|
|
61
|
+
if (local === '/') {
|
|
62
|
+
const author = userForAuthor(site.users, site.settings.author);
|
|
63
|
+
return author === undefined ? undefined : { user: author, root: true };
|
|
64
|
+
}
|
|
65
|
+
const author = parseAuthorPath(local);
|
|
66
|
+
if (author?.pageNumber !== 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const user = site.users.find((candidate) => candidate.username === author.username);
|
|
69
|
+
return user === undefined ? undefined : { user, root: false };
|
|
70
|
+
}
|
|
71
|
+
function withoutWww(hostname) {
|
|
72
|
+
return hostname.startsWith('www.') ? hostname.slice(4) : hostname;
|
|
73
|
+
}
|
|
74
|
+
function withSlash(pathname) {
|
|
75
|
+
return pathname.endsWith('/') ? pathname : `${pathname}/`;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=identity.js.map
|