@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.
Files changed (232) hide show
  1. package/README.md +170 -4
  2. package/admin/pages/account/login.njk +3 -0
  3. package/admin/pages/documents/editor.njk +7 -0
  4. package/admin/pages/indieauth/consent.njk +51 -0
  5. package/admin/pages/indieauth/refused.njk +11 -0
  6. package/admin/pages/settings/general.njk +9 -0
  7. package/admin/pages/settings/reading.njk +93 -0
  8. package/admin/pages/users/apps.njk +74 -0
  9. package/admin/pages/users/edit.njk +12 -0
  10. package/admin/static/admin.css +27 -0
  11. package/dist/admin/accounts.d.ts +3 -0
  12. package/dist/admin/accounts.d.ts.map +1 -1
  13. package/dist/admin/accounts.js +5 -0
  14. package/dist/admin/accounts.js.map +1 -1
  15. package/dist/admin/documents.d.ts +2 -0
  16. package/dist/admin/documents.d.ts.map +1 -1
  17. package/dist/admin/documents.js +17 -1
  18. package/dist/admin/documents.js.map +1 -1
  19. package/dist/admin/headers.d.ts +5 -1
  20. package/dist/admin/headers.d.ts.map +1 -1
  21. package/dist/admin/headers.js +7 -3
  22. package/dist/admin/headers.js.map +1 -1
  23. package/dist/admin/menu.d.ts.map +1 -1
  24. package/dist/admin/menu.js +2 -1
  25. package/dist/admin/menu.js.map +1 -1
  26. package/dist/admin/routes.d.ts.map +1 -1
  27. package/dist/admin/routes.js +48 -5
  28. package/dist/admin/routes.js.map +1 -1
  29. package/dist/admin/settings-general.js +1 -1
  30. package/dist/admin/settings-general.js.map +1 -1
  31. package/dist/admin/settings-reading.d.ts.map +1 -1
  32. package/dist/admin/settings-reading.js +15 -1
  33. package/dist/admin/settings-reading.js.map +1 -1
  34. package/dist/admin/settings.d.ts +79 -2
  35. package/dist/admin/settings.d.ts.map +1 -1
  36. package/dist/admin/settings.js +168 -2
  37. package/dist/admin/settings.js.map +1 -1
  38. package/dist/admin/templates.d.ts +3 -0
  39. package/dist/admin/templates.d.ts.map +1 -1
  40. package/dist/admin/templates.js +3 -0
  41. package/dist/admin/templates.js.map +1 -1
  42. package/dist/admin/users.d.ts +8 -0
  43. package/dist/admin/users.d.ts.map +1 -1
  44. package/dist/admin/users.js +9 -0
  45. package/dist/admin/users.js.map +1 -1
  46. package/dist/config.d.ts +30 -0
  47. package/dist/config.d.ts.map +1 -1
  48. package/dist/config.js +1 -0
  49. package/dist/config.js.map +1 -1
  50. package/dist/env.d.ts +14 -0
  51. package/dist/env.d.ts.map +1 -1
  52. package/dist/federation/article.d.ts.map +1 -1
  53. package/dist/federation/article.js +18 -5
  54. package/dist/federation/article.js.map +1 -1
  55. package/dist/images/icons.d.ts +44 -25
  56. package/dist/images/icons.d.ts.map +1 -1
  57. package/dist/images/icons.js +205 -60
  58. package/dist/images/icons.js.map +1 -1
  59. package/dist/images/index.d.ts +2 -2
  60. package/dist/images/index.d.ts.map +1 -1
  61. package/dist/images/index.js +1 -1
  62. package/dist/images/index.js.map +1 -1
  63. package/dist/images/markup.d.ts +14 -0
  64. package/dist/images/markup.d.ts.map +1 -1
  65. package/dist/images/markup.js +16 -11
  66. package/dist/images/markup.js.map +1 -1
  67. package/dist/images/variants.d.ts.map +1 -1
  68. package/dist/images/variants.js +3 -4
  69. package/dist/images/variants.js.map +1 -1
  70. package/dist/index.d.ts +14 -4
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +28 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/indexnow.d.ts +34 -0
  75. package/dist/indexnow.d.ts.map +1 -0
  76. package/dist/indexnow.js +141 -0
  77. package/dist/indexnow.js.map +1 -0
  78. package/dist/indieauth/bearer.d.ts +39 -0
  79. package/dist/indieauth/bearer.d.ts.map +1 -0
  80. package/dist/indieauth/bearer.js +63 -0
  81. package/dist/indieauth/bearer.js.map +1 -0
  82. package/dist/indieauth/client.d.ts +61 -0
  83. package/dist/indieauth/client.d.ts.map +1 -0
  84. package/dist/indieauth/client.js +139 -0
  85. package/dist/indieauth/client.js.map +1 -0
  86. package/dist/indieauth/connected-apps.d.ts +23 -0
  87. package/dist/indieauth/connected-apps.d.ts.map +1 -0
  88. package/dist/indieauth/connected-apps.js +73 -0
  89. package/dist/indieauth/connected-apps.js.map +1 -0
  90. package/dist/indieauth/consent.d.ts +28 -0
  91. package/dist/indieauth/consent.d.ts.map +1 -0
  92. package/dist/indieauth/consent.js +185 -0
  93. package/dist/indieauth/consent.js.map +1 -0
  94. package/dist/indieauth/discovery.d.ts +87 -0
  95. package/dist/indieauth/discovery.d.ts.map +1 -0
  96. package/dist/indieauth/discovery.js +127 -0
  97. package/dist/indieauth/discovery.js.map +1 -0
  98. package/dist/indieauth/grants.d.ts +57 -0
  99. package/dist/indieauth/grants.d.ts.map +1 -0
  100. package/dist/indieauth/grants.js +35 -0
  101. package/dist/indieauth/grants.js.map +1 -0
  102. package/dist/indieauth/identity.d.ts +36 -0
  103. package/dist/indieauth/identity.d.ts.map +1 -0
  104. package/dist/indieauth/identity.js +78 -0
  105. package/dist/indieauth/identity.js.map +1 -0
  106. package/dist/indieauth/redeem.d.ts +47 -0
  107. package/dist/indieauth/redeem.d.ts.map +1 -0
  108. package/dist/indieauth/redeem.js +69 -0
  109. package/dist/indieauth/redeem.js.map +1 -0
  110. package/dist/indieauth/request.d.ts +47 -0
  111. package/dist/indieauth/request.d.ts.map +1 -0
  112. package/dist/indieauth/request.js +104 -0
  113. package/dist/indieauth/request.js.map +1 -0
  114. package/dist/indieauth/token.d.ts +19 -0
  115. package/dist/indieauth/token.d.ts.map +1 -0
  116. package/dist/indieauth/token.js +110 -0
  117. package/dist/indieauth/token.js.map +1 -0
  118. package/dist/indieauth/tokens.d.ts +131 -0
  119. package/dist/indieauth/tokens.d.ts.map +1 -0
  120. package/dist/indieauth/tokens.js +205 -0
  121. package/dist/indieauth/tokens.js.map +1 -0
  122. package/dist/web/archive.d.ts +1 -1
  123. package/dist/web/archive.d.ts.map +1 -1
  124. package/dist/web/archive.js +3 -2
  125. package/dist/web/archive.js.map +1 -1
  126. package/dist/web/context.d.ts +25 -0
  127. package/dist/web/context.d.ts.map +1 -1
  128. package/dist/web/context.js +18 -0
  129. package/dist/web/context.js.map +1 -1
  130. package/dist/web/feed-atom.d.ts.map +1 -1
  131. package/dist/web/feed-atom.js +3 -1
  132. package/dist/web/feed-atom.js.map +1 -1
  133. package/dist/web/feed-item.d.ts +11 -2
  134. package/dist/web/feed-item.d.ts.map +1 -1
  135. package/dist/web/feed-item.js +9 -3
  136. package/dist/web/feed-item.js.map +1 -1
  137. package/dist/web/feed-json.d.ts +4 -0
  138. package/dist/web/feed-json.d.ts.map +1 -1
  139. package/dist/web/feed-json.js +4 -1
  140. package/dist/web/feed-json.js.map +1 -1
  141. package/dist/web/feed-rss.d.ts.map +1 -1
  142. package/dist/web/feed-rss.js +16 -3
  143. package/dist/web/feed-rss.js.map +1 -1
  144. package/dist/web/feed-source.d.ts +28 -0
  145. package/dist/web/feed-source.d.ts.map +1 -1
  146. package/dist/web/feed-source.js +31 -0
  147. package/dist/web/feed-source.js.map +1 -1
  148. package/dist/web/feed-xml.d.ts +2 -0
  149. package/dist/web/feed-xml.d.ts.map +1 -1
  150. package/dist/web/feed-xml.js +2 -0
  151. package/dist/web/feed-xml.js.map +1 -1
  152. package/dist/web/feeds.d.ts.map +1 -1
  153. package/dist/web/feeds.js +8 -1
  154. package/dist/web/feeds.js.map +1 -1
  155. package/dist/web/index.d.ts +3 -1
  156. package/dist/web/index.d.ts.map +1 -1
  157. package/dist/web/index.js +2 -1
  158. package/dist/web/index.js.map +1 -1
  159. package/dist/web/indexnow.d.ts +9 -0
  160. package/dist/web/indexnow.d.ts.map +1 -0
  161. package/dist/web/indexnow.js +27 -0
  162. package/dist/web/indexnow.js.map +1 -0
  163. package/dist/web/llms.d.ts +58 -0
  164. package/dist/web/llms.d.ts.map +1 -0
  165. package/dist/web/llms.js +91 -0
  166. package/dist/web/llms.js.map +1 -0
  167. package/dist/web/locale.d.ts +41 -0
  168. package/dist/web/locale.d.ts.map +1 -0
  169. package/dist/web/locale.js +56 -0
  170. package/dist/web/locale.js.map +1 -0
  171. package/dist/web/manifest.d.ts +50 -0
  172. package/dist/web/manifest.d.ts.map +1 -0
  173. package/dist/web/manifest.js +56 -0
  174. package/dist/web/manifest.js.map +1 -0
  175. package/dist/web/negotiate.d.ts.map +1 -1
  176. package/dist/web/negotiate.js +6 -0
  177. package/dist/web/negotiate.js.map +1 -1
  178. package/dist/web/render.d.ts +6 -1
  179. package/dist/web/render.d.ts.map +1 -1
  180. package/dist/web/render.js +41 -10
  181. package/dist/web/render.js.map +1 -1
  182. package/dist/web/robots.d.ts +89 -0
  183. package/dist/web/robots.d.ts.map +1 -0
  184. package/dist/web/robots.js +259 -0
  185. package/dist/web/robots.js.map +1 -0
  186. package/dist/web/routes.d.ts.map +1 -1
  187. package/dist/web/routes.js +105 -6
  188. package/dist/web/routes.js.map +1 -1
  189. package/dist/web/share-image.d.ts +51 -0
  190. package/dist/web/share-image.d.ts.map +1 -0
  191. package/dist/web/share-image.js +43 -0
  192. package/dist/web/share-image.js.map +1 -0
  193. package/dist/web/sitemap.d.ts +0 -26
  194. package/dist/web/sitemap.d.ts.map +1 -1
  195. package/dist/web/sitemap.js +0 -44
  196. package/dist/web/sitemap.js.map +1 -1
  197. package/dist/web/templates.d.ts +16 -9
  198. package/dist/web/templates.d.ts.map +1 -1
  199. package/dist/web/templates.js +83 -55
  200. package/dist/web/templates.js.map +1 -1
  201. package/dist/web/themes.d.ts +33 -0
  202. package/dist/web/themes.d.ts.map +1 -1
  203. package/dist/web/themes.js +57 -0
  204. package/dist/web/themes.js.map +1 -1
  205. package/dist/webmention/discovery.d.ts +4 -2
  206. package/dist/webmention/discovery.d.ts.map +1 -1
  207. package/dist/webmention/discovery.js +14 -8
  208. package/dist/webmention/discovery.js.map +1 -1
  209. package/dist/webmention/fetch-public.d.ts +2 -0
  210. package/dist/webmention/fetch-public.d.ts.map +1 -1
  211. package/dist/webmention/fetch-public.js +1 -1
  212. package/dist/webmention/fetch-public.js.map +1 -1
  213. package/package.json +1 -1
  214. package/themes/default/README.md +119 -29
  215. package/themes/default/layouts/base.njk +95 -16
  216. package/themes/default/layouts/front-page.njk +6 -1
  217. package/themes/default/layouts/home.njk +10 -0
  218. package/themes/default/layouts/page.njk +4 -3
  219. package/themes/default/layouts/post.njk +5 -4
  220. package/themes/default/layouts/search.njk +4 -5
  221. package/themes/default/partials/archive.njk +3 -1
  222. package/themes/default/partials/bio.njk +9 -2
  223. package/themes/default/partials/breadcrumbs.njk +20 -0
  224. package/themes/default/partials/conversation.njk +4 -1
  225. package/themes/default/partials/jsonld.njk +19 -4
  226. package/themes/default/partials/kicker.njk +6 -4
  227. package/themes/default/partials/lang.njk +7 -0
  228. package/themes/default/partials/post-list.njk +5 -3
  229. package/themes/default/partials/reply-context.njk +4 -2
  230. package/themes/default/src/style.css +14 -0
  231. package/themes/default/static/style.css +53 -0
  232. package/themes/default/theme.json +2 -0
