imapkit 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +37 -21
  2. package/bin/help.txt +25 -1
  3. package/bin/imapkit.js +35 -0
  4. package/dist/browser/imapkit.js +99 -35
  5. package/dist/cjs/control.d.ts +16 -5
  6. package/dist/cjs/control.js +35 -4
  7. package/dist/cjs/oauth/http.d.ts +28 -0
  8. package/dist/cjs/oauth/http.js +216 -0
  9. package/dist/cjs/oauth/jwt.d.ts +33 -0
  10. package/dist/cjs/oauth/jwt.js +58 -0
  11. package/dist/cjs/oauth/pages.d.ts +47 -0
  12. package/dist/cjs/oauth/pages.js +310 -0
  13. package/dist/cjs/oauth/policy.d.ts +48 -0
  14. package/dist/cjs/oauth/policy.js +66 -0
  15. package/dist/cjs/oauth/provider.d.ts +455 -0
  16. package/dist/cjs/oauth/provider.js +1422 -0
  17. package/dist/cjs/plugins/oauthbearer.js +10 -15
  18. package/dist/cjs/plugins/xoauth2.js +4 -3
  19. package/dist/cjs/rest.d.ts +18 -1
  20. package/dist/cjs/rest.js +49 -31
  21. package/dist/cjs/server.d.ts +25 -1
  22. package/dist/cjs/server.js +73 -11
  23. package/dist/cjs/smtp-listener.d.ts +3 -2
  24. package/dist/cjs/smtp-listener.js +15 -3
  25. package/dist/cjs/types.d.ts +10 -0
  26. package/dist/esm/control.d.ts +16 -5
  27. package/dist/esm/control.js +35 -5
  28. package/dist/esm/oauth/http.d.ts +28 -0
  29. package/dist/esm/oauth/http.js +211 -0
  30. package/dist/esm/oauth/jwt.d.ts +33 -0
  31. package/dist/esm/oauth/jwt.js +50 -0
  32. package/dist/esm/oauth/pages.d.ts +47 -0
  33. package/dist/esm/oauth/pages.js +305 -0
  34. package/dist/esm/oauth/policy.d.ts +48 -0
  35. package/dist/esm/oauth/policy.js +59 -0
  36. package/dist/esm/oauth/provider.d.ts +455 -0
  37. package/dist/esm/oauth/provider.js +1415 -0
  38. package/dist/esm/plugins/oauthbearer.js +10 -15
  39. package/dist/esm/plugins/xoauth2.js +4 -3
  40. package/dist/esm/rest.d.ts +18 -1
  41. package/dist/esm/rest.js +48 -32
  42. package/dist/esm/server.d.ts +25 -1
  43. package/dist/esm/server.js +74 -12
  44. package/dist/esm/smtp-listener.d.ts +3 -2
  45. package/dist/esm/smtp-listener.js +15 -3
  46. package/dist/esm/types.d.ts +10 -0
  47. package/dist/plugin-help.json +2 -2
  48. package/package.json +6 -6
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { ImapKitError } from './store-operations.js';
10
10
  import type { FlagMode, UidValidityOptions } from './store-operations.js';
11
+ import type { ConsentPolicy } from './oauth/policy.js';
11
12
  import type { IMAPConnection, IMAPServer, Mailbox, Message, StorageMailbox, StorageNamespace, UserData } from './types.js';
12
13
  /** A mailbox as `getMailbox()` and `listMailboxes()` describe it */
