@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 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/indieauth/identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { User } from '../admin/accounts.ts';
|
|
2
|
+
import type { AuthorizationCode, ExpiringStore } from './grants.ts';
|
|
3
|
+
/** The OAuth errors a redemption can end in. */
|
|
4
|
+
export type RedemptionError = 'invalid_request' | 'invalid_grant' | 'unsupported_grant_type';
|
|
5
|
+
export type Redemption = {
|
|
6
|
+
readonly ok: true;
|
|
7
|
+
readonly grant: AuthorizationCode;
|
|
8
|
+
} | {
|
|
9
|
+
readonly ok: false;
|
|
10
|
+
readonly error: RedemptionError;
|
|
11
|
+
readonly description: string;
|
|
12
|
+
};
|
|
13
|
+
/** The form fields a client posts to redeem a code, as they arrived. */
|
|
14
|
+
export type RedemptionForm = Readonly<Record<string, string | undefined>>;
|
|
15
|
+
/** A parsed form body as a {@link RedemptionForm}, a file upload read as absent. */
|
|
16
|
+
export declare function redemptionForm(body: Readonly<Record<string, unknown>>): RedemptionForm;
|
|
17
|
+
/**
|
|
18
|
+
* Take the code a client posts and check it against what it was issued for
|
|
19
|
+
* (TASK-159). The token endpoint (TASK-160) redeems codes through this too,
|
|
20
|
+
* so the two cannot disagree about what makes a code good.
|
|
21
|
+
*
|
|
22
|
+
* The code is taken before it is checked, so a try with the wrong verifier,
|
|
23
|
+
* client or redirect spends it: a code somebody is guessing the verifier for
|
|
24
|
+
* is a code that has leaked. `grant_type` may be left out, as clients written
|
|
25
|
+
* to the IndieAuth spec before 2020 do.
|
|
26
|
+
*/
|
|
27
|
+
export declare function redeemCode(codes: ExpiringStore<AuthorizationCode>, form: RedemptionForm): Redemption;
|
|
28
|
+
/** The IndieAuth profile URL response: who signed in, and what they let the client see. */
|
|
29
|
+
export interface ProfileResponse {
|
|
30
|
+
readonly me: string;
|
|
31
|
+
readonly profile?: {
|
|
32
|
+
readonly name?: string;
|
|
33
|
+
readonly url?: string;
|
|
34
|
+
readonly photo?: string;
|
|
35
|
+
readonly email?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What profile redemption hands back for `grant`, signed in as `user`. The
|
|
40
|
+
* token endpoint answers it beside the token, for a code or a refresh.
|
|
41
|
+
*
|
|
42
|
+
* The name, url and photo come with the profile scope and the email with the
|
|
43
|
+
* email scope; the IndieAuth spec keeps all four under `profile`. The url is
|
|
44
|
+
* the me, the page that already carries this person's h-card.
|
|
45
|
+
*/
|
|
46
|
+
export declare function profileResponse(grant: Pick<AuthorizationCode, 'me' | 'scopes'>, user: User, baseUrl: string): ProfileResponse;
|
|
47
|
+
//# sourceMappingURL=redeem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redeem.d.ts","sourceRoot":"","sources":["../../src/indieauth/redeem.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEpE,gDAAgD;AAChD,MAAM,MAAM,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,wBAAwB,CAAC;AAE7F,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1F,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAE1E,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,cAAc,CAOtF;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,EACvC,IAAI,EAAE,cAAc,GACnB,UAAU,CA2BZ;AAMD,2FAA2F;AAC3F,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,IAAI,CAAC,iBAAiB,EAAE,IAAI,GAAG,QAAQ,CAAC,EAC/C,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,GACd,eAAe,CAcjB"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { avatarUrl } from '../federation/actor.js';
|
|
3
|
+
/** A parsed form body as a {@link RedemptionForm}, a file upload read as absent. */
|
|
4
|
+
export function redemptionForm(body) {
|
|
5
|
+
return Object.fromEntries(Object.entries(body).map(([name, value]) => [
|
|
6
|
+
name,
|
|
7
|
+
typeof value === 'string' ? value : undefined,
|
|
8
|
+
]));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Take the code a client posts and check it against what it was issued for
|
|
12
|
+
* (TASK-159). The token endpoint (TASK-160) redeems codes through this too,
|
|
13
|
+
* so the two cannot disagree about what makes a code good.
|
|
14
|
+
*
|
|
15
|
+
* The code is taken before it is checked, so a try with the wrong verifier,
|
|
16
|
+
* client or redirect spends it: a code somebody is guessing the verifier for
|
|
17
|
+
* is a code that has leaked. `grant_type` may be left out, as clients written
|
|
18
|
+
* to the IndieAuth spec before 2020 do.
|
|
19
|
+
*/
|
|
20
|
+
export function redeemCode(codes, form) {
|
|
21
|
+
const grantType = form['grant_type'];
|
|
22
|
+
if (grantType !== undefined && grantType !== 'authorization_code') {
|
|
23
|
+
return refuse('unsupported_grant_type', 'Only authorization_code is supported.');
|
|
24
|
+
}
|
|
25
|
+
const code = form['code'];
|
|
26
|
+
const clientId = form['client_id'];
|
|
27
|
+
const redirectUri = form['redirect_uri'];
|
|
28
|
+
const verifier = form['code_verifier'];
|
|
29
|
+
if (!code || !clientId || !redirectUri || !verifier) {
|
|
30
|
+
return refuse('invalid_request', 'code, client_id, redirect_uri and code_verifier are all required.');
|
|
31
|
+
}
|
|
32
|
+
const grant = codes.take(code);
|
|
33
|
+
if (grant === undefined) {
|
|
34
|
+
return refuse('invalid_grant', 'The code is unknown, expired or already used.');
|
|
35
|
+
}
|
|
36
|
+
if (grant.clientId !== clientId || grant.redirectUri !== redirectUri) {
|
|
37
|
+
return refuse('invalid_grant', 'The code was issued to another client or redirect_uri.');
|
|
38
|
+
}
|
|
39
|
+
if (createHash('sha256').update(verifier).digest('base64url') !== grant.codeChallenge) {
|
|
40
|
+
return refuse('invalid_grant', 'The code_verifier does not match the code_challenge.');
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, grant };
|
|
43
|
+
}
|
|
44
|
+
function refuse(error, description) {
|
|
45
|
+
return { ok: false, error, description };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* What profile redemption hands back for `grant`, signed in as `user`. The
|
|
49
|
+
* token endpoint answers it beside the token, for a code or a refresh.
|
|
50
|
+
*
|
|
51
|
+
* The name, url and photo come with the profile scope and the email with the
|
|
52
|
+
* email scope; the IndieAuth spec keeps all four under `profile`. The url is
|
|
53
|
+
* the me, the page that already carries this person's h-card.
|
|
54
|
+
*/
|
|
55
|
+
export function profileResponse(grant, user, baseUrl) {
|
|
56
|
+
const photo = user.profile?.avatar === undefined ? undefined : avatarUrl(user.profile.avatar, baseUrl);
|
|
57
|
+
const profile = {
|
|
58
|
+
...(grant.scopes.includes('profile')
|
|
59
|
+
? {
|
|
60
|
+
name: user.profile?.displayName ?? user.username,
|
|
61
|
+
url: grant.me,
|
|
62
|
+
...(photo === undefined ? {} : { photo }),
|
|
63
|
+
}
|
|
64
|
+
: {}),
|
|
65
|
+
...(grant.scopes.includes('email') && user.email !== undefined ? { email: user.email } : {}),
|
|
66
|
+
};
|
|
67
|
+
return Object.keys(profile).length === 0 ? { me: grant.me } : { me: grant.me, profile };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=redeem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redeem.js","sourceRoot":"","sources":["../../src/indieauth/redeem.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SCOPES } from './discovery.ts';
|
|
2
|
+
/** A scope this site can grant. */
|
|
3
|
+
export type Scope = (typeof SCOPES)[number];
|
|
4
|
+
/** A sign-in a client asked for, every field checked. */
|
|
5
|
+
export interface AuthorizationRequest {
|
|
6
|
+
/** The client's URL, which is its identifier. */
|
|
7
|
+
readonly clientId: string;
|
|
8
|
+
/** Where the person is sent back with the answer. */
|
|
9
|
+
readonly redirectUri: string;
|
|
10
|
+
/** Handed back untouched, so the client can match the answer to its request. */
|
|
11
|
+
readonly state: string;
|
|
12
|
+
/** The S256 PKCE challenge the code will be bound to. */
|
|
13
|
+
readonly codeChallenge: string;
|
|
14
|
+
/** The scopes asked for that this site offers, in the order asked. */
|
|
15
|
+
readonly scopes: readonly Scope[];
|
|
16
|
+
/** The URL the person typed into the client, when the client passed it on. */
|
|
17
|
+
readonly me?: string;
|
|
18
|
+
/** The RFC 8707 resource the token is wanted for, when the client named one. */
|
|
19
|
+
readonly resource?: string;
|
|
20
|
+
}
|
|
21
|
+
/** The OAuth error codes a refused request is reported with. */
|
|
22
|
+
export type AuthorizationError = 'invalid_request' | 'unsupported_response_type' | 'invalid_target';
|
|
23
|
+
/**
|
|
24
|
+
* What a request to the authorization endpoint came to.
|
|
25
|
+
*
|
|
26
|
+
* `unredirectable` is a fault in `client_id` or `redirect_uri`: there is no
|
|
27
|
+
* address known to be the client's, so the person is shown a page. `refused`
|
|
28
|
+
* is any other fault, reported at `redirect_uri` once the client's metadata
|
|
29
|
+
* shows that address is the client's.
|
|
30
|
+
*/
|
|
31
|
+
export type ParsedAuthorizationRequest = {
|
|
32
|
+
readonly kind: 'valid';
|
|
33
|
+
readonly request: AuthorizationRequest;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: 'refused';
|
|
36
|
+
readonly clientId: string;
|
|
37
|
+
readonly redirectUri: string;
|
|
38
|
+
readonly state: string | undefined;
|
|
39
|
+
readonly error: AuthorizationError;
|
|
40
|
+
readonly description: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: 'unredirectable';
|
|
43
|
+
readonly message: string;
|
|
44
|
+
};
|
|
45
|
+
/** Read an authorization request off its query string, for a site at `baseUrl`. */
|
|
46
|
+
export declare function parseAuthorizationRequest(params: URLSearchParams, baseUrl: string): ParsedAuthorizationRequest;
|
|
47
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/indieauth/request.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,mCAAmC;AACnC,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,gEAAgE;AAChE,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,2BAA2B,GAAG,gBAAgB,CAAC;AAEpG;;;;;;;GAOG;AACH,MAAM,MAAM,0BAA0B,GAClC;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAClE;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAelE,mFAAmF;AACnF,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,MAAM,GACd,0BAA0B,CAyD5B"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { SCOPES } from './discovery.js';
|
|
3
|
+
/** An S256 challenge: 32 bytes of hash in unpadded base64url. */
|
|
4
|
+
const S256_CHALLENGE = /^[A-Za-z0-9_-]{43}$/;
|
|
5
|
+
/** Schemes a redirect may never use, whatever a client's metadata lists. */
|
|
6
|
+
const SCRIPTING_SCHEMES = new Set([
|
|
7
|
+
'javascript:',
|
|
8
|
+
'data:',
|
|
9
|
+
'vbscript:',
|
|
10
|
+
'file:',
|
|
11
|
+
'blob:',
|
|
12
|
+
'about:',
|
|
13
|
+
]);
|
|
14
|
+
/** Read an authorization request off its query string, for a site at `baseUrl`. */
|
|
15
|
+
export function parseAuthorizationRequest(params, baseUrl) {
|
|
16
|
+
const clientId = clientIdentifier(params.get('client_id') ?? '');
|
|
17
|
+
if (clientId === undefined) {
|
|
18
|
+
return { kind: 'unredirectable', message: 'The app did not say which app it is.' };
|
|
19
|
+
}
|
|
20
|
+
const redirectUri = redirectAddress(params.get('redirect_uri') ?? '');
|
|
21
|
+
if (redirectUri === undefined) {
|
|
22
|
+
return { kind: 'unredirectable', message: 'The app did not say where to send you back.' };
|
|
23
|
+
}
|
|
24
|
+
const state = params.get('state') ?? undefined;
|
|
25
|
+
const refuse = (error, description) => ({
|
|
26
|
+
kind: 'refused',
|
|
27
|
+
clientId,
|
|
28
|
+
redirectUri,
|
|
29
|
+
state,
|
|
30
|
+
error,
|
|
31
|
+
description,
|
|
32
|
+
});
|
|
33
|
+
if (params.get('response_type') !== 'code') {
|
|
34
|
+
return refuse('unsupported_response_type', 'response_type must be code');
|
|
35
|
+
}
|
|
36
|
+
if (state === undefined || state === '')
|
|
37
|
+
return refuse('invalid_request', 'state is required');
|
|
38
|
+
const codeChallenge = params.get('code_challenge') ?? '';
|
|
39
|
+
if (!S256_CHALLENGE.test(codeChallenge)) {
|
|
40
|
+
return refuse('invalid_request', 'code_challenge must be an S256 PKCE challenge');
|
|
41
|
+
}
|
|
42
|
+
if (params.get('code_challenge_method') !== 'S256') {
|
|
43
|
+
return refuse('invalid_request', 'code_challenge_method must be S256');
|
|
44
|
+
}
|
|
45
|
+
const resources = params.getAll('resource');
|
|
46
|
+
if (resources.length > 1)
|
|
47
|
+
return refuse('invalid_target', 'name at most one resource');
|
|
48
|
+
const resource = resources[0];
|
|
49
|
+
if (resource !== undefined && !onSite(resource, baseUrl)) {
|
|
50
|
+
return refuse('invalid_target', 'resource must be an absolute URL on this site');
|
|
51
|
+
}
|
|
52
|
+
const offered = new Set(SCOPES);
|
|
53
|
+
const scopes = [...new Set((params.get('scope') ?? '').split(' '))].filter((scope) => offered.has(scope));
|
|
54
|
+
const me = params.get('me') ?? '';
|
|
55
|
+
return {
|
|
56
|
+
kind: 'valid',
|
|
57
|
+
request: {
|
|
58
|
+
clientId,
|
|
59
|
+
redirectUri,
|
|
60
|
+
state,
|
|
61
|
+
codeChallenge,
|
|
62
|
+
scopes,
|
|
63
|
+
...(me === '' ? {} : { me }),
|
|
64
|
+
...(resource === undefined ? {} : { resource }),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A client_id the IndieAuth spec allows, as given, or `undefined`.
|
|
70
|
+
*
|
|
71
|
+
* http or https, with no fragment, credentials or dot segments, and a domain
|
|
72
|
+
* name for a host: an IP address only when it is the loopback one a client
|
|
73
|
+
* in development runs on.
|
|
74
|
+
*/
|
|
75
|
+
function clientIdentifier(value) {
|
|
76
|
+
const url = URL.parse(value);
|
|
77
|
+
if (url === null || (url.protocol !== 'https:' && url.protocol !== 'http:'))
|
|
78
|
+
return undefined;
|
|
79
|
+
if (url.username !== '' || url.password !== '' || value.includes('#'))
|
|
80
|
+
return undefined;
|
|
81
|
+
const path = value
|
|
82
|
+
.slice(url.protocol.length + 2)
|
|
83
|
+
.replace(/^[^/?]*/, '')
|
|
84
|
+
.split('?')[0] ?? '';
|
|
85
|
+
if (path.split('/').some((segment) => segment === '.' || segment === '..'))
|
|
86
|
+
return undefined;
|
|
87
|
+
const host = url.hostname.replace(/^\[|\]$/g, '');
|
|
88
|
+
if (isIP(host) !== 0 && host !== '127.0.0.1' && host !== '::1')
|
|
89
|
+
return undefined;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
/** An absolute redirect_uri with no fragment and no scripting scheme, or `undefined`. */
|
|
93
|
+
function redirectAddress(value) {
|
|
94
|
+
const url = URL.parse(value);
|
|
95
|
+
if (url === null || value.includes('#') || SCRIPTING_SCHEMES.has(url.protocol))
|
|
96
|
+
return undefined;
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
/** Whether `value` is an absolute URL, with no fragment, on the site's own origin. */
|
|
100
|
+
function onSite(value, baseUrl) {
|
|
101
|
+
const url = URL.parse(value);
|
|
102
|
+
return url !== null && !value.includes('#') && url.origin === new URL(baseUrl).origin;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/indieauth/request.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { GeekityEnv } from '../env.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The token endpoint (TASK-160): a client redeems its code here for a bearer
|
|
5
|
+
* token, or trades a refresh token for a new pair.
|
|
6
|
+
*
|
|
7
|
+
* A code is checked by the same {@link redeemCode} profile redemption uses, so
|
|
8
|
+
* a code spent at one endpoint is spent at both. Outside `/admin`, because the
|
|
9
|
+
* client posting here has no admin session, and not exempt from maintenance
|
|
10
|
+
* mode, like the authorization endpoint beside it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mountTokenEndpoint(app: Hono<GeekityEnv>): void;
|
|
13
|
+
/**
|
|
14
|
+
* The endpoints that answer for a token once it is issued (TASK-161):
|
|
15
|
+
* introspection, revocation and userinfo. Outside `/admin` and behind the
|
|
16
|
+
* maintenance gate, like the token endpoint.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mountTokenInfoEndpoints(app: Hono<GeekityEnv>): void;
|
|
19
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/indieauth/token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAI1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAoB5C;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CA2B9D;AA0BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CA6CnE"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { findUserById } from '../admin/accounts.js';
|
|
2
|
+
import { requireBearer } from './bearer.js';
|
|
3
|
+
import { INTROSPECTION_PATH, REVOCATION_PATH, TOKEN_PATH, USERINFO_PATH, siteBaseUrl, } from './discovery.js';
|
|
4
|
+
import { profileResponse, redeemCode, redemptionForm } from './redeem.js';
|
|
5
|
+
import { ACCESS_TOKEN_LIFETIME_MS, findAccessToken, issueTokens, refreshTokens, revokeToken, } from './tokens.js';
|
|
6
|
+
/**
|
|
7
|
+
* The token endpoint (TASK-160): a client redeems its code here for a bearer
|
|
8
|
+
* token, or trades a refresh token for a new pair.
|
|
9
|
+
*
|
|
10
|
+
* A code is checked by the same {@link redeemCode} profile redemption uses, so
|
|
11
|
+
* a code spent at one endpoint is spent at both. Outside `/admin`, because the
|
|
12
|
+
* client posting here has no admin session, and not exempt from maintenance
|
|
13
|
+
* mode, like the authorization endpoint beside it.
|
|
14
|
+
*/
|
|
15
|
+
export function mountTokenEndpoint(app) {
|
|
16
|
+
app.post(TOKEN_PATH, async (c) => {
|
|
17
|
+
c.header('cache-control', 'no-store');
|
|
18
|
+
const form = redemptionForm(await c.req.parseBody());
|
|
19
|
+
const now = c.var.config.now();
|
|
20
|
+
if (form['grant_type'] === 'refresh_token') {
|
|
21
|
+
const refreshed = await refreshTokens(c.var.config.dataDir, form, now);
|
|
22
|
+
if (!refreshed.ok)
|
|
23
|
+
return refuse(c, refreshed.error, refreshed.description);
|
|
24
|
+
const user = findUserById(c.var.config.dataDir, refreshed.issued.token.userId);
|
|
25
|
+
if (user === undefined)
|
|
26
|
+
return refuse(c, 'invalid_grant', GONE);
|
|
27
|
+
return answer(c, refreshed.issued, user);
|
|
28
|
+
}
|
|
29
|
+
const redeemed = redeemCode(c.var.indieauth.codes, form);
|
|
30
|
+
if (!redeemed.ok)
|
|
31
|
+
return refuse(c, redeemed.error, redeemed.description);
|
|
32
|
+
const { grant } = redeemed;
|
|
33
|
+
if (grant.scopes.length === 0) {
|
|
34
|
+
return refuse(c, 'invalid_grant', 'The code was approved with no scope, so earns no token.');
|
|
35
|
+
}
|
|
36
|
+
if (!sameResource(form, grant.resource)) {
|
|
37
|
+
return refuse(c, 'invalid_target', 'resource must be the one the person approved.');
|
|
38
|
+
}
|
|
39
|
+
const user = findUserById(c.var.config.dataDir, grant.userId);
|
|
40
|
+
if (user === undefined)
|
|
41
|
+
return refuse(c, 'invalid_grant', GONE);
|
|
42
|
+
return answer(c, await issueTokens(c.var.config.dataDir, grant, now), user);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const GONE = 'The person who approved this is gone.';
|
|
46
|
+
/** Whether the request names no resource, or the one the code was approved for (RFC 8707). */
|
|
47
|
+
function sameResource(form, approved) {
|
|
48
|
+
const named = form['resource'];
|
|
49
|
+
return named === undefined || named === approved;
|
|
50
|
+
}
|
|
51
|
+
function answer(c, issued, user) {
|
|
52
|
+
const { token } = issued;
|
|
53
|
+
return c.json({
|
|
54
|
+
access_token: issued.accessToken,
|
|
55
|
+
token_type: 'Bearer',
|
|
56
|
+
scope: token.scopes.join(' '),
|
|
57
|
+
expires_in: ACCESS_TOKEN_LIFETIME_MS / 1000,
|
|
58
|
+
refresh_token: issued.refreshToken,
|
|
59
|
+
...profileResponse(token, user, siteBaseUrl(c)),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function refuse(c, error, description) {
|
|
63
|
+
return c.json({ error, error_description: description }, 400);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The endpoints that answer for a token once it is issued (TASK-161):
|
|
67
|
+
* introspection, revocation and userinfo. Outside `/admin` and behind the
|
|
68
|
+
* maintenance gate, like the token endpoint.
|
|
69
|
+
*/
|
|
70
|
+
export function mountTokenInfoEndpoints(app) {
|
|
71
|
+
// RFC 7662. The IndieAuth spec asks that introspection need authorization
|
|
72
|
+
// of its own: here, another live token, and it answers only for tokens held
|
|
73
|
+
// by the same person, so one person's token can never tell anything about
|
|
74
|
+
// another's.
|
|
75
|
+
app.post(INTROSPECTION_PATH, requireBearer({ audience: 'authorization-server' }), async (c) => {
|
|
76
|
+
c.header('cache-control', 'no-store');
|
|
77
|
+
const presented = (await c.req.parseBody())['token'];
|
|
78
|
+
if (typeof presented !== 'string' || presented === '') {
|
|
79
|
+
return refuse(c, 'invalid_request', 'token is required.');
|
|
80
|
+
}
|
|
81
|
+
const token = findAccessToken(c.var.config.dataDir, presented, c.var.config.now());
|
|
82
|
+
if (token === undefined || token.userId !== c.var.bearer.user.id) {
|
|
83
|
+
return c.json({ active: false });
|
|
84
|
+
}
|
|
85
|
+
return c.json({
|
|
86
|
+
active: true,
|
|
87
|
+
me: token.me,
|
|
88
|
+
client_id: token.clientId,
|
|
89
|
+
scope: token.scopes.join(' '),
|
|
90
|
+
exp: Math.floor(Date.parse(token.expiresAt) / 1000),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// RFC 7009: holding the token is the authorization, and the answer is 200
|
|
94
|
+
// whether or not it was one, so a client learns nothing by guessing.
|
|
95
|
+
app.post(REVOCATION_PATH, async (c) => {
|
|
96
|
+
c.header('cache-control', 'no-store');
|
|
97
|
+
const presented = (await c.req.parseBody())['token'];
|
|
98
|
+
if (typeof presented !== 'string' || presented === '') {
|
|
99
|
+
return refuse(c, 'invalid_request', 'token is required.');
|
|
100
|
+
}
|
|
101
|
+
await revokeToken(c.var.config.dataDir, presented, c.var.config.now());
|
|
102
|
+
return c.body(null, 200);
|
|
103
|
+
});
|
|
104
|
+
app.get(USERINFO_PATH, requireBearer({ audience: 'authorization-server', scope: 'profile' }), (c) => {
|
|
105
|
+
c.header('cache-control', 'no-store');
|
|
106
|
+
const { token, user } = c.var.bearer;
|
|
107
|
+
return c.json(profileResponse(token, user, siteBaseUrl(c)).profile ?? {});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/indieauth/token.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AuthorizationCode } from './grants.ts';
|
|
2
|
+
import type { RedemptionForm } from './redeem.ts';
|
|
3
|
+
import type { Scope } from './request.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The access tokens the token endpoint has issued: `data/indieauth-tokens.json`
|
|
6
|
+
* (TASK-160, decision-24).
|
|
7
|
+
*
|
|
8
|
+
* A token is irreducible state under decision-9: a client holding one cannot
|
|
9
|
+
* be handed it again, so losing it signs the person out of every app. So it
|
|
10
|
+
* lives in a file in `data/`, written 0600 like `users.json`, and holds a
|
|
11
|
+
* SHA-256 hash of each token rather than the token. A token is 32 random
|
|
12
|
+
* bytes, so the hash needs no salt: nobody can guess their way back to one.
|
|
13
|
+
*
|
|
14
|
+
* One record per connection, that is per approval of a client: a refresh
|
|
15
|
+
* replaces both hashes in place, so the record keeps the id and the time the
|
|
16
|
+
* person approved it while the tokens change under it.
|
|
17
|
+
*/
|
|
18
|
+
/** The file, relative to `dataDir`. */
|
|
19
|
+
export declare const TOKENS_FILE = "indieauth-tokens.json";
|
|
20
|
+
/** The permissions the file is written with: nobody but the site's own user. */
|
|
21
|
+
export declare const TOKENS_FILE_MODE = 384;
|
|
22
|
+
/** How long an access token works for. */
|
|
23
|
+
export declare const ACCESS_TOKEN_LIFETIME_MS: number;
|
|
24
|
+
/** How long a refresh token works for, counted from its own issue: each refresh starts it again. */
|
|
25
|
+
export declare const REFRESH_TOKEN_LIFETIME_MS: number;
|
|
26
|
+
/**
|
|
27
|
+
* How stale a connection's last use may be. A token's use is written only
|
|
28
|
+
* when the stored one is older than this, so a client calling the API all
|
|
29
|
+
* day rewrites the file once an hour rather than once a request (TASK-162,
|
|
30
|
+
* decision-24).
|
|
31
|
+
*/
|
|
32
|
+
export declare const LAST_USE_RESOLUTION_MS: number;
|
|
33
|
+
/** One connection as the file keeps it. */
|
|
34
|
+
export interface StoredToken {
|
|
35
|
+
/** Stable for the life of the connection, so a screen can name it. */
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly userId: number;
|
|
38
|
+
/** The canonical me URL the client was handed. */
|
|
39
|
+
readonly me: string;
|
|
40
|
+
readonly clientId: string;
|
|
41
|
+
/** What the client called itself on the consent screen, when it said. */
|
|
42
|
+
readonly clientName?: string;
|
|
43
|
+
readonly scopes: readonly Scope[];
|
|
44
|
+
/** The RFC 8707 resource the token is good at, or absent when the client named none. */
|
|
45
|
+
readonly resource?: string;
|
|
46
|
+
/** When the person approved the connection, as an ISO 8601 instant. */
|
|
47
|
+
readonly issuedAt: string;
|
|
48
|
+
/** When a resource server last accepted its token, to {@link LAST_USE_RESOLUTION_MS}. */
|
|
49
|
+
readonly lastUsedAt?: string;
|
|
50
|
+
readonly accessTokenHash: string;
|
|
51
|
+
/** When the current access token stops working. */
|
|
52
|
+
readonly expiresAt: string;
|
|
53
|
+
readonly refreshTokenHash: string;
|
|
54
|
+
/** When the current refresh token stops working. */
|
|
55
|
+
readonly refreshExpiresAt: string;
|
|
56
|
+
}
|
|
57
|
+
/** Tokens just issued: the only time the tokens themselves exist anywhere. */
|
|
58
|
+
export interface IssuedTokens {
|
|
59
|
+
readonly accessToken: string;
|
|
60
|
+
readonly refreshToken: string;
|
|
61
|
+
readonly token: StoredToken;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Who is asking a token to be checked: a resource server, by the resource
|
|
65
|
+
* identifier clients name it with.
|
|
66
|
+
*
|
|
67
|
+
* `acceptsUnbound` says whether it takes a token issued with no resource
|
|
68
|
+
* named. Micropub clients predate RFC 8707 and name none, so the Micropub
|
|
69
|
+
* endpoint will; MCP clients always name one, and the MCP spec asks a server
|
|
70
|
+
* to take only tokens issued for it, so the MCP endpoint will not.
|
|
71
|
+
*/
|
|
72
|
+
export interface Audience {
|
|
73
|
+
readonly resource: string;
|
|
74
|
+
readonly acceptsUnbound: boolean;
|
|
75
|
+
}
|
|
76
|
+
export type Refresh = {
|
|
77
|
+
readonly ok: true;
|
|
78
|
+
readonly issued: IssuedTokens;
|
|
79
|
+
} | {
|
|
80
|
+
readonly ok: false;
|
|
81
|
+
readonly error: 'invalid_request' | 'invalid_grant';
|
|
82
|
+
readonly description: string;
|
|
83
|
+
};
|
|
84
|
+
/** Where the tokens file lives for a given data directory. */
|
|
85
|
+
export declare function tokensFile(dataDir: string): string;
|
|
86
|
+
/** Every connection in the file, live or not. */
|
|
87
|
+
export declare function listTokens(dataDir: string): StoredToken[];
|
|
88
|
+
/** Issue an access token and a refresh token for an approved code. */
|
|
89
|
+
export declare function issueTokens(dataDir: string, grant: AuthorizationCode, now: Date): Promise<IssuedTokens>;
|
|
90
|
+
/**
|
|
91
|
+
* Trade a refresh token for new tokens, from the client it was issued to.
|
|
92
|
+
*
|
|
93
|
+
* Both tokens rotate: the old access token and the old refresh token stop
|
|
94
|
+
* working the moment this returns. The scopes and resource stay the ones the
|
|
95
|
+
* person approved; a `scope` in the request is ignored, which RFC 6749 allows
|
|
96
|
+
* because the answer says the scope it holds.
|
|
97
|
+
*/
|
|
98
|
+
export declare function refreshTokens(dataDir: string, form: RedemptionForm, now: Date): Promise<Refresh>;
|
|
99
|
+
/**
|
|
100
|
+
* The connection a live access token belongs to, whoever is asking; the
|
|
101
|
+
* authorization server's own endpoints take any token it issued. `undefined`
|
|
102
|
+
* for a token that is unknown or expired.
|
|
103
|
+
*/
|
|
104
|
+
export declare function findAccessToken(dataDir: string, accessToken: string, now: Date): StoredToken | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* The connection a live access token belongs to, when `audience` may accept
|
|
107
|
+
* it; `undefined` for a token that is unknown, expired or bound to another
|
|
108
|
+
* resource.
|
|
109
|
+
*/
|
|
110
|
+
export declare function verifyAccessToken(dataDir: string, accessToken: string, audience: Audience, now: Date): StoredToken | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* End the connection `presented` belongs to, whether it is the access token
|
|
113
|
+
* or the refresh token, so both stop working at once. Answers whether there
|
|
114
|
+
* was one.
|
|
115
|
+
*/
|
|
116
|
+
export declare function revokeToken(dataDir: string, presented: string, now: Date): Promise<boolean>;
|
|
117
|
+
/**
|
|
118
|
+
* End connection `id` of `userId`, as the connected apps screen does, and
|
|
119
|
+
* answer what it was. Somebody else's connection, or one already gone, answers
|
|
120
|
+
* `undefined` and writes nothing.
|
|
121
|
+
*/
|
|
122
|
+
export declare function revokeConnection(dataDir: string, userId: number, id: string, now: Date): Promise<StoredToken | undefined>;
|
|
123
|
+
/**
|
|
124
|
+
* Note that `token` was just accepted, unless its last use is already within
|
|
125
|
+
* {@link LAST_USE_RESOLUTION_MS}: the record says when it was last written,
|
|
126
|
+
* so no state outside the file is needed to keep the writes rare.
|
|
127
|
+
*/
|
|
128
|
+
export declare function recordUse(dataDir: string, token: StoredToken, now: Date): Promise<void>;
|
|
129
|
+
/** Revoke every token `userId` holds. */
|
|
130
|
+
export declare function revokeTokensForUser(dataDir: string, userId: number): Promise<void>;
|
|
131
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/indieauth/tokens.ts"],"names":[],"mappings":"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"}
|