@@ -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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"}
@@ -0,0 +1,205 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { readFileIfPresentSync, updateFileAtomically } from '../files/atomic.js';
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 const TOKENS_FILE = 'indieauth-tokens.json';
20
+ /** The permissions the file is written with: nobody but the site's own user. */
21
+ export const TOKENS_FILE_MODE = 0o600;
22
+ /** How long an access token works for. */
23
+ export const ACCESS_TOKEN_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
24
+ /** How long a refresh token works for, counted from its own issue: each refresh starts it again. */
25
+ export const REFRESH_TOKEN_LIFETIME_MS = 60 * 24 * 60 * 60 * 1000;
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 const LAST_USE_RESOLUTION_MS = 60 * 60 * 1000;
33
+ /** Where the tokens file lives for a given data directory. */
34
+ export function tokensFile(dataDir) {
35
+ return path.join(dataDir, TOKENS_FILE);
36
+ }
37
+ /** Every connection in the file, live or not. */
38
+ export function listTokens(dataDir) {
39
+ const text = readFileIfPresentSync(tokensFile(dataDir));
40
+ return text === undefined ? [] : JSON.parse(text).tokens;
41
+ }
42
+ /** Issue an access token and a refresh token for an approved code. */
43
+ export async function issueTokens(dataDir, grant, now) {
44
+ const minted = mint(now);
45
+ const token = {
46
+ id: randomBytes(16).toString('base64url'),
47
+ userId: grant.userId,
48
+ me: grant.me,
49
+ clientId: grant.clientId,
50
+ ...(grant.clientName === undefined ? {} : { clientName: grant.clientName }),
51
+ scopes: grant.scopes,
52
+ ...(grant.resource === undefined ? {} : { resource: grant.resource }),
53
+ issuedAt: now.toISOString(),
54
+ ...minted.hashes,
55
+ };
56
+ await update(dataDir, now, (tokens) => [...tokens, token]);
57
+ return { accessToken: minted.accessToken, refreshToken: minted.refreshToken, token };
58
+ }
59
+ /**
60
+ * Trade a refresh token for new tokens, from the client it was issued to.
61
+ *
62
+ * Both tokens rotate: the old access token and the old refresh token stop
63
+ * working the moment this returns. The scopes and resource stay the ones the
64
+ * person approved; a `scope` in the request is ignored, which RFC 6749 allows
65
+ * because the answer says the scope it holds.
66
+ */
67
+ export async function refreshTokens(dataDir, form, now) {
68
+ const presented = form['refresh_token'];
69
+ const clientId = form['client_id'];
70
+ if (!presented || !clientId) {
71
+ return {
72
+ ok: false,
73
+ error: 'invalid_request',
74
+ description: 'refresh_token and client_id are both required.',
75
+ };
76
+ }
77
+ const hash = hashToken(presented);
78
+ const refused = {
79
+ ok: false,
80
+ error: 'invalid_grant',
81
+ description: 'The refresh token is unknown, expired, already used or another client’s.',
82
+ };
83
+ // Looked for before the write, so a client guessing tokens cannot make the
84
+ // site rewrite the file; the write below checks again under its lock.
85
+ const matches = (token) => token.refreshTokenHash === hash &&
86
+ token.clientId === clientId &&
87
+ Date.parse(token.refreshExpiresAt) > now.getTime();
88
+ if (!listTokens(dataDir).some(matches))
89
+ return refused;
90
+ let issued;
91
+ await update(dataDir, now, (tokens) => tokens.map((token) => {
92
+ if (!matches(token))
93
+ return token;
94
+ const minted = mint(now);
95
+ const rotated = { ...token, ...minted.hashes };
96
+ issued = {
97
+ accessToken: minted.accessToken,
98
+ refreshToken: minted.refreshToken,
99
+ token: rotated,
100
+ };
101
+ return rotated;
102
+ }));
103
+ if (issued === undefined)
104
+ return refused;
105
+ return { ok: true, issued };
106
+ }
107
+ /**
108
+ * The connection a live access token belongs to, whoever is asking; the
109
+ * authorization server's own endpoints take any token it issued. `undefined`
110
+ * for a token that is unknown or expired.
111
+ */
112
+ export function findAccessToken(dataDir, accessToken, now) {
113
+ const hash = hashToken(accessToken);
114
+ const token = listTokens(dataDir).find((candidate) => candidate.accessTokenHash === hash);
115
+ if (token === undefined || Date.parse(token.expiresAt) <= now.getTime())
116
+ return undefined;
117
+ return token;
118
+ }
119
+ /**
120
+ * The connection a live access token belongs to, when `audience` may accept
121
+ * it; `undefined` for a token that is unknown, expired or bound to another
122
+ * resource.
123
+ */
124
+ export function verifyAccessToken(dataDir, accessToken, audience, now) {
125
+ const token = findAccessToken(dataDir, accessToken, now);
126
+ if (token === undefined)
127
+ return undefined;
128
+ const bound = token.resource === undefined ? audience.acceptsUnbound : token.resource === audience.resource;
129
+ return bound ? token : undefined;
130
+ }
131
+ /**
132
+ * End the connection `presented` belongs to, whether it is the access token
133
+ * or the refresh token, so both stop working at once. Answers whether there
134
+ * was one.
135
+ */
136
+ export async function revokeToken(dataDir, presented, now) {
137
+ const hash = hashToken(presented);
138
+ const matches = (token) => token.accessTokenHash === hash || token.refreshTokenHash === hash;
139
+ // Looked for first, so a guessed token cannot make the site rewrite the file.
140
+ if (!listTokens(dataDir).some(matches))
141
+ return false;
142
+ await update(dataDir, now, (tokens) => tokens.filter((token) => !matches(token)));
143
+ return true;
144
+ }
145
+ /**
146
+ * End connection `id` of `userId`, as the connected apps screen does, and
147
+ * answer what it was. Somebody else's connection, or one already gone, answers
148
+ * `undefined` and writes nothing.
149
+ */
150
+ export async function revokeConnection(dataDir, userId, id, now) {
151
+ const matches = (token) => token.id === id && token.userId === userId;
152
+ const revoked = listTokens(dataDir).find(matches);
153
+ if (revoked === undefined)
154
+ return undefined;
155
+ await update(dataDir, now, (tokens) => tokens.filter((token) => !matches(token)));
156
+ return revoked;
157
+ }
158
+ /**
159
+ * Note that `token` was just accepted, unless its last use is already within
160
+ * {@link LAST_USE_RESOLUTION_MS}: the record says when it was last written,
161
+ * so no state outside the file is needed to keep the writes rare.
162
+ */
163
+ export async function recordUse(dataDir, token, now) {
164
+ const last = token.lastUsedAt === undefined ? undefined : Date.parse(token.lastUsedAt);
165
+ if (last !== undefined && now.getTime() - last <= LAST_USE_RESOLUTION_MS)
166
+ return;
167
+ const lastUsedAt = now.toISOString();
168
+ await update(dataDir, now, (tokens) => tokens.map((stored) => (stored.id === token.id ? { ...stored, lastUsedAt } : stored)));
169
+ }
170
+ /** Revoke every token `userId` holds. */
171
+ export async function revokeTokensForUser(dataDir, userId) {
172
+ if (listTokens(dataDir).every((token) => token.userId !== userId))
173
+ return;
174
+ await update(dataDir, new Date(), (tokens) => tokens.filter((token) => token.userId !== userId));
175
+ }
176
+ /**
177
+ * Rewrite the file through `change`, dropping every connection whose refresh
178
+ * token has expired first: such a connection can never be used again, and
179
+ * sweeping on write keeps the file to the ones that can.
180
+ */
181
+ async function update(dataDir, now, change) {
182
+ await updateFileAtomically(tokensFile(dataDir), (current) => {
183
+ const tokens = current === undefined ? [] : JSON.parse(current).tokens;
184
+ const live = tokens.filter((token) => Date.parse(token.refreshExpiresAt) > now.getTime());
185
+ return `${JSON.stringify({ tokens: change(live) }, null, 2)}\n`;
186
+ }, { mode: TOKENS_FILE_MODE });
187
+ }
188
+ function mint(now) {
189
+ const accessToken = randomBytes(32).toString('base64url');
190
+ const refreshToken = randomBytes(32).toString('base64url');
191
+ return {
192
+ accessToken,
193
+ refreshToken,
194
+ hashes: {
195
+ accessTokenHash: hashToken(accessToken),
196
+ expiresAt: new Date(now.getTime() + ACCESS_TOKEN_LIFETIME_MS).toISOString(),
197
+ refreshTokenHash: hashToken(refreshToken),
198
+ refreshExpiresAt: new Date(now.getTime() + REFRESH_TOKEN_LIFETIME_MS).toISOString(),
199
+ },
200
+ };
201
+ }
202
+ function hashToken(token) {
203
+ return createHash('sha256').update(token).digest('base64url');
204
+ }
205
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/indieauth/tokens.ts"],"names":[],"mappings":"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"}
@@ -45,5 +45,5 @@ export interface ArchiveMonth {
45
45
  * own: an archive is a calendar, and a post that is on no day of it is not a
46
46
  * month a reader could be shown.
47
47
  */
48
- export declare function archiveMonths(posts: readonly Document[], timezone?: string): ArchiveMonth[];
48
+ export declare function archiveMonths(posts: readonly Document[], timezone?: string, locale?: string): ArchiveMonth[];
49
49
  //# sourceMappingURL=archive.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/web/archive.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAIvD;;;;;;;;;;;;;;GAcG;AAEH,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,YAAY,CAAC;AAElD,+DAA+D;AAC/D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,QAAQ,GAAE,MAAyB,GAClC,YAAY,EAAE,CAyBhB"}
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/web/archive.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAKvD;;;;;;;;;;;;;;GAcG;AAEH,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,YAAY,CAAC;AAElD,+DAA+D;AAC/D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,QAAQ,GAAE,MAAyB,EACnC,MAAM,GAAE,MAAuB,GAC9B,YAAY,EAAE,CAyBhB"}