13
14
  interface MailboxInfo {
@@ -68,7 +69,7 @@ interface NewMessage {
68
69
  internaldate?: Date | string | undefined;
69
70
  }
70
71
  /** The credentials of a user, see UserData */
71
- type UserOptions = Pick<UserData, 'password' | 'xoauth2'>;
72
+ type UserOptions = Pick<UserData, 'password' | 'xoauth2' | 'displayName' | 'oauth'>;
72
73
  /** What a REST route handler gets, see src/rest.ts */
73
74
  interface RouteRequest {
74
75
  params: Record<string, string>;
@@ -89,6 +90,16 @@ type SessionFilter = number | {
89
90
  session?: number | undefined;
90
91
  user?: string | undefined;
91
92
  };
93
+ /**
94
+ * Checks the `oauth` settings of a user
95
+ *
96
+ * @param {*} value `{ consent }`
97
+ * @param {String} what Setting name for the error message
98
+ * @return {Object} a copy
99
+ */
100
+ declare function checkUserOAuth(value: unknown, what: string): {
101
+ consent?: ConsentPolicy;
102
+ };
92
103
  declare class Control {
93
104
  [key: string]: any;
94
105
  server: IMAPServer;
@@ -364,14 +375,14 @@ declare class Control {
364
375
  * Adds a user
365
376
  *
366
377
  * @param {String} name User name
367
- * @param {Object} options `{ password, xoauth2: { accessToken, sessionTimeout } }`
378
+ * @param {Object} options `{ password, xoauth2: { accessToken, sessionTimeout }, displayName, oauth: { consent } }`
368
379
  */
369
380
  addUser(name: string, options: UserOptions): void;
370
381
  /**
371
- * Changes the password or the XOAUTH2 token of a user
382
+ * Changes the password, the XOAUTH2 token, the display name or the OAuth consent policy of a user
372
383
  *
373
384
  * @param {String} name User name
374
- * @param {Object} options `{ password, xoauth2 }`, null xoauth2 removes the token
385
+ * @param {Object} options `{ password, xoauth2, displayName, oauth }`, null xoauth2, displayName or oauth removes it
375
386
  */
376
387
  updateUser(name: string, options: UserOptions): void;
377
388
  /**
@@ -421,5 +432,5 @@ declare class Control {
421
432
  graceful?: boolean | undefined;
422
433
  }): Promise<void>;
423
434
  }
424
- export { Control, ImapKitError };
435
+ export { Control, ImapKitError, checkUserOAuth };
425
436
  export type { MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter, ControlRoute, RouteRequest };
@@ -12,9 +12,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.ImapKitError = exports.Control = void 0;
15
+ exports.checkUserOAuth = checkUserOAuth;
15
16
  const mailbox_name_js_1 = __importDefault(require("./mailbox-name.js"));
16
17
  const store_operations_js_1 = require("./store-operations.js");
17
18
  Object.defineProperty(exports, "ImapKitError", { enumerable: true, get: function () { return store_operations_js_1.ImapKitError; } });
19
+ const policy_js_1 = require("./oauth/policy.js");
18
20
  /**
19
21
  * Copies a value of a plugin into the snapshot as JSON data, values that JSON can not hold are left out
20
22
  *
@@ -50,6 +52,20 @@ const NAMESPACE_SKIP = new Set(['folders']);
50
52
  // properties the server derives when it loads a mailbox, a snapshot leaves them out
51
53
  const DERIVED_MAILBOX_KEYS = new Set(['path', 'namespace', 'messages', 'folders', 'subscribed', 'flags']);
52
54
  const DERIVED_FLAGS = new Set(['\\HasChildren', '\\HasNoChildren']);
55
+ /**
56
+ * Checks the `oauth` settings of a user
57
+ *
58
+ * @param {*} value `{ consent }`
59
+ * @param {String} what Setting name for the error message
60
+ * @return {Object} a copy
61
+ */
62
+ function checkUserOAuth(value, what) {
63
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
64
+ throw new store_operations_js_1.ImapKitError(what + ' must be { consent }', 'INVALID');
65
+ }
66
+ const { consent } = value;
67
+ return consent === undefined ? {} : { consent: (0, policy_js_1.checkOutcome)(consent, what + '.consent') };
68
+ }
53
69
  class Control {
54
70
  constructor(server) {
55
71
  this.server = server;
@@ -501,7 +517,7 @@ class Control {
501
517
  * Adds a user
502
518
  *
503
519
  * @param {String} name User name
504
- * @param {Object} options `{ password, xoauth2: { accessToken, sessionTimeout } }`
520
+ * @param {Object} options `{ password, xoauth2: { accessToken, sessionTimeout }, displayName, oauth: { consent } }`
505
521
  */
506
522
  addUser(name, options) {
507
523
  if (typeof name !== 'string' || !name) {
@@ -513,10 +529,10 @@ class Control {
513
529
  this.server.users[name] = this.userData({}, options);
514
530
  }
515
531
  /**
516
- * Changes the password or the XOAUTH2 token of a user
532
+ * Changes the password, the XOAUTH2 token, the display name or the OAuth consent policy of a user
517
533
  *
518
534
  * @param {String} name User name
519
- * @param {Object} options `{ password, xoauth2 }`, null xoauth2 removes the token
535
+ * @param {Object} options `{ password, xoauth2, displayName, oauth }`, null xoauth2, displayName or oauth removes it
520
536
  */
521
537
  updateUser(name, options) {
522
538
  this.server.users[name] = this.userData(this.requireUser(name), options);
@@ -541,8 +557,23 @@ class Control {
541
557
  return this.server.users[name];
542
558
  }
543
559
  userData(user, options) {
544
- const { password, xoauth2 } = options || {};
560
+ const { password, xoauth2, displayName, oauth } = options || {};
545
561
  const result = Object.assign({}, user);
562
+ if (displayName === null) {
563
+ delete result.displayName;
564
+ }
565
+ else if (displayName !== undefined) {
566
+ if (typeof displayName !== 'string') {
567
+ throw new store_operations_js_1.ImapKitError('displayName must be a string', 'INVALID');
568
+ }
569
+ result.displayName = displayName;
570
+ }
571
+ if (oauth === null) {
572
+ delete result.oauth;
573
+ }
574
+ else if (oauth !== undefined) {
575
+ result.oauth = checkUserOAuth(oauth, 'oauth');
576
+ }
546
577
  if (password !== undefined) {
547
578
  if (typeof password !== 'string') {
548
579
  throw new store_operations_js_1.ImapKitError('Password must be a string', 'INVALID');
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The HTTP server of the OAuth provider (`oauth` option, `--oauth-port`), on node:http or node:https only.
3
+ * Routes: `/` (index), `/.well-known/openid-configuration` and `/.well-known/oauth-authorization-server`
4
+ * (discovery), `/authorize` (GET and POST, OpenID Connect Core 1.0 section 3.1.2.1), `/authorize/decision` (the
5
+ * consent page form), `/token`, `/revoke`, `/userinfo` and `/jwks`.
6
+ *
7
+ * Bound to a loopback address (the default), it only answers requests for a loopback host name or the host of the
8
+ * `issuer` option, so a web page can not rebind its DNS name to it. The JSON endpoints allow cross-origin requests,
9
+ * like real providers do for browser-based clients; the pages are never framed and run no script.
10
+ */
11
+ import http from 'node:http';
12
+ import type { OAuthProvider } from './provider.js';
13
+ /**
14
+ * Creates the HTTP server of the provider, it does not listen yet
15
+ *
16
+ * @param {Object} provider OAuth provider
17
+ * @param {Object} options `{ secure, host, credentials }`, credentials are the TLS key and certificate of `secure`
18
+ * @return {Object} HTTP or HTTPS server
19
+ */
20
+ declare function createOAuthServer(provider: OAuthProvider, options: {
21
+ secure?: boolean | undefined;
22
+ host: string;
23
+ credentials: {
24
+ key: string | Buffer;
25
+ cert: string | Buffer;
26
+ };
27
+ }): http.Server;
28
+ export { createOAuthServer };
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * The HTTP server of the OAuth provider (`oauth` option, `--oauth-port`), on node:http or node:https only.
4
+ * Routes: `/` (index), `/.well-known/openid-configuration` and `/.well-known/oauth-authorization-server`
5
+ * (discovery), `/authorize` (GET and POST, OpenID Connect Core 1.0 section 3.1.2.1), `/authorize/decision` (the
6
+ * consent page form), `/token`, `/revoke`, `/userinfo` and `/jwks`.
7
+ *
8
+ * Bound to a loopback address (the default), it only answers requests for a loopback host name or the host of the
9
+ * `issuer` option, so a web page can not rebind its DNS name to it. The JSON endpoints allow cross-origin requests,
10
+ * like real providers do for browser-based clients; the pages are never framed and run no script.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createOAuthServer = createOAuthServer;
17
+ const node_http_1 = __importDefault(require("node:http"));
18
+ const node_https_1 = __importDefault(require("node:https"));
19
+ const pages_js_1 = require("./pages.js");
20
+ const rest_js_1 = require("../rest.js");
21
+ // a form of a mail client sign-in is small, larger bodies are refused
22
+ const BODY_LIMIT = 64 * 1024;
23
+ class HttpError extends Error {
24
+ constructor(status, message) {
25
+ super(message);
26
+ this.status = status;
27
+ }
28
+ }
29
+ /**
30
+ * Reads an application/x-www-form-urlencoded body
31
+ *
32
+ * @param {Object} req HTTP request
33
+ * @return {Promise<URLSearchParams>} the form
34
+ */
35
+ async function readForm(req) {
36
+ // RFC 6749 sections 3.2 and 4.1.3: the parameters are form encoded
37
+ if (!/^application\/x-www-form-urlencoded\b/i.test(req.headers['content-type'] || '')) {
38
+ req.resume();
39
+ throw new HttpError(415, 'The body must be application/x-www-form-urlencoded (RFC 6749 section 4.1.3)');
40
+ }
41
+ const body = await (0, rest_js_1.readRaw)(req, BODY_LIMIT, () => new HttpError(413, 'The body is larger than 64 KiB'));
42
+ return new URLSearchParams(body.toString('utf-8'));
43
+ }
44
+ const SECURITY_HEADERS = {
45
+ 'Cache-Control': 'no-store',
46
+ Pragma: 'no-cache',
47
+ 'X-Content-Type-Options': 'nosniff',
48
+ 'Referrer-Policy': 'no-referrer'
49
+ };
50
+ const CORS_HEADERS = {
51
+ 'Access-Control-Allow-Origin': '*',
52
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type',
53
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
54
+ };
55
+ /**
56
+ * The form-action sources of the consent page. Browsers check form-action on the redirect after the form too,
57
+ * so the origin of the client's redirect URI must be allowed
58
+ *
59
+ * @param {Object} request Pending request
60
+ * @return {String} CSP source list
61
+ */
62
+ function formActionSources(request) {
63
+ try {
64
+ const url = new URL(request.redirectUri);
65
+ return "'self' " + (url.origin !== 'null' ? url.origin : url.protocol);
66
+ }
67
+ catch {
68
+ return "'self'";
69
+ }
70
+ }
71
+ function sendHtml(res, status, html, formAction = "'none'") {
72
+ res.writeHead(status, {
73
+ ...SECURITY_HEADERS,
74
+ 'Content-Type': 'text/html; charset=utf-8',
75
+ 'Content-Length': Buffer.byteLength(html),
76
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'; form-action " + formAction,
77
+ 'X-Frame-Options': 'DENY'
78
+ });
79
+ res.end(html);
80
+ }
81
+ function sendJson(res, result) {
82
+ if (result.htmlError) {
83
+ // like a provider behind a broken proxy
84
+ sendHtml(res, result.status, '<!doctype html><title>Error</title><h1>Something went wrong</h1>');
85
+ return;
86
+ }
87
+ const json = JSON.stringify(result.body);
88
+ res.writeHead(result.status, {
89
+ ...SECURITY_HEADERS,
90
+ ...CORS_HEADERS,
91
+ ...(result.headers || {}),
92
+ 'Content-Type': 'application/json; charset=utf-8',
93
+ 'Content-Length': Buffer.byteLength(json)
94
+ });
95
+ res.end(json);
96
+ }
97
+ /**
98
+ * Checks the Origin of the consent form: the issuer, or the loopback name the page was opened on. A browser that
99
+ * sends no Origin, or "null" (Referrer-Policy: no-referrer), is let through; the request ID protects the form then
100
+ *
101
+ * @param {String} [origin] Origin header
102
+ * @param {String} issuer Issuer URL
103
+ * @param {String} [host] Host header
104
+ * @return {Boolean} true if the form may come from there
105
+ */
106
+ function sameSite(origin, issuer, host) {
107
+ if (!origin || origin === 'null' || origin === new URL(issuer).origin) {
108
+ return true;
109
+ }
110
+ try {
111
+ const url = new URL(origin);
112
+ return (0, rest_js_1.isLoopback)(url.hostname) && url.host === host;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Creates the HTTP server of the provider, it does not listen yet
120
+ *
121
+ * @param {Object} provider OAuth provider
122
+ * @param {Object} options `{ secure, host, credentials }`, credentials are the TLS key and certificate of `secure`
123
+ * @return {Object} HTTP or HTTPS server
124
+ */
125
+ function createOAuthServer(provider, options) {
126
+ const answerAuthorize = (res, result, status) => {
127
+ if (result.type === 'redirect') {
128
+ res.writeHead(status, { ...SECURITY_HEADERS, Location: result.location, 'Content-Type': 'text/plain; charset=utf-8' });
129
+ res.end('Redirecting to ' + result.location + '\n');
130
+ }
131
+ else if (result.type === 'page') {
132
+ sendHtml(res, result.page.status, (0, pages_js_1.messagePage)(result.page, provider.issuer));
133
+ }
134
+ else {
135
+ const html = (0, pages_js_1.consentPage)({ request: result.request, users: provider.pageUsers(), scopes: provider.scopes, issuer: provider.issuer });
136
+ sendHtml(res, 200, html, formActionSources(result.request));
137
+ }
138
+ };
139
+ const checkHost = (req) => {
140
+ if (!(0, rest_js_1.isLoopback)(options.host)) {
141
+ return;
142
+ }
143
+ const host = (req.headers.host || '').replace(/:\d+$/, '');
144
+ let issuerHost = '';
145
+ try {
146
+ issuerHost = new URL(provider.issuer).hostname;
147
+ }
148
+ catch {
149
+ // no issuer yet
150
+ }
151
+ if (!(0, rest_js_1.isLoopback)(host) && host.replace(/^\[|\]$/g, '') !== issuerHost.replace(/^\[|\]$/g, '')) {
152
+ // a page that rebinds its DNS name to 127.0.0.1 sends its own host name
153
+ throw new HttpError(421, 'Requests must use a loopback host name or the host of the issuer');
154
+ }
155
+ };
156
+ const handle = async (req, res) => {
157
+ checkHost(req);
158
+ const url = new URL(req.url || '/', 'http://localhost');
159
+ const method = (req.method || 'GET').toUpperCase();
160
+ const route = method + ' ' + url.pathname;
161
+ if (method === 'OPTIONS') {
162
+ res.writeHead(204, { ...CORS_HEADERS, 'Access-Control-Max-Age': '600' });
163
+ return res.end();
164
+ }
165
+ switch (route) {
166
+ case 'GET /':
167
+ return sendHtml(res, 200, (0, pages_js_1.indexPage)({
168
+ issuer: provider.issuer,
169
+ consent: provider.consent,
170
+ clients: [...provider.clients.values()],
171
+ users: provider.pageUsers(),
172
+ mailScope: provider.mailScope
173
+ }));
174
+ case 'GET /.well-known/openid-configuration':
175
+ case 'GET /.well-known/oauth-authorization-server':
176
+ return sendJson(res, { status: 200, body: provider.metadata() });
177
+ case 'GET /jwks':
178
+ return sendJson(res, { status: 200, body: provider.jwks() });
179
+ case 'GET /authorize':
180
+ return answerAuthorize(res, provider.authorize(url.searchParams), 302);
181
+ case 'POST /authorize':
182
+ // RFC 9700 section 4.12: 303 turns the POST into a GET, never 307
183
+ return answerAuthorize(res, provider.authorize(await readForm(req)), 303);
184
+ case 'POST /authorize/decision': {
185
+ if (!sameSite(req.headers.origin, provider.issuer, req.headers.host)) {
186
+ throw new HttpError(403, 'The consent form must come from the consent page');
187
+ }
188
+ return answerAuthorize(res, provider.answerConsent(await readForm(req)), 303);
189
+ }
190
+ case 'POST /token':
191
+ return sendJson(res, await provider.token(await readForm(req), req.headers.authorization));
192
+ case 'POST /revoke':
193
+ return sendJson(res, provider.revoke(await readForm(req), req.headers.authorization));
194
+ case 'GET /userinfo':
195
+ return sendJson(res, provider.userinfo(req.headers.authorization));
196
+ case 'POST /userinfo':
197
+ req.resume();
198
+ return sendJson(res, provider.userinfo(req.headers.authorization));
199
+ default: {
200
+ const known = ['/', '/authorize', '/authorize/decision', '/token', '/revoke', '/userinfo', '/jwks'];
201
+ const wellKnown = url.pathname.startsWith('/.well-known/');
202
+ throw new HttpError(known.includes(url.pathname) || wellKnown ? 405 : 404, known.includes(url.pathname) ? 'Method not allowed' : 'Not found');
203
+ }
204
+ }
205
+ };
206
+ const listener = (req, res) => {
207
+ handle(req, res).catch((err) => {
208
+ const status = err instanceof HttpError ? err.status : 500;
209
+ if (!res.headersSent) {
210
+ // the token and revocation endpoints answer errors in JSON (RFC 6749 section 5.2)
211
+ sendJson(res, { status, body: { error: status === 500 ? 'server_error' : 'invalid_request', error_description: err.message } });
212
+ }
213
+ });
214
+ };
215
+ return options.secure ? node_https_1.default.createServer(options.credentials, listener) : node_http_1.default.createServer(listener);
216
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Signs the ID tokens of the OAuth provider: RS256 JSON Web Tokens (RFC 7519, RFC 7515) and the JWK Set of the
3
+ * public key (RFC 7517), with the RFC 7638 thumbprint as the key ID
4
+ */
5
+ import crypto from 'node:crypto';
6
+ /** An RSA signing key with its public JWK */
7
+ export interface SigningKey {
8
+ privateKey: crypto.KeyObject;
9
+ jwk: Record<string, string>;
10
+ kid: string;
11
+ }
12
+ /**
13
+ * Loads or makes the signing key
14
+ *
15
+ * @param {String|Buffer} [pem] PKCS#8 or PKCS#1 private key in PEM, a new 2048 bit key if not set (see defaultSigningKey())
16
+ * @return {Object} the key, its public JWK and key ID
17
+ * @throws {Error} for a key that is not RSA
18
+ */
19
+ export declare function loadSigningKey(pem?: string | Buffer): SigningKey;
20
+ /**
21
+ * The shared signing key of the providers without the signingKey option, made when the first one needs it
22
+ *
23
+ * @return {Object} signing key
24
+ */
25
+ export declare function defaultSigningKey(): SigningKey;
26
+ /**
27
+ * Signs a JWT with RS256
28
+ *
29
+ * @param {Object} key Signing key
30
+ * @param {Object} claims JWT claims
31
+ * @return {String} compact serialization
32
+ */
33
+ export declare function signJwt(key: SigningKey, claims: Record<string, unknown>): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * Signs the ID tokens of the OAuth provider: RS256 JSON Web Tokens (RFC 7519, RFC 7515) and the JWK Set of the
4
+ * public key (RFC 7517), with the RFC 7638 thumbprint as the key ID
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.loadSigningKey = loadSigningKey;
11
+ exports.defaultSigningKey = defaultSigningKey;
12
+ exports.signJwt = signJwt;
13
+ const node_crypto_1 = __importDefault(require("node:crypto"));
14
+ const base64url = (data) => Buffer.from(data).toString('base64url');
15
+ /**
16
+ * Loads or makes the signing key
17
+ *
18
+ * @param {String|Buffer} [pem] PKCS#8 or PKCS#1 private key in PEM, a new 2048 bit key if not set (see defaultSigningKey())
19
+ * @return {Object} the key, its public JWK and key ID
20
+ * @throws {Error} for a key that is not RSA
21
+ */
22
+ function loadSigningKey(pem) {
23
+ const privateKey = pem ? node_crypto_1.default.createPrivateKey(pem) : node_crypto_1.default.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey;
24
+ if (privateKey.asymmetricKeyType !== 'rsa') {
25
+ throw new Error('oauth.signingKey must be an RSA private key, ID tokens are signed with RS256');
26
+ }
27
+ const { n, e } = node_crypto_1.default.createPublicKey(privateKey).export({ format: 'jwk' });
28
+ // RFC 7638 section 3.2: the required members in lexicographic order, no white space
29
+ const kid = base64url(node_crypto_1.default
30
+ .createHash('sha256')
31
+ .update(JSON.stringify({ e, kty: 'RSA', n }))
32
+ .digest());
33
+ return { privateKey, kid, jwk: { kty: 'RSA', use: 'sig', alg: 'RS256', kid, n, e } };
34
+ }
35
+ // the key of every provider without a signingKey option, made once: a 2048 bit RSA key takes a moment
36
+ let defaultKey = null;
37
+ /**
38
+ * The shared signing key of the providers without the signingKey option, made when the first one needs it
39
+ *
40
+ * @return {Object} signing key
41
+ */
42
+ function defaultSigningKey() {
43
+ if (!defaultKey) {
44
+ defaultKey = loadSigningKey();
45
+ }
46
+ return defaultKey;
47
+ }
48
+ /**
49
+ * Signs a JWT with RS256
50
+ *
51
+ * @param {Object} key Signing key
52
+ * @param {Object} claims JWT claims
53
+ * @return {String} compact serialization
54
+ */
55
+ function signJwt(key, claims) {
56
+ const input = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT', kid: key.kid })) + '.' + base64url(JSON.stringify(claims));
57
+ return input + '.' + base64url(node_crypto_1.default.sign('sha256', Buffer.from(input), key.privateKey));
58
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The pages of the OAuth provider: the consent page, error and info pages, and the index page at `/`. They are a
3
+ * test tool's pages and say so (a striped "test server" ribbon, ImapKit's own mark, no password field), so that
4
+ * nobody takes them for a real provider's sign-in. They run no script and load nothing, src/oauth/http.ts sends
5
+ * them with a strict Content-Security-Policy.
6
+ */
7
+ import type { OAuthClient, PageData, PendingRequest } from './provider.js';
8
+ import type { ConsentPolicy } from './policy.js';
9
+ /** A user on the consent page */
10
+ export interface PageUser {
11
+ name: string;
12
+ displayName: string | undefined;
13
+ consent: ConsentPolicy | undefined;
14
+ }
15
+ /**
16
+ * The consent page: pick an account, Allow or Deny
17
+ *
18
+ * @param {Object} data `{ request, users, scopes, issuer }`
19
+ * @return {String} HTML
20
+ */
21
+ export declare function consentPage(data: {
22
+ request: PendingRequest;
23
+ users: PageUser[];
24
+ scopes: Record<string, string>;
25
+ issuer: string;
26
+ }): string;
27
+ /**
28
+ * An error or info page, instead of a redirect
29
+ *
30
+ * @param {Object} page Page data
31
+ * @param {String} issuer Issuer URL
32
+ * @return {String} HTML
33
+ */
34
+ export declare function messagePage(page: PageData, issuer: string): string;
35
+ /**
36
+ * The index page at `/`: issuer, clients and endpoints
37
+ *
38
+ * @param {Object} data `{ issuer, consent, clients, users, mailScope }`
39
+ * @return {String} HTML
40
+ */
41
+ export declare function indexPage(data: {
42
+ issuer: string;
43
+ consent: ConsentPolicy;
44
+ clients: OAuthClient[];
45
+ users: PageUser[];
46
+ mailScope: string;
47
+ }): string;