@geekity/cms 0.12.0 → 0.13.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 +170 -4
- package/admin/pages/account/login.njk +3 -0
- package/admin/pages/documents/editor.njk +7 -0
- package/admin/pages/indieauth/consent.njk +51 -0
- package/admin/pages/indieauth/refused.njk +11 -0
- package/admin/pages/settings/general.njk +9 -0
- 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 +2 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +17 -1
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/headers.d.ts +5 -1
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +7 -3
- 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.js +1 -1
- 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 +79 -2
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +168 -2
- 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 +30 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/env.d.ts +14 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +18 -5
- package/dist/federation/article.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.map +1 -1
- package/dist/images/variants.js +3 -4
- package/dist/images/variants.js.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- 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 +78 -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/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/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 +3 -1
- package/dist/web/feed-atom.js.map +1 -1
- package/dist/web/feed-item.d.ts +11 -2
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +9 -3
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/feed-json.d.ts +4 -0
- package/dist/web/feed-json.d.ts.map +1 -1
- package/dist/web/feed-json.js +4 -1
- 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 +16 -3
- package/dist/web/feed-rss.js.map +1 -1
- package/dist/web/feed-source.d.ts +28 -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 +8 -1
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/index.d.ts +3 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +2 -1
- 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/render.d.ts +6 -1
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +41 -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 +105 -6
- 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 +83 -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 +1 -1
- package/themes/default/README.md +119 -29
- package/themes/default/layouts/base.njk +95 -16
- package/themes/default/layouts/front-page.njk +6 -1
- package/themes/default/layouts/home.njk +10 -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 +9 -2
- package/themes/default/partials/breadcrumbs.njk +20 -0
- package/themes/default/partials/conversation.njk +4 -1
- package/themes/default/partials/jsonld.njk +19 -4
- 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 +53 -0
- package/themes/default/theme.json +2 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { findUserById } from '../admin/accounts.js';
|
|
2
|
+
import { PROTECTED_RESOURCE_METADATA_PATH, siteBaseUrl } from './discovery.js';
|
|
3
|
+
import { findAccessToken, recordUse, verifyAccessToken } from './tokens.js';
|
|
4
|
+
/**
|
|
5
|
+
* Turn the access token on a request into the user and scopes it grants, and
|
|
6
|
+
* refuse the request when it has none, a bad one, or one without `scope`.
|
|
7
|
+
*
|
|
8
|
+
* The token comes from an `Authorization: Bearer` header or, as Micropub
|
|
9
|
+
* allows, an `access_token` field of a form body. A refusal carries
|
|
10
|
+
* `WWW-Authenticate: Bearer` naming the protected resource metadata (RFC
|
|
11
|
+
* 9728), which is how an MCP client finds out where to sign in.
|
|
12
|
+
*/
|
|
13
|
+
export function requireBearer(guard) {
|
|
14
|
+
return async (c, next) => {
|
|
15
|
+
const { config } = c.var;
|
|
16
|
+
const presented = await presentedToken(c);
|
|
17
|
+
if (presented === undefined) {
|
|
18
|
+
return refuse(c, 401, 'unauthorized', 'An access token is required.', {});
|
|
19
|
+
}
|
|
20
|
+
const now = config.now();
|
|
21
|
+
const token = guard.audience === 'authorization-server'
|
|
22
|
+
? findAccessToken(config.dataDir, presented, now)
|
|
23
|
+
: verifyAccessToken(config.dataDir, presented, guard.audience, now);
|
|
24
|
+
const user = token === undefined ? undefined : findUserById(config.dataDir, token.userId);
|
|
25
|
+
if (token === undefined || user === undefined) {
|
|
26
|
+
const description = 'The access token is unknown, expired, revoked or not for this resource.';
|
|
27
|
+
return refuse(c, 401, 'invalid_token', description, { error: 'invalid_token' });
|
|
28
|
+
}
|
|
29
|
+
if (guard.scope !== undefined && !token.scopes.includes(guard.scope)) {
|
|
30
|
+
const description = `The access token was not granted the ${guard.scope} scope.`;
|
|
31
|
+
return refuse(c, 403, 'insufficient_scope', description, {
|
|
32
|
+
error: 'insufficient_scope',
|
|
33
|
+
scope: guard.scope,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
await recordUse(config.dataDir, token, now);
|
|
37
|
+
c.set('bearer', { token, user });
|
|
38
|
+
await next();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function presentedToken(c) {
|
|
42
|
+
const header = /^Bearer +(\S+)$/i.exec(c.req.header('authorization') ?? '');
|
|
43
|
+
if (header !== null)
|
|
44
|
+
return header[1];
|
|
45
|
+
const type = c.req.header('content-type') ?? '';
|
|
46
|
+
if (!/^(application\/x-www-form-urlencoded|multipart\/form-data)\b/i.test(type)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const field = (await c.req.parseBody())['access_token'];
|
|
50
|
+
return typeof field === 'string' && field !== '' ? field : undefined;
|
|
51
|
+
}
|
|
52
|
+
function refuse(c, status, error, description, params) {
|
|
53
|
+
const challenge = Object.entries({
|
|
54
|
+
...params,
|
|
55
|
+
resource_metadata: `${siteBaseUrl(c)}${PROTECTED_RESOURCE_METADATA_PATH}`,
|
|
56
|
+
})
|
|
57
|
+
.map(([name, value]) => `${name}="${value}"`)
|
|
58
|
+
.join(', ');
|
|
59
|
+
c.header('www-authenticate', `Bearer ${challenge}`);
|
|
60
|
+
c.header('cache-control', 'no-store');
|
|
61
|
+
return c.json({ error, error_description: description }, status);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=bearer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearer.js","sourceRoot":"","sources":["../../src/indieauth/bearer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { HostLookup } from '../webmention/public-address.ts';
|
|
2
|
+
/** What a client says about itself at its client_id URL. */
|
|
3
|
+
export interface ClientInformation {
|
|
4
|
+
/** What to call it on the consent screen. */
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
/** Its home page. */
|
|
7
|
+
readonly url?: string;
|
|
8
|
+
/** Where its logo is, an http or https URL. */
|
|
9
|
+
readonly logo?: string;
|
|
10
|
+
/** Where it may be sent back to, besides its own origin. */
|
|
11
|
+
readonly redirectUris: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** How long a client is given to answer. */
|
|
14
|
+
export declare const CLIENT_FETCH_TIMEOUT_MS = 5000;
|
|
15
|
+
/** The most of a client's page or document that is read. */
|
|
16
|
+
export declare const CLIENT_FETCH_MAX_BYTES: number;
|
|
17
|
+
/** The most of a client's logo that is read. */
|
|
18
|
+
export declare const CLIENT_LOGO_MAX_BYTES: number;
|
|
19
|
+
/** What {@link fetchClientInformation} and {@link fetchClientLogo} need. */
|
|
20
|
+
export interface FetchClientOptions {
|
|
21
|
+
readonly lookup: HostLookup;
|
|
22
|
+
readonly timeoutMs?: number | undefined;
|
|
23
|
+
readonly maxBytes?: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** What fetching one client_id came to. */
|
|
26
|
+
export type ClientFetch = {
|
|
27
|
+
readonly ok: true;
|
|
28
|
+
readonly client: ClientInformation;
|
|
29
|
+
} | {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly reason: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Fetch a client_id URL and read what it says about the client.
|
|
35
|
+
*
|
|
36
|
+
* The URL is whatever the person's browser was sent here with, so the fetch
|
|
37
|
+
* goes through {@link fetchPublic}: public hosts only, every redirect hop
|
|
38
|
+
* checked, one timeout and a byte limit.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fetchClientInformation(clientId: string, options: FetchClientOptions): Promise<ClientFetch>;
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a client's logo as a `data:` URI, or `undefined` when it cannot be had.
|
|
43
|
+
*
|
|
44
|
+
* Fetched here rather than linked from the page, so the admin's `img-src`
|
|
45
|
+
* never names the client and the client never learns the address of the
|
|
46
|
+
* person approving it, or when. The fetch is held to {@link fetchPublic}'s
|
|
47
|
+
* rules like the client_id itself, to image types only, and to
|
|
48
|
+
* {@link CLIENT_LOGO_MAX_BYTES}.
|
|
49
|
+
*/
|
|
50
|
+
export declare function fetchClientLogo(url: string, options: FetchClientOptions): Promise<string | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* What a client_id document says, by its content type.
|
|
53
|
+
*
|
|
54
|
+
* A JSON document is a client metadata document, which the OAuth Client ID
|
|
55
|
+
* Metadata Document draft and MCP use. Its `client_id` must be its own URL;
|
|
56
|
+
* one that names another client is read as saying nothing. Anything else is
|
|
57
|
+
* read as an HTML page with an IndieAuth `h-app` (or the older `h-x-app`) and
|
|
58
|
+
* `rel="redirect_uri"` links, in the response's `Link` header or in the page.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readClientInformation(body: string, type: string, clientId: string, base?: string, link?: string | null): ClientInformation;
|
|
61
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/indieauth/client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,qBAAqB;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,QAAa,CAAC;AAEjD,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,QAAY,CAAC;AAM/C,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,WAAW,CAAC,CAiBtB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,MAAiB,EACvB,IAAI,GAAE,MAAM,GAAG,IAAW,GACzB,iBAAiB,CAEnB"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { linkTargets } from '../webmention/discovery.js';
|
|
2
|
+
import { fetchPublic, webUrl } from '../webmention/fetch-public.js';
|
|
3
|
+
import { elementsIn, hasRel, parseHtml } from '../webmention/html.js';
|
|
4
|
+
import { itemsIn } from '../webmention/microformats.js';
|
|
5
|
+
/** How long a client is given to answer. */
|
|
6
|
+
export const CLIENT_FETCH_TIMEOUT_MS = 5000;
|
|
7
|
+
/** The most of a client's page or document that is read. */
|
|
8
|
+
export const CLIENT_FETCH_MAX_BYTES = 512 * 1024;
|
|
9
|
+
/** The most of a client's logo that is read. */
|
|
10
|
+
export const CLIENT_LOGO_MAX_BYTES = 64 * 1024;
|
|
11
|
+
/** The image types a logo may be, which a browser shows in an `<img>`. */
|
|
12
|
+
const LOGO_TYPE = /^\s*(image\/(?:png|jpeg|gif|webp|avif|svg\+xml|x-icon|vnd\.microsoft\.icon))\s*(?:;|$)/i;
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a client_id URL and read what it says about the client.
|
|
15
|
+
*
|
|
16
|
+
* The URL is whatever the person's browser was sent here with, so the fetch
|
|
17
|
+
* goes through {@link fetchPublic}: public hosts only, every redirect hop
|
|
18
|
+
* checked, one timeout and a byte limit.
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchClientInformation(clientId, options) {
|
|
21
|
+
const fetched = await fetchPublic(clientId, {
|
|
22
|
+
lookup: options.lookup,
|
|
23
|
+
timeoutMs: options.timeoutMs ?? CLIENT_FETCH_TIMEOUT_MS,
|
|
24
|
+
maxBytes: options.maxBytes ?? CLIENT_FETCH_MAX_BYTES,
|
|
25
|
+
accept: 'application/json, text/html;q=0.9',
|
|
26
|
+
contentType: {
|
|
27
|
+
pattern: /^\s*(application\/(?:[\w.+-]+\+)?json|text\/html|application\/xhtml\+xml)/i,
|
|
28
|
+
name: 'JSON client metadata or an HTML page',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (!fetched.ok)
|
|
32
|
+
return fetched;
|
|
33
|
+
const body = new TextDecoder().decode(fetched.body);
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
client: readClientInformation(body, fetched.type, clientId, fetched.url, fetched.link),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch a client's logo as a `data:` URI, or `undefined` when it cannot be had.
|
|
41
|
+
*
|
|
42
|
+
* Fetched here rather than linked from the page, so the admin's `img-src`
|
|
43
|
+
* never names the client and the client never learns the address of the
|
|
44
|
+
* person approving it, or when. The fetch is held to {@link fetchPublic}'s
|
|
45
|
+
* rules like the client_id itself, to image types only, and to
|
|
46
|
+
* {@link CLIENT_LOGO_MAX_BYTES}.
|
|
47
|
+
*/
|
|
48
|
+
export async function fetchClientLogo(url, options) {
|
|
49
|
+
const fetched = await fetchPublic(url, {
|
|
50
|
+
lookup: options.lookup,
|
|
51
|
+
timeoutMs: options.timeoutMs ?? CLIENT_FETCH_TIMEOUT_MS,
|
|
52
|
+
maxBytes: options.maxBytes ?? CLIENT_LOGO_MAX_BYTES,
|
|
53
|
+
accept: 'image/*',
|
|
54
|
+
contentType: { pattern: LOGO_TYPE, name: 'an image' },
|
|
55
|
+
});
|
|
56
|
+
if (!fetched.ok)
|
|
57
|
+
return undefined;
|
|
58
|
+
const type = LOGO_TYPE.exec(fetched.type)?.[1]?.toLowerCase() ?? 'image/png';
|
|
59
|
+
return `data:${type};base64,${Buffer.from(fetched.body).toString('base64')}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What a client_id document says, by its content type.
|
|
63
|
+
*
|
|
64
|
+
* A JSON document is a client metadata document, which the OAuth Client ID
|
|
65
|
+
* Metadata Document draft and MCP use. Its `client_id` must be its own URL;
|
|
66
|
+
* one that names another client is read as saying nothing. Anything else is
|
|
67
|
+
* read as an HTML page with an IndieAuth `h-app` (or the older `h-x-app`) and
|
|
68
|
+
* `rel="redirect_uri"` links, in the response's `Link` header or in the page.
|
|
69
|
+
*/
|
|
70
|
+
export function readClientInformation(body, type, clientId, base = clientId, link = null) {
|
|
71
|
+
return /json/i.test(type) ? fromJson(body, clientId) : fromHtml(body, base, link);
|
|
72
|
+
}
|
|
73
|
+
function fromJson(body, clientId) {
|
|
74
|
+
let document;
|
|
75
|
+
try {
|
|
76
|
+
document = JSON.parse(body);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { redirectUris: [] };
|
|
80
|
+
}
|
|
81
|
+
if (typeof document !== 'object' || document === null)
|
|
82
|
+
return { redirectUris: [] };
|
|
83
|
+
const fields = document;
|
|
84
|
+
if (!sameUrl(fields['client_id'], clientId))
|
|
85
|
+
return { redirectUris: [] };
|
|
86
|
+
const name = fields['client_name'];
|
|
87
|
+
const url = fields['client_uri'];
|
|
88
|
+
const logo = logoUrl(fields['logo_uri']);
|
|
89
|
+
const redirectUris = Array.isArray(fields['redirect_uris'])
|
|
90
|
+
? fields['redirect_uris'].filter((uri) => typeof uri === 'string')
|
|
91
|
+
: [];
|
|
92
|
+
return {
|
|
93
|
+
...(typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : {}),
|
|
94
|
+
...(typeof url === 'string' && URL.canParse(url) ? { url } : {}),
|
|
95
|
+
...(logo === undefined ? {} : { logo }),
|
|
96
|
+
redirectUris,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function fromHtml(body, base, link) {
|
|
100
|
+
const root = parseHtml(body);
|
|
101
|
+
const app = firstApp(itemsIn(root, base));
|
|
102
|
+
const name = app?.properties['name']?.[0]?.text.trim();
|
|
103
|
+
const url = app?.properties['url']?.[0]?.text;
|
|
104
|
+
const logo = logoUrl(app?.properties['logo']?.[0]?.text);
|
|
105
|
+
const hrefs = linkTargets(link, 'redirect_uri');
|
|
106
|
+
for (const element of elementsIn(root)) {
|
|
107
|
+
if ((element.name === 'link' || element.name === 'a') && hasRel(element, 'redirect_uri')) {
|
|
108
|
+
hrefs.push(element.attributes['href'] ?? '');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const redirectUris = hrefs.flatMap((href) => URL.parse(href, base)?.href ?? []);
|
|
112
|
+
return {
|
|
113
|
+
...(name === undefined || name === '' ? {} : { name }),
|
|
114
|
+
...(url === undefined ? {} : { url }),
|
|
115
|
+
...(logo === undefined ? {} : { logo }),
|
|
116
|
+
redirectUris,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** `value` when it is an absolute http or https URL. */
|
|
120
|
+
function logoUrl(value) {
|
|
121
|
+
return typeof value === 'string' ? webUrl(value)?.href : undefined;
|
|
122
|
+
}
|
|
123
|
+
function firstApp(items) {
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
if (item.types.includes('h-app') || item.types.includes('h-x-app'))
|
|
126
|
+
return item;
|
|
127
|
+
const nested = firstApp(item.children);
|
|
128
|
+
if (nested !== undefined)
|
|
129
|
+
return nested;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
function sameUrl(value, clientId) {
|
|
134
|
+
if (typeof value !== 'string')
|
|
135
|
+
return false;
|
|
136
|
+
const url = URL.parse(value);
|
|
137
|
+
return url !== null && url.href === new URL(clientId).href;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/indieauth/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Users > Connected apps (TASK-162): the apps that hold a token for whoever is
|
|
3
|
+
* signed in, and the button that cuts one off.
|
|
4
|
+
*
|
|
5
|
+
* Only ever your own connections, because a token acts as the person who
|
|
6
|
+
* approved it. Revoking deletes the connection from `data/indieauth-tokens.json`,
|
|
7
|
+
* so the access token and the refresh token stop working on the next request.
|
|
8
|
+
*/
|
|
9
|
+
import type { Hono } from 'hono';
|
|
10
|
+
import type { AdminRender } from '../admin/documents.ts';
|
|
11
|
+
import type { GeekityEnv } from '../env.ts';
|
|
12
|
+
/** Where a Revoke button posts. */
|
|
13
|
+
export declare const REVOKE_CONNECTION_PATH = "/admin/users/apps/revoke";
|
|
14
|
+
/** The field a Revoke button submits: the connection's id. */
|
|
15
|
+
export declare const CONNECTION_FIELD = "connection";
|
|
16
|
+
/**
|
|
17
|
+
* Register the screen and its Revoke button. Mounted before the users
|
|
18
|
+
* screens, whose `/admin/users/:id` would otherwise answer `apps` with a 404.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mountConnectedApps(app: Hono<GeekityEnv>, options: {
|
|
21
|
+
render: AdminRender;
|
|
22
|
+
}): void;
|
|
23
|
+
//# sourceMappingURL=connected-apps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connected-apps.d.ts","sourceRoot":"","sources":["../../src/indieauth/connected-apps.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAK5C,mCAAmC;AACnC,eAAO,MAAM,sBAAsB,6BAAkC,CAAC;AAEtE,8DAA8D;AAC9D,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAgChG"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Users > Connected apps (TASK-162): the apps that hold a token for whoever is
|
|
3
|
+
* signed in, and the button that cuts one off.
|
|
4
|
+
*
|
|
5
|
+
* Only ever your own connections, because a token acts as the person who
|
|
6
|
+
* approved it. Revoking deletes the connection from `data/indieauth-tokens.json`,
|
|
7
|
+
* so the access token and the refresh token stop working on the next request.
|
|
8
|
+
*/
|
|
9
|
+
import { flash } from '../admin/flash.js';
|
|
10
|
+
import { formatInTimezone } from '../admin/formatting.js';
|
|
11
|
+
import { readSiteSettings } from '../admin/settings.js';
|
|
12
|
+
import { ADMIN_TEMPLATES } from '../admin/templates.js';
|
|
13
|
+
import { CONNECTED_APPS_CHILD, CONNECTED_APPS_PATH } from '../admin/users.js';
|
|
14
|
+
import { SCOPE_LABELS } from './consent.js';
|
|
15
|
+
import { listTokens, revokeConnection } from './tokens.js';
|
|
16
|
+
/** Where a Revoke button posts. */
|
|
17
|
+
export const REVOKE_CONNECTION_PATH = `${CONNECTED_APPS_PATH}/revoke`;
|
|
18
|
+
/** The field a Revoke button submits: the connection's id. */
|
|
19
|
+
export const CONNECTION_FIELD = 'connection';
|
|
20
|
+
/**
|
|
21
|
+
* Register the screen and its Revoke button. Mounted before the users
|
|
22
|
+
* screens, whose `/admin/users/:id` would otherwise answer `apps` with a 404.
|
|
23
|
+
*/
|
|
24
|
+
export function mountConnectedApps(app, options) {
|
|
25
|
+
const { render } = options;
|
|
26
|
+
app.get(CONNECTED_APPS_PATH, (c) => {
|
|
27
|
+
const { config } = c.var;
|
|
28
|
+
const userId = c.var.session?.userId;
|
|
29
|
+
const timezone = readSiteSettings(config.contentDir).timezone;
|
|
30
|
+
const now = config.now().getTime();
|
|
31
|
+
const connections = listTokens(config.dataDir)
|
|
32
|
+
.filter((token) => token.userId === userId && Date.parse(token.refreshExpiresAt) > now)
|
|
33
|
+
.sort((a, b) => b.issuedAt.localeCompare(a.issuedAt))
|
|
34
|
+
.map((token) => row(token, timezone));
|
|
35
|
+
return render(c, ADMIN_TEMPLATES.connectedApps, {
|
|
36
|
+
section: 'users',
|
|
37
|
+
child: CONNECTED_APPS_CHILD,
|
|
38
|
+
revokeUrl: REVOKE_CONNECTION_PATH,
|
|
39
|
+
connectionField: CONNECTION_FIELD,
|
|
40
|
+
connections,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
app.post(REVOKE_CONNECTION_PATH, async (c) => {
|
|
44
|
+
const { config, session } = c.var;
|
|
45
|
+
const id = (await c.req.parseBody())[CONNECTION_FIELD];
|
|
46
|
+
const revoked = typeof id === 'string' && session?.userId != null
|
|
47
|
+
? await revokeConnection(config.dataDir, session.userId, id, config.now())
|
|
48
|
+
: undefined;
|
|
49
|
+
if (revoked === undefined)
|
|
50
|
+
flash(c, 'warning', 'That app was already disconnected.');
|
|
51
|
+
else
|
|
52
|
+
flash(c, 'notice', `${clientLabel(revoked)} can no longer act as you.`);
|
|
53
|
+
return c.redirect(CONNECTED_APPS_PATH, 303);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** One connection as the table shows it. */
|
|
57
|
+
function row(token, timezone) {
|
|
58
|
+
const when = (instant) => ({ iso: instant, text: formatInTimezone(instant, timezone) });
|
|
59
|
+
return {
|
|
60
|
+
id: token.id,
|
|
61
|
+
name: clientLabel(token),
|
|
62
|
+
url: token.clientId,
|
|
63
|
+
scopes: token.scopes.map((scope) => SCOPE_LABELS[scope]),
|
|
64
|
+
issued: when(token.issuedAt),
|
|
65
|
+
lastUsed: token.lastUsedAt === undefined ? undefined : when(token.lastUsedAt),
|
|
66
|
+
expires: when(token.refreshExpiresAt),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** What to call a client: its own name, or its URL when it gave none. */
|
|
70
|
+
function clientLabel(token) {
|
|
71
|
+
return token.clientName ?? token.clientId;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=connected-apps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connected-apps.js","sourceRoot":"","sources":["../../src/indieauth/connected-apps.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { AdminRender } from '../admin/documents.ts';
|
|
3
|
+
import type { GeekityEnv } from '../env.ts';
|
|
4
|
+
import type { Scope } from './request.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Where the person approves or denies a sign-in. Under `/admin`, so the admin
|
|
7
|
+
* guard signs them in first and checks the form's CSRF token.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONSENT_PATH = "/admin/indieauth/consent";
|
|
10
|
+
/** What each scope lets a client learn or do, as the consent and connected apps screens say it. */
|
|
11
|
+
export declare const SCOPE_LABELS: Readonly<Record<Scope, string>>;
|
|
12
|
+
/**
|
|
13
|
+
* The authorization endpoint (TASK-158, TASK-159).
|
|
14
|
+
*
|
|
15
|
+
* A GET is the person arriving from a client, and is handed, query and all,
|
|
16
|
+
* to the consent screen. The screen is under `/admin` so that the admin's
|
|
17
|
+
* guard, login and headers apply to it; the endpoint is not, because a POST
|
|
18
|
+
* here is a client redeeming its code for the profile, with no admin session.
|
|
19
|
+
* No access token is issued on this path; that is the token endpoint's job.
|
|
20
|
+
* Not exempt from maintenance mode: the identity URLs that advertise it
|
|
21
|
+
* answer 503 then too.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mountAuthorizationEndpoint(app: Hono<GeekityEnv>): void;
|
|
24
|
+
/** Register the consent screen and the Approve and Deny buttons behind it. */
|
|
25
|
+
export declare function mountConsentScreen(app: Hono<GeekityEnv>, options: {
|
|
26
|
+
render: AdminRender;
|
|
27
|
+
}): void;
|
|
28
|
+
//# sourceMappingURL=consent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.d.ts","sourceRoot":"","sources":["../../src/indieauth/consent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAI1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAIzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAQ5C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C;;;GAGG;AACH,eAAO,MAAM,YAAY,6BAAsC,CAAC;AAEhE,mGAAmG;AACnG,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAOxD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAwBtE;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CA6GhG"}
|
|
@@ -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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"}
|