@forestadmin/agent-bff 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -5
- package/dist/agent/agent-stub.d.ts +3 -0
- package/dist/agent/agent-stub.js +19 -0
- package/dist/agent/build-agent-query.d.ts +8 -0
- package/dist/agent/build-agent-query.js +7 -0
- package/dist/api-key/api-key-middleware.js +17 -22
- package/dist/auth/auth-mode-middleware.d.ts +7 -0
- package/dist/auth/auth-mode-middleware.js +53 -0
- package/dist/auth/auth-mode.d.ts +7 -0
- package/dist/auth/auth-mode.js +25 -0
- package/dist/cli-core.js +60 -4
- package/dist/config/env-config.d.ts +3 -0
- package/dist/config/env-config.js +15 -1
- package/dist/cors/cors-middleware.d.ts +9 -0
- package/dist/cors/cors-middleware.js +29 -0
- package/dist/cors/origin.d.ts +7 -0
- package/dist/cors/origin.js +47 -0
- package/dist/cors/per-key-origin.d.ts +3 -0
- package/dist/cors/per-key-origin.js +16 -0
- package/dist/http/bff-http-error.d.ts +34 -0
- package/dist/http/bff-http-error.js +55 -0
- package/dist/http/error-middleware.d.ts +7 -0
- package/dist/http/error-middleware.js +49 -0
- package/dist/timezone/timezone-middleware.d.ts +7 -0
- package/dist/timezone/timezone-middleware.js +24 -0
- package/dist/timezone/timezone.d.ts +8 -0
- package/dist/timezone/timezone.js +35 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
Standalone REST BFF (Backend-For-Frontend) that lets a trusted third-party UI call a Forest Admin
|
|
4
4
|
agent from a browser without learning MCP or JSON:API.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
It is a bootable Koa 3 server with a `/health` endpoint, a version header, env-driven config
|
|
7
|
+
validation, OAuth (Mode 1) + API-key (Mode 2) auth, and a hardened request edge (timezone, CORS,
|
|
8
|
+
auth-mode precedence, structured error contract). The data-endpoint proxy and OpenAPI generation
|
|
9
|
+
land in later slices.
|
|
9
10
|
|
|
10
11
|
## Usage
|
|
11
12
|
|
|
@@ -34,14 +35,61 @@ yarn start:dev # node --env-file=.env dist/cli.js
|
|
|
34
35
|
| `FOREST_SERVER_URL` | yes | Forest SaaS API base URL. |
|
|
35
36
|
| `FOREST_APP_URL` | yes | Forest front base URL (OAuth front-channel, later slices). |
|
|
36
37
|
| `AGENT_URL` | yes | The customer agent base URL the BFF calls via agent-client. |
|
|
38
|
+
| `BFF_TOKEN_ENCRYPTION_KEY`| for OAuth | Base64-encoded 32-byte AES-256 key encrypting stored refresh tokens. Until it is set, the `/oauth/*` token-issuance routes are disabled and `/health` reports `degraded`; already-issued `bff_access` tokens still authenticate on `/agent/*` whenever `FOREST_AUTH_SECRET` is present. |
|
|
37
39
|
| `HTTP_PORT` | no | Server port, integer 0–65535. Defaults to `3450`. `0` binds an OS-assigned ephemeral port. |
|
|
40
|
+
| `BFF_ALLOWED_ORIGINS`| no | Comma-separated CORS allow-list of exact origins (scheme + host + port). No wildcard. Empty ⇒ no cross-origin browser access. |
|
|
41
|
+
| `BFF_DEFAULT_TIMEZONE`| no | Fallback IANA timezone used when a request carries neither an `X-Forest-Timezone` header nor a body `timezone`. |
|
|
38
42
|
|
|
39
43
|
### Config validation
|
|
40
44
|
|
|
41
|
-
- A malformed value (a non-http(s) `*_URL`, a `HTTP_PORT` that is not a decimal integer in 0–65535
|
|
42
|
-
fails fast at boot: the process exits with a clear error and
|
|
45
|
+
- A malformed value (a non-http(s) `*_URL`, a `HTTP_PORT` that is not a decimal integer in 0–65535,
|
|
46
|
+
a non-IANA `BFF_DEFAULT_TIMEZONE`) fails fast at boot: the process exits with a clear error and
|
|
47
|
+
never echoes the offending value.
|
|
43
48
|
- A required var that is absent (or empty / whitespace-only) does not crash the server. It boots and
|
|
44
49
|
reports the gap through `/health` (503 `degraded`).
|
|
50
|
+
- Malformed `BFF_ALLOWED_ORIGINS` entries (including a literal `*`) are dropped and logged once at
|
|
51
|
+
boot (`Warn`); they never enter the allow-list, so a wildcard origin can never be served.
|
|
52
|
+
|
|
53
|
+
## Request edge (`/agent/*`)
|
|
54
|
+
|
|
55
|
+
Every agent call flows through a request edge that enforces three cross-cutting concerns before the
|
|
56
|
+
(Slice-3) proxy runs. Errors use a structured, type-first contract — `{ error: { type, status,
|
|
57
|
+
message, details? } }` — so consumers branch on `error.type`, never on message text.
|
|
58
|
+
|
|
59
|
+
### Auth-mode precedence
|
|
60
|
+
|
|
61
|
+
| Presented credentials | Result |
|
|
62
|
+
| ---------------------------------------------- | ------------------------------- |
|
|
63
|
+
| `Authorization: Bearer <bff_access>` | Mode 1 (OAuth session) |
|
|
64
|
+
| `X-Forest-Bff-Key` | Mode 2 (API key) |
|
|
65
|
+
| both | `400 ambiguous_credentials` |
|
|
66
|
+
| neither | `401 unauthorized` |
|
|
67
|
+
|
|
68
|
+
A Mode 1 `bff_access` that is malformed or wrong-typed → `401 unauthorized`; one that is validly
|
|
69
|
+
signed but expired → `401 session_expired` (the client should refresh). Refresh-token reuse remains
|
|
70
|
+
a `POST /oauth/token` concern (`session_invalidated`, OAuth/RFC shape).
|
|
71
|
+
|
|
72
|
+
### CORS
|
|
73
|
+
|
|
74
|
+
Two layers, both driven by exact-origin matching (case-insensitive scheme/host, default ports
|
|
75
|
+
normalized away, no trailing slash, no wildcard, no subdomain matching):
|
|
76
|
+
|
|
77
|
+
- **Layer 1 (transport)** — the only layer that sets `Access-Control-Allow-Origin`. An origin in
|
|
78
|
+
`BFF_ALLOWED_ORIGINS` is echoed back exactly; anything else gets no CORS headers (the browser
|
|
79
|
+
blocks). Applies to `POST /oauth/token` too. Preflight `OPTIONS` from an allow-listed origin gets
|
|
80
|
+
the allowed methods + headers; credentials are never enabled.
|
|
81
|
+
- **Layer 2 (per-key authorization, Mode 2 only)** — when the resolved key has a non-empty
|
|
82
|
+
`allowedOrigins`, the request `Origin` must also be in that list (a missing `Origin` is rejected),
|
|
83
|
+
else `403 origin_not_allowed`. An empty per-key list is a no-op.
|
|
84
|
+
|
|
85
|
+
**Local development:** browsers still enforce CORS against `localhost`, so add your dev origin(s) to
|
|
86
|
+
`BFF_ALLOWED_ORIGINS` (e.g. `BFF_ALLOWED_ORIGINS=http://localhost:4200`) — there is no dev bypass.
|
|
87
|
+
|
|
88
|
+
### Timezone
|
|
89
|
+
|
|
90
|
+
The BFF always forwards an explicit `timezone` to the agent, resolved in order: (1) `X-Forest-Timezone`
|
|
91
|
+
header, (2) body `timezone` field, (3) `BFF_DEFAULT_TIMEZONE`. None → `400 missing_timezone`; a
|
|
92
|
+
non-IANA value → `400 invalid_timezone`.
|
|
45
93
|
|
|
46
94
|
## Sessions & token rotation
|
|
47
95
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = createAgentStubMiddleware;
|
|
4
|
+
const build_agent_query_1 = require("./build-agent-query");
|
|
5
|
+
function createAgentStubMiddleware() {
|
|
6
|
+
return async function agentStubMiddleware(ctx) {
|
|
7
|
+
const query = (0, build_agent_query_1.buildAgentQuery)({ timezone: ctx.state.timezone });
|
|
8
|
+
ctx.state.agentQuery = query;
|
|
9
|
+
ctx.status = 501;
|
|
10
|
+
ctx.body = {
|
|
11
|
+
error: {
|
|
12
|
+
type: 'not_implemented',
|
|
13
|
+
status: 501,
|
|
14
|
+
message: 'Agent proxy is not implemented yet',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWdlbnQtc3R1Yi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hZ2VudC9hZ2VudC1zdHViLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBSUEsNENBY0M7QUFoQkQsMkRBQXNEO0FBRXRELFNBQXdCLHlCQUF5QjtJQUMvQyxPQUFPLEtBQUssVUFBVSxtQkFBbUIsQ0FBQyxHQUFHO1FBQzNDLE1BQU0sS0FBSyxHQUFHLElBQUEsbUNBQWUsRUFBQyxFQUFFLFFBQVEsRUFBRSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQWtCLEVBQUUsQ0FBQyxDQUFDO1FBQzFFLEdBQUcsQ0FBQyxLQUFLLENBQUMsVUFBVSxHQUFHLEtBQUssQ0FBQztRQUU3QixHQUFHLENBQUMsTUFBTSxHQUFHLEdBQUcsQ0FBQztRQUNqQixHQUFHLENBQUMsSUFBSSxHQUFHO1lBQ1QsS0FBSyxFQUFFO2dCQUNMLElBQUksRUFBRSxpQkFBaUI7Z0JBQ3ZCLE1BQU0sRUFBRSxHQUFHO2dCQUNYLE9BQU8sRUFBRSxvQ0FBb0M7YUFDOUM7U0FDRixDQUFDO0lBQ0osQ0FBQyxDQUFDO0FBQ0osQ0FBQyJ9
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAgentQuery = buildAgentQuery;
|
|
4
|
+
function buildAgentQuery({ timezone }) {
|
|
5
|
+
return { timezone };
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGQtYWdlbnQtcXVlcnkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWdlbnQvYnVpbGQtYWdlbnQtcXVlcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFRQSwwQ0FFQztBQUZELFNBQWdCLGVBQWUsQ0FBQyxFQUFFLFFBQVEsRUFBeUI7SUFDakUsT0FBTyxFQUFFLFFBQVEsRUFBRSxDQUFDO0FBQ3RCLENBQUMifQ==
|
|
@@ -5,25 +5,9 @@ exports.default = createApiKeyMiddleware;
|
|
|
5
5
|
const api_key_1 = require("./api-key");
|
|
6
6
|
const api_key_error_1 = require("./api-key-error");
|
|
7
7
|
exports.BFF_KEY_HEADER = 'X-Forest-Bff-Key';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ctx.set('Retry-After', String(error.retryAfter));
|
|
12
|
-
ctx.status = error.status;
|
|
13
|
-
ctx.body = (0, api_key_error_1.toErrorBody)(error);
|
|
14
|
-
logger('Warn', 'BFF API key rejected', {
|
|
15
|
-
keyHash: (0, api_key_1.fingerprintApiKey)(rawKey),
|
|
16
|
-
type: error.type,
|
|
17
|
-
});
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
logger('Error', 'BFF API key middleware failure', {
|
|
21
|
-
keyHash: (0, api_key_1.fingerprintApiKey)(rawKey),
|
|
22
|
-
cause: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
23
|
-
});
|
|
24
|
-
ctx.status = 500;
|
|
25
|
-
ctx.body = { error: { type: 'server_error', status: 500, message: 'API key processing failed' } };
|
|
26
|
-
}
|
|
8
|
+
// On authentication failure this middleware rethrows `ApiKeyError` rather than
|
|
9
|
+
// writing the response itself; it must be mounted behind an error middleware
|
|
10
|
+
// that serializes the structured body (see `createErrorMiddleware`).
|
|
27
11
|
function createApiKeyMiddleware({ authenticator, logger, }) {
|
|
28
12
|
return async function apiKeyMiddleware(ctx, next) {
|
|
29
13
|
const rawKey = ctx.get(exports.BFF_KEY_HEADER);
|
|
@@ -36,8 +20,19 @@ function createApiKeyMiddleware({ authenticator, logger, }) {
|
|
|
36
20
|
authenticated = await authenticator.authenticate(rawKey);
|
|
37
21
|
}
|
|
38
22
|
catch (error) {
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
if (error instanceof api_key_error_1.ApiKeyError) {
|
|
24
|
+
logger('Warn', 'BFF API key rejected', {
|
|
25
|
+
keyHash: (0, api_key_1.fingerprintApiKey)(rawKey),
|
|
26
|
+
type: error.type,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
logger('Error', 'BFF API key middleware failure', {
|
|
31
|
+
keyHash: (0, api_key_1.fingerprintApiKey)(rawKey),
|
|
32
|
+
cause: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
41
36
|
}
|
|
42
37
|
ctx.state.agentToken = authenticated.agentToken;
|
|
43
38
|
ctx.state.apiKeyIdentity = authenticated.identity;
|
|
@@ -49,4 +44,4 @@ function createApiKeyMiddleware({ authenticator, logger, }) {
|
|
|
49
44
|
await next();
|
|
50
45
|
};
|
|
51
46
|
}
|
|
52
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLWtleS1taWRkbGV3YXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS1rZXkvYXBpLWtleS1taWRkbGV3YXJlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQWlCQSx5Q0EyQ0M7QUF4REQsdUNBQThDO0FBQzlDLG1EQUE4QztBQUVqQyxRQUFBLGNBQWMsR0FBRyxrQkFBa0IsQ0FBQztBQU9qRCwrRUFBK0U7QUFDL0UsNkVBQTZFO0FBQzdFLHFFQUFxRTtBQUNyRSxTQUF3QixzQkFBc0IsQ0FBQyxFQUM3QyxhQUFhLEVBQ2IsTUFBTSxHQUNrQjtJQUN4QixPQUFPLEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxHQUFHLEVBQUUsSUFBSTtRQUM5QyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLHNCQUFjLENBQUMsQ0FBQztRQUV2QyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDWixNQUFNLElBQUksRUFBRSxDQUFDO1lBRWIsT0FBTztRQUNULENBQUM7UUFFRCxJQUFJLGFBQWtDLENBQUM7UUFFdkMsSUFBSSxDQUFDO1lBQ0gsYUFBYSxHQUFHLE1BQU0sYUFBYSxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUMzRCxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLElBQUksS0FBSyxZQUFZLDJCQUFXLEVBQUUsQ0FBQztnQkFDakMsTUFBTSxDQUFDLE1BQU0sRUFBRSxzQkFBc0IsRUFBRTtvQkFDckMsT0FBTyxFQUFFLElBQUEsMkJBQWlCLEVBQUMsTUFBTSxDQUFDO29CQUNsQyxJQUFJLEVBQUUsS0FBSyxDQUFDLElBQUk7aUJBQ2pCLENBQUMsQ0FBQztZQUNMLENBQUM7aUJBQU0sQ0FBQztnQkFDTixNQUFNLENBQUMsT0FBTyxFQUFFLGdDQUFnQyxFQUFFO29CQUNoRCxPQUFPLEVBQUUsSUFBQSwyQkFBaUIsRUFBQyxNQUFNLENBQUM7b0JBQ2xDLEtBQUssRUFBRSxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxHQUFHLEtBQUssQ0FBQyxJQUFJLEtBQUssS0FBSyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDO2lCQUNsRixDQUFDLENBQUM7WUFDTCxDQUFDO1lBRUQsTUFBTSxLQUFLLENBQUM7UUFDZCxDQUFDO1FBRUQsR0FBRyxDQUFDLEtBQUssQ0FBQyxVQUFVLEdBQUcsYUFBYSxDQUFDLFVBQVUsQ0FBQztRQUNoRCxHQUFHLENBQUMsS0FBSyxDQUFDLGNBQWMsR0FBRyxhQUFhLENBQUMsUUFBUSxDQUFDO1FBQ2xELEdBQUcsQ0FBQyxHQUFHLENBQUMsZUFBZSxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQ3JDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsc0JBQXNCLEVBQUU7WUFDckMsT0FBTyxFQUFFLElBQUEsMkJBQWlCLEVBQUMsTUFBTSxDQUFDO1lBQ2xDLFdBQVcsRUFBRSxhQUFhLENBQUMsUUFBUSxDQUFDLFdBQVc7U0FDaEQsQ0FBQyxDQUFDO1FBRUgsTUFBTSxJQUFJLEVBQUUsQ0FBQztJQUNmLENBQUMsQ0FBQztBQUNKLENBQUMifQ==
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Middleware } from 'koa';
|
|
2
|
+
export declare const BFF_KEY_HEADER = "X-Forest-Bff-Key";
|
|
3
|
+
export interface AuthModeMiddlewareOptions {
|
|
4
|
+
authSecret: string;
|
|
5
|
+
}
|
|
6
|
+
export default function createAuthModeMiddleware({ authSecret, }: AuthModeMiddlewareOptions): Middleware;
|
|
7
|
+
//# sourceMappingURL=auth-mode-middleware.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
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_KEY_HEADER = void 0;
|
|
7
|
+
exports.default = createAuthModeMiddleware;
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
const auth_mode_1 = require("./auth-mode");
|
|
10
|
+
const bff_http_error_1 = require("../http/bff-http-error");
|
|
11
|
+
const bff_token_1 = require("../oauth/bff-token");
|
|
12
|
+
exports.BFF_KEY_HEADER = 'X-Forest-Bff-Key';
|
|
13
|
+
function verifyBffAccess(token, authSecret) {
|
|
14
|
+
let decoded;
|
|
15
|
+
// Verify the signature first but ignore expiration, so the token type can be
|
|
16
|
+
// checked before expiry: a wrong-typed token must be `unauthorized`, and only
|
|
17
|
+
// a genuine expired `bff_access` should map to `session_expired`.
|
|
18
|
+
try {
|
|
19
|
+
decoded = jsonwebtoken_1.default.verify(token, authSecret, {
|
|
20
|
+
algorithms: ['HS256'],
|
|
21
|
+
ignoreExpiration: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw (0, bff_http_error_1.unauthorized)();
|
|
26
|
+
}
|
|
27
|
+
if (typeof decoded !== 'object' ||
|
|
28
|
+
decoded === null ||
|
|
29
|
+
decoded.type !== bff_token_1.BFF_ACCESS_TOKEN_TYPE) {
|
|
30
|
+
throw (0, bff_http_error_1.unauthorized)();
|
|
31
|
+
}
|
|
32
|
+
const { exp } = decoded;
|
|
33
|
+
if (typeof exp !== 'number') {
|
|
34
|
+
throw (0, bff_http_error_1.unauthorized)();
|
|
35
|
+
}
|
|
36
|
+
if (exp * 1000 <= Date.now()) {
|
|
37
|
+
throw (0, bff_http_error_1.sessionExpired)();
|
|
38
|
+
}
|
|
39
|
+
return decoded;
|
|
40
|
+
}
|
|
41
|
+
function createAuthModeMiddleware({ authSecret, }) {
|
|
42
|
+
return async function authModeMiddleware(ctx, next) {
|
|
43
|
+
const bearer = (0, auth_mode_1.extractBearerToken)(ctx.get('Authorization'));
|
|
44
|
+
const apiKey = ctx.get(exports.BFF_KEY_HEADER);
|
|
45
|
+
const mode = (0, auth_mode_1.resolveAuthMode)({ hasBearer: bearer !== undefined, hasApiKey: apiKey !== '' });
|
|
46
|
+
ctx.state.authMode = mode;
|
|
47
|
+
if (mode === 'oauth') {
|
|
48
|
+
ctx.state.principal = verifyBffAccess(bearer, authSecret);
|
|
49
|
+
}
|
|
50
|
+
await next();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aC1tb2RlLW1pZGRsZXdhcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXV0aC9hdXRoLW1vZGUtbWlkZGxld2FyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFtREEsMkNBaUJDO0FBakVELGdFQUF3QztBQUV4QywyQ0FBa0U7QUFDbEUsMkRBQXNFO0FBQ3RFLGtEQUEyRDtBQUU5QyxRQUFBLGNBQWMsR0FBRyxrQkFBa0IsQ0FBQztBQU1qRCxTQUFTLGVBQWUsQ0FBQyxLQUFhLEVBQUUsVUFBa0I7SUFDeEQsSUFBSSxPQUFnQixDQUFDO0lBRXJCLDZFQUE2RTtJQUM3RSw4RUFBOEU7SUFDOUUsa0VBQWtFO0lBQ2xFLElBQUksQ0FBQztRQUNILE9BQU8sR0FBRyxzQkFBWSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFO1lBQy9DLFVBQVUsRUFBRSxDQUFDLE9BQU8sQ0FBQztZQUNyQixnQkFBZ0IsRUFBRSxJQUFJO1NBQ3ZCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxNQUFNLElBQUEsNkJBQVksR0FBRSxDQUFDO0lBQ3ZCLENBQUM7SUFFRCxJQUNFLE9BQU8sT0FBTyxLQUFLLFFBQVE7UUFDM0IsT0FBTyxLQUFLLElBQUk7UUFDZixPQUE4QixDQUFDLElBQUksS0FBSyxpQ0FBcUIsRUFDOUQsQ0FBQztRQUNELE1BQU0sSUFBQSw2QkFBWSxHQUFFLENBQUM7SUFDdkIsQ0FBQztJQUVELE1BQU0sRUFBRSxHQUFHLEVBQUUsR0FBRyxPQUE0QixDQUFDO0lBRTdDLElBQUksT0FBTyxHQUFHLEtBQUssUUFBUSxFQUFFLENBQUM7UUFDNUIsTUFBTSxJQUFBLDZCQUFZLEdBQUUsQ0FBQztJQUN2QixDQUFDO0lBRUQsSUFBSSxHQUFHLEdBQUcsSUFBSSxJQUFJLElBQUksQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDO1FBQzdCLE1BQU0sSUFBQSwrQkFBYyxHQUFFLENBQUM7SUFDekIsQ0FBQztJQUVELE9BQU8sT0FBZ0MsQ0FBQztBQUMxQyxDQUFDO0FBRUQsU0FBd0Isd0JBQXdCLENBQUMsRUFDL0MsVUFBVSxHQUNnQjtJQUMxQixPQUFPLEtBQUssVUFBVSxrQkFBa0IsQ0FBQyxHQUFHLEVBQUUsSUFBSTtRQUNoRCxNQUFNLE1BQU0sR0FBRyxJQUFBLDhCQUFrQixFQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQztRQUM1RCxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLHNCQUFjLENBQUMsQ0FBQztRQUV2QyxNQUFNLElBQUksR0FBRyxJQUFBLDJCQUFlLEVBQUMsRUFBRSxTQUFTLEVBQUUsTUFBTSxLQUFLLFNBQVMsRUFBRSxTQUFTLEVBQUUsTUFBTSxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFFNUYsR0FBRyxDQUFDLEtBQUssQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDO1FBRTFCLElBQUksSUFBSSxLQUFLLE9BQU8sRUFBRSxDQUFDO1lBQ3JCLEdBQUcsQ0FBQyxLQUFLLENBQUMsU0FBUyxHQUFHLGVBQWUsQ0FBQyxNQUFnQixFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQ3RFLENBQUM7UUFFRCxNQUFNLElBQUksRUFBRSxDQUFDO0lBQ2YsQ0FBQyxDQUFDO0FBQ0osQ0FBQyJ9
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type AuthMode = 'oauth' | 'api-key';
|
|
2
|
+
export declare function extractBearerToken(authorization: string | undefined): string | undefined;
|
|
3
|
+
export declare function resolveAuthMode({ hasBearer, hasApiKey, }: {
|
|
4
|
+
hasBearer: boolean;
|
|
5
|
+
hasApiKey: boolean;
|
|
6
|
+
}): AuthMode;
|
|
7
|
+
//# sourceMappingURL=auth-mode.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractBearerToken = extractBearerToken;
|
|
4
|
+
exports.resolveAuthMode = resolveAuthMode;
|
|
5
|
+
const bff_http_error_1 = require("../http/bff-http-error");
|
|
6
|
+
const BEARER_PATTERN = /^Bearer[ \t]+(.+)$/i;
|
|
7
|
+
function extractBearerToken(authorization) {
|
|
8
|
+
if (!authorization)
|
|
9
|
+
return undefined;
|
|
10
|
+
const match = BEARER_PATTERN.exec(authorization.trim());
|
|
11
|
+
if (!match)
|
|
12
|
+
return undefined;
|
|
13
|
+
const token = match[1].trim();
|
|
14
|
+
return token === '' ? undefined : token;
|
|
15
|
+
}
|
|
16
|
+
function resolveAuthMode({ hasBearer, hasApiKey, }) {
|
|
17
|
+
if (hasBearer && hasApiKey)
|
|
18
|
+
throw (0, bff_http_error_1.ambiguousCredentials)();
|
|
19
|
+
if (hasApiKey)
|
|
20
|
+
return 'api-key';
|
|
21
|
+
if (hasBearer)
|
|
22
|
+
return 'oauth';
|
|
23
|
+
throw (0, bff_http_error_1.unauthorized)();
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aC1tb2RlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2F1dGgvYXV0aC1tb2RlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBTUEsZ0RBU0M7QUFFRCwwQ0FZQztBQTdCRCwyREFBNEU7QUFJNUUsTUFBTSxjQUFjLEdBQUcscUJBQXFCLENBQUM7QUFFN0MsU0FBZ0Isa0JBQWtCLENBQUMsYUFBaUM7SUFDbEUsSUFBSSxDQUFDLGFBQWE7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUVyQyxNQUFNLEtBQUssR0FBRyxjQUFjLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3hELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFFN0IsTUFBTSxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0lBRTlCLE9BQU8sS0FBSyxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUM7QUFDMUMsQ0FBQztBQUVELFNBQWdCLGVBQWUsQ0FBQyxFQUM5QixTQUFTLEVBQ1QsU0FBUyxHQUlWO0lBQ0MsSUFBSSxTQUFTLElBQUksU0FBUztRQUFFLE1BQU0sSUFBQSxxQ0FBb0IsR0FBRSxDQUFDO0lBQ3pELElBQUksU0FBUztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQ2hDLElBQUksU0FBUztRQUFFLE9BQU8sT0FBTyxDQUFDO0lBRTlCLE1BQU0sSUFBQSw2QkFBWSxHQUFFLENBQUM7QUFDdkIsQ0FBQyJ9
|
package/dist/cli-core.js
CHANGED
|
@@ -7,20 +7,48 @@ exports.default = runCli;
|
|
|
7
7
|
exports.reportFatalError = reportFatalError;
|
|
8
8
|
const bodyparser_1 = require("@koa/bodyparser");
|
|
9
9
|
const console_logger_1 = __importDefault(require("./adapters/console-logger"));
|
|
10
|
+
const agent_stub_1 = __importDefault(require("./agent/agent-stub"));
|
|
10
11
|
const api_key_authenticator_1 = __importDefault(require("./api-key/api-key-authenticator"));
|
|
11
12
|
const api_key_client_1 = __importDefault(require("./api-key/api-key-client"));
|
|
12
13
|
const api_key_middleware_1 = __importDefault(require("./api-key/api-key-middleware"));
|
|
13
14
|
const resolve_cache_1 = __importDefault(require("./api-key/resolve-cache"));
|
|
15
|
+
const auth_mode_middleware_1 = __importDefault(require("./auth/auth-mode-middleware"));
|
|
14
16
|
const env_config_1 = require("./config/env-config");
|
|
17
|
+
const cors_middleware_1 = __importDefault(require("./cors/cors-middleware"));
|
|
18
|
+
const per_key_origin_1 = __importDefault(require("./cors/per-key-origin"));
|
|
15
19
|
const errors_1 = require("./errors");
|
|
20
|
+
const bff_http_error_1 = require("./http/bff-http-error");
|
|
16
21
|
const bff_http_server_1 = __importDefault(require("./http/bff-http-server"));
|
|
22
|
+
const error_middleware_1 = __importDefault(require("./http/error-middleware"));
|
|
17
23
|
const forest_server_client_1 = __importDefault(require("./oauth/forest-server-client"));
|
|
18
24
|
const oauth_routes_1 = __importDefault(require("./oauth/oauth-routes"));
|
|
19
25
|
const session_store_1 = __importDefault(require("./oauth/session-store"));
|
|
20
26
|
const token_cipher_1 = __importDefault(require("./oauth/token-cipher"));
|
|
27
|
+
const timezone_middleware_1 = __importDefault(require("./timezone/timezone-middleware"));
|
|
21
28
|
const version_1 = __importDefault(require("./version"));
|
|
22
29
|
const BODY_LIMIT = '16kb';
|
|
23
30
|
const SESSION_TTL_SECONDS = 24 * 60 * 60;
|
|
31
|
+
function isAgentPath(path) {
|
|
32
|
+
return path === '/agent' || path.startsWith('/agent/');
|
|
33
|
+
}
|
|
34
|
+
function agentScoped(middleware) {
|
|
35
|
+
return async function scoped(ctx, next) {
|
|
36
|
+
if (!isAgentPath(ctx.path)) {
|
|
37
|
+
await next();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await middleware(ctx, next);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function createApiKeyUnavailableGuard(logger) {
|
|
44
|
+
return async function apiKeyUnavailableGuard(ctx, next) {
|
|
45
|
+
if (ctx.state.authMode === 'api-key') {
|
|
46
|
+
logger('Error', 'API key auth requested but the resolver is not configured');
|
|
47
|
+
throw (0, bff_http_error_1.unauthorized)('Credentials could not be validated');
|
|
48
|
+
}
|
|
49
|
+
await next();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
24
52
|
function resolveOAuthConfig(config) {
|
|
25
53
|
const { forestServerUrl, forestEnvSecret, forestAppUrl, forestAuthSecret, tokenEncryptionKey } = config;
|
|
26
54
|
if (forestServerUrl &&
|
|
@@ -54,7 +82,7 @@ async function buildOAuthMiddlewares(config, logger) {
|
|
|
54
82
|
environmentId,
|
|
55
83
|
logger,
|
|
56
84
|
});
|
|
57
|
-
return [
|
|
85
|
+
return [oauthRoutes];
|
|
58
86
|
}
|
|
59
87
|
function resolveApiKeyConfig(config) {
|
|
60
88
|
const { forestServerUrl, forestEnvSecret, forestAuthSecret } = config;
|
|
@@ -81,11 +109,39 @@ function buildApiKeyMiddleware(config, logger) {
|
|
|
81
109
|
});
|
|
82
110
|
return (0, api_key_middleware_1.default)({ authenticator, logger });
|
|
83
111
|
}
|
|
112
|
+
function buildAgentMiddlewares(config, logger) {
|
|
113
|
+
const { forestAuthSecret, defaultTimezone } = config;
|
|
114
|
+
if (!forestAuthSecret) {
|
|
115
|
+
logger('Warn', 'Agent edge disabled: FOREST_AUTH_SECRET is missing');
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const apiKeyStep = buildApiKeyMiddleware(config, logger) ?? createApiKeyUnavailableGuard(logger);
|
|
119
|
+
const chain = [
|
|
120
|
+
(0, auth_mode_middleware_1.default)({ authSecret: forestAuthSecret }),
|
|
121
|
+
apiKeyStep,
|
|
122
|
+
(0, per_key_origin_1.default)(),
|
|
123
|
+
(0, timezone_middleware_1.default)({ defaultTimezone }),
|
|
124
|
+
(0, agent_stub_1.default)(),
|
|
125
|
+
];
|
|
126
|
+
return chain.map(agentScoped);
|
|
127
|
+
}
|
|
84
128
|
async function runCli(env, logger = (0, console_logger_1.default)()) {
|
|
85
129
|
const config = (0, env_config_1.parseConfig)(env);
|
|
130
|
+
if (config.invalidAllowedOrigins.length > 0) {
|
|
131
|
+
logger('Warn', 'Ignoring malformed BFF_ALLOWED_ORIGINS entries', {
|
|
132
|
+
entries: config.invalidAllowedOrigins,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
86
135
|
const oauthMiddlewares = await buildOAuthMiddlewares(config, logger);
|
|
87
|
-
const
|
|
88
|
-
const
|
|
136
|
+
const agentMiddlewares = buildAgentMiddlewares(config, logger);
|
|
137
|
+
const agentErrorMiddleware = agentMiddlewares.length > 0 ? [agentScoped((0, error_middleware_1.default)({ logger }))] : [];
|
|
138
|
+
const middlewares = [
|
|
139
|
+
(0, cors_middleware_1.default)({ allowedOrigins: config.allowedOrigins }),
|
|
140
|
+
...agentErrorMiddleware,
|
|
141
|
+
(0, bodyparser_1.bodyParser)({ jsonLimit: BODY_LIMIT }),
|
|
142
|
+
...oauthMiddlewares,
|
|
143
|
+
...agentMiddlewares,
|
|
144
|
+
];
|
|
89
145
|
const server = new bff_http_server_1.default({
|
|
90
146
|
port: config.httpPort,
|
|
91
147
|
version: version_1.default,
|
|
@@ -100,4 +156,4 @@ function reportFatalError(err) {
|
|
|
100
156
|
process.stderr.write(`Error: ${(0, errors_1.extractErrorMessage)(err)}\n`);
|
|
101
157
|
process.exitCode = 1;
|
|
102
158
|
}
|
|
103
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
159
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLWNvcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY2xpLWNvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFnTEEseUJBa0NDO0FBRUQsNENBR0M7QUFuTkQsZ0RBQTZDO0FBRTdDLCtFQUE0RDtBQUM1RCxvRUFBMkQ7QUFDM0QsNEZBQXdFO0FBQ3hFLDhFQUFvRDtBQUNwRCxzRkFBa0U7QUFDbEUsNEVBQXlEO0FBQ3pELHVGQUFtRTtBQUNuRSxvREFBa0Q7QUFDbEQsNkVBQTBEO0FBQzFELDJFQUFpRTtBQUNqRSxxQ0FBK0M7QUFDL0MsMERBQXFEO0FBQ3JELDZFQUFtRDtBQUNuRCwrRUFBNEQ7QUFDNUQsd0ZBQThEO0FBQzlELHdFQUFxRDtBQUNyRCwwRUFBK0Q7QUFDL0Qsd0VBQXFEO0FBQ3JELHlGQUFzRTtBQUN0RSx3REFBZ0M7QUFFaEMsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDO0FBQzFCLE1BQU0sbUJBQW1CLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLENBQUM7QUFFekMsU0FBUyxXQUFXLENBQUMsSUFBWTtJQUMvQixPQUFPLElBQUksS0FBSyxRQUFRLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUN6RCxDQUFDO0FBRUQsU0FBUyxXQUFXLENBQUMsVUFBc0I7SUFDekMsT0FBTyxLQUFLLFVBQVUsTUFBTSxDQUFDLEdBQUcsRUFBRSxJQUFJO1FBQ3BDLElBQUksQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDM0IsTUFBTSxJQUFJLEVBQUUsQ0FBQztZQUViLE9BQU87UUFDVCxDQUFDO1FBRUQsTUFBTSxVQUFVLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxDQUFDO0lBQzlCLENBQUMsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFTLDRCQUE0QixDQUFDLE1BQWM7SUFDbEQsT0FBTyxLQUFLLFVBQVUsc0JBQXNCLENBQUMs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
|
|
@@ -8,6 +8,9 @@ export interface BFFConfig {
|
|
|
8
8
|
forestAppUrl?: string;
|
|
9
9
|
agentUrl?: string;
|
|
10
10
|
tokenEncryptionKey?: string;
|
|
11
|
+
allowedOrigins: string[];
|
|
12
|
+
invalidAllowedOrigins: string[];
|
|
13
|
+
defaultTimezone?: string;
|
|
11
14
|
httpPort: number;
|
|
12
15
|
presence: PresenceMap;
|
|
13
16
|
hasAllRequired: boolean;
|
|
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.REQUIRED_KEYS = void 0;
|
|
7
7
|
exports.parseConfig = parseConfig;
|
|
8
8
|
const zod_1 = require("zod");
|
|
9
|
+
const origin_1 = require("../cors/origin");
|
|
9
10
|
const defaults_1 = __importDefault(require("../defaults"));
|
|
10
11
|
const errors_1 = require("../errors");
|
|
12
|
+
const timezone_1 = require("../timezone/timezone");
|
|
11
13
|
exports.REQUIRED_KEYS = [
|
|
12
14
|
'FOREST_AUTH_SECRET',
|
|
13
15
|
'FOREST_ENV_SECRET',
|
|
@@ -47,6 +49,13 @@ function parseEncryptionKey(raw) {
|
|
|
47
49
|
}
|
|
48
50
|
return value;
|
|
49
51
|
}
|
|
52
|
+
function parseDefaultTimezone(raw) {
|
|
53
|
+
const value = normalize(raw);
|
|
54
|
+
if (value !== undefined && !(0, timezone_1.isValidTimezone)(value)) {
|
|
55
|
+
throw new errors_1.ConfigurationError(`Invalid configuration: BFF_DEFAULT_TIMEZONE must be a valid IANA timezone.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
50
59
|
function parseConfig(env) {
|
|
51
60
|
const normalized = Object.fromEntries(exports.REQUIRED_KEYS.map(key => [key, normalize(env[key])]));
|
|
52
61
|
for (const key of URL_KEYS) {
|
|
@@ -57,6 +66,8 @@ function parseConfig(env) {
|
|
|
57
66
|
}
|
|
58
67
|
const presence = Object.fromEntries(exports.REQUIRED_KEYS.map(key => [key, normalized[key] !== undefined]));
|
|
59
68
|
const tokenEncryptionKey = parseEncryptionKey(env.BFF_TOKEN_ENCRYPTION_KEY);
|
|
69
|
+
const { origins: allowedOrigins, invalid: invalidAllowedOrigins } = (0, origin_1.parseAllowedOrigins)(env.BFF_ALLOWED_ORIGINS);
|
|
70
|
+
const defaultTimezone = parseDefaultTimezone(env.BFF_DEFAULT_TIMEZONE);
|
|
60
71
|
return {
|
|
61
72
|
forestAuthSecret: normalized.FOREST_AUTH_SECRET,
|
|
62
73
|
forestEnvSecret: normalized.FOREST_ENV_SECRET,
|
|
@@ -64,9 +75,12 @@ function parseConfig(env) {
|
|
|
64
75
|
forestAppUrl: normalized.FOREST_APP_URL,
|
|
65
76
|
agentUrl: normalized.AGENT_URL,
|
|
66
77
|
tokenEncryptionKey,
|
|
78
|
+
allowedOrigins,
|
|
79
|
+
invalidAllowedOrigins,
|
|
80
|
+
defaultTimezone,
|
|
67
81
|
httpPort: parsePort(env.HTTP_PORT),
|
|
68
82
|
presence,
|
|
69
83
|
hasAllRequired: exports.REQUIRED_KEYS.every(key => presence[key]) && tokenEncryptionKey !== undefined,
|
|
70
84
|
};
|
|
71
85
|
}
|
|
72
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
86
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52LWNvbmZpZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb25maWcvZW52LWNvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUE2RkEsa0NBcUNDO0FBbElELDZCQUF3QjtBQUV4QiwyQ0FBcUQ7QUFDckQsMkRBQTJDO0FBQzNDLHNDQUErQztBQUMvQyxtREFBdUQ7QUFFMUMsUUFBQSxhQUFhLEdBQUc7SUFDM0Isb0JBQW9CO0lBQ3BCLG1CQUFtQjtJQUNuQixtQkFBbUI7SUFDbkIsZ0JBQWdCO0lBQ2hCLFdBQVc7Q0FDSCxDQUFDO0FBSVgsTUFBTSxRQUFRLEdBQUcsQ0FBQyxtQkFBbUIsRUFBRSxnQkFBZ0IsRUFBRSxXQUFXLENBQVUsQ0FBQztBQW1CL0UsTUFBTSxlQUFlLEdBQUcsT0FBTyxDQUFDO0FBQ2hDLE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQztBQUN2QixNQUFNLG9CQUFvQixHQUFHLEVBQUUsQ0FBQztBQUNoQyxNQUFNLGNBQWMsR0FBRyx3QkFBd0IsQ0FBQztBQUNoRCxNQUFNLGVBQWUsR0FBRyxPQUFDLENBQUMsR0FBRyxDQUFDLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7QUFFeEQsU0FBUyxTQUFTLENBQUMsS0FBeUI7SUFDMUMsT0FBTyxLQUFLLEtBQUssU0FBUyxJQUFJLEtBQUssQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDO0FBQ3hFLENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBQyxHQUF1QjtJQUN4QyxNQUFNLE9BQU8sR0FBRyxHQUFHLEVBQUUsSUFBSSxFQUFFLENBQUM7SUFDNUIsSUFBSSxPQUFPLEtBQUssU0FBUyxJQUFJLE9BQU8sS0FBSyxFQUFFO1FBQUUsT0FBTyxrQkFBZ0IsQ0FBQztJQUVyRSxNQUFNLElBQUksR0FBRyxlQUFlLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FB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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Middleware } from 'koa';
|
|
2
|
+
export declare const ALLOWED_METHODS = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
|
|
3
|
+
export declare const ALLOWED_HEADERS = "Authorization, Content-Type, X-Forest-Timezone, X-Forest-Bff-Key, X-Request-Id";
|
|
4
|
+
export declare const PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
5
|
+
export interface CorsMiddlewareOptions {
|
|
6
|
+
allowedOrigins: string[];
|
|
7
|
+
}
|
|
8
|
+
export default function createCorsMiddleware({ allowedOrigins, }: CorsMiddlewareOptions): Middleware;
|
|
9
|
+
//# sourceMappingURL=cors-middleware.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PREFLIGHT_MAX_AGE_SECONDS = exports.ALLOWED_HEADERS = exports.ALLOWED_METHODS = void 0;
|
|
4
|
+
exports.default = createCorsMiddleware;
|
|
5
|
+
const origin_1 = require("./origin");
|
|
6
|
+
exports.ALLOWED_METHODS = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
|
|
7
|
+
exports.ALLOWED_HEADERS = 'Authorization, Content-Type, X-Forest-Timezone, X-Forest-Bff-Key, X-Request-Id';
|
|
8
|
+
exports.PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
9
|
+
function createCorsMiddleware({ allowedOrigins, }) {
|
|
10
|
+
return async function corsMiddleware(ctx, next) {
|
|
11
|
+
const origin = ctx.get('Origin');
|
|
12
|
+
if (origin)
|
|
13
|
+
ctx.vary('Origin');
|
|
14
|
+
const allowed = origin ? (0, origin_1.originAllowed)(origin, allowedOrigins) : false;
|
|
15
|
+
if (allowed)
|
|
16
|
+
ctx.set('Access-Control-Allow-Origin', origin);
|
|
17
|
+
if (ctx.method === 'OPTIONS') {
|
|
18
|
+
if (allowed) {
|
|
19
|
+
ctx.set('Access-Control-Allow-Methods', exports.ALLOWED_METHODS);
|
|
20
|
+
ctx.set('Access-Control-Allow-Headers', exports.ALLOWED_HEADERS);
|
|
21
|
+
ctx.set('Access-Control-Max-Age', String(exports.PREFLIGHT_MAX_AGE_SECONDS));
|
|
22
|
+
}
|
|
23
|
+
ctx.status = 204;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await next();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29ycy1taWRkbGV3YXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NvcnMvY29ycy1taWRkbGV3YXJlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQWFBLHVDQTBCQztBQXJDRCxxQ0FBeUM7QUFFNUIsUUFBQSxlQUFlLEdBQUcsd0NBQXdDLENBQUM7QUFDM0QsUUFBQSxlQUFlLEdBQzFCLGdGQUFnRixDQUFDO0FBQ3RFLFFBQUEseUJBQXlCLEdBQUcsR0FBRyxDQUFDO0FBTTdDLFNBQXdCLG9CQUFvQixDQUFDLEVBQzNDLGNBQWMsR0FDUTtJQUN0QixPQUFPLEtBQUssVUFBVSxjQUFjLENBQUMsR0FBRyxFQUFFLElBQUk7UUFDNUMsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUVqQyxJQUFJLE1BQU07WUFBRSxHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBRS9CLE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBQSxzQkFBYSxFQUFDLE1BQU0sRUFBRSxjQUFjLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDO1FBRXZFLElBQUksT0FBTztZQUFFLEdBQUcsQ0FBQyxHQUFHLENBQUMsNkJBQTZCLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFFNUQsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLFNBQVMsRUFBRSxDQUFDO1lBQzdCLElBQUksT0FBTyxFQUFFLENBQUM7Z0JBQ1osR0FBRyxDQUFDLEdBQUcsQ0FBQyw4QkFBOEIsRUFBRSx1QkFBZSxDQUFDLENBQUM7Z0JBQ3pELEdBQUcsQ0FBQyxHQUFHLENBQUMsOEJBQThCLEVBQUUsdUJBQWUsQ0FBQyxDQUFDO2dCQUN6RCxHQUFHLENBQUMsR0FBRyxDQUFDLHdCQUF3QixFQUFFLE1BQU0sQ0FBQyxpQ0FBeUIsQ0FBQyxDQUFDLENBQUM7WUFDdkUsQ0FBQztZQUVELEdBQUcsQ0FBQyxNQUFNLEdBQUcsR0FBRyxDQUFDO1lBRWpCLE9BQU87UUFDVCxDQUFDO1FBRUQsTUFBTSxJQUFJLEVBQUUsQ0FBQztJQUNmLENBQUMsQ0FBQztBQUNKLENBQUMifQ==
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function normalizeOrigin(raw: string | undefined | null): string | null;
|
|
2
|
+
export declare function parseAllowedOrigins(raw: string | undefined): {
|
|
3
|
+
origins: string[];
|
|
4
|
+
invalid: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function originAllowed(requestOrigin: string | undefined, allowList: string[]): boolean;
|
|
7
|
+
//# sourceMappingURL=origin.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeOrigin = normalizeOrigin;
|
|
4
|
+
exports.parseAllowedOrigins = parseAllowedOrigins;
|
|
5
|
+
exports.originAllowed = originAllowed;
|
|
6
|
+
function normalizeOrigin(raw) {
|
|
7
|
+
if (raw === undefined || raw === null)
|
|
8
|
+
return null;
|
|
9
|
+
const trimmed = raw.trim();
|
|
10
|
+
if (trimmed === '' || trimmed === 'null')
|
|
11
|
+
return null;
|
|
12
|
+
let url;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(trimmed);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (url.origin === 'null')
|
|
20
|
+
return null;
|
|
21
|
+
return url.origin;
|
|
22
|
+
}
|
|
23
|
+
function parseAllowedOrigins(raw) {
|
|
24
|
+
if (raw === undefined || raw.trim() === '')
|
|
25
|
+
return { origins: [], invalid: [] };
|
|
26
|
+
const origins = [];
|
|
27
|
+
const invalid = [];
|
|
28
|
+
const entries = raw
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(entry => entry.trim())
|
|
31
|
+
.filter(entry => entry !== '');
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const normalized = normalizeOrigin(entry);
|
|
34
|
+
if (normalized === null)
|
|
35
|
+
invalid.push(entry);
|
|
36
|
+
else if (!origins.includes(normalized))
|
|
37
|
+
origins.push(normalized);
|
|
38
|
+
}
|
|
39
|
+
return { origins, invalid };
|
|
40
|
+
}
|
|
41
|
+
function originAllowed(requestOrigin, allowList) {
|
|
42
|
+
const normalized = normalizeOrigin(requestOrigin);
|
|
43
|
+
if (normalized === null)
|
|
44
|
+
return false;
|
|
45
|
+
return allowList.some(entry => normalizeOrigin(entry) === normalized);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3JpZ2luLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NvcnMvb3JpZ2luLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsMENBaUJDO0FBRUQsa0RBc0JDO0FBRUQsc0NBS0M7QUFoREQsU0FBZ0IsZUFBZSxDQUFDLEdBQThCO0lBQzVELElBQUksR0FBRyxLQUFLLFNBQVMsSUFBSSxHQUFHLEtBQUssSUFBSTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBRW5ELE1BQU0sT0FBTyxHQUFHLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUMzQixJQUFJLE9BQU8sS0FBSyxFQUFFLElBQUksT0FBTyxLQUFLLE1BQU07UUFBRSxPQUFPLElBQUksQ0FBQztJQUV0RCxJQUFJLEdBQVEsQ0FBQztJQUViLElBQUksQ0FBQztRQUNILEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN6QixDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBRUQsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLE1BQU07UUFBRSxPQUFPLElBQUksQ0FBQztJQUV2QyxPQUFPLEdBQUcsQ0FBQyxNQUFNLENBQUM7QUFDcEIsQ0FBQztBQUVELFNBQWdCLG1CQUFtQixDQUFDLEdBQXVCO0lBSXpELElBQUksR0FBRyxLQUFLLFNBQVMsSUFBSSxHQUFHLENBQUMsSUFBSSxFQUFFLEtBQUssRUFBRTtRQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsRUFBRSxFQUFFLE9BQU8sRUFBRSxFQUFFLEVBQUUsQ0FBQztJQUVoRixNQUFNLE9BQU8sR0FBYSxFQUFFLENBQUM7SUFDN0IsTUFBTSxPQUFPLEdBQWEsRUFBRSxDQUFDO0lBRTdCLE1BQU0sT0FBTyxHQUFHLEdBQUc7U0FDaEIsS0FBSyxDQUFDLEdBQUcsQ0FBQztTQUNWLEdBQUcsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztTQUMxQixNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxLQUFLLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFakMsS0FBSyxNQUFNLEtBQUssSUFBSSxPQUFPLEVBQUUsQ0FBQztRQUM1QixNQUFNLFVBQVUsR0FBRyxlQUFlLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFMUMsSUFBSSxVQUFVLEtBQUssSUFBSTtZQUFFLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7YUFDeEMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDO1lBQUUsT0FBTyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNuRSxDQUFDO0lBRUQsT0FBTyxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsQ0FBQztBQUM5QixDQUFDO0FBRUQsU0FBZ0IsYUFBYSxDQUFDLGFBQWlDLEVBQUUsU0FBbUI7SUFDbEYsTUFBTSxVQUFVLEdBQUcsZUFBZSxDQUFDLGFBQWEsQ0FBQyxDQUFDO0lBQ2xELElBQUksVUFBVSxLQUFLLElBQUk7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUV0QyxPQUFPLFNBQVMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxlQUFlLENBQUMsS0FBSyxDQUFDLEtBQUssVUFBVSxDQUFDLENBQUM7QUFDeEUsQ0FBQyJ9
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = createPerKeyOriginMiddleware;
|
|
4
|
+
const origin_1 = require("./origin");
|
|
5
|
+
const bff_http_error_1 = require("../http/bff-http-error");
|
|
6
|
+
function createPerKeyOriginMiddleware() {
|
|
7
|
+
return async function perKeyOriginMiddleware(ctx, next) {
|
|
8
|
+
const identity = ctx.state.apiKeyIdentity;
|
|
9
|
+
const allowedOrigins = identity?.allowedOrigins ?? [];
|
|
10
|
+
if (allowedOrigins.length > 0 && !(0, origin_1.originAllowed)(ctx.get('Origin'), allowedOrigins)) {
|
|
11
|
+
throw (0, bff_http_error_1.originNotAllowed)();
|
|
12
|
+
}
|
|
13
|
+
await next();
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVyLWtleS1vcmlnaW4uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY29ycy9wZXIta2V5LW9yaWdpbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUtBLCtDQVdDO0FBZEQscUNBQXlDO0FBQ3pDLDJEQUEwRDtBQUUxRCxTQUF3Qiw0QkFBNEI7SUFDbEQsT0FBTyxLQUFLLFVBQVUsc0JBQXNCLENBQUMsR0FBRyxFQUFFLElBQUk7UUFDcEQsTUFBTSxRQUFRLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxjQUEyRCxDQUFDO1FBQ3ZGLE1BQU0sY0FBYyxHQUFHLFFBQVEsRUFBRSxjQUFjLElBQUksRUFBRSxDQUFDO1FBRXRELElBQUksY0FBYyxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFBLHNCQUFhLEVBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsRUFBRSxjQUFjLENBQUMsRUFBRSxDQUFDO1lBQ25GLE1BQU0sSUFBQSxpQ0FBZ0IsR0FBRSxDQUFDO1FBQzNCLENBQUM7UUFFRCxNQUFNLElBQUksRUFBRSxDQUFDO0lBQ2YsQ0FBQyxDQUFDO0FBQ0osQ0FBQyJ9
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare class BffHttpError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly details?: unknown;
|
|
5
|
+
constructor(status: number, type: string, message: string, details?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export interface BffErrorBody {
|
|
8
|
+
error: {
|
|
9
|
+
type: string;
|
|
10
|
+
status: number;
|
|
11
|
+
message: string;
|
|
12
|
+
details?: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function isSerializableError(error: unknown): error is {
|
|
16
|
+
status: number;
|
|
17
|
+
type: string;
|
|
18
|
+
message: string;
|
|
19
|
+
details?: unknown;
|
|
20
|
+
retryAfter?: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function toErrorBody(error: {
|
|
23
|
+
status: number;
|
|
24
|
+
type: string;
|
|
25
|
+
message: string;
|
|
26
|
+
details?: unknown;
|
|
27
|
+
}): BffErrorBody;
|
|
28
|
+
export declare function unauthorized(message?: string): BffHttpError;
|
|
29
|
+
export declare function ambiguousCredentials(message?: string): BffHttpError;
|
|
30
|
+
export declare function sessionExpired(message?: string): BffHttpError;
|
|
31
|
+
export declare function originNotAllowed(message?: string): BffHttpError;
|
|
32
|
+
export declare function missingTimezone(message?: string): BffHttpError;
|
|
33
|
+
export declare function invalidTimezone(value: string): BffHttpError;
|
|
34
|
+
//# sourceMappingURL=bff-http-error.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BffHttpError = void 0;
|
|
4
|
+
exports.isSerializableError = isSerializableError;
|
|
5
|
+
exports.toErrorBody = toErrorBody;
|
|
6
|
+
exports.unauthorized = unauthorized;
|
|
7
|
+
exports.ambiguousCredentials = ambiguousCredentials;
|
|
8
|
+
exports.sessionExpired = sessionExpired;
|
|
9
|
+
exports.originNotAllowed = originNotAllowed;
|
|
10
|
+
exports.missingTimezone = missingTimezone;
|
|
11
|
+
exports.invalidTimezone = invalidTimezone;
|
|
12
|
+
class BffHttpError extends Error {
|
|
13
|
+
constructor(status, type, message, details) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'BffHttpError';
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.type = type;
|
|
18
|
+
this.details = details;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.BffHttpError = BffHttpError;
|
|
22
|
+
function isSerializableError(error) {
|
|
23
|
+
return (typeof error === 'object' &&
|
|
24
|
+
error !== null &&
|
|
25
|
+
typeof error.status === 'number' &&
|
|
26
|
+
typeof error.type === 'string' &&
|
|
27
|
+
typeof error.message === 'string');
|
|
28
|
+
}
|
|
29
|
+
function toErrorBody(error) {
|
|
30
|
+
const body = {
|
|
31
|
+
error: { type: error.type, status: error.status, message: error.message },
|
|
32
|
+
};
|
|
33
|
+
if (error.details !== undefined)
|
|
34
|
+
body.error.details = error.details;
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
function unauthorized(message = 'Missing or invalid credentials') {
|
|
38
|
+
return new BffHttpError(401, 'unauthorized', message);
|
|
39
|
+
}
|
|
40
|
+
function ambiguousCredentials(message = 'Both Authorization and X-Forest-Bff-Key were provided') {
|
|
41
|
+
return new BffHttpError(400, 'ambiguous_credentials', message);
|
|
42
|
+
}
|
|
43
|
+
function sessionExpired(message = 'The BFF session has expired') {
|
|
44
|
+
return new BffHttpError(401, 'session_expired', message);
|
|
45
|
+
}
|
|
46
|
+
function originNotAllowed(message = 'Origin is not allowed for this key') {
|
|
47
|
+
return new BffHttpError(403, 'origin_not_allowed', message);
|
|
48
|
+
}
|
|
49
|
+
function missingTimezone(message = 'A timezone is required but none was provided') {
|
|
50
|
+
return new BffHttpError(400, 'missing_timezone', message);
|
|
51
|
+
}
|
|
52
|
+
function invalidTimezone(value) {
|
|
53
|
+
return new BffHttpError(400, 'invalid_timezone', `Invalid timezone: "${value}"`);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmZmLWh0dHAtZXJyb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaHR0cC9iZmYtaHR0cC1lcnJvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUF1QkEsa0RBY0M7QUFFRCxrQ0FhQztBQUVELG9DQUVDO0FBRUQsb0RBSUM7QUFFRCx3Q0FFQztBQUVELDRDQUVDO0FBRUQsMENBSUM7QUFFRCwwQ0FFQztBQWhGRCxNQUFhLFlBQWEsU0FBUSxLQUFLO0lBS3JDLFlBQVksTUFBYyxFQUFFLElBQVksRUFBRSxPQUFlLEVBQUUsT0FBaUI7UUFDMUUsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2YsSUFBSSxDQUFDLElBQUksR0FBRyxjQUFjLENBQUM7UUFDM0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7UUFDakIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7SUFDekIsQ0FBQztDQUNGO0FBWkQsb0NBWUM7QUFXRCxTQUFnQixtQkFBbUIsQ0FBQyxLQUFjO0lBT2hELE9BQU8sQ0FDTCxPQUFPLEtBQUssS0FBSyxRQUFRO1FBQ3pCLEtBQUssS0FBSyxJQUFJO1FBQ2QsT0FBUSxLQUE4QixDQUFDLE1BQU0sS0FBSyxRQUFRO1FBQzFELE9BQVEsS0FBNEIsQ0FBQyxJQUFJLEtBQUssUUFBUTtRQUN0RCxPQUFRLEtBQStCLENBQUMsT0FBTyxLQUFLLFFBQVEsQ0FDN0QsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFnQixXQUFXLENBQUMsS0FLM0I7SUFDQyxNQUFNLElBQUksR0FBaUI7UUFDekIsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLEtBQUssQ0FBQyxPQUFPLEVBQUU7S0FDMUUsQ0FBQztJQUVGLElBQUksS0FBSyxDQUFDLE9BQU8sS0FBSyxTQUFTO1FBQUUsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBQztJQUVwRSxPQUFPLElBQUksQ0FBQztBQUNkLENBQUM7QUFFRCxTQUFnQixZQUFZLENBQUMsT0FBTyxHQUFHLGdDQUFnQztJQUNyRSxPQUFPLElBQUksWUFBWSxDQUFDLEdBQUcsRUFBRSxjQUFjLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDeEQsQ0FBQztBQUVELFNBQWdCLG9CQUFvQixDQUNsQyxPQUFPLEdBQUcsdURBQXVEO0lBRWpFLE9BQU8sSUFBSSxZQUFZLENBQUMsR0FBRyxFQUFFLHVCQUF1QixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQ2pFLENBQUM7QUFFRCxTQUFnQixjQUFjLENBQUMsT0FBTyxHQUFHLDZCQUE2QjtJQUNwRSxPQUFPLElBQUksWUFBWSxDQUFDLEdBQUcsRUFBRSxpQkFBaUIsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUMzRCxDQUFDO0FBRUQsU0FBZ0IsZ0JBQWdCLENBQUMsT0FBTyxHQUFHLG9DQUFvQztJQUM3RSxPQUFPLElBQUksWUFBWSxDQUFDLEdBQUcsRUFBRSxvQkFBb0IsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUM5RCxDQUFDO0FBRUQsU0FBZ0IsZUFBZSxDQUM3QixPQUFPLEdBQUcsOENBQThDO0lBRXhELE9BQU8sSUFBSSxZQUFZLENBQUMsR0FBRyxFQUFFLGtCQUFrQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQzVELENBQUM7QUFFRCxTQUFnQixlQUFlLENBQUMsS0FBYTtJQUMzQyxPQUFPLElBQUksWUFBWSxDQUFDLEdBQUcsRUFBRSxrQkFBa0IsRUFBRSxzQkFBc0IsS0FBSyxHQUFHLENBQUMsQ0FBQztBQUNuRixDQUFDIn0=
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Logger } from '../ports/logger-port';
|
|
2
|
+
import type { Middleware } from 'koa';
|
|
3
|
+
export interface ErrorMiddlewareOptions {
|
|
4
|
+
logger: Logger;
|
|
5
|
+
}
|
|
6
|
+
export default function createErrorMiddleware({ logger }: ErrorMiddlewareOptions): Middleware;
|
|
7
|
+
//# sourceMappingURL=error-middleware.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = createErrorMiddleware;
|
|
4
|
+
const bff_http_error_1 = require("./bff-http-error");
|
|
5
|
+
const CLIENT_ERROR_TYPES = {
|
|
6
|
+
413: 'payload_too_large',
|
|
7
|
+
415: 'unsupported_media_type',
|
|
8
|
+
};
|
|
9
|
+
function clientErrorStatus(error) {
|
|
10
|
+
if (typeof error !== 'object' || error === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
const { status, statusCode } = error;
|
|
13
|
+
const value = typeof status === 'number' ? status : statusCode;
|
|
14
|
+
return typeof value === 'number' && value >= 400 && value < 500 ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
function createErrorMiddleware({ logger }) {
|
|
17
|
+
return async function errorMiddleware(ctx, next) {
|
|
18
|
+
try {
|
|
19
|
+
await next();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if ((0, bff_http_error_1.isSerializableError)(error)) {
|
|
23
|
+
if (error.retryAfter !== undefined)
|
|
24
|
+
ctx.set('Retry-After', String(error.retryAfter));
|
|
25
|
+
ctx.status = error.status;
|
|
26
|
+
ctx.body = (0, bff_http_error_1.toErrorBody)(error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const clientStatus = clientErrorStatus(error);
|
|
30
|
+
if (clientStatus !== undefined) {
|
|
31
|
+
ctx.status = clientStatus;
|
|
32
|
+
ctx.body = (0, bff_http_error_1.toErrorBody)({
|
|
33
|
+
status: clientStatus,
|
|
34
|
+
type: CLIENT_ERROR_TYPES[clientStatus] ?? 'invalid_request',
|
|
35
|
+
message: 'Invalid request',
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
logger('Error', 'Unhandled BFF edge error', {
|
|
40
|
+
cause: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
41
|
+
});
|
|
42
|
+
ctx.status = 500;
|
|
43
|
+
ctx.body = {
|
|
44
|
+
error: { type: 'internal_error', status: 500, message: 'Internal server error' },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3ItbWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9odHRwL2Vycm9yLW1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUF1QkEsd0NBbUNDO0FBdkRELHFEQUFvRTtBQU1wRSxNQUFNLGtCQUFrQixHQUEyQjtJQUNqRCxHQUFHLEVBQUUsbUJBQW1CO0lBQ3hCLEdBQUcsRUFBRSx3QkFBd0I7Q0FDOUIsQ0FBQztBQUVGLFNBQVMsaUJBQWlCLENBQUMsS0FBYztJQUN2QyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sU0FBUyxDQUFDO0lBRWxFLE1BQU0sRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLEdBQUcsS0FBbUQsQ0FBQztJQUNuRixNQUFNLEtBQUssR0FBRyxPQUFPLE1BQU0sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDO0lBRS9ELE9BQU8sT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssSUFBSSxHQUFHLElBQUksS0FBSyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7QUFDdEYsQ0FBQztBQUVELFNBQXdCLHFCQUFxQixDQUFDLEVBQUUsTUFBTSxFQUEwQjtJQUM5RSxPQUFPLEtBQUssVUFBVSxlQUFlLENBQUMsR0FBRyxFQUFFLElBQUk7UUFDN0MsSUFBSSxDQUFDO1lBQ0gsTUFBTSxJQUFJLEVBQUUsQ0FBQztRQUNmLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsSUFBSSxJQUFBLG9DQUFtQixFQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7Z0JBQy9CLElBQUksS0FBSyxDQUFDLFVBQVUsS0FBSyxTQUFTO29CQUFFLEdBQUcsQ0FBQyxHQUFHLENBQUMsYUFBYSxFQUFFLE1BQU0sQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQztnQkFDckYsR0FBRyxDQUFDLE1BQU0sR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDO2dCQUMxQixHQUFHLENBQUMsSUFBSSxHQUFHLElBQUEsNEJBQVcsRUFBQyxLQUFLLENBQUMsQ0FBQztnQkFFOUIsT0FBTztZQUNULENBQUM7WUFFRCxNQUFNLFlBQVksR0FBRyxpQkFBaUIsQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUU5QyxJQUFJLFlBQVksS0FBSyxTQUFTLEVBQUUsQ0FBQztnQkFDL0IsR0FBRyxDQUFDLE1BQU0sR0FBRyxZQUFZLENBQUM7Z0JBQzFCLEdBQUcsQ0FBQyxJQUFJLEdBQUcsSUFBQSw0QkFBVyxFQUFDO29CQUNyQixNQUFNLEVBQUUsWUFBWTtvQkFDcEIsSUFBSSxFQUFFLGtCQUFrQixDQUFDLFlBQVksQ0FBQyxJQUFJLGlCQUFpQjtvQkFDM0QsT0FBTyxFQUFFLGlCQUFpQjtpQkFDM0IsQ0FBQyxDQUFDO2dCQUVILE9BQU87WUFDVCxDQUFDO1lBRUQsTUFBTSxDQUFDLE9BQU8sRUFBRSwwQkFBMEIsRUFBRTtnQkFDMUMsS0FBSyxFQUFFLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsS0FBSyxDQUFDLElBQUksS0FBSyxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUM7YUFDbEYsQ0FBQyxDQUFDO1lBQ0gsR0FBRyxDQUFDLE1BQU0sR0FBRyxHQUFHLENBQUM7WUFDakIsR0FBRyxDQUFDLElBQUksR0FBRztnQkFDVCxLQUFLLEVBQUUsRUFBRSxJQUFJLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsdUJBQXVCLEVBQUU7YUFDakYsQ0FBQztRQUNKLENBQUM7SUFDSCxDQUFDLENBQUM7QUFDSixDQUFDIn0=
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Middleware } from 'koa';
|
|
2
|
+
export declare const TIMEZONE_HEADER = "X-Forest-Timezone";
|
|
3
|
+
export interface TimezoneMiddlewareOptions {
|
|
4
|
+
defaultTimezone?: string;
|
|
5
|
+
}
|
|
6
|
+
export default function createTimezoneMiddleware({ defaultTimezone, }: TimezoneMiddlewareOptions): Middleware;
|
|
7
|
+
//# sourceMappingURL=timezone-middleware.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TIMEZONE_HEADER = void 0;
|
|
4
|
+
exports.default = createTimezoneMiddleware;
|
|
5
|
+
const timezone_1 = require("./timezone");
|
|
6
|
+
exports.TIMEZONE_HEADER = 'X-Forest-Timezone';
|
|
7
|
+
function bodyTimezone(ctx) {
|
|
8
|
+
const { body } = ctx.request;
|
|
9
|
+
if (typeof body !== 'object' || body === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
const value = body.timezone;
|
|
12
|
+
return typeof value === 'string' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function createTimezoneMiddleware({ defaultTimezone, }) {
|
|
15
|
+
return async function timezoneMiddleware(ctx, next) {
|
|
16
|
+
ctx.state.timezone = (0, timezone_1.resolveTimezone)({
|
|
17
|
+
header: ctx.get(exports.TIMEZONE_HEADER),
|
|
18
|
+
body: bodyTimezone(ctx),
|
|
19
|
+
fallback: defaultTimezone,
|
|
20
|
+
});
|
|
21
|
+
await next();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGltZXpvbmUtbWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90aW1lem9uZS90aW1lem9uZS1taWRkbGV3YXJlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQW9CQSwyQ0FZQztBQTlCRCx5Q0FBNkM7QUFFaEMsUUFBQSxlQUFlLEdBQUcsbUJBQW1CLENBQUM7QUFNbkQsU0FBUyxZQUFZLENBQUMsR0FBOEI7SUFDbEQsTUFBTSxFQUFFLElBQUksRUFBRSxHQUFHLEdBQUcsQ0FBQyxPQUE2QixDQUFDO0lBRW5ELElBQUksT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLElBQUksS0FBSyxJQUFJO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFFaEUsTUFBTSxLQUFLLEdBQUksSUFBK0IsQ0FBQyxRQUFRLENBQUM7SUFFeEQsT0FBTyxPQUFPLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO0FBQ3ZELENBQUM7QUFFRCxTQUF3Qix3QkFBd0IsQ0FBQyxFQUMvQyxlQUFlLEdBQ1c7SUFDMUIsT0FBTyxLQUFLLFVBQVUsa0JBQWtCLENBQUMsR0FBRyxFQUFFLElBQUk7UUFDaEQsR0FBRyxDQUFDLEtBQUssQ0FBQyxRQUFRLEdBQUcsSUFBQSwwQkFBZSxFQUFDO1lBQ25DLE1BQU0sRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLHVCQUFlLENBQUM7WUFDaEMsSUFBSSxFQUFFLFlBQVksQ0FBQyxHQUFHLENBQUM7WUFDdkIsUUFBUSxFQUFFLGVBQWU7U0FDMUIsQ0FBQyxDQUFDO1FBRUgsTUFBTSxJQUFJLEVBQUUsQ0FBQztJQUNmLENBQUMsQ0FBQztBQUNKLENBQUMifQ==
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface TimezoneSources {
|
|
2
|
+
header?: string;
|
|
3
|
+
body?: string;
|
|
4
|
+
fallback?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function isValidTimezone(value: string): boolean;
|
|
7
|
+
export declare function resolveTimezone({ header, body, fallback }: TimezoneSources): string;
|
|
8
|
+
//# sourceMappingURL=timezone.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidTimezone = isValidTimezone;
|
|
4
|
+
exports.resolveTimezone = resolveTimezone;
|
|
5
|
+
const bff_http_error_1 = require("../http/bff-http-error");
|
|
6
|
+
const VALID_TIMEZONES = new Set();
|
|
7
|
+
function isValidTimezone(value) {
|
|
8
|
+
if (value === '')
|
|
9
|
+
return false;
|
|
10
|
+
if (VALID_TIMEZONES.has(value))
|
|
11
|
+
return true;
|
|
12
|
+
let canonical;
|
|
13
|
+
try {
|
|
14
|
+
canonical = Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Cache the canonical form, not the raw input: Intl matches IANA names
|
|
20
|
+
// case-insensitively, so caching raw casings would grow this Set without
|
|
21
|
+
// bound on attacker-controlled input.
|
|
22
|
+
VALID_TIMEZONES.add(canonical);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
function resolveTimezone({ header, body, fallback }) {
|
|
26
|
+
const candidate = [header, body, fallback]
|
|
27
|
+
.map(value => value?.trim())
|
|
28
|
+
.find(value => value !== undefined && value !== '');
|
|
29
|
+
if (candidate === undefined)
|
|
30
|
+
throw (0, bff_http_error_1.missingTimezone)();
|
|
31
|
+
if (!isValidTimezone(candidate))
|
|
32
|
+
throw (0, bff_http_error_1.invalidTimezone)(candidate);
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGltZXpvbmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdGltZXpvbmUvdGltZXpvbmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFVQSwwQ0FrQkM7QUFFRCwwQ0FTQztBQXZDRCwyREFBMEU7QUFFMUUsTUFBTSxlQUFlLEdBQUcsSUFBSSxHQUFHLEVBQVUsQ0FBQztBQVExQyxTQUFnQixlQUFlLENBQUMsS0FBYTtJQUMzQyxJQUFJLEtBQUssS0FBSyxFQUFFO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDL0IsSUFBSSxlQUFlLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBRTVDLElBQUksU0FBaUIsQ0FBQztJQUV0QixJQUFJLENBQUM7UUFDSCxTQUFTLEdBQUcsSUFBSSxDQUFDLGNBQWMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxlQUFlLEVBQUUsQ0FBQyxRQUFRLENBQUM7SUFDM0YsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUVELHVFQUF1RTtJQUN2RSx5RUFBeUU7SUFDekUsc0NBQXNDO0lBQ3RDLGVBQWUsQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLENBQUM7SUFFL0IsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDO0FBRUQsU0FBZ0IsZUFBZSxDQUFDLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQW1CO0lBQ3pFLE1BQU0sU0FBUyxHQUFHLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxRQUFRLENBQUM7U0FDdkMsR0FBRyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDO1NBQzNCLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLEtBQUssS0FBSyxTQUFTLElBQUksS0FBSyxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBRXRELElBQUksU0FBUyxLQUFLLFNBQVM7UUFBRSxNQUFNLElBQUEsZ0NBQWUsR0FBRSxDQUFDO0lBQ3JELElBQUksQ0FBQyxlQUFlLENBQUMsU0FBUyxDQUFDO1FBQUUsTUFBTSxJQUFBLGdDQUFlLEVBQUMsU0FBUyxDQUFDLENBQUM7SUFFbEUsT0FBTyxTQUFTLENBQUM7QUFDbkIsQ0FBQyJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestadmin/agent-bff",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forest-bff": "dist/cli.js"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "jest"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@forestadmin/forestadmin-client": "1.40.
|
|
34
|
+
"@forestadmin/forestadmin-client": "1.40.4",
|
|
35
35
|
"@koa/bodyparser": "^6.1.0",
|
|
36
36
|
"jsonwebtoken": "^9.0.3",
|
|
37
37
|
"koa": "^3.0.1",
|