@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,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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"}
|
package/dist/web/archive.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|