@forestadmin/agent-bff 1.0.0 → 1.1.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/dist/cli-core.js CHANGED
@@ -5,14 +5,63 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = runCli;
7
7
  exports.reportFatalError = reportFatalError;
8
+ const bodyparser_1 = require("@koa/bodyparser");
8
9
  const console_logger_1 = __importDefault(require("./adapters/console-logger"));
9
10
  const env_config_1 = require("./config/env-config");
10
11
  const errors_1 = require("./errors");
11
12
  const bff_http_server_1 = __importDefault(require("./http/bff-http-server"));
13
+ const forest_server_client_1 = __importDefault(require("./oauth/forest-server-client"));
14
+ const oauth_routes_1 = __importDefault(require("./oauth/oauth-routes"));
15
+ const session_store_1 = __importDefault(require("./oauth/session-store"));
16
+ const token_cipher_1 = __importDefault(require("./oauth/token-cipher"));
12
17
  const version_1 = __importDefault(require("./version"));
18
+ const BODY_LIMIT = '16kb';
19
+ const SESSION_TTL_SECONDS = 24 * 60 * 60;
20
+ function resolveOAuthConfig(config) {
21
+ const { forestServerUrl, forestEnvSecret, forestAppUrl, forestAuthSecret, tokenEncryptionKey } = config;
22
+ if (forestServerUrl &&
23
+ forestEnvSecret &&
24
+ forestAppUrl &&
25
+ forestAuthSecret &&
26
+ tokenEncryptionKey) {
27
+ return { forestServerUrl, forestEnvSecret, forestAppUrl, forestAuthSecret, tokenEncryptionKey };
28
+ }
29
+ return undefined;
30
+ }
31
+ async function buildOAuthMiddlewares(config, logger) {
32
+ const oauthConfig = resolveOAuthConfig(config);
33
+ if (!oauthConfig) {
34
+ logger('Warn', 'OAuth routes disabled: required configuration is missing');
35
+ return [];
36
+ }
37
+ const { forestServerUrl, forestEnvSecret, forestAppUrl, forestAuthSecret, tokenEncryptionKey } = oauthConfig;
38
+ const serverClient = new forest_server_client_1.default({ forestServerUrl, envSecret: forestEnvSecret });
39
+ const environmentId = await serverClient.fetchEnvironmentId();
40
+ const sessionStore = (0, session_store_1.default)({
41
+ cipher: (0, token_cipher_1.default)(tokenEncryptionKey),
42
+ now: () => Date.now(),
43
+ sessionTtlSeconds: SESSION_TTL_SECONDS,
44
+ });
45
+ const oauthRoutes = (0, oauth_routes_1.default)({
46
+ serverClient,
47
+ sessionStore,
48
+ forestAppUrl,
49
+ authSecret: forestAuthSecret,
50
+ environmentId,
51
+ logger,
52
+ });
53
+ return [(0, bodyparser_1.bodyParser)({ jsonLimit: BODY_LIMIT }), oauthRoutes];
54
+ }
13
55
  async function runCli(env, logger = (0, console_logger_1.default)()) {
14
56
  const config = (0, env_config_1.parseConfig)(env);
15
- const server = new bff_http_server_1.default({ port: config.httpPort, version: version_1.default, config, logger });
57
+ const middlewares = await buildOAuthMiddlewares(config, logger);
58
+ const server = new bff_http_server_1.default({
59
+ port: config.httpPort,
60
+ version: version_1.default,
61
+ config,
62
+ logger,
63
+ middlewares,
64
+ });
16
65
  await server.start();
17
66
  return server;
18
67
  }
@@ -20,4 +69,4 @@ function reportFatalError(err) {
20
69
  process.stderr.write(`Error: ${(0, errors_1.extractErrorMessage)(err)}\n`);
21
70
  process.exitCode = 1;
22
71
  }
