@geekity/cms 0.11.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 +177 -6
- 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 +68 -22
- 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 +7 -2
- package/themes/default/README.md +320 -139
- package/themes/default/layouts/404.njk +2 -0
- package/themes/default/layouts/500.njk +2 -0
- package/themes/default/layouts/503.njk +2 -0
- package/themes/default/layouts/author.njk +3 -1
- package/themes/default/layouts/base.njk +96 -17
- package/themes/default/layouts/category.njk +4 -2
- package/themes/default/layouts/front-page.njk +7 -2
- package/themes/default/layouts/home.njk +10 -0
- package/themes/default/layouts/page.njk +7 -4
- package/themes/default/layouts/post.njk +35 -25
- package/themes/default/layouts/search.njk +5 -9
- package/themes/default/layouts/tag.njk +7 -1
- 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/comment-form.njk +8 -0
- package/themes/default/partials/contact-form.njk +3 -0
- package/themes/default/partials/conversation.njk +14 -4
- package/themes/default/partials/jsonld.njk +19 -4
- package/themes/default/partials/kicker.njk +36 -0
- package/themes/default/partials/lang.njk +7 -0
- package/themes/default/partials/post-list.njk +43 -47
- package/themes/default/partials/reply-context.njk +14 -5
- package/themes/default/src/style.css +1109 -0
- package/themes/default/static/style.css +2124 -1160
- package/themes/default/theme.json +3 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Hono, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { GeekityEnv } from '../env.ts';
|
|
3
|
+
/** Where the authorization server metadata document is published. */
|
|
4
|
+
export declare const INDIEAUTH_METADATA_PATH = "/_geekity/indieauth/metadata";
|
|
5
|
+
/** RFC 8414's well-known location for the same document. */
|
|
6
|
+
export declare const OAUTH_METADATA_WELL_KNOWN_PATH = "/.well-known/oauth-authorization-server";
|
|
7
|
+
/** Where a client sends the person to approve a sign-in (TASK-158). */
|
|
8
|
+
export declare const AUTHORIZATION_PATH = "/_geekity/indieauth/auth";
|
|
9
|
+
/** Where a client redeems a code for an access token (TASK-160). */
|
|
10
|
+
export declare const TOKEN_PATH = "/_geekity/indieauth/token";
|
|
11
|
+
/** Where a resource server asks what a token is good for (TASK-161). */
|
|
12
|
+
export declare const INTROSPECTION_PATH = "/_geekity/indieauth/introspect";
|
|
13
|
+
/** Where a client hands a token back to end it (TASK-161). */
|
|
14
|
+
export declare const REVOCATION_PATH = "/_geekity/indieauth/revoke";
|
|
15
|
+
/** Where a client asks for the profile a token was approved to see (TASK-161). */
|
|
16
|
+
export declare const USERINFO_PATH = "/_geekity/indieauth/userinfo";
|
|
17
|
+
/**
|
|
18
|
+
* RFC 9728's well-known location for the protected resource metadata, which
|
|
19
|
+
* an MCP client reads to learn where to sign in (TASK-161).
|
|
20
|
+
*/
|
|
21
|
+
export declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
22
|
+
/**
|
|
23
|
+
* The scopes a client may ask for: the profile ones (TASK-158) and the
|
|
24
|
+
* Micropub ones a token is issued for (TASK-160).
|
|
25
|
+
*/
|
|
26
|
+
export declare const SCOPES: readonly ['profile', 'email', 'create', 'update', 'delete', 'media'];
|
|
27
|
+
/** RFC 8414 authorization server metadata, as the IndieAuth spec profiles it. */
|
|
28
|
+
export interface AuthorizationServerMetadata {
|
|
29
|
+
issuer: string;
|
|
30
|
+
authorization_endpoint: string;
|
|
31
|
+
token_endpoint: string;
|
|
32
|
+
introspection_endpoint: string;
|
|
33
|
+
revocation_endpoint: string;
|
|
34
|
+
userinfo_endpoint: string;
|
|
35
|
+
response_types_supported: readonly string[];
|
|
36
|
+
grant_types_supported: readonly string[];
|
|
37
|
+
code_challenge_methods_supported: readonly string[];
|
|
38
|
+
scopes_supported: readonly string[];
|
|
39
|
+
authorization_response_iss_parameter_supported: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* That a client may use the URL of a JSON client metadata document as its
|
|
42
|
+
* client_id, which the MCP authorization spec asks for (TASK-158).
|
|
43
|
+
*/
|
|
44
|
+
client_id_metadata_document_supported: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The metadata for a site at `baseUrl`.
|
|
48
|
+
*
|
|
49
|
+
* The issuer is the base URL with no trailing slash: RFC 8414 builds the
|
|
50
|
+
* well-known URL by putting `/.well-known/oauth-authorization-server` between
|
|
51
|
+
* the issuer's host and its path, so an issuer with no path is the one whose
|
|
52
|
+
* document lives at exactly that URL, and a client that checks the two agree
|
|
53
|
+
* finds they do.
|
|
54
|
+
*/
|
|
55
|
+
export declare function authorizationServerMetadata(baseUrl: string): AuthorizationServerMetadata;
|
|
56
|
+
/** RFC 9728 protected resource metadata. */
|
|
57
|
+
export interface ProtectedResourceMetadata {
|
|
58
|
+
resource: string;
|
|
59
|
+
authorization_servers: readonly string[];
|
|
60
|
+
scopes_supported: readonly string[];
|
|
61
|
+
bearer_methods_supported: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The protected resource metadata for a site at `baseUrl`: the site is the
|
|
65
|
+
* resource, and its own authorization server is the one to sign in at. A
|
|
66
|
+
* token may come in the header or, as Micropub allows, in a form body.
|
|
67
|
+
*/
|
|
68
|
+
export declare function protectedResourceMetadata(baseUrl: string): ProtectedResourceMetadata;
|
|
69
|
+
/** The site's base URL as every absolute URL it hands out is built from. */
|
|
70
|
+
export declare function siteBaseUrl(c: {
|
|
71
|
+
var: Pick<GeekityEnv['Variables'], 'config'>;
|
|
72
|
+
}): string;
|
|
73
|
+
/**
|
|
74
|
+
* Mount the authorization server metadata at its own path and at the
|
|
75
|
+
* well-known one, and the protected resource metadata at its well-known one.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mountIndieAuthDiscovery(app: Hono<GeekityEnv>): void;
|
|
78
|
+
/**
|
|
79
|
+
* Point every identity URL at the metadata, with a `Link` header and a
|
|
80
|
+
* `<link>` before `</head>` (TASK-157).
|
|
81
|
+
*
|
|
82
|
+
* Added to the response rather than left to the layouts so that every theme
|
|
83
|
+
* carries it, a custom one that replaces the packaged base layout included. A
|
|
84
|
+
* 404 for a username nobody has is left alone: it is nobody's identity.
|
|
85
|
+
*/
|
|
86
|
+
export declare const advertiseIndieAuthMetadata: MiddlewareHandler<GeekityEnv>;
|
|
87
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/indieauth/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAI5C,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,iCAAiC,CAAC;AAEtE,4DAA4D;AAC5D,eAAO,MAAM,8BAA8B,4CAA4C,CAAC;AAExF,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAE7D,oEAAoE;AACpE,eAAO,MAAM,UAAU,8BAA8B,CAAC;AAEtD,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,eAAe,+BAA+B,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,aAAa,iCAAiC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,gCAAgC,0CAA0C,CAAC;AAExF;;;GAGG;AACH,eAAO,MAAM,MAAM,YAAI,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAC;AAE3F,iFAAiF;AACjF,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,gCAAgC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpD,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8CAA8C,EAAE,OAAO,CAAC;IACxD;;;OAGG;IACH,qCAAqC,EAAE,OAAO,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,2BAA2B,CAexF;AAED,4CAA4C;AAC5C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,yBAAyB,CAOpF;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,CAAC,EAAE;IAAE,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAA;CAAE,GAAG,MAAM,CAGvF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAOnE;AAeD;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,iBAAiB,CAAC,UAAU,CAiBpE,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { effectiveBaseUrl, readSiteSettings } from '../admin/settings.js';
|
|
2
|
+
import { parseAuthorPath } from '../web/authors.js';
|
|
3
|
+
import { requestPath } from '../web/routes.js';
|
|
4
|
+
/** Where the authorization server metadata document is published. */
|
|
5
|
+
export const INDIEAUTH_METADATA_PATH = '/_geekity/indieauth/metadata';
|
|
6
|
+
/** RFC 8414's well-known location for the same document. */
|
|
7
|
+
export const OAUTH_METADATA_WELL_KNOWN_PATH = '/.well-known/oauth-authorization-server';
|
|
8
|
+
/** Where a client sends the person to approve a sign-in (TASK-158). */
|
|
9
|
+
export const AUTHORIZATION_PATH = '/_geekity/indieauth/auth';
|
|
10
|
+
/** Where a client redeems a code for an access token (TASK-160). */
|
|
11
|
+
export const TOKEN_PATH = '/_geekity/indieauth/token';
|
|
12
|
+
/** Where a resource server asks what a token is good for (TASK-161). */
|
|
13
|
+
export const INTROSPECTION_PATH = '/_geekity/indieauth/introspect';
|
|
14
|
+
/** Where a client hands a token back to end it (TASK-161). */
|
|
15
|
+
export const REVOCATION_PATH = '/_geekity/indieauth/revoke';
|
|
16
|
+
/** Where a client asks for the profile a token was approved to see (TASK-161). */
|
|
17
|
+
export const USERINFO_PATH = '/_geekity/indieauth/userinfo';
|
|
18
|
+
/**
|
|
19
|
+
* RFC 9728's well-known location for the protected resource metadata, which
|
|
20
|
+
* an MCP client reads to learn where to sign in (TASK-161).
|
|
21
|
+
*/
|
|
22
|
+
export const PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource';
|
|
23
|
+
/**
|
|
24
|
+
* The scopes a client may ask for: the profile ones (TASK-158) and the
|
|
25
|
+
* Micropub ones a token is issued for (TASK-160).
|
|
26
|
+
*/
|
|
27
|
+
export const SCOPES = ['profile', 'email', 'create', 'update', 'delete', 'media'];
|
|
28
|
+
/**
|
|
29
|
+
* The metadata for a site at `baseUrl`.
|
|
30
|
+
*
|
|
31
|
+
* The issuer is the base URL with no trailing slash: RFC 8414 builds the
|
|
32
|
+
* well-known URL by putting `/.well-known/oauth-authorization-server` between
|
|
33
|
+
* the issuer's host and its path, so an issuer with no path is the one whose
|
|
34
|
+
* document lives at exactly that URL, and a client that checks the two agree
|
|
35
|
+
* finds they do.
|
|
36
|
+
*/
|
|
37
|
+
export function authorizationServerMetadata(baseUrl) {
|
|
38
|
+
return {
|
|
39
|
+
issuer: baseUrl,
|
|
40
|
+
authorization_endpoint: `${baseUrl}${AUTHORIZATION_PATH}`,
|
|
41
|
+
token_endpoint: `${baseUrl}${TOKEN_PATH}`,
|
|
42
|
+
introspection_endpoint: `${baseUrl}${INTROSPECTION_PATH}`,
|
|
43
|
+
revocation_endpoint: `${baseUrl}${REVOCATION_PATH}`,
|
|
44
|
+
userinfo_endpoint: `${baseUrl}${USERINFO_PATH}`,
|
|
45
|
+
response_types_supported: ['code'],
|
|
46
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
47
|
+
code_challenge_methods_supported: ['S256'],
|
|
48
|
+
scopes_supported: SCOPES,
|
|
49
|
+
authorization_response_iss_parameter_supported: true,
|
|
50
|
+
client_id_metadata_document_supported: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The protected resource metadata for a site at `baseUrl`: the site is the
|
|
55
|
+
* resource, and its own authorization server is the one to sign in at. A
|
|
56
|
+
* token may come in the header or, as Micropub allows, in a form body.
|
|
57
|
+
*/
|
|
58
|
+
export function protectedResourceMetadata(baseUrl) {
|
|
59
|
+
return {
|
|
60
|
+
resource: baseUrl,
|
|
61
|
+
authorization_servers: [authorizationServerMetadata(baseUrl).issuer],
|
|
62
|
+
scopes_supported: SCOPES,
|
|
63
|
+
bearer_methods_supported: ['header', 'body'],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The site's base URL as every absolute URL it hands out is built from. */
|
|
67
|
+
export function siteBaseUrl(c) {
|
|
68
|
+
const { config } = c.var;
|
|
69
|
+
return effectiveBaseUrl(config, readSiteSettings(config.contentDir));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mount the authorization server metadata at its own path and at the
|
|
73
|
+
* well-known one, and the protected resource metadata at its well-known one.
|
|
74
|
+
*/
|
|
75
|
+
export function mountIndieAuthDiscovery(app) {
|
|
76
|
+
for (const pathname of [INDIEAUTH_METADATA_PATH, OAUTH_METADATA_WELL_KNOWN_PATH]) {
|
|
77
|
+
app.get(pathname, (c) => c.json(authorizationServerMetadata(siteBaseUrl(c))));
|
|
78
|
+
}
|
|
79
|
+
app.get(PROTECTED_RESOURCE_METADATA_PATH, (c) => c.json(protectedResourceMetadata(siteBaseUrl(c))));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a path is an identity URL a person may type into a sign-in form:
|
|
83
|
+
* the root, and every author archive's first page (decision-23).
|
|
84
|
+
*
|
|
85
|
+
* The root is advertised on a multi-author site too, where it names nobody: a
|
|
86
|
+
* person who types it still reaches this server, signs in as whoever they log
|
|
87
|
+
* in as, and the client is handed that user's author URL, which shares the
|
|
88
|
+
* host and advertises the same server.
|
|
89
|
+
*/
|
|
90
|
+
function isIdentityPath(pathname) {
|
|
91
|
+
return pathname === '/' || parseAuthorPath(pathname)?.pageNumber === 0;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Point every identity URL at the metadata, with a `Link` header and a
|
|
95
|
+
* `<link>` before `</head>` (TASK-157).
|
|
96
|
+
*
|
|
97
|
+
* Added to the response rather than left to the layouts so that every theme
|
|
98
|
+
* carries it, a custom one that replaces the packaged base layout included. A
|
|
99
|
+
* 404 for a username nobody has is left alone: it is nobody's identity.
|
|
100
|
+
*/
|
|
101
|
+
export const advertiseIndieAuthMetadata = async (c, next) => {
|
|
102
|
+
await next();
|
|
103
|
+
if (!isIdentityPath(requestPath(c)))
|
|
104
|
+
return;
|
|
105
|
+
const { status } = c.res;
|
|
106
|
+
if (status !== 304 && (status < 200 || status >= 300))
|
|
107
|
+
return;
|
|
108
|
+
const href = `${siteBaseUrl(c)}${INDIEAUTH_METADATA_PATH}`;
|
|
109
|
+
c.res.headers.append('link', `<${href}>; rel="indieauth-metadata"`);
|
|
110
|
+
if (status === 304 || !(c.res.headers.get('content-type') ?? '').startsWith('text/html'))
|
|
111
|
+
return;
|
|
112
|
+
const headers = new Headers(c.res.headers);
|
|
113
|
+
headers.delete('content-length');
|
|
114
|
+
const body = withHeadLink(await c.res.text(), `<link rel="indieauth-metadata" href="${href}">`);
|
|
115
|
+
// Cleared first, because assigning over a response copies its headers onto
|
|
116
|
+
// the new one.
|
|
117
|
+
c.res = undefined;
|
|
118
|
+
c.res = new Response(body, { status, headers });
|
|
119
|
+
};
|
|
120
|
+
/** `html` with `link` just before its `</head>`, or unchanged when it has none. */
|
|
121
|
+
function withHeadLink(html, link) {
|
|
122
|
+
const head = /<\/head\s*>/i.exec(html);
|
|
123
|
+
if (head === null)
|
|
124
|
+
return html;
|
|
125
|
+
return `${html.slice(0, head.index)}${link}${html.slice(head.index)}`;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/indieauth/discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Clock } from '../content/store.ts';
|
|
2
|
+
import type { AuthorizationRequest, Scope } from './request.ts';
|
|
3
|
+
/**
|
|
4
|
+
* What an authorization code stands for: who approved what, for which client,
|
|
5
|
+
* and the PKCE challenge the client must answer to redeem it (TASK-159,
|
|
6
|
+
* TASK-160).
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthorizationCode {
|
|
9
|
+
readonly clientId: string;
|
|
10
|
+
/** What the client called itself, for the connected apps screen. */
|
|
11
|
+
readonly clientName?: string;
|
|
12
|
+
readonly redirectUri: string;
|
|
13
|
+
readonly codeChallenge: string;
|
|
14
|
+
readonly userId: number;
|
|
15
|
+
/** The canonical me URL the client is handed back. */
|
|
16
|
+
readonly me: string;
|
|
17
|
+
/** The scopes the person left ticked. */
|
|
18
|
+
readonly scopes: readonly Scope[];
|
|
19
|
+
/** The RFC 8707 resource the token is to be issued for. */
|
|
20
|
+
readonly resource?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A request on the consent screen, waiting for the person to decide. */
|
|
23
|
+
export interface PendingConsent {
|
|
24
|
+
readonly request: AuthorizationRequest;
|
|
25
|
+
/** Who was shown the screen. Nobody else may answer it. */
|
|
26
|
+
readonly userId: number;
|
|
27
|
+
/** The me URL the screen named. */
|
|
28
|
+
readonly me: string;
|
|
29
|
+
/** What the client called itself on the screen, when it said. */
|
|
30
|
+
readonly clientName?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Values kept for a while under an unguessable key, each taken at most once. */
|
|
33
|
+
export interface ExpiringStore<T> {
|
|
34
|
+
/** Keep `value` and answer the key it is kept under. */
|
|
35
|
+
put(value: T): string;
|
|
36
|
+
/** The value under `key`, removed as it is read, or `undefined` once it has expired. */
|
|
37
|
+
take(key: string): T | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** How long a code may wait to be redeemed. The IndieAuth spec says at most ten minutes. */
|
|
40
|
+
export declare const CODE_LIFETIME_MS: number;
|
|
41
|
+
/** How long a consent screen may sit before its answer is refused. */
|
|
42
|
+
export declare const CONSENT_LIFETIME_MS: number;
|
|
43
|
+
/**
|
|
44
|
+
* The authorization server's short-lived state: codes and the consent screens
|
|
45
|
+
* they come from.
|
|
46
|
+
*
|
|
47
|
+
* Held in memory rather than in `data/`. Decision-9 asks files for state a
|
|
48
|
+
* restart must not lose, and these live for minutes: a restart costs whoever
|
|
49
|
+
* was mid-sign-in one more try, which is less than writing every sign-in to
|
|
50
|
+
* disk and sweeping it after.
|
|
51
|
+
*/
|
|
52
|
+
export interface IndieAuthState {
|
|
53
|
+
readonly codes: ExpiringStore<AuthorizationCode>;
|
|
54
|
+
readonly consents: ExpiringStore<PendingConsent>;
|
|
55
|
+
}
|
|
56
|
+
export declare function createIndieAuthState(now: Clock): IndieAuthState;
|
|
57
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../../src/indieauth/grants.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,wDAAwD;IACxD,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;IACtB,wFAAwF;IACxF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAClC;AAED,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAE9C,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CAClD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,KAAK,GAAG,cAAc,CAK/D"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
/** How long a code may wait to be redeemed. The IndieAuth spec says at most ten minutes. */
|
|
3
|
+
export const CODE_LIFETIME_MS = 5 * 60 * 1000;
|
|
4
|
+
/** How long a consent screen may sit before its answer is refused. */
|
|
5
|
+
export const CONSENT_LIFETIME_MS = 30 * 60 * 1000;
|
|
6
|
+
export function createIndieAuthState(now) {
|
|
7
|
+
return {
|
|
8
|
+
codes: expiringStore(CODE_LIFETIME_MS, now),
|
|
9
|
+
consents: expiringStore(CONSENT_LIFETIME_MS, now),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function expiringStore(lifetimeMs, now) {
|
|
13
|
+
const entries = new Map();
|
|
14
|
+
return {
|
|
15
|
+
put(value) {
|
|
16
|
+
const at = now().getTime();
|
|
17
|
+
// Swept on every put, so nothing outlives its lifetime by more than the
|
|
18
|
+
// gap to the next sign-in.
|
|
19
|
+
for (const [key, entry] of entries)
|
|
20
|
+
if (entry.expiresAt <= at)
|
|
21
|
+
entries.delete(key);
|
|
22
|
+
const key = randomBytes(32).toString('base64url');
|
|
23
|
+
entries.set(key, { value, expiresAt: at + lifetimeMs });
|
|
24
|
+
return key;
|
|
25
|
+
},
|
|
26
|
+
take(key) {
|
|
27
|
+
const entry = entries.get(key);
|
|
28
|
+
entries.delete(key);
|
|
29
|
+
if (entry === undefined || entry.expiresAt <= now().getTime())
|
|
30
|
+
return undefined;
|
|
31
|
+
return entry.value;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=grants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../../src/indieauth/grants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA4C1C,4FAA4F;AAC5F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAgBlD,MAAM,UAAU,oBAAoB,CAAC,GAAU;IAC7C,OAAO;QACL,KAAK,EAAE,aAAa,CAAC,gBAAgB,EAAE,GAAG,CAAC;QAC3C,QAAQ,EAAE,aAAa,CAAC,mBAAmB,EAAE,GAAG,CAAC;KAClD,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAI,UAAkB,EAAE,GAAU;IACtD,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2C,CAAC;IAEnE,OAAO;QACL,GAAG,CAAC,KAAK;YACP,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;YAC3B,wEAAwE;YACxE,2BAA2B;YAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO;gBAAE,IAAI,KAAK,CAAC,SAAS,IAAI,EAAE;oBAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnF,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;YACxD,OAAO,GAAG,CAAC;QACb,CAAC;QACD,IAAI,CAAC,GAAG;YACN,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC,OAAO,EAAE;gBAAE,OAAO,SAAS,CAAC;YAChF,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { User } from '../admin/accounts.ts';
|
|
2
|
+
import type { SiteSettings } from '../admin/settings.ts';
|
|
3
|
+
/** What resolving a me URL needs to know about the site. */
|
|
4
|
+
export interface IdentitySite {
|
|
5
|
+
/** The site's base URL, which every identity URL is under. */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
users: readonly User[];
|
|
8
|
+
settings: Pick<SiteSettings, 'author' | 'soloAuthor'>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The one user a me URL names, or `undefined` when it names nobody
|
|
12
|
+
* (decision-23).
|
|
13
|
+
*
|
|
14
|
+
* `/author/{username}/` names that user. The root names the user the author
|
|
15
|
+
* setting names, and only on a solo author site (TASK-180); on a multi-author
|
|
16
|
+
* site it is nobody's. Anything else names nobody, a query or a fragment
|
|
17
|
+
* included.
|
|
18
|
+
*
|
|
19
|
+
* A URL that differs from the site's only in http or https, a `www.` prefix
|
|
20
|
+
* on either side, the case of its scheme and host, or a missing trailing slash
|
|
21
|
+
* is the same URL: those are the spellings a person types and a client's
|
|
22
|
+
* redirects pass through, and the me handed back is always the canonical one.
|
|
23
|
+
*/
|
|
24
|
+
export declare function userForMe(me: string, site: IdentitySite): User | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The me a sign-in hands back for `user`, who has just approved it
|
|
27
|
+
* (decision-23).
|
|
28
|
+
*
|
|
29
|
+
* The URL the person typed when it names them, spelled canonically: the root
|
|
30
|
+
* for the solo author who typed it, their author URL otherwise. A typed URL
|
|
31
|
+
* that names somebody else, or nobody, is ignored, and the answer is the
|
|
32
|
+
* signed-in user's own author URL: a person can only ever sign in as
|
|
33
|
+
* themselves.
|
|
34
|
+
*/
|
|
35
|
+
export declare function meForSignIn(typed: string | undefined, user: User, site: IdentitySite): string;
|
|
36
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/indieauth/identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,CAE1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAK7F"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { authorHref, parseAuthorPath, userForAuthor } from '../web/authors.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one user a me URL names, or `undefined` when it names nobody
|
|
4
|
+
* (decision-23).
|
|
5
|
+
*
|
|
6
|
+
* `/author/{username}/` names that user. The root names the user the author
|
|
7
|
+
* setting names, and only on a solo author site (TASK-180); on a multi-author
|
|
8
|
+
* site it is nobody's. Anything else names nobody, a query or a fragment
|
|
9
|
+
* included.
|
|
10
|
+
*
|
|
11
|
+
* A URL that differs from the site's only in http or https, a `www.` prefix
|
|
12
|
+
* on either side, the case of its scheme and host, or a missing trailing slash
|
|
13
|
+
* is the same URL: those are the spellings a person types and a client's
|
|
14
|
+
* redirects pass through, and the me handed back is always the canonical one.
|
|
15
|
+
*/
|
|
16
|
+
export function userForMe(me, site) {
|
|
17
|
+
return identityNamed(me, site)?.user;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The me a sign-in hands back for `user`, who has just approved it
|
|
21
|
+
* (decision-23).
|
|
22
|
+
*
|
|
23
|
+
* The URL the person typed when it names them, spelled canonically: the root
|
|
24
|
+
* for the solo author who typed it, their author URL otherwise. A typed URL
|
|
25
|
+
* that names somebody else, or nobody, is ignored, and the answer is the
|
|
26
|
+
* signed-in user's own author URL: a person can only ever sign in as
|
|
27
|
+
* themselves.
|
|
28
|
+
*/
|
|
29
|
+
export function meForSignIn(typed, user, site) {
|
|
30
|
+
const named = typed === undefined ? undefined : identityNamed(typed, site);
|
|
31
|
+
const base = site.baseUrl.replace(/\/$/, '');
|
|
32
|
+
if (named?.user.id === user.id && named.root)
|
|
33
|
+
return `${base}/`;
|
|
34
|
+
return `${base}${authorHref(user.username)}`;
|
|
35
|
+
}
|
|
36
|
+
/** The user a me URL names, and whether it named them by the site root. */
|
|
37
|
+
function identityNamed(me, site) {
|
|
38
|
+
const typed = URL.parse(me);
|
|
39
|
+
const base = URL.parse(site.baseUrl);
|
|
40
|
+
if (typed === null || base === null)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typed.protocol !== 'https:' && typed.protocol !== 'http:')
|
|
43
|
+
return undefined;
|
|
44
|
+
if (typed.username !== '' || typed.password !== '')
|
|
45
|
+
return undefined;
|
|
46
|
+
if (typed.search !== '' || typed.hash !== '' || me.includes('#') || me.includes('?')) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (withoutWww(typed.hostname) !== withoutWww(base.hostname))
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typed.port !== base.port)
|
|
52
|
+
return undefined;
|
|
53
|
+
const root = withSlash(base.pathname);
|
|
54
|
+
const pathname = withSlash(typed.pathname);
|
|
55
|
+
if (!pathname.startsWith(root))
|
|
56
|
+
return undefined;
|
|
57
|
+
const local = `/${pathname.slice(root.length)}`;
|
|
58
|
+
// The author setting may hold a display name, which the homepage's bio reads
|
|
59
|
+
// the same way; an author path holds a username, matched exactly.
|
|
60
|
+
if (local === '/') {
|
|
61
|
+
const author = site.settings.soloAuthor
|
|
62
|
+
? userForAuthor(site.users, site.settings.author)
|
|
63
|
+
: undefined;
|
|
64
|
+
return author === undefined ? undefined : { user: author, root: true };
|
|
65
|
+
}
|
|
66
|
+
const author = parseAuthorPath(local);
|
|
67
|
+
if (author?.pageNumber !== 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
const user = site.users.find((candidate) => candidate.username === author.username);
|
|
70
|
+
return user === undefined ? undefined : { user, root: false };
|
|
71
|
+
}
|
|
72
|
+
function withoutWww(hostname) {
|
|
73
|
+
return hostname.startsWith('www.') ? hostname.slice(4) : hostname;
|
|
74
|
+
}
|
|
75
|
+
function withSlash(pathname) {
|
|
76
|
+
return pathname.endsWith('/') ? pathname : `${pathname}/`;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -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"}
|