23
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLWNvcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY2xpLWNvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFRQSx5QkFVQztBQUVELDRDQUdDO0FBckJELCtFQUE0RDtBQUM1RCxvREFBa0Q7QUFDbEQscUNBQStDO0FBQy9DLDZFQUFtRDtBQUNuRCx3REFBZ0M7QUFFakIsS0FBSyxVQUFVLE1BQU0sQ0FDbEMsR0FBc0IsRUFDdEIsU0FBaUIsSUFBQSx3QkFBbUIsR0FBRTtJQUV0QyxNQUFNLE1BQU0sR0FBRyxJQUFBLHdCQUFXLEVBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEMsTUFBTSxNQUFNLEdBQUcsSUFBSSx5QkFBYSxDQUFDLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxRQUFRLEVBQUUsT0FBTyxFQUFQLGlCQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFFckYsTUFBTSxNQUFNLENBQUMsS0FBSyxFQUFFLENBQUM7SUFFckIsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQztBQUVELFNBQWdCLGdCQUFnQixDQUFDLEdBQVk7SUFDM0MsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsVUFBVSxJQUFBLDRCQUFtQixFQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUM3RCxPQUFPLENBQUMsUUFBUSxHQUFHLENBQUMsQ0FBQztBQUN2QixDQUFDIn0=
72
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLWNvcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY2xpLWNvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUE2RUEseUJBaUJDO0FBRUQsNENBR0M7QUEvRkQsZ0RBQTZDO0FBRTdDLCtFQUE0RDtBQUM1RCxvREFBa0Q7QUFDbEQscUNBQStDO0FBQy9DLDZFQUFtRDtBQUNuRCx3RkFBOEQ7QUFDOUQsd0VBQXFEO0FBQ3JELDBFQUErRDtBQUMvRCx3RUFBcUQ7QUFDckQsd0RBQWdDO0FBRWhDLE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQztBQUMxQixNQUFNLG1CQUFtQixHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxDQUFDO0FBVXpDLFNBQVMsa0JBQWtCLENBQUMsTUFBaUI7SUFDM0MsTUFBTSxFQUFFLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGtCQUFrQixFQUFFLEdBQzVGLE1BQU0sQ0FBQztJQUVULElBQ0UsZUFBZTtRQUNmLGVBQWU7UUFDZixZQUFZO1FBQ1osZ0JBQWdCO1FBQ2hCLGtCQUFrQixFQUNsQixDQUFDO1FBQ0QsT0FBTyxFQUFFLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGtCQUFrQixFQUFFLENBQUM7SUFDbEcsQ0FBQztJQUVELE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUM7QUFFRCxLQUFLLFVBQVUscUJBQXFCLENBQUMsTUFBaUIsRUFBRSxNQUFjO0lBQ3BFLE1BQU0sV0FBVyxHQUFHLGtCQUFrQixDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRS9DLElBQUksQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUNqQixNQUFNLENBQUMsTUFBTSxFQUFFLDBEQUEwRCxDQUFDLENBQUM7UUFFM0UsT0FBTyxFQUFFLENBQUM7SUFDWixDQUFDO0lBRUQsTUFBTSxFQUFFLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGtCQUFrQixFQUFFLEdBQzVGLFdBQVcsQ0FBQztJQUVkLE1BQU0sWUFBWSxHQUFHLElBQUksOEJBQWtCLENBQUMsRUFBRSxlQUFlLEVBQUUsU0FBUyxFQUFFLGVBQWUsRUFBRSxDQUFDLENBQUM7SUFDN0YsTUFBTSxhQUFhLEdBQUcsTUFBTSxZQUFZLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztJQUU5RCxNQUFNLFlBQVksR0FBRyxJQUFBLHVCQUEwQixFQUFDO1FBQzlDLE1BQU0sRUFBRSxJQUFBLHNCQUFpQixFQUFDLGtCQUFrQixDQUFDO1FBQzdDLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFO1FBQ3JCLGlCQUFpQixFQUFFLG1CQUFtQjtLQUN2QyxDQUFDLENBQUM7SUFFSCxNQUFNLFdBQVcsR0FBRyxJQUFBLHNCQUFpQixFQUFDO1FBQ3BDLFlBQVk7UUFDWixZQUFZO1FBQ1osWUFBWTtRQUNaLFVBQVUsRUFBRSxnQkFBZ0I7UUFDNUIsYUFBYTtRQUNiLE1BQU07S0FDUCxDQUFDLENBQUM7SUFFSCxPQUFPLENBQUMsSUFBQSx1QkFBVSxFQUFDLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsV0FBVyxDQUFDLENBQUM7QUFDOUQsQ0FBQztBQUVjLEtBQUssVUFBVSxNQUFNLENBQ2xDLEdBQXNCLEVBQ3RCLFNBQWlCLElBQUEsd0JBQW1CLEdBQUU7SUFFdEMsTUFBTSxNQUFNLEdBQUcsSUFBQSx3QkFBVyxFQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hDLE1BQU0sV0FBVyxHQUFHLE1BQU0scUJBQXFCLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ2hFLE1BQU0sTUFBTSxHQUFHLElBQUkseUJBQWEsQ0FBQztRQUMvQixJQUFJLEVBQUUsTUFBTSxDQUFDLFFBQVE7UUFDckIsT0FBTyxFQUFQLGlCQUFPO1FBQ1AsTUFBTTtRQUNOLE1BQU07UUFDTixXQUFXO0tBQ1osQ0FBQyxDQUFDO0lBRUgsTUFBTSxNQUFNLENBQUMsS0FBSyxFQUFFLENBQUM7SUFFckIsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQztBQUVELFNBQWdCLGdCQUFnQixDQUFDLEdBQVk7SUFDM0MsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsVUFBVSxJQUFBLDRCQUFtQixFQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUM3RCxPQUFPLENBQUMsUUFBUSxHQUFHLENBQUMsQ0FBQztBQUN2QixDQUFDIn0=
@@ -7,6 +7,7 @@ export interface BFFConfig {
7
7
  forestServerUrl?: string;
8
8
  forestAppUrl?: string;
9
9
  agentUrl?: string;
10
+ tokenEncryptionKey?: string;
10
11
  httpPort: number;
11
12
  presence: PresenceMap;
12
13
  hasAllRequired: boolean;
@@ -18,6 +18,8 @@ exports.REQUIRED_KEYS = [
18
18
  const URL_KEYS = ['FOREST_SERVER_URL', 'FOREST_APP_URL', 'AGENT_URL'];
19
19
  const DECIMAL_INTEGER = /^\d+$/;
20
20
  const MAX_PORT = 65535;
21
+ const ENCRYPTION_KEY_BYTES = 32;
22
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
21
23
  const HTTP_URL_SCHEMA = zod_1.z.url({ protocol: /^https?$/ });
22
24
  function normalize(value) {
23
25
  return value === undefined || value.trim() === '' ? undefined : value;
@@ -35,6 +37,16 @@ function parsePort(raw) {
35
37
  function isHttpUrl(value) {
36
38
  return !/\s/.test(value) && HTTP_URL_SCHEMA.safeParse(value).success;
37
39
  }
40
+ function isValidEncryptionKey(value) {
41
+ return BASE64_PATTERN.test(value) && Buffer.from(value, 'base64').length === ENCRYPTION_KEY_BYTES;
42
+ }
43
+ function parseEncryptionKey(raw) {
44
+ const value = normalize(raw);
45
+ if (value !== undefined && !isValidEncryptionKey(value)) {
46
+ throw new errors_1.ConfigurationError(`Invalid configuration: BFF_TOKEN_ENCRYPTION_KEY must be base64-encoded and exactly ${ENCRYPTION_KEY_BYTES} bytes (AES-256).`);
47
+ }
48
+ return value;
49
+ }
38
50
  function parseConfig(env) {
39
51
  const normalized = Object.fromEntries(exports.REQUIRED_KEYS.map(key => [key, normalize(env[key])]));
40
52
  for (const key of URL_KEYS) {
@@ -44,15 +56,17 @@ function parseConfig(env) {
44
56
  }
45
57
  }
46
58
  const presence = Object.fromEntries(exports.REQUIRED_KEYS.map(key => [key, normalized[key] !== undefined]));
59
+ const tokenEncryptionKey = parseEncryptionKey(env.BFF_TOKEN_ENCRYPTION_KEY);
47
60
  return {
48
61
  forestAuthSecret: normalized.FOREST_AUTH_SECRET,
49
62
  forestEnvSecret: normalized.FOREST_ENV_SECRET,
50
63
  forestServerUrl: normalized.FOREST_SERVER_URL,
51
64
  forestAppUrl: normalized.FOREST_APP_URL,
52
65
  agentUrl: normalized.AGENT_URL,
66
+ tokenEncryptionKey,
53
67
  httpPort: parsePort(env.HTTP_PORT),
54
68
  presence,
55
- hasAllRequired: exports.REQUIRED_KEYS.every(key => presence[key]),
69
+ hasAllRequired: exports.REQUIRED_KEYS.every(key => presence[key]) && tokenEncryptionKey !== undefined,
56
70
  };
57
71
  }
58
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52LWNvbmZpZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb25maWcvZW52LWNvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUF5REEsa0NBMkJDO0FBcEZELDZCQUF3QjtBQUV4QiwyREFBMkM7QUFDM0Msc0NBQStDO0FBRWxDLFFBQUEsYUFBYSxHQUFHO0lBQzNCLG9CQUFvQjtJQUNwQixtQkFBbUI7SUFDbkIsbUJBQW1CO0lBQ25CLGdCQUFnQjtJQUNoQixXQUFXO0NBQ0gsQ0FBQztBQUlYLE1BQU0sUUFBUSxHQUFHLENBQUMsbUJBQW1CLEVBQUUsZ0JBQWdCLEVBQUUsV0FBVyxDQUFVLENBQUM7QUFlL0UsTUFBTSxlQUFlLEdBQUcsT0FBTyxDQUFDO0FBQ2hDLE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQztBQUN2QixNQUFNLGVBQWUsR0FBRyxPQUFDLENBQUMsR0FBRyxDQUFDLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7QUFFeEQsU0FBUyxTQUFTLENBQUMsS0FBeUI7SUFDMUMsT0FBTyxLQUFLLEtBQUssU0FBUyxJQUFJLEtBQUssQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDO0FBQ3hFLENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBQyxHQUF1QjtJQUN4QyxNQUFNLE9BQU8sR0FBRyxHQUFHLEVBQUUsSUFBSSxFQUFFLENBQUM7SUFDNUIsSUFBSSxPQUFPLEtBQUssU0FBUyxJQUFJLE9BQU8sS0FBSyxFQUFFO1FBQUUsT0FBTyxrQkFBZ0IsQ0FBQztJQUVyRSxNQUFNLElBQUksR0FBRyxlQUFlLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztJQUVuRSxJQUFJLE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksSUFBSSxHQUFHLFFBQVEsRUFBRSxDQUFDO1FBQzFDLE1BQU0sSUFBSSwyQkFBa0IsQ0FDMUIscUVBQXFFLFFBQVEsR0FBRyxDQUNqRixDQUFDO0lBQ0osQ0FBQztJQUVELE9BQU8sSUFBSSxDQUFDO0FBQ2QsQ0FBQztBQUVELFNBQVMsU0FBUyxDQUFDLEtBQWE7SUFDOUIsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksZUFBZSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxPQUFPLENBQUM7QUFDdkUsQ0FBQztBQUVELFNBQWdCLFdBQVcsQ0FBQyxHQUFzQjtJQUNoRCxNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsV0FBVyxDQUNuQyxxQkFBYSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUMsR0FBRyxFQUFFLFNBQVMsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQ1YsQ0FBQztJQUU3QyxLQUFLLE1BQU0sR0FBRyxJQUFJLFFBQVEsRUFBRSxDQUFDO1FBQzNCLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUU5QixJQUFJLEtBQUssS0FBSyxTQUFTLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUM3QyxNQUFNLElBQUksMkJBQWtCLENBQUMsMEJBQTBCLEdBQUcsK0JBQStCLENBQUMsQ0FBQztRQUM3RixDQUFDO0lBQ0gsQ0FBQztJQUVELE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxXQUFXLENBQ2pDLHFCQUFhLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQyxHQUFHLEVBQUUsVUFBVSxDQUFDLEdBQUcsQ0FBQyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQ2hELENBQUM7SUFFakIsT0FBTztRQUNMLGdCQUFnQixFQUFFLFVBQVUsQ0FBQyxrQkFBa0I7UUFDL0MsZUFBZSxFQUFFLFVBQVUsQ0FBQyxpQkFBaUI7UUFDN0MsZUFBZSxFQUFFLFVBQVUsQ0FBQyxpQkFBaUI7UUFDN0MsWUFBWSxFQUFFLFVBQVUsQ0FBQyxjQUFjO1FBQ3ZDLFFBQVEsRUFBRSxVQUFVLENBQUMsU0FBUztRQUM5QixRQUFRLEVBQUUsU0FBUyxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUM7UUFDbEMsUUFBUTtRQUNSLGNBQWMsRUFBRSxxQkFBYSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQztLQUMxRCxDQUFDO0FBQ0osQ0FBQyJ9
72
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52LWNvbmZpZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb25maWcvZW52LWNvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUE0RUEsa0NBOEJDO0FBMUdELDZCQUF3QjtBQUV4QiwyREFBMkM7QUFDM0Msc0NBQStDO0FBRWxDLFFBQUEsYUFBYSxHQUFHO0lBQzNCLG9CQUFvQjtJQUNwQixtQkFBbUI7SUFDbkIsbUJBQW1CO0lBQ25CLGdCQUFnQjtJQUNoQixXQUFXO0NBQ0gsQ0FBQztBQUlYLE1BQU0sUUFBUSxHQUFHLENBQUMsbUJBQW1CLEVBQUUsZ0JBQWdCLEVBQUUsV0FBVyxDQUFVLENBQUM7QUFnQi9FLE1BQU0sZUFBZSxHQUFHLE9BQU8sQ0FBQztBQUNoQyxNQUFNLFFBQVEsR0FBRyxLQUFLLENBQUM7QUFDdkIsTUFBTSxvQkFBb0IsR0FBRyxFQUFFLENBQUM7QUFDaEMsTUFBTSxjQUFjLEdBQUcsd0JBQXdCLENBQUM7QUFDaEQsTUFBTSxlQUFlLEdBQUcsT0FBQyxDQUFDLEdBQUcsQ0FBQyxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsQ0FBQyxDQUFDO0FBRXhELFNBQVMsU0FBUyxDQUFDLEtBQXlCO0lBQzFDLE9BQU8sS0FBSyxLQUFLLFNBQVMsSUFBSSxLQUFLLENBQUMsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQztBQUN4RSxDQUFDO0FBRUQsU0FBUyxTQUFTLENBQUMsR0FBdUI7SUFDeEMsTUFBTSxPQUFPLEdBQUcsR0FBRyxFQUFFLElBQUksRUFBRSxDQUFDO0lBQzVCLElBQUksT0FBTyxLQUFLLFNBQVMsSUFBSSxPQUFPLEtBQUssRUFBRTtRQUFFLE9BQU8sa0JBQWdCLENBQUM7SUFFckUsTUFBTSxJQUFJLEdBQUcsZUFBZSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFD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
@@ -1,11 +1,13 @@
1
1
  import type { BFFConfig } from '../config/env-config';
2
2
  import type { Logger } from '../ports/logger-port';
3
+ import type { Middleware } from 'koa';
3
4
  import http from 'http';
4
5
  export interface BFFHttpServerOptions {
5
6
  port: number;
6
7
  version: string;
7
8
  config: BFFConfig;
8
9
  logger?: Logger;
10
+ middlewares?: Middleware[];
9
11
  }
10
12
  export default class BFFHttpServer {
11
13
  private readonly app;
@@ -25,6 +25,9 @@ class BFFHttpServer {
25
25
  }
26
26
  await next();
27
27
  });
28
+ for (const middleware of this.options.middlewares ?? []) {
29
+ this.app.use(middleware);
30
+ }
28
31
  }
29
32
  async start() {
30
33
  if (this.server)
@@ -80,4 +83,4 @@ class BFFHttpServer {
80
83
  }
81
84
  }
82
85
  exports.default = BFFHttpServer;
83
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmZmLWh0dHAtc2VydmVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2h0dHAvYmZmLWh0dHAtc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBSUEsZ0RBQXdCO0FBQ3hCLDhDQUFzQjtBQUV0QixnRkFBNkQ7QUFTN0QsTUFBcUIsYUFBYTtJQU1oQyxZQUFZLE9BQTZCO1FBRmpDLFdBQU0sR0FBa0IsSUFBSSxDQUFDO1FBR25DLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO1FBQ3ZCLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLE1BQU0sSUFBSSxJQUFBLHdCQUFtQixHQUFFLENBQUM7UUFDdEQsSUFBSSxDQUFDLEdBQUcsR0FBRyxJQUFJLGFBQUcsRUFBRSxDQUFDO1FBRXJCLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLEVBQUU7WUFDL0IsR0FBRyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQ3RELE1BQU0sSUFBSSxFQUFFLENBQUM7UUFDZixDQUFDLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLEVBQUU7WUFDL0IsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLEtBQUssS0FBSyxJQUFJLEdBQUcsQ0FBQyxNQUFNLEtBQUssTUFBTSxDQUFDLElBQUksR0FBRyxDQUFDLElBQUksS0FBSyxTQUFTLEVBQUUsQ0FBQztnQkFDOUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxPQUFPLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDO2dCQUN6QyxHQUFHLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FB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
86
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmZmLWh0dHAtc2VydmVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2h0dHAvYmZmLWh0dHAtc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBS0EsZ0RBQXdCO0FBQ3hCLDhDQUFzQjtBQUV0QixnRkFBNkQ7QUFVN0QsTUFBcUIsYUFBYTtJQU1oQyxZQUFZLE9BQTZCO1FBRmpDLFdBQU0sR0FBa0IsSUFBSSxDQUFDO1FBR25DLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO1FBQ3ZCLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLE1BQU0sSUFBSSxJQUFBLHdCQUFtQixHQUFFLENBQUM7UUFDdEQsSUFBSSxDQUFDLEdBQUcsR0FBRyxJQUFJLGFBQUcsRUFBRSxDQUFDO1FBRXJCLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLEVBQUU7WUFDL0IsR0FBRyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQ3RELE1BQU0sSUFBSSxFQUFFLENBQUM7UUFDZixDQUFDLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLEVBQUU7WUFDL0IsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLEtBQUssS0FBSyxJQUFJLEdBQUcsQ0FBQyxNQUFNLEtBQUssTUFBTSxDQUFDLElBQUksR0FBRyxDQUFDLElBQUksS0FBSyxTQUFTLEVBQUUsQ0FBQztnQkFDOUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxPQUFPLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDO2dCQUN6QyxHQUFHLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FB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
package/dist/index.d.ts CHANGED
@@ -7,4 +7,15 @@ export { default as DEFAULT_BFF_PORT } from './defaults';
7
7
  export { default as createConsoleLogger } from './adapters/console-logger';
8
8
  export type { Logger, LoggerLevel } from './ports/logger-port';
9
9
  export { default as version } from './version';
10
+ export { default as ForestServerClient, OAuthExchangeError } from './oauth/forest-server-client';
11
+ export type { ServerTokens, RegisteredClient, ExchangeCodeParams, } from './oauth/forest-server-client';
12
+ export { default as createOAuthRoutes } from './oauth/oauth-routes';
13
+ export { default as createInMemorySessionStore } from './oauth/session-store';
14
+ export type { SessionStore, StoredSession, CreatedSession } from './oauth/session-store';
15
+ export { default as createTokenCipher } from './oauth/token-cipher';
16
+ export { default as ensureFreshServerAccess } from './oauth/session-lifecycle';
17
+ export { issueBffAccessToken, BFF_ACCESS_TOKEN_TYPE } from './oauth/bff-token';
18
+ export type { BffAccessTokenPayload } from './oauth/bff-token';
19
+ export { createPkcePair } from './oauth/pkce';
20
+ export { OAuthRequestError } from './oauth/oauth-error';
10
21
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.version = exports.createConsoleLogger = exports.DEFAULT_BFF_PORT = exports.ConfigurationError = exports.runCli = exports.REQUIRED_KEYS = exports.parseConfig = exports.BFFHttpServer = void 0;
6
+ exports.OAuthRequestError = exports.createPkcePair = exports.BFF_ACCESS_TOKEN_TYPE = exports.issueBffAccessToken = exports.ensureFreshServerAccess = exports.createTokenCipher = exports.createInMemorySessionStore = exports.createOAuthRoutes = exports.OAuthExchangeError = exports.ForestServerClient = exports.version = exports.createConsoleLogger = exports.DEFAULT_BFF_PORT = exports.ConfigurationError = exports.runCli = exports.REQUIRED_KEYS = exports.parseConfig = exports.BFFHttpServer = void 0;
7
7
  var bff_http_server_1 = require("./http/bff-http-server");
8
8
  Object.defineProperty(exports, "BFFHttpServer", { enumerable: true, get: function () { return __importDefault(bff_http_server_1).default; } });
9
9
  var env_config_1 = require("./config/env-config");
@@ -19,4 +19,22 @@ var console_logger_1 = require("./adapters/console-logger");
19
19
  Object.defineProperty(exports, "createConsoleLogger", { enumerable: true, get: function () { return __importDefault(console_logger_1).default; } });
20
20
  var version_1 = require("./version");
21
21
  Object.defineProperty(exports, "version", { enumerable: true, get: function () { return __importDefault(version_1).default; } });
22
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMERBQWtFO0FBQXpELGlJQUFBLE9BQU8sT0FBaUI7QUFDakMsa0RBQWlFO0FBQXhELHlHQUFBLFdBQVcsT0FBQTtBQUFFLDJHQUFBLGFBQWEsT0FBQTtBQUVuQyx1Q0FBK0M7QUFBdEMsbUhBQUEsT0FBTyxPQUFVO0FBQzFCLG1DQUE4QztBQUFyQyw0R0FBQSxrQkFBa0IsT0FBQTtBQUMzQix1Q0FBeUQ7QUFBaEQsNkhBQUEsT0FBTyxPQUFvQjtBQUNwQyw0REFBMkU7QUFBbEUsc0lBQUEsT0FBTyxPQUF1QjtBQUV2QyxxQ0FBK0M7QUFBdEMsbUhBQUEsT0FBTyxPQUFXIn0=
22
+ var forest_server_client_1 = require("./oauth/forest-server-client");
23
+ Object.defineProperty(exports, "ForestServerClient", { enumerable: true, get: function () { return __importDefault(forest_server_client_1).default; } });
24
+ Object.defineProperty(exports, "OAuthExchangeError", { enumerable: true, get: function () { return forest_server_client_1.OAuthExchangeError; } });
25
+ var oauth_routes_1 = require("./oauth/oauth-routes");
26
+ Object.defineProperty(exports, "createOAuthRoutes", { enumerable: true, get: function () { return __importDefault(oauth_routes_1).default; } });
27
+ var session_store_1 = require("./oauth/session-store");
28
+ Object.defineProperty(exports, "createInMemorySessionStore", { enumerable: true, get: function () { return __importDefault(session_store_1).default; } });
29
+ var token_cipher_1 = require("./oauth/token-cipher");
30
+ Object.defineProperty(exports, "createTokenCipher", { enumerable: true, get: function () { return __importDefault(token_cipher_1).default; } });
31
+ var session_lifecycle_1 = require("./oauth/session-lifecycle");
32
+ Object.defineProperty(exports, "ensureFreshServerAccess", { enumerable: true, get: function () { return __importDefault(session_lifecycle_1).default; } });
33
+ var bff_token_1 = require("./oauth/bff-token");
34
+ Object.defineProperty(exports, "issueBffAccessToken", { enumerable: true, get: function () { return bff_token_1.issueBffAccessToken; } });
35
+ Object.defineProperty(exports, "BFF_ACCESS_TOKEN_TYPE", { enumerable: true, get: function () { return bff_token_1.BFF_ACCESS_TOKEN_TYPE; } });
36
+ var pkce_1 = require("./oauth/pkce");
37
+ Object.defineProperty(exports, "createPkcePair", { enumerable: true, get: function () { return pkce_1.createPkcePair; } });
38
+ var oauth_error_1 = require("./oauth/oauth-error");
39
+ Object.defineProperty(exports, "OAuthRequestError", { enumerable: true, get: function () { return oauth_error_1.OAuthRequestError; } });
40
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMERBQWtFO0FBQXpELGlJQUFBLE9BQU8sT0FBaUI7QUFDakMsa0RBQWlFO0FBQXhELHlHQUFBLFdBQVcsT0FBQTtBQUFFLDJHQUFBLGFBQWEsT0FBQTtBQUVuQyx1Q0FBK0M7QUFBdEMsbUhBQUEsT0FBTyxPQUFVO0FBQzFCLG1DQUE4QztBQUFyQyw0R0FBQSxrQkFBa0IsT0FBQTtBQUMzQix1Q0FBeUQ7QUFBaEQsNkhBQUEsT0FBTyxPQUFvQjtBQUNwQyw0REFBMkU7QUFBbEUsc0lBQUEsT0FBTyxPQUF1QjtBQUV2QyxxQ0FBK0M7QUFBdEMsbUhBQUEsT0FBTyxPQUFXO0FBQzNCLHFFQUFpRztBQUF4RiwySUFBQSxPQUFPLE9BQXNCO0FBQUUsMEhBQUEsa0JBQWtCLE9BQUE7QUFNMUQscURBQW9FO0FBQTNELGtJQUFBLE9BQU8sT0FBcUI7QUFDckMsdURBQThFO0FBQXJFLDRJQUFBLE9BQU8sT0FBOEI7QUFFOUMscURBQW9FO0FBQTNELGtJQUFBLE9BQU8sT0FBcUI7QUFDckMsK0RBQStFO0FBQXRFLDZJQUFBLE9BQU8sT0FBMkI7QUFDM0MsK0NBQStFO0FBQXRFLGdIQUFBLG1CQUFtQixPQUFBO0FBQUUsa0hBQUEscUJBQXFCLE9BQUE7QUFFbkQscUNBQThDO0FBQXJDLHNHQUFBLGNBQWMsT0FBQTtBQUN2QixtREFBd0Q7QUFBL0MsZ0hBQUEsaUJBQWlCLE9BQUEifQ==
@@ -0,0 +1,24 @@
1
+ import type { UserInfo } from '@forestadmin/forestadmin-client';
2
+ export declare const BFF_ACCESS_TOKEN_TYPE = "bff_access";
3
+ export declare const BFF_ACCESS_TOKEN_MAX_EXPIRES_IN: number;
4
+ export interface IssueBffAccessTokenParams {
5
+ sid: string;
6
+ user: UserInfo;
7
+ renderingId: number;
8
+ authSecret: string;
9
+ expiresInSeconds: number;
10
+ }
11
+ export interface BffAccessTokenPayload {
12
+ type: typeof BFF_ACCESS_TOKEN_TYPE;
13
+ sid: string;
14
+ id: number;
15
+ email: string;
16
+ first_name: string;
17
+ last_name: string;
18
+ team: string;
19
+ rendering_id: string;
20
+ permission_level: string;
21
+ tags: Record<string, string>;
22
+ }
23
+ export declare function issueBffAccessToken({ sid, user, renderingId, authSecret, expiresInSeconds, }: IssueBffAccessTokenParams): string;
24
+ //# sourceMappingURL=bff-token.d.ts.map
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN = exports.BFF_ACCESS_TOKEN_TYPE = void 0;
7
+ exports.issueBffAccessToken = issueBffAccessToken;
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ exports.BFF_ACCESS_TOKEN_TYPE = 'bff_access';
10
+ exports.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN = 15 * 60;
11
+ function issueBffAccessToken({ sid, user, renderingId, authSecret, expiresInSeconds, }) {
12
+ const payload = {
13
+ type: exports.BFF_ACCESS_TOKEN_TYPE,
14
+ sid,
15
+ id: user.id,
16
+ email: user.email,
17
+ first_name: user.firstName,
18
+ last_name: user.lastName,
19
+ team: user.team,
20
+ rendering_id: String(renderingId),
21
+ permission_level: user.permissionLevel,
22
+ tags: user.tags ?? {},
23
+ };
24
+ return jsonwebtoken_1.default.sign(payload, authSecret, {
25
+ algorithm: 'HS256',
26
+ expiresIn: Math.min(expiresInSeconds, exports.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN),
27
+ });
28
+ }
29
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmZmLXRva2VuLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL29hdXRoL2JmZi10b2tlbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUE0QkEsa0RBd0JDO0FBbERELGdFQUF3QztBQUUzQixRQUFBLHFCQUFxQixHQUFHLFlBQVksQ0FBQztBQUNyQyxRQUFBLCtCQUErQixHQUFHLEVBQUUsR0FBRyxFQUFFLENBQUM7QUF1QnZELFNBQWdCLG1CQUFtQixDQUFDLEVBQ2xDLEdBQUcsRUFDSCxJQUFJLEVBQ0osV0FBVyxFQUNYLFVBQVUsRUFDVixnQkFBZ0IsR0FDVTtJQUMxQixNQUFNLE9BQU8sR0FBMEI7UUFDckMsSUFBSSxFQUFFLDZCQUFxQjtRQUMzQixHQUFHO1FBQ0gsRUFBRSxFQUFFLElBQUksQ0FBQyxFQUFFO1FBQ1gsS0FBSyxFQUFFLElBQUksQ0FBQyxLQUFLO1FBQ2pCLFVBQVUsRUFBRSxJQUFJLENBQUMsU0FBUztRQUMxQixTQUFTLEVBQUUsSUFBSSxDQUFDLFFBQVE7UUFDeEIsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1FBQ2YsWUFBWSxFQUFFLE1BQU0sQ0FBQyxXQUFXLENBQUM7UUFDakMsZ0JBQWdCLEVBQUUsSUFBSSxDQUFDLGVBQWU7UUFDdEMsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJLElBQUksRUFBRTtLQUN0QixDQUFDO0lBRUYsT0FBTyxzQkFBWSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsVUFBVSxFQUFFO1FBQzVDLFNBQVMsRUFBRSxPQUFPO1FBQ2xCLFNBQVMsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLGdCQUFnQixFQUFFLHVDQUErQixDQUFDO0tBQ3ZFLENBQUMsQ0FBQztBQUNMLENBQUMifQ==
@@ -0,0 +1,40 @@
1
+ import type { UserInfo } from '@forestadmin/forestadmin-client';
2
+ import OAuthExchangeError from './oauth-exchange-error';
3
+ export { OAuthExchangeError };
4
+ export interface RegisteredClient {
5
+ client_id: string;
6
+ client_name?: string;
7
+ redirect_uris?: string[];
8
+ grant_types?: string[];
9
+ }
10
+ export interface ServerTokens {
11
+ saasAccessToken: string;
12
+ saasRefreshToken: string;
13
+ renderingId: number;
14
+ expiresAt: number;
15
+ }
16
+ export interface ExchangeCodeParams {
17
+ code: string;
18
+ codeVerifier: string;
19
+ redirectUri: string;
20
+ clientId: string;
21
+ }
22
+ export interface ForestServerClientOptions {
23
+ forestServerUrl: string;
24
+ envSecret: string;
25
+ }
26
+ export default class ForestServerClient {
27
+ private readonly forestServerUrl;
28
+ private readonly envSecret;
29
+ private readonly forestClient;
30
+ constructor({ forestServerUrl, envSecret }: ForestServerClientOptions);
31
+ private url;
32
+ fetchEnvironmentId(): Promise<number>;
33
+ getRegisteredClient(clientId: string): Promise<RegisteredClient | undefined>;
34
+ exchangeCode({ code, codeVerifier, redirectUri, clientId, }: ExchangeCodeParams): Promise<ServerTokens>;
35
+ refreshServerToken(saasRefreshToken: string): Promise<ServerTokens>;
36
+ private postToken;
37
+ private static toServerTokens;
38
+ getUserInfo(renderingId: number, saasAccessToken: string): Promise<UserInfo>;
39
+ }
40
+ //# sourceMappingURL=forest-server-client.d.ts.map
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OAuthExchangeError = void 0;
7
+ const forestadmin_client_1 = __importDefault(require("@forestadmin/forestadmin-client"));
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ const oauth_exchange_error_1 = __importDefault(require("./oauth-exchange-error"));
10
+ exports.OAuthExchangeError = oauth_exchange_error_1.default;
11
+ const DEFAULT_HEADERS = { 'Content-Type': 'application/json' };
12
+ const REQUEST_TIMEOUT_MS = 60000;
13
+ function fetchWithTimeout(url, init) {
14
+ return fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
15
+ }
16
+ class ForestServerClient {
17
+ constructor({ forestServerUrl, envSecret }) {
18
+ this.forestServerUrl = forestServerUrl;
19
+ this.envSecret = envSecret;
20
+ this.forestClient = (0, forestadmin_client_1.default)({ forestServerUrl, envSecret });
21
+ }
22
+ url(path) {
23
+ return new URL(path, this.forestServerUrl).toString();
24
+ }
25
+ async fetchEnvironmentId() {
26
+ const response = await fetchWithTimeout(this.url('/liana/environment'), {
27
+ method: 'GET',
28
+ headers: { ...DEFAULT_HEADERS, 'forest-secret-key': this.envSecret },
29
+ });
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch environment: ${response.status} ${response.statusText}`);
32
+ }
33
+ const body = (await response.json());
34
+ const id = body.data?.id;
35
+ if (typeof id !== 'number' && (typeof id !== 'string' || !/^\d+$/.test(id))) {
36
+ throw new Error('Failed to parse environment id from the Forest server response');
37
+ }
38
+ const environmentId = Number(id);
39
+ if (!Number.isInteger(environmentId) || environmentId <= 0) {
40
+ throw new Error('Failed to parse environment id from the Forest server response');
41
+ }
42
+ return environmentId;
43
+ }
44
+ async getRegisteredClient(clientId) {
45
+ const response = await fetchWithTimeout(this.url(`/oauth/register/${encodeURIComponent(clientId)}`), {
46
+ method: 'GET',
47
+ headers: DEFAULT_HEADERS,
48
+ });
49
+ if (response.status === 404)
50
+ return undefined;
51
+ if (!response.ok) {
52
+ throw new Error(`Failed to fetch registered client: ${response.status} ${response.statusText}`);
53
+ }
54
+ return response.json();
55
+ }
56
+ async exchangeCode({ code, codeVerifier, redirectUri, clientId, }) {
57
+ return this.postToken({
58
+ grant_type: 'authorization_code',
59
+ code,
60
+ redirect_uri: redirectUri,
61
+ client_id: clientId,
62
+ code_verifier: codeVerifier,
63
+ }, true);
64
+ }
65
+ async refreshServerToken(saasRefreshToken) {
66
+ return this.postToken({ grant_type: 'refresh_token', refresh_token: saasRefreshToken }, false);
67
+ }
68
+ async postToken(payload, isInitialExchange) {
69
+ const response = await fetchWithTimeout(this.url('/oauth/token'), {
70
+ method: 'POST',
71
+ headers: { ...DEFAULT_HEADERS, 'forest-secret-key': this.envSecret },
72
+ body: JSON.stringify(payload),
73
+ });
74
+ if (!response.ok) {
75
+ const errorBody = (await response.json().catch(() => ({})));
76
+ throw new oauth_exchange_error_1.default(errorBody.error || 'server_error', errorBody.error_description || 'Failed to exchange token with the Forest server');
77
+ }
78
+ const tokens = (await response.json());
79
+ const saasRefreshToken = tokens.refresh_token ?? payload.refresh_token;
80
+ if (saasRefreshToken === undefined) {
81
+ throw new oauth_exchange_error_1.default('invalid_grant', 'The Forest server did not return a refresh token');
82
+ }
83
+ return ForestServerClient.toServerTokens(tokens.access_token, saasRefreshToken, isInitialExchange);
84
+ }
85
+ static toServerTokens(saasAccessToken, saasRefreshToken, requireRenderingId) {
86
+ const decoded = jsonwebtoken_1.default.decode(saasAccessToken);
87
+ const decodedRenderingId = Number(decoded?.meta?.renderingId);
88
+ const hasRenderingId = Number.isInteger(decodedRenderingId);
89
+ if (requireRenderingId && !hasRenderingId) {
90
+ throw new Error('Failed to decode renderingId from the Forest server access token');
91
+ }
92
+ return {
93
+ saasAccessToken,
94
+ saasRefreshToken,
95
+ renderingId: hasRenderingId ? decodedRenderingId : 0,
96
+ expiresAt: decoded?.exp ?? 0,
97
+ };
98
+ }
99
+ async getUserInfo(renderingId, saasAccessToken) {
100
+ return this.forestClient.authService.getUserInfo(renderingId, saasAccessToken);
101
+ }
102
+ }
103
+ exports.default = ForestServerClient;
104
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9yZXN0LXNlcnZlci1jbGllbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb2F1dGgvZm9yZXN0LXNlcnZlci1jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBRUEseUZBQXNFO0FBQ3RFLGdFQUF3QztBQUV4QyxrRkFBd0Q7QUFFL0MsNkJBRkYsOEJBQWtCLENBRUU7QUE0QjNCLE1BQU0sZUFBZSxHQUFHLEVBQUUsY0FBYyxFQUFFLGtCQUFrQixFQUFXLENBQUM7QUFDeEUsTUFBTSxrQkFBa0IsR0FBRyxLQUFNLENBQUM7QUFFbEMsU0FBUyxnQkFBZ0IsQ0FBQyxHQUFXLEVBQUUsSUFBaUI7SUFDdEQsT0FBTyxLQUFLLENBQUMsR0FBRyxFQUFFLEVBQUUsR0FBRyxJQUFJLEVBQUUsTUFBTSxFQUFFLFdBQVcsQ0FBQyxPQUFPLENBQUMsa0JBQWtCLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDbEYsQ0FBQztBQUVELE1BQXFCLGtCQUFrQjtJQUtyQyxZQUFZLEVBQUUsZUFBZSxFQUFFLFNBQVMsRUFBNkI7UUFDbkUsSUFBSSxDQUFDLGVBQWUsR0FBRyxlQUFlLENBQUM7UUFDdkMsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUM7UUFDM0IsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFBLDRCQUF1QixFQUFDLEVBQUUsZUFBZSxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUVPLEdBQUcsQ0FBQyxJQUFZO1FBQ3RCLE9BQU8sSUFBSSxHQUFHLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxlQUFlLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQztJQUN4RCxDQUFDO0lBRUQsS0FBSyxDQUFDLGtCQUFrQjtRQUN0QixNQUFNLFFBQVEsR0FBRyxNQUFNLGdCQUFnQixDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsb0JBQW9CLENBQUMsRUFBRTtZQUN0RSxNQUFNLEVBQUUsS0FBSztZQUNiLE9B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
@@ -0,0 +1,18 @@
1
+ export declare class OAuthRequestError extends Error {
2
+ readonly status: number;
3
+ readonly type: string;
4
+ constructor(status: number, type: string, message: string);
5
+ }
6
+ export interface OAuthErrorBody {
7
+ error: string;
8
+ error_description: string;
9
+ }
10
+ export declare function toErrorBody(error: OAuthRequestError): OAuthErrorBody;
11
+ export declare function invalidRequest(message: string): OAuthRequestError;
12
+ export declare function invalidClient(message: string): OAuthRequestError;
13
+ export declare function invalidGrant(message: string): OAuthRequestError;
14
+ export declare function unsupportedGrantType(message: string): OAuthRequestError;
15
+ export declare function forestIdentityNotAllowed(message: string): OAuthRequestError;
16
+ export declare function sessionExpired(message: string): OAuthRequestError;
17
+ export declare function serverError(message: string): OAuthRequestError;
18
+ //# sourceMappingURL=oauth-error.d.ts.map
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthRequestError = void 0;
4
+ exports.toErrorBody = toErrorBody;
5
+ exports.invalidRequest = invalidRequest;
6
+ exports.invalidClient = invalidClient;
7
+ exports.invalidGrant = invalidGrant;
8
+ exports.unsupportedGrantType = unsupportedGrantType;
9
+ exports.forestIdentityNotAllowed = forestIdentityNotAllowed;
10
+ exports.sessionExpired = sessionExpired;
11
+ exports.serverError = serverError;
12
+ class OAuthRequestError extends Error {
13
+ constructor(status, type, message) {
14
+ super(message);
15
+ this.name = 'OAuthRequestError';
16
+ this.status = status;
17
+ this.type = type;
18
+ }
19
+ }
20
+ exports.OAuthRequestError = OAuthRequestError;
21
+ function toErrorBody(error) {
22
+ return { error: error.type, error_description: error.message };
23
+ }
24
+ function invalidRequest(message) {
25
+ return new OAuthRequestError(400, 'invalid_request', message);
26
+ }
27
+ function invalidClient(message) {
28
+ return new OAuthRequestError(400, 'invalid_client', message);
29
+ }
30
+ function invalidGrant(message) {
31
+ return new OAuthRequestError(400, 'invalid_grant', message);
32
+ }
33
+ function unsupportedGrantType(message) {
34
+ return new OAuthRequestError(400, 'unsupported_grant_type', message);
35
+ }
36
+ function forestIdentityNotAllowed(message) {
37
+ return new OAuthRequestError(403, 'forest_identity_not_allowed', message);
38
+ }
39
+ function sessionExpired(message) {
40
+ return new OAuthRequestError(401, 'session_expired', message);
41
+ }
42
+ function serverError(message) {
43
+ return new OAuthRequestError(502, 'server_error', message);
44
+ }
45
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2F1dGgtZXJyb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb2F1dGgvb2F1dGgtZXJyb3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBaUJBLGtDQUVDO0FBRUQsd0NBRUM7QUFFRCxzQ0FFQztBQUVELG9DQUVDO0FBRUQsb0RBRUM7QUFFRCw0REFFQztBQUVELHdDQUVDO0FBRUQsa0NBRUM7QUEvQ0QsTUFBYSxpQkFBa0IsU0FBUSxLQUFLO0lBSTFDLFlBQVksTUFBYyxFQUFFLElBQVksRUFBRSxPQUFlO1FBQ3ZELEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUNmLElBQUksQ0FBQyxJQUFJLEdBQUcsbUJBQW1CLENBQUM7UUFDaEMsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7SUFDbkIsQ0FBQztDQUNGO0FBVkQsOENBVUM7QUFPRCxTQUFnQixXQUFXLENBQUMsS0FBd0I7SUFDbEQsT0FBTyxFQUFFLEtBQUssRUFBRSxLQUFLLENBQUMsSUFBSSxFQUFFLGlCQUFpQixFQUFFLEtBQUssQ0FBQyxPQUFPLEVBQUUsQ0FBQztBQUNqRSxDQUFDO0FBRUQsU0FBZ0IsY0FBYyxDQUFDLE9BQWU7SUFDNUMsT0FBTyxJQUFJLGlCQUFpQixDQUFDLEdBQUcsRUFBRSxpQkFBaUIsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUNoRSxDQUFDO0FBRUQsU0FBZ0IsYUFBYSxDQUFDLE9BQWU7SUFDM0MsT0FBTyxJQUFJLGlCQUFpQixDQUFDLEdBQUcsRUFBRSxnQkFBZ0IsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUMvRCxDQUFDO0FBRUQsU0FBZ0IsWUFBWSxDQUFDLE9BQWU7SUFDMUMsT0FBTyxJQUFJLGlCQUFpQixDQUFDLEdBQUcsRUFBRSxlQUFlLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDOUQsQ0FBQztBQUVELFNBQWdCLG9CQUFvQixDQUFDLE9BQWU7SUFDbEQsT0FBTyxJQUFJLGlCQUFpQixDQUFDLEdBQUcsRUFBRSx3QkFBd0IsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUN2RSxDQUFDO0FBRUQsU0FBZ0Isd0JBQXdCLENBQUMsT0FBZTtJQUN0RCxPQUFPLElBQUksaUJBQWlCLENBQUMsR0FBRyxFQUFFLDZCQUE2QixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQzVFLENBQUM7QUFFRCxTQUFnQixjQUFjLENBQUMsT0FBZTtJQUM1QyxPQUFPLElBQUksaUJBQWlCLENBQUMsR0FBRyxFQUFFLGlCQUFpQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQ2hFLENBQUM7QUFFRCxTQUFnQixXQUFXLENBQUMsT0FBZTtJQUN6QyxPQUFPLElBQUksaUJBQWlCLENBQUMsR0FBRyxFQUFFLGNBQWMsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUM3RCxDQUFDIn0=
@@ -0,0 +1,5 @@
1
+ export default class OAuthExchangeError extends Error {
2
+ readonly error: string;
3
+ constructor(error: string, description: string);
4
+ }
5
+ //# sourceMappingURL=oauth-exchange-error.d.ts.map
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class OAuthExchangeError extends Error {
4
+ constructor(error, description) {
5
+ super(description);
6
+ this.name = 'OAuthExchangeError';
7
+ this.error = error;
8
+ }
9
+ }
10
+ exports.default = OAuthExchangeError;
11
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2F1dGgtZXhjaGFuZ2UtZXJyb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb2F1dGgvb2F1dGgtZXhjaGFuZ2UtZXJyb3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxNQUFxQixrQkFBbUIsU0FBUSxLQUFLO0lBR25ELFlBQVksS0FBYSxFQUFFLFdBQW1CO1FBQzVDLEtBQUssQ0FBQyxXQUFXLENBQUMsQ0FBQztRQUNuQixJQUFJLENBQUMsSUFBSSxHQUFHLG9CQUFvQixDQUFDO1FBQ2pDLElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDO0lBQ3JCLENBQUM7Q0FDRjtBQVJELHFDQVFDIn0=
@@ -0,0 +1,14 @@
1
+ import type ForestServerClient from './forest-server-client';
2
+ import type { SessionStore } from './session-store';
3
+ import type { Logger } from '../ports/logger-port';
4
+ import type { Middleware } from 'koa';
5
+ export interface OAuthRoutesOptions {
6
+ serverClient: ForestServerClient;
7
+ sessionStore: SessionStore;
8
+ forestAppUrl: string;
9
+ authSecret: string;
10
+ environmentId: number;
11
+ logger: Logger;
12
+ }
13
+ export default function createOAuthRoutes(options: OAuthRoutesOptions): Middleware;
14
+ //# sourceMappingURL=oauth-routes.d.ts.map
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = createOAuthRoutes;
4
+ const bff_token_1 = require("./bff-token");
5
+ const forest_server_client_1 = require("./forest-server-client");
6
+ const oauth_error_1 = require("./oauth-error");
7
+ const CODE_VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]{43,128}$/;
8
+ const S256_CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
9
+ const SAFE_EXCHANGE_ERRORS = new Set([
10
+ 'invalid_request',
11
+ 'invalid_client',
12
+ 'invalid_grant',
13
+ 'unauthorized_client',
14
+ 'unsupported_grant_type',
15
+ 'invalid_scope',
16
+ ]);
17
+ function getQueryParam(ctx, key) {
18
+ const value = ctx.query[key];
19
+ if (Array.isArray(value)) {
20
+ throw (0, oauth_error_1.invalidRequest)(`Parameter must appear at most once: ${key}`);
21
+ }
22
+ return value;
23
+ }
24
+ function requireBodyString(body, key) {
25
+ const value = body[key];
26
+ if (typeof value !== 'string' || value === '') {
27
+ throw (0, oauth_error_1.invalidRequest)(`Missing required parameter: ${key}`);
28
+ }
29
+ return value;
30
+ }
31
+ function assertValidCodeVerifier(codeVerifier) {
32
+ if (!CODE_VERIFIER_PATTERN.test(codeVerifier)) {
33
+ throw (0, oauth_error_1.invalidRequest)('code_verifier is malformed');
34
+ }
35
+ }
36
+ function assertRegisteredRedirectUri(redirectUris, redirectUri, onMismatch) {
37
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
38
+ throw (0, oauth_error_1.invalidRequest)('Client has no registered redirect_uri');
39
+ }
40
+ if (!redirectUris.includes(redirectUri)) {
41
+ throw onMismatch('redirect_uri does not match the registered client');
42
+ }
43
+ }
44
+ function toSafeExchangeError(saasError) {
45
+ if (SAFE_EXCHANGE_ERRORS.has(saasError)) {
46
+ return new oauth_error_1.OAuthRequestError(400, saasError, 'Authorization code exchange failed');
47
+ }
48
+ return new oauth_error_1.OAuthRequestError(502, 'server_error', 'Forest server rejected the authorization code');
49
+ }
50
+ function mapIdentityError(error) {
51
+ const name = error instanceof Error ? error.name : '';
52
+ if (name === 'ForbiddenError') {
53
+ return (0, oauth_error_1.forestIdentityNotAllowed)('Caller is not an active user for this rendering');
54
+ }
55
+ return new oauth_error_1.OAuthRequestError(502, 'identity_resolution_failed', 'Failed to resolve identity');
56
+ }
57
+ function redirectAuthorizeError(ctx, redirectUri, state, error) {
58
+ const target = new URL(redirectUri);
59
+ target.searchParams.set('error', error.type);
60
+ target.searchParams.set('error_description', error.message);
61
+ if (state !== undefined && state !== '')
62
+ target.searchParams.set('state', state);
63
+ ctx.redirect(target.toString());
64
+ }
65
+ async function handleAuthorize(ctx, options) {
66
+ // Validate client_id and redirect_uri first: until redirect_uri is confirmed
67
+ // against the registered client, errors must not redirect (RFC 6749 §10.15).
68
+ const clientId = getQueryParam(ctx, 'client_id');
69
+ if (clientId === undefined || clientId === '') {
70
+ throw (0, oauth_error_1.invalidRequest)('Missing required parameter: client_id');
71
+ }
72
+ const redirectUri = getQueryParam(ctx, 'redirect_uri');
73
+ if (redirectUri === undefined || redirectUri === '') {
74
+ throw (0, oauth_error_1.invalidRequest)('Missing required parameter: redirect_uri');
75
+ }
76
+ const client = await options.serverClient.getRegisteredClient(clientId);
77
+ if (!client) {
78
+ throw (0, oauth_error_1.invalidRequest)('Unknown client_id');
79
+ }
80
+ assertRegisteredRedirectUri(client.redirect_uris, redirectUri, oauth_error_1.invalidRequest);
81
+ // redirect_uri is now trusted: remaining validation errors redirect back to
82
+ // the client with error + state (RFC 6749 §4.1.2.1).
83
+ let state;
84
+ try {
85
+ state = getQueryParam(ctx, 'state');
86
+ const responseType = getQueryParam(ctx, 'response_type');
87
+ if (responseType !== 'code') {
88
+ throw (0, oauth_error_1.invalidRequest)('response_type must be "code"');
89
+ }
90
+ if (getQueryParam(ctx, 'code_challenge_method') !== 'S256') {
91
+ throw (0, oauth_error_1.invalidRequest)('code_challenge_method must be S256');
92
+ }
93
+ const codeChallenge = getQueryParam(ctx, 'code_challenge');
94
+ if (codeChallenge === undefined || !S256_CODE_CHALLENGE_PATTERN.test(codeChallenge)) {
95
+ throw (0, oauth_error_1.invalidRequest)('code_challenge is missing or malformed');
96
+ }
97
+ if (state === undefined || state === '') {
98
+ throw (0, oauth_error_1.invalidRequest)('Missing required parameter: state');
99
+ }
100
+ const appBase = new URL(options.forestAppUrl);
101
+ const authorizeUrl = new URL(`${appBase.pathname.replace(/\/$/, '')}/oauth/authorize`, appBase);
102
+ authorizeUrl.searchParams.set('client_id', clientId);
103
+ authorizeUrl.searchParams.set('redirect_uri', redirectUri);
104
+ authorizeUrl.searchParams.set('response_type', 'code');
105
+ authorizeUrl.searchParams.set('code_challenge', codeChallenge);
106
+ authorizeUrl.searchParams.set('code_challenge_method', 'S256');
107
+ authorizeUrl.searchParams.set('state', state);
108
+ authorizeUrl.searchParams.set('environmentId', String(options.environmentId));
109
+ ctx.redirect(authorizeUrl.toString());
110
+ }
111
+ catch (error) {
112
+ if (error instanceof oauth_error_1.OAuthRequestError) {
113
+ redirectAuthorizeError(ctx, redirectUri, state, error);
114
+ return;
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+ function computeExpiresIn(serverTokens) {
120
+ const saasRemaining = serverTokens.expiresAt - Math.floor(Date.now() / 1000);
121
+ if (saasRemaining <= 0) {
122
+ throw (0, oauth_error_1.invalidGrant)('The Forest server access token is already expired');
123
+ }
124
+ return Math.min(bff_token_1.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN, saasRemaining);
125
+ }
126
+ async function parseTokenRequest(ctx, options) {
127
+ const { body } = ctx.request;
128
+ if (typeof body !== 'object' || body === null) {
129
+ throw (0, oauth_error_1.invalidRequest)('Missing request body');
130
+ }
131
+ const params = body;
132
+ if (params.grant_type !== 'authorization_code') {
133
+ throw (0, oauth_error_1.unsupportedGrantType)('Only authorization_code is supported');
134
+ }
135
+ const request = {
136
+ code: requireBodyString(params, 'code'),
137
+ clientId: requireBodyString(params, 'client_id'),
138
+ codeVerifier: requireBodyString(params, 'code_verifier'),
139
+ redirectUri: requireBodyString(params, 'redirect_uri'),
140
+ };
141
+ assertValidCodeVerifier(request.codeVerifier);
142
+ const client = await options.serverClient.getRegisteredClient(request.clientId);
143
+ if (!client) {
144
+ throw (0, oauth_error_1.invalidClient)('Unknown client_id');
145
+ }
146
+ assertRegisteredRedirectUri(client.redirect_uris, request.redirectUri, oauth_error_1.invalidGrant);
147
+ return request;
148
+ }
149
+ async function exchangeForServerTokens(request, options) {
150
+ try {
151
+ return await options.serverClient.exchangeCode(request);
152
+ }
153
+ catch (error) {
154
+ if (error instanceof forest_server_client_1.OAuthExchangeError) {
155
+ options.logger('Warn', 'Forest server code exchange rejected', { saasError: error.error });
156
+ throw toSafeExchangeError(error.error);
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+ function isRetriableExchangeError(error) {
162
+ return !(error instanceof oauth_error_1.OAuthRequestError) || error.status >= 500;
163
+ }
164
+ async function resolveIdentity(serverTokens, options) {
165
+ try {
166
+ return await options.serverClient.getUserInfo(serverTokens.renderingId, serverTokens.saasAccessToken);
167
+ }
168
+ catch (error) {
169
+ options.logger('Warn', 'Failed to resolve user info after exchange', {
170
+ renderingId: serverTokens.renderingId,
171
+ });
172
+ throw mapIdentityError(error);
173
+ }
174
+ }
175
+ async function handleToken(ctx, options) {
176
+ const request = await parseTokenRequest(ctx, options);
177
+ if (!options.sessionStore.claimAuthorizationCode(request.code)) {
178
+ throw (0, oauth_error_1.invalidGrant)('Authorization code has already been used');
179
+ }
180
+ let serverTokens;
181
+ try {
182
+ serverTokens = await exchangeForServerTokens(request, options);
183
+ }
184
+ catch (error) {
185
+ if (isRetriableExchangeError(error)) {
186
+ options.sessionStore.releaseAuthorizationCode(request.code);
187
+ }
188
+ throw error;
189
+ }
190
+ const expiresInSeconds = computeExpiresIn(serverTokens);
191
+ const user = await resolveIdentity(serverTokens, options);
192
+ const { sid, refreshToken } = options.sessionStore.create({
193
+ saasAccessToken: serverTokens.saasAccessToken,
194
+ saasRefreshToken: serverTokens.saasRefreshToken,
195
+ renderingId: serverTokens.renderingId,
196
+ userId: user.id,
197
+ });
198
+ const accessToken = (0, bff_token_1.issueBffAccessToken)({
199
+ sid,
200
+ user,
201
+ renderingId: serverTokens.renderingId,
202
+ authSecret: options.authSecret,
203
+ expiresInSeconds,
204
+ });
205
+ options.logger('Info', 'Issued BFF session token', {
206
+ renderingId: serverTokens.renderingId,
207
+ userId: user.id,
208
+ });
209
+ ctx.set('Cache-Control', 'no-store');
210
+ ctx.set('Pragma', 'no-cache');
211
+ ctx.status = 200;
212
+ ctx.body = {
213
+ access_token: accessToken,
214
+ token_type: 'Bearer',
215
+ expires_in: expiresInSeconds,
216
+ refresh_token: refreshToken,
217
+ };
218
+ }
219
+ function matchRoute(ctx) {
220
+ if (ctx.method === 'GET' && ctx.path === '/oauth/authorize')
221
+ return handleAuthorize;
222
+ if (ctx.method === 'POST' && ctx.path === '/oauth/token')
223
+ return handleToken;
224
+ return undefined;
225
+ }
226
+ function writeError(ctx, error, options) {
227
+ if (error instanceof oauth_error_1.OAuthRequestError) {
228
+ ctx.status = error.status;
229
+ ctx.body = (0, oauth_error_1.toErrorBody)(error);
230
+ return;
231
+ }
232
+ options.logger('Error', 'OAuth route failure', {
233
+ path: ctx.path,
234
+ cause: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
235
+ });
236
+ ctx.status = 500;
237
+ ctx.body = { error: 'server_error', error_description: 'OAuth processing failed' };
238
+ }
239
+ function createOAuthRoutes(options) {
240
+ return async function oauthRoutes(ctx, next) {
241
+ const handler = matchRoute(ctx);
242
+ if (!handler) {
243
+ await next();
244
+ return;
245
+ }
246
+ try {
247
+ await handler(ctx, options);
248
+ }
249
+ catch (error) {
250
+ writeError(ctx, error, options);
251
+ }
252
+ };
253
+ }
254
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2F1dGgtcm91dGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL29hdXRoL29hdXRoLXJvdXRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQWdXQSxvQ0FnQkM7QUF6V0QsMkNBQW1GO0FBQ25GLGlFQUE0RDtBQUM1RCwrQ0FRdUI7QUFXdkIsTUFBTSxxQkFBcUIsR0FBRyw0QkFBNEIsQ0FBQztBQUMzRCxNQUFNLDJCQUEyQixHQUFHLHFCQUFxQixDQUFDO0FBRTFELE1BQU0sb0JBQW9CLEdBQUcsSUFBSSxHQUFHLENBQUM7SUFDbkMsaUJBQWlCO0lBQ2pCLGdCQUFnQjtJQUNoQixlQUFlO0lBQ2YscUJBQXFCO0lBQ3JCLHdCQUF3QjtJQUN4QixlQUFlO0NBQ2hCLENBQUMsQ0FBQztBQUVILFNBQVMsYUFBYSxDQUFDLEdBQVksRUFBRSxHQUFXO0lBQzlDLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFFN0IsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDekIsTUFBTSxJQUFBLDRCQUFjLEVBQUMsdUNBQXVDLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDckUsQ0FBQztJQUVELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELFNBQVMsaUJBQWlCLENBQUMsSUFBNkIsRUFBRSxHQUFXO0lBQ25FLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUV4QixJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLEtBQUssRUFBRSxFQUFFLENBQUM7UUFDOUMsTUFBTSxJQUFBLDRCQUFjLEVBQUMsK0JBQStCLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDN0QsQ0FBQztJQUVELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELFNBQVMsdUJBQXVCLENBQUMsWUFBb0I7SUFDbkQsSUFBSSxDQUFDLHFCQUFx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
@@ -0,0 +1,8 @@
1
+ export interface PkcePair {
2
+ codeVerifier: string;
3
+ codeChallenge: string;
4
+ }
5
+ export declare function createVerifier(): string;
6
+ export declare function challengeFromVerifier(verifier: string): string;
7
+ export declare function createPkcePair(): PkcePair;
8
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createVerifier = createVerifier;
7
+ exports.challengeFromVerifier = challengeFromVerifier;
8
+ exports.createPkcePair = createPkcePair;
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ function base64url(buffer) {
11
+ return buffer.toString('base64url');
12
+ }
13
+ function createVerifier() {
14
+ return base64url(crypto_1.default.randomBytes(64));
15
+ }
16
+ function challengeFromVerifier(verifier) {
17
+ return base64url(crypto_1.default.createHash('sha256').update(verifier).digest());
18
+ }
19
+ function createPkcePair() {
20
+ const codeVerifier = createVerifier();
21
+ return { codeVerifier, codeChallenge: challengeFromVerifier(codeVerifier) };
22
+ }
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGtjZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vYXV0aC9wa2NlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBV0Esd0NBRUM7QUFFRCxzREFFQztBQUVELHdDQUlDO0FBdkJELG9EQUE0QjtBQU81QixTQUFTLFNBQVMsQ0FBQyxNQUFjO0lBQy9CLE9BQU8sTUFBTSxDQUFDLFFBQVEsQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUN0QyxDQUFDO0FBRUQsU0FBZ0IsY0FBYztJQUM1QixPQUFPLFNBQVMsQ0FBQyxnQkFBTSxDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBQzNDLENBQUM7QUFFRCxTQUFnQixxQkFBcUIsQ0FBQyxRQUFnQjtJQUNwRCxPQUFPLFNBQVMsQ0FBQyxnQkFBTSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztBQUMxRSxDQUFDO0FBRUQsU0FBZ0IsY0FBYztJQUM1QixNQUFNLFlBQVksR0FBRyxjQUFjLEVBQUUsQ0FBQztJQUV0QyxPQUFPLEVBQUUsWUFBWSxFQUFFLGFBQWEsRUFBRSxxQkFBcUIsQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDO0FBQzlFLENBQUMifQ==
@@ -0,0 +1,9 @@
1
+ import type ForestServerClient from './forest-server-client';
2
+ import type { SessionStore } from './session-store';
3
+ export interface EnsureFreshServerAccessParams {
4
+ sid: string;
5
+ store: SessionStore;
6
+ serverClient: ForestServerClient;
7
+ }
8
+ export default function ensureFreshServerAccess({ sid, store, serverClient, }: EnsureFreshServerAccessParams): Promise<string>;
9
+ //# sourceMappingURL=session-lifecycle.d.ts.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = ensureFreshServerAccess;
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const forest_server_client_1 = require("./forest-server-client");
9
+ const oauth_error_1 = require("./oauth-error");
10
+ const CLIENT_ERROR_CODES = new Set(['invalid_grant', 'invalid_request', 'invalid_client']);
11
+ const inFlightRefreshesBySid = new Map();
12
+ function accessTokenExpiry(saasAccessToken) {
13
+ const decoded = jsonwebtoken_1.default.decode(saasAccessToken);
14
+ return decoded?.exp ?? 0;
15
+ }
16
+ async function refreshAndPersist(sid, store, serverClient) {
17
+ const currentRefresh = store.getSaasRefreshToken(sid);
18
+ if (currentRefresh === undefined) {
19
+ throw (0, oauth_error_1.sessionExpired)('Session not found or expired');
20
+ }
21
+ let rotated;
22
+ try {
23
+ rotated = await serverClient.refreshServerToken(currentRefresh);
24
+ }
25
+ catch (error) {
26
+ if (error instanceof forest_server_client_1.OAuthExchangeError && CLIENT_ERROR_CODES.has(error.error)) {
27
+ throw (0, oauth_error_1.sessionExpired)('The Forest server rejected the refresh token');
28
+ }
29
+ throw (0, oauth_error_1.serverError)('Failed to reach the Forest server to refresh the session');
30
+ }
31
+ if (!store.get(sid)) {
32
+ throw (0, oauth_error_1.sessionExpired)('Session expired during token refresh');
33
+ }
34
+ store.updateSaasTokens(sid, {
35
+ saasAccessToken: rotated.saasAccessToken,
36
+ saasRefreshToken: rotated.saasRefreshToken,
37
+ });
38
+ return rotated.saasAccessToken;
39
+ }
40
+ async function ensureFreshServerAccess({ sid, store, serverClient, }) {
41
+ const session = store.get(sid);
42
+ if (!session) {
43
+ throw (0, oauth_error_1.sessionExpired)('Session not found or expired');
44
+ }
45
+ if (accessTokenExpiry(session.saasAccessToken) > Math.floor(Date.now() / 1000)) {
46
+ return session.saasAccessToken;
47
+ }
48
+ const existing = inFlightRefreshesBySid.get(sid);
49
+ if (existing)
50
+ return existing;
51
+ const refresh = refreshAndPersist(sid, store, serverClient).finally(() => {
52
+ inFlightRefreshesBySid.delete(sid);
53
+ });
54
+ inFlightRefreshesBySid.set(sid, refresh);
55
+ return refresh;
56
+ }
57
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vzc2lvbi1saWZlY3ljbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb2F1dGgvc2Vzc2lvbi1saWZlY3ljbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUEyREEsMENBd0JDO0FBaEZELGdFQUF3QztBQUV4QyxpRUFBNEQ7QUFDNUQsK0NBQTREO0FBUTVELE1BQU0sa0JBQWtCLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxlQUFlLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDO0FBRTNGLE1BQU0sc0JBQXNCLEdBQUcsSUFBSSxHQUFHLEVBQTJCLENBQUM7QUFFbEUsU0FBUyxpQkFBaUIsQ0FBQyxlQUF1QjtJQUNoRCxNQUFNLE9BQU8sR0FBRyxzQkFBWSxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQTRCLENBQUM7SUFFaEYsT0FBTyxPQUFPLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUMzQixDQUFDO0FBRUQsS0FBSyxVQUFVLGlCQUFpQixDQUM5QixHQUFXLEVBQ1gsS0FBbUIsRUFDbkIsWUFBZ0M7SUFFaEMsTUFBTSxjQUFjLEdBQUcsS0FBSyxDQUFDLG1CQUFtQixDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRXRELElBQUksY0FBYyxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQ2pDLE1BQU0sSUFBQSw0QkFBYyxFQUFDLDhCQUE4QixDQUFDLENBQUM7SUFDdkQsQ0FBQztJQUVELElBQUksT0FBc0UsQ0FBQztJQUUzRSxJQUFJLENBQUM7UUFDSCxPQUFPLEdBQUcsTUFBTSxZQUFZLENBQUMsa0JBQWtCLENBQUMsY0FBYyxDQUFDLENBQUM7SUFDbEUsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixJQUFJLEtBQUssWUFBWSx5Q0FBa0IsSUFBSSxrQkFBa0IsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDL0UsTUFBTSxJQUFBLDRCQUFjLEVBQUMsOENBQThDLENBQUMsQ0FBQztRQUN2RSxDQUFDO1FBRUQsTUFBTSxJQUFBLHlCQUFXLEVBQUMsMERBQTBELENBQUMsQ0FBQztJQUNoRixDQUFDO0lBRUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUNwQixNQUFNLElBQUEsNEJBQWMsRUFBQyxzQ0FBc0MsQ0FBQyxDQUFDO0lBQy9ELENBQUM7SUFFRCxLQUFLLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxFQUFFO1FBQzFCLGVBQWUsRUFBRSxPQUFPLENBQUMsZUFBZTtRQUN4QyxnQkFBZ0IsRUFBRSxPQUFPLENBQUMsZ0JBQWdCO0tBQzNDLENBQUMsQ0FBQztJQUVILE9BQU8sT0FBTyxDQUFDLGVBQWUsQ0FBQztBQUNqQyxDQUFDO0FBRWMsS0FBSyxVQUFVLHVCQUF1QixDQUFDLEVBQ3BELEdBQUcsRUFDSCxLQUFLLEVBQ0wsWUFBWSxHQUNrQjtJQUM5QixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRS9CLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNiLE1BQU0sSUFBQSw0QkFBYyxFQUFDLDhCQUE4QixDQUFDLENBQUM7SUFDdkQsQ0FBQztJQUVELElBQUksaUJBQWlCLENBQUMsT0FBTyxDQUFDLGVBQWUsQ0FBQyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDL0UsT0FBTyxPQUFPLENBQUMsZUFBZSxDQUFDO0lBQ2pDLENBQUM7SUFFRCxNQUFNLFFBQVEsR0FBRyxzQkFBc0IsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDakQsSUFBSSxRQUFRO1FBQUUsT0FBTyxRQUFRLENBQUM7SUFFOUIsTUFBTSxPQUFPLEdBQUcsaUJBQWlCLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxZQUFZLENBQUMsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFO1FBQ3ZFLHNCQUFzQixDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNyQyxDQUFDLENBQUMsQ0FBQztJQUNILHNCQUFzQixDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFFekMsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQyJ9
@@ -0,0 +1,42 @@
1
+ import type { EncryptedBlob, TokenCipher } from './token-cipher';
2
+ export interface CreateSessionInput {
3
+ saasAccessToken: string;
4
+ saasRefreshToken: string;
5
+ renderingId: number;
6
+ userId: number;
7
+ }
8
+ export interface StoredSession {
9
+ saasAccessToken: string;
10
+ saasRefreshTokenBlob: EncryptedBlob;
11
+ refreshTokenHash: string;
12
+ renderingId: number;
13
+ userId: number;
14
+ expiresAt: number;
15
+ }
16
+ export interface CreatedSession {
17
+ sid: string;
18
+ refreshToken: string;
19
+ }
20
+ export interface UpdateSaasTokensInput {
21
+ saasAccessToken: string;
22
+ saasRefreshToken: string;
23
+ }
24
+ export interface SessionStore {
25
+ create(input: CreateSessionInput): CreatedSession;
26
+ get(sid: string): StoredSession | undefined;
27
+ getSaasRefreshToken(sid: string): string | undefined;
28
+ updateSaasTokens(sid: string, tokens: UpdateSaasTokensInput): void;
29
+ claimAuthorizationCode(code: string): boolean;
30
+ releaseAuthorizationCode(code: string): void;
31
+ pendingClaimCount(): number;
32
+ }
33
+ export interface SessionStoreOptions {
34
+ cipher: TokenCipher;
35
+ now: () => number;
36
+ sessionTtlSeconds: number;
37
+ authCodeTtlSeconds?: number;
38
+ maxPendingCodes?: number;
39
+ }
40
+ export declare function createSid(): string;
41
+ export default function createInMemorySessionStore({ cipher, now, sessionTtlSeconds, authCodeTtlSeconds, maxPendingCodes, }: SessionStoreOptions): SessionStore;
42
+ //# sourceMappingURL=session-store.d.ts.map
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createSid = createSid;
7
+ exports.default = createInMemorySessionStore;
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const DEFAULT_AUTH_CODE_TTL_SECONDS = 5 * 60;
10
+ const DEFAULT_MAX_PENDING_CODES = 10000;
11
+ function createSid() {
12
+ return crypto_1.default.randomBytes(32).toString('base64url');
13
+ }
14
+ function createOpaqueToken() {
15
+ return crypto_1.default.randomBytes(32).toString('base64url');
16
+ }
17
+ function hashToken(token) {
18
+ return crypto_1.default.createHash('sha256').update(token).digest('base64url');
19
+ }
20
+ function createInMemorySessionStore({ cipher, now, sessionTtlSeconds, authCodeTtlSeconds = DEFAULT_AUTH_CODE_TTL_SECONDS, maxPendingCodes = DEFAULT_MAX_PENDING_CODES, }) {
21
+ const sessions = new Map();
22
+ const usedCodes = new Map();
23
+ function purgeExpiredCodes() {
24
+ const current = now();
25
+ for (const [code, expiresAt] of usedCodes) {
26
+ if (current >= expiresAt)
27
+ usedCodes.delete(code);
28
+ }
29
+ }
30
+ function purgeExpiredSessions() {
31
+ const current = now();
32
+ for (const [sid, session] of sessions) {
33
+ if (current >= session.expiresAt)
34
+ sessions.delete(sid);
35
+ }
36
+ }
37
+ function liveSession(sid) {
38
+ const session = sessions.get(sid);
39
+ if (!session)
40
+ return undefined;
41
+ if (now() >= session.expiresAt) {
42
+ sessions.delete(sid);
43
+ return undefined;
44
+ }
45
+ return session;
46
+ }
47
+ return {
48
+ create(input) {
49
+ purgeExpiredSessions();
50
+ const sid = createSid();
51
+ const refreshToken = createOpaqueToken();
52
+ sessions.set(sid, {
53
+ saasAccessToken: input.saasAccessToken,
54
+ saasRefreshTokenBlob: cipher.encrypt(input.saasRefreshToken),
55
+ refreshTokenHash: hashToken(refreshToken),
56
+ renderingId: input.renderingId,
57
+ userId: input.userId,
58
+ expiresAt: now() + sessionTtlSeconds * 1000,
59
+ });
60
+ return { sid, refreshToken };
61
+ },
62
+ get(sid) {
63
+ const session = liveSession(sid);
64
+ return session ? { ...session } : undefined;
65
+ },
66
+ getSaasRefreshToken(sid) {
67
+ const session = liveSession(sid);
68
+ return session ? cipher.decrypt(session.saasRefreshTokenBlob) : undefined;
69
+ },
70
+ updateSaasTokens(sid, tokens) {
71
+ const session = liveSession(sid);
72
+ if (!session)
73
+ return;
74
+ session.saasAccessToken = tokens.saasAccessToken;
75
+ session.saasRefreshTokenBlob = cipher.encrypt(tokens.saasRefreshToken);
76
+ },
77
+ claimAuthorizationCode(code) {
78
+ purgeExpiredCodes();
79
+ if (usedCodes.has(code))
80
+ return false;
81
+ if (usedCodes.size >= maxPendingCodes)
82
+ return false;
83
+ usedCodes.set(code, now() + authCodeTtlSeconds * 1000);
84
+ return true;
85
+ },
86
+ releaseAuthorizationCode(code) {
87
+ usedCodes.delete(code);
88
+ },
89
+ pendingClaimCount() {
90
+ purgeExpiredCodes();
91
+ return usedCodes.size;
92
+ },
93
+ };
94
+ }
95
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vzc2lvbi1zdG9yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vYXV0aC9zZXNzaW9uLXN0b3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBbURBLDhCQUVDO0FBVUQsNkNBa0dDO0FBL0pELG9EQUE0QjtBQThDNUIsTUFBTSw2QkFBNkIsR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDO0FBQzdDLE1BQU0seUJBQXlCLEdBQUcsS0FBTSxDQUFDO0FBRXpDLFNBQWdCLFNBQVM7SUFDdkIsT0FBTyxnQkFBTSxDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUMsQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDLENBQUM7QUFDdEQsQ0FBQztBQUVELFNBQVMsaUJBQWlCO0lBQ3hCLE9BQU8sZ0JBQU0sQ0FBQyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUMsUUFBUSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0FBQ3RELENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBQyxLQUFhO0lBQzlCLE9BQU8sZ0JBQU0sQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUN2RSxDQUFDO0FBRUQsU0FBd0IsMEJBQTBCLENBQUMsRUFDakQsTUFBTSxFQUNOLEdBQUcsRUFDSCxpQkFBaUIsRUFDakIsa0JBQWtCLEdBQUcsNkJBQTZCLEVBQ2xELGVBQWUsR0FBRyx5QkFBeUIsR0FDdkI7SUFDcEIsTUFBTSxRQUFRLEdBQUcsSUFBSSxHQUFHLEVBQXlCLENBQUM7SUFDbEQsTUFBTSxTQUFTLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7SUFFNUMsU0FBUyxpQkFBaUI7UUFDeEIsTUFBTSxPQUFPLEdBQUcsR0FBRyxFQUFFLENBQUM7UUFFdEIsS0FBSyxNQUFNLENBQUMsSUFBSSxFQUFFLFNBQVMsQ0FBQyxJQUFJLFNBQVMsRUFBRSxDQUFDO1lBQzFDLElBQUksT0FBTyxJQUFJLFNBQVM7Z0JBQUUsU0FBUyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNuRCxDQUFDO0lBQ0gsQ0FBQztJQUVELFNBQVMsb0JBQW9CO1FBQzNCLE1BQU0sT0FBTyxHQUFHLEdBQUcsRUFBRSxDQUFDO1FBRXRCLEtBQUssTUFBTSxDQUFDLEdBQUcsRUFBRSxPQUFPLENBQUMsSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUN0QyxJQUFJLE9BQU8sSUFBSSxPQUFPLENBQUMsU0FBUztnQkFBRSxRQUFRLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3pELENBQUM7SUFDSCxDQUFDO0lBRUQsU0FBUyxXQUFXLENBQUMsR0FBVztRQUM5QixNQUFNLE9BQU8sR0FBRyxRQUFRLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2xDLElBQUksQ0FBQyxPQUFPO1lBQUUsT0FBTyxTQUFTLENBQUM7UUFFL0IsSUFBSSxHQUFHLEVBQUUsSUFBSSxPQUFPLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDL0IsUUFBUSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUVyQixPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBRUQsT0FBTyxPQUFPLENBQUM7SUFDakIsQ0FBQztJQUVELE9BQU87UUFDTCxNQUFNLENBQUMsS0FBSztZQUNWLG9CQUFvQixFQUFFLENBQUM7WUFDdkIsTUFBTSxHQUFHLEdBQUcsU0FBUyxFQUFFLENBQUM7WUFDeEIsTUFBTSxZQUFZLEdBQUcsaUJBQWlCLEVBQUUsQ0FBQztZQUV6QyxRQUFRLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRTtnQkFDaEIsZUFBZSxFQUFFLEtBQUssQ0FBQyxlQUFlO2dCQUN0QyxvQkFBb0IsRUFBRSxNQUFNLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxnQkFBZ0IsQ0FBQztnQkFDNUQsZ0JBQWdCLEVBQUUsU0FBUyxDQUFDLFlBQVksQ0FBQztnQkFDekMsV0FBVyxFQUFFLEtBQUssQ0FBQyxXQUFXO2dCQUM5QixNQUFNLEVBQUUsS0FBSyxDQUFDLE1BQU07Z0JBQ3BCLFNBQVMsRUFBRSxHQUFHLEVBQUUsR0FBRyxpQkFBaUIsR0FBRyxJQUFJO2FBQzVDLENBQUMsQ0FBQztZQUVILE9BQU8sRUFBRSxHQUFHLEVBQUUsWUFBWSxFQUFFLENBQUM7UUFDL0IsQ0FBQztRQUVELEdBQUcsQ0FBQyxHQUFHO1lBQ0wsTUFBTSxPQUFPLEdBQUcsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBRWpDLE9BQU8sT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQUcsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztRQUM5QyxDQUFDO1FBRUQsbUJBQW1CLENBQUMsR0FBRztZQUNyQixNQUFNLE9BQU8sR0FBRyxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUM7WUFFakMsT0FBTyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztRQUM1RSxDQUFDO1FBRUQsZ0JBQWdCLENBQUMsR0FBRyxFQUFFLE1BQU07WUFDMUIsTUFBTSxPQUFPLEdBQUcsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ2pDLElBQUksQ0FBQyxPQUFPO2dCQUFFLE9BQU87WUFFckIsT0FBTyxDQUFDLGVBQWUsR0FBRyxNQUFNLENBQUMsZUFBZSxDQUFDO1lBQ2pELE9BQU8sQ0FBQyxvQkFBb0IsR0FBRyxNQUFNLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO1FBQ3pFLENBQUM7UUFFRCxzQkFBc0IsQ0FBQyxJQUFJO1lBQ3pCLGlCQUFpQixFQUFFLENBQUM7WUFDcEIsSUFBSSxTQUFTLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQztnQkFBRSxPQUFPLEtBQUssQ0FBQztZQUV0QyxJQUFJLFNBQVMsQ0FBQyxJQUFJLElBQUksZUFBZTtnQkFBRSxPQUFPLEtBQUssQ0FBQztZQUVwRCxTQUFTLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxHQUFHLEVBQUUsR0FBRyxrQkFBa0IsR0FBRyxJQUFJLENBQUMsQ0FBQztZQUV2RCxPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCx3QkFBd0IsQ0FBQyxJQUFJO1lBQzNCLFNBQVMsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDekIsQ0FBQztRQUVELGlCQUFpQjtZQUNmLGlCQUFpQixFQUFFLENBQUM7WUFFcEIsT0FBTyxTQUFTLENBQUMsSUFBSSxDQUFDO1FBQ3hCLENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQyJ9
@@ -0,0 +1,11 @@
1
+ export interface EncryptedBlob {
2
+ iv: string;
3
+ authTag: string;
4
+ ciphertext: string;
5
+ }
6
+ export interface TokenCipher {
7
+ encrypt(plaintext: string): EncryptedBlob;
8
+ decrypt(blob: EncryptedBlob): string;
9
+ }
10
+ export default function createTokenCipher(base64Key: string): TokenCipher;
11
+ //# sourceMappingURL=token-cipher.d.ts.map
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = createTokenCipher;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const ALGORITHM = 'aes-256-gcm';
9
+ const IV_BYTES = 12;
10
+ function bytes(value) {
11
+ return new Uint8Array(typeof value === 'string' ? Buffer.from(value, 'base64') : value);
12
+ }
13
+ function toBase64(value) {
14
+ return Buffer.from(value).toString('base64');
15
+ }
16
+ function createTokenCipher(base64Key) {
17
+ const key = bytes(base64Key);
18
+ return {
19
+ encrypt(plaintext) {
20
+ const iv = new Uint8Array(crypto_1.default.randomBytes(IV_BYTES));
21
+ const cipher = crypto_1.default.createCipheriv(ALGORITHM, key, iv);
22
+ const ciphertext = new Uint8Array(Buffer.concat([
23
+ new Uint8Array(cipher.update(plaintext, 'utf8')),
24
+ new Uint8Array(cipher.final()),
25
+ ]));
26
+ return {
27
+ iv: toBase64(iv),
28
+ authTag: toBase64(new Uint8Array(cipher.getAuthTag())),
29
+ ciphertext: toBase64(ciphertext),
30
+ };
31
+ },
32
+ decrypt({ iv, authTag, ciphertext }) {
33
+ const decipher = crypto_1.default.createDecipheriv(ALGORITHM, key, bytes(iv));
34
+ decipher.setAuthTag(bytes(authTag));
35
+ return Buffer.concat([
36
+ new Uint8Array(decipher.update(bytes(ciphertext))),
37
+ new Uint8Array(decipher.final()),
38
+ ]).toString('utf8');
39
+ },
40
+ };
41
+ }
42
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW4tY2lwaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL29hdXRoL3Rva2VuLWNpcGhlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQXdCQSxvQ0E4QkM7QUF0REQsb0RBQTRCO0FBRTVCLE1BQU0sU0FBUyxHQUFHLGFBQWEsQ0FBQztBQUNoQyxNQUFNLFFBQVEsR0FBRyxFQUFFLENBQUM7QUFhcEIsU0FBUyxLQUFLLENBQUMsS0FBMEI7SUFDdkMsT0FBTyxJQUFJLFVBQVUsQ0FBQyxPQUFPLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUMxRixDQUFDO0FBRUQsU0FBUyxRQUFRLENBQUMsS0FBaUI7SUFDakMsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUMvQyxDQUFDO0FBRUQsU0FBd0IsaUJBQWlCLENBQUMsU0FBaUI7SUFDekQsTUFBTSxHQUFHLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBRTdCLE9BQU87UUFDTCxPQUFPLENBQUMsU0FBUztZQUNmLE1BQU0sRUFBRSxHQUFHLElBQUksVUFBVSxDQUFDLGdCQUFNLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUM7WUFDeEQsTUFBTSxNQUFNLEdBQUcsZ0JBQU0sQ0FBQyxjQUFjLENBQUMsU0FBUyxFQUFFLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztZQUN6RCxNQUFNLFVBQVUsR0FBRyxJQUFJLFVBQVUsQ0FDL0IsTUFBTSxDQUFDLE1BQU0sQ0FBQztnQkFDWixJQUFJLFVBQVUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxNQUFNLENBQUMsQ0FBQztnQkFDaEQsSUFBSSxVQUFVLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO2FBQy9CLENBQUMsQ0FDSCxDQUFDO1lBRUYsT0FBTztnQkFDTCxFQUFFLEVBQUUsUUFBUSxDQUFDLEVBQUUsQ0FBQztnQkFDaEIsT0FBTyxFQUFFLFFBQVEsQ0FBQyxJQUFJLFVBQVUsQ0FBQyxNQUFNLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztnQkFDdEQsVUFBVSxFQUFFLFFBQVEsQ0FBQyxVQUFVLENBQUM7YUFDakMsQ0FBQztRQUNKLENBQUM7UUFDRCxPQUFPLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRTtZQUNqQyxNQUFNLFFBQVEsR0FBRyxnQkFBTSxDQUFDLGdCQUFnQixDQUFDLFNBQVMsRUFBRSxHQUFHLEVBQUUsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7WUFDcEUsUUFBUSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUVwQyxPQUFPLE1BQU0sQ0FBQyxNQUFNLENBQUM7Z0JBQ25CLElBQUksVUFBVSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUM7Z0JBQ2xELElBQUksVUFBVSxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsQ0FBQzthQUNqQyxDQUFDLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQ3RCLENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQyJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestadmin/agent-bff",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "forest-bff": "dist/cli.js"
@@ -31,10 +31,14 @@
31
31
  "test": "jest"
32
32
  },
33
33
  "dependencies": {
34
+ "@forestadmin/forestadmin-client": "1.40.3",
35
+ "@koa/bodyparser": "^6.1.0",
36
+ "jsonwebtoken": "^9.0.3",
34
37
  "koa": "^3.0.1",
35
38
  "zod": "4.3.6"
36
39
  },
37
40
  "devDependencies": {
41
+ "@types/jsonwebtoken": "^9.0.1",
38
42
  "@types/koa": "^2.13.5",
39
43
  "@types/supertest": "^6.0.2",
40
44
  "supertest": "^7.1.3"