@forestadmin/agent-bff 1.31.0 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -1
- package/dist/action/action-routes-middleware.d.ts +3 -1
- package/dist/action/action-routes-middleware.js +48 -13
- package/dist/activity-log/activity-log-drainer.d.ts +24 -0
- package/dist/activity-log/activity-log-drainer.js +63 -0
- package/dist/activity-log/activity-log-writer.d.ts +26 -0
- package/dist/activity-log/activity-log-writer.js +23 -0
- package/dist/activity-log/activity-logs-creator.d.ts +42 -0
- package/dist/activity-log/activity-logs-creator.js +209 -0
- package/dist/activity-log/activity-logs-service.d.ts +18 -0
- package/dist/activity-log/activity-logs-service.js +19 -0
- package/dist/activity-log/with-activity-log.d.ts +25 -0
- package/dist/activity-log/with-activity-log.js +63 -0
- package/dist/api-key/api-key-authenticator.d.ts +11 -0
- package/dist/api-key/api-key-authenticator.js +12 -2
- package/dist/api-key/api-key-client.d.ts +6 -0
- package/dist/api-key/api-key-client.js +2 -1
- package/dist/api-key/api-key-middleware.d.ts +10 -1
- package/dist/api-key/api-key-middleware.js +18 -1
- package/dist/api-key/resolve-cache.d.ts +12 -0
- package/dist/api-key/resolve-cache.js +38 -6
- package/dist/auth/auth-mode.d.ts +1 -0
- package/dist/auth/auth-mode.js +10 -3
- package/dist/auth/forest-server-token-middleware.d.ts +25 -0
- package/dist/auth/forest-server-token-middleware.js +90 -0
- package/dist/build-bff.d.ts +8 -0
- package/dist/build-bff.js +29 -10
- package/dist/cli-core.d.ts +9 -0
- package/dist/cli-core.js +38 -3
- package/dist/data/agent-query.d.ts +12 -0
- package/dist/data/agent-query.js +19 -1
- package/dist/data/data-routes-middleware.d.ts +3 -1
- package/dist/data/data-routes-middleware.js +41 -4
- package/dist/http/bff-http-server.d.ts +22 -0
- package/dist/http/bff-http-server.js +50 -5
- package/dist/http/bff-local-errors.d.ts +15 -0
- package/dist/http/bff-local-errors.js +29 -3
- package/dist/openapi/openapi-document.js +3 -3
- package/dist/tracing-handle.d.ts +26 -0
- package/dist/tracing-handle.js +40 -0
- package/dist/tracing-preload.d.ts +2 -0
- package/dist/tracing-preload.js +27 -0
- package/dist/tracing.d.ts +79 -0
- package/dist/tracing.js +168 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,112 @@ dropped collection reachable.
|
|
|
36
36
|
Two ways to run it: embedded in a Forest agent (`agent.addBff()`, see
|
|
37
37
|
[Embedded in an agent](#embedded-in-an-agent)) or standalone, described here.
|
|
38
38
|
|
|
39
|
+
### Docker (recommended)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cp .env.example .env # then fill in the secrets
|
|
43
|
+
docker compose up
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The template targets a local, non-containerised run, so one value has to change for Docker:
|
|
47
|
+
set `AGENT_URL=http://host.docker.internal:3351`. Left at `localhost`, it resolves to the BFF
|
|
48
|
+
container itself and every agent call fails (see the note below).
|
|
49
|
+
|
|
50
|
+
The `docker-compose.yml` at the root of this package starts a single BFF instance. See
|
|
51
|
+
`.env.example` for the full list of environment variables and their descriptions.
|
|
52
|
+
|
|
53
|
+
Or run the image directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
docker run -d \
|
|
57
|
+
-p 3450:3450 \
|
|
58
|
+
--stop-timeout 15 \
|
|
59
|
+
--add-host host.docker.internal:host-gateway \
|
|
60
|
+
-e FOREST_AUTH_SECRET="..." \
|
|
61
|
+
-e FOREST_ENV_SECRET="..." \
|
|
62
|
+
-e FOREST_SERVER_URL="https://api.forestadmin.com" \
|
|
63
|
+
-e FOREST_APP_URL="https://app.forestadmin.com" \
|
|
64
|
+
-e AGENT_URL="http://host.docker.internal:3351" \
|
|
65
|
+
-e BFF_TOKEN_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
|
|
66
|
+
ghcr.io/forestadmin/agent-bff:latest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> **Note:** When the BFF runs in Docker and your agent runs on the host machine, use
|
|
70
|
+
> `host.docker.internal` instead of `localhost` in `AGENT_URL`. Docker Desktop resolves that
|
|
71
|
+
> name natively; on Docker Engine for Linux it does not exist unless you map it, hence the
|
|
72
|
+
> `--add-host` above (the Compose setup does the same through `extra_hosts`).
|
|
73
|
+
|
|
74
|
+
The image's entry point is the CLI, so the subcommands below work the same way:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
docker run --rm ghcr.io/forestadmin/agent-bff:latest openapi > openapi.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Tags follow the npm package: `:latest`, `:1`, `:1.20` and the immutable `:1.20.2`.
|
|
81
|
+
|
|
82
|
+
The package is public, so none of the commands above need a login. That visibility is set once,
|
|
83
|
+
by hand, on the GHCR package: a package GHCR creates on its first push is private, and the
|
|
84
|
+
workflow's `GITHUB_TOKEN` can push to it but not change what it is. Until someone flips it (the
|
|
85
|
+
same step `workflow-executor` went through), pulls need
|
|
86
|
+
`docker login ghcr.io -u <user> -p <token-with-read:packages>`.
|
|
87
|
+
|
|
88
|
+
On `SIGTERM` or `SIGINT` the BFF stops accepting connections and gives the requests already in
|
|
89
|
+
flight 10 seconds to finish before cutting their sockets, then exits 0. A second signal gives up on
|
|
90
|
+
the wait and exits 1.
|
|
91
|
+
|
|
92
|
+
Allow for that in your orchestrator's grace period. The whole budget is up to 11 seconds — the 10
|
|
93
|
+
second deadline plus a 1 second fallback for the exit itself — and `docker stop` defaults to 10,
|
|
94
|
+
so under load it would SIGKILL exactly when the shutdown is doing its job. Hence `--stop-timeout 15`
|
|
95
|
+
above and `stop_grace_period: 15s` in the Compose file; on Kubernetes the default
|
|
96
|
+
`terminationGracePeriodSeconds` of 30 already covers it.
|
|
97
|
+
|
|
98
|
+
### Observability (OpenTelemetry)
|
|
99
|
+
|
|
100
|
+
The Docker image ships with [OpenTelemetry](https://opentelemetry.io/) APM built in, and works with
|
|
101
|
+
any OTLP-compatible backend (Datadog, Grafana Tempo, Jaeger, Honeycomb, etc.). It is **off by
|
|
102
|
+
default** and turns on as soon as you point it at an OTLP receiver — no code changes or extra
|
|
103
|
+
installs required. A setup that cannot start logs a warning and runs untraced rather than taking
|
|
104
|
+
the process down with it. Tracing is set up before the app starts (auto-instrumentation for HTTP and the
|
|
105
|
+
outbound calls to the agent and the Forest SaaS). The graceful shutdown described above waits for
|
|
106
|
+
the buffered spans to be exported before it exits, but gives that its own 2 second deadline rather
|
|
107
|
+
than the 10 seconds in-flight requests get: an unreachable collector costs you the last spans, never
|
|
108
|
+
the ability to stop. Worst case it adds ~3 seconds to a shutdown.
|
|
109
|
+
|
|
110
|
+
Configure it entirely through the standard OTel environment variables:
|
|
111
|
+
|
|
112
|
+
| Variable | Description |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP receiver URL (e.g. `http://collector:4318`). **Tracing stays off until this or `OTEL_TRACES_EXPORTER` is set.** |
|
|
115
|
+
| `OTEL_SERVICE_NAME` | Service name reported in traces. Falls back to `service.name` in `OTEL_RESOURCE_ATTRIBUTES`, then to `forestadmin-agent-bff`. |
|
|
116
|
+
| `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, e.g. `deployment.environment=production`. A `service.name` here is honoured when `OTEL_SERVICE_NAME` is unset. |
|
|
117
|
+
| `OTEL_SDK_DISABLED` | Set to `true` (case-insensitive) to force-disable tracing whatever else is configured. |
|
|
118
|
+
| `OTEL_TRACES_EXPORTER` | Which exporter the SDK builds: `otlp` (the default), `console`, `zipkin`, `none`, or a list. Setting it alone turns tracing on without an OTLP endpoint, which is what makes `console` usable for debugging. `none` keeps instrumentation running with nothing exported, so trace context still propagates to the agent and the Forest SaaS. |
|
|
119
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Per-signal endpoint, taking precedence over the generic one above. Setting either turns tracing on. |
|
|
120
|
+
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (the default), `http/json` or `grpc`, with `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` for traces alone. |
|
|
121
|
+
| `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER` | Default to `none` here, against the SDK's own `otlp`: this image arms tracing, and leaving them unset would otherwise export metrics and logs to `http://localhost:4318` on the side. Set either one (`otlp`, `console`, …) to opt that signal back in. |
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
docker run -d \
|
|
125
|
+
-p 3450:3450 \
|
|
126
|
+
--stop-timeout 15 \
|
|
127
|
+
--add-host host.docker.internal:host-gateway \
|
|
128
|
+
-e FOREST_AUTH_SECRET="..." \
|
|
129
|
+
-e FOREST_ENV_SECRET="..." \
|
|
130
|
+
-e FOREST_SERVER_URL="https://api.forestadmin.com" \
|
|
131
|
+
-e FOREST_APP_URL="https://app.forestadmin.com" \
|
|
132
|
+
-e AGENT_URL="http://host.docker.internal:3351" \
|
|
133
|
+
-e BFF_TOKEN_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
|
|
134
|
+
-e OTEL_EXPORTER_OTLP_ENDPOINT="http://collector:4318" \
|
|
135
|
+
ghcr.io/forestadmin/agent-bff:latest
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
> **Note:** These variables only do anything in the Docker image. The image's entry point loads
|
|
139
|
+
> the tracing preload before the CLI; the npm `forest-bff` bin runs the CLI on its own, and the
|
|
140
|
+
> OpenTelemetry packages are not npm dependencies — so outside Docker an `OTEL_*` variable is
|
|
141
|
+
> read by nothing and the process starts untraced, silently.
|
|
142
|
+
|
|
143
|
+
### Without Docker
|
|
144
|
+
|
|
39
145
|
Packaged / production — run the bin:
|
|
40
146
|
|
|
41
147
|
```bash
|
|
@@ -99,7 +205,7 @@ yarn start:dev # node --env-file=.env dist/cli.js
|
|
|
99
205
|
| `FOREST_APP_URL` | yes | Forest front base URL, used to build the OAuth front-channel redirect (`src/oauth/oauth-routes.ts`). |
|
|
100
206
|
| `AGENT_URL` | yes | The customer agent base URL the BFF calls via agent-client. |
|
|
101
207
|
| `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 `configured.oauth: false` — but it stays `ok`, since the key gates OAuth and not boot; already-issued `bff_access` tokens still authenticate on `/agent/*` whenever `FOREST_AUTH_SECRET` is present. |
|
|
102
|
-
| `HTTP_PORT` | no | Server port, integer 0–65535. Defaults to `3450`. `0` binds an OS-assigned ephemeral port. |
|
|
208
|
+
| `HTTP_PORT` | no | Server port, integer 0–65535. Defaults to `3450`. `0` binds an OS-assigned ephemeral port — useful for a local run, unusable in the Docker image, where nothing outside the process learns which port it got: it can be neither published nor probed, and the image's healthcheck would report the container unhealthy forever. |
|
|
103
209
|
| `BFF_ALLOWED_ORIGINS` | no | Comma-separated CORS allow-list of origins (scheme + host + port). An entry may carry a single `*` as the leading host label — `https://*.apps.zdusercontent.com` — which matches exactly one DNS label there, and nothing else: not two labels, not the apex, and never the scheme or the port. The host left after `*.` must be at least two non-empty labels, so `https://*.com` is refused; a two-label public suffix such as `https://*.co.uk` is not, and would allow every site under it. Any other `*` in the host is refused and warned about at boot; a `*` outside the host — in userinfo, a path or a query — is stripped along with the rest of the URL, so `https://*@example.com` is simply the exact origin `https://example.com`. Empty ⇒ no cross-origin browser access. |
|
|
104
210
|
| `BFF_DEFAULT_TIMEZONE` | no | Fallback IANA timezone used when a request carries neither an `X-Forest-Timezone` header nor a body `timezone`. |
|
|
105
211
|
| `BFF_PUBLIC_URL` | no | The BFF's own external base URL, published as `servers[0].url` in the OpenAPI document so a generated client resolves endpoints without being configured by hand. Absent, `servers[0].url` stays `/`, which a consumer that fetched the document over HTTP resolves against that URL — but which leaves a client generated from an offline `forest-bff openapi` export with no base URL at all. Trailing slashes are stripped. A malformed value fails the boot, and so does one carrying credentials, a query string or a fragment: credentials would be published to every reader of the document, and anything behind a `?` or `#` swallows the path a generated client appends. |
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentActionClient, AgentActionClientOptions } from './agent-action-client';
|
|
2
|
+
import type { ActivityLogWriter } from '../activity-log/activity-log-writer';
|
|
2
3
|
import type { AgentTransport } from '../agent/agent-transport';
|
|
3
4
|
import type { Logger } from '../ports/logger-port';
|
|
4
5
|
import type ReadModelStore from '../read-model/read-model-store';
|
|
@@ -7,7 +8,8 @@ export interface ActionRoutesMiddlewareOptions {
|
|
|
7
8
|
store: ReadModelStore;
|
|
8
9
|
transport: AgentTransport;
|
|
9
10
|
logger: Logger;
|
|
11
|
+
activityLogs: ActivityLogWriter;
|
|
10
12
|
createClient?: (options: AgentActionClientOptions) => AgentActionClient;
|
|
11
13
|
}
|
|
12
|
-
export default function createActionRoutesMiddleware({ store, transport, logger, createClient, }: ActionRoutesMiddlewareOptions): Middleware;
|
|
14
|
+
export default function createActionRoutesMiddleware({ store, transport, logger, activityLogs, createClient, }: ActionRoutesMiddlewareOptions): Middleware;
|
|
13
15
|
//# sourceMappingURL=action-routes-middleware.d.ts.map
|
|
@@ -45,8 +45,19 @@ const agent_action_client_1 = __importStar(require("./agent-action-client"));
|
|
|
45
45
|
const sanitize_action_html_1 = __importDefault(require("./sanitize-action-html"));
|
|
46
46
|
const agent_error_mapper_1 = require("../http/agent-error-mapper");
|
|
47
47
|
const agent_route_helpers_1 = require("../http/agent-route-helpers");
|
|
48
|
+
const bff_http_error_1 = require("../http/bff-http-error");
|
|
48
49
|
const bff_local_errors_1 = require("../http/bff-local-errors");
|
|
49
50
|
const ACTION_ROUTE = /^\/agent\/v1\/([^/]+)\/actions\/([^/]+)\/(form|execute)$/;
|
|
51
|
+
const EXECUTE_VERB = 'execute';
|
|
52
|
+
/**
|
|
53
|
+
* An approval request is a business outcome, not a failure: the action was routed for review. The
|
|
54
|
+
* BFF answers it with a 403, but recording the entry as `failed` would make the same event count
|
|
55
|
+
* differently here and in mcp-server, which records it as a success — and action-failure statistics
|
|
56
|
+
* would mix refusals with runs that never happened.
|
|
57
|
+
*/
|
|
58
|
+
function isApprovalRequest(error) {
|
|
59
|
+
return error instanceof bff_http_error_1.BffHttpError && error.type === bff_local_errors_1.ACTION_REQUIRES_APPROVAL_TYPE;
|
|
60
|
+
}
|
|
50
61
|
// recordIds are opaque BFF ids; only presence and array-ness are checked. An empty array is allowed
|
|
51
62
|
// on purpose (global/bulk actions have no selected records); the spec AC only covers a missing
|
|
52
63
|
// recordIds, so `[]` is an explicit BFF choice, not a spec requirement.
|
|
@@ -148,7 +159,7 @@ async function handleExecute({ ctx, action, values, logger, }) {
|
|
|
148
159
|
ctx.status = status;
|
|
149
160
|
ctx.body = body;
|
|
150
161
|
}
|
|
151
|
-
function createActionRoutesMiddleware({ store, transport, logger, createClient = agent_action_client_1.default, }) {
|
|
162
|
+
function createActionRoutesMiddleware({ store, transport, logger, activityLogs, createClient = agent_action_client_1.default, }) {
|
|
152
163
|
return async function actionRoutesMiddleware(ctx, next) {
|
|
153
164
|
const match = ACTION_ROUTE.exec(ctx.path);
|
|
154
165
|
if (!match || ctx.method !== 'POST') {
|
|
@@ -162,12 +173,15 @@ function createActionRoutesMiddleware({ store, transport, logger, createClient =
|
|
|
162
173
|
const readModel = await (0, agent_route_helpers_1.resolveReadModel)(store);
|
|
163
174
|
// The read-model's action map IS the allow-list, so an absent action cannot be told from a
|
|
164
175
|
// known-but-disallowed one — every non-exposed action maps to 404 here; `action_not_allowed`
|
|
165
|
-
// (403) has no local trigger, mirroring `collection_not_allowed`/`relation_not_allowed`.
|
|
166
|
-
// URL identity is resolved before the body, so a bad action 404s before its payload is read.
|
|
176
|
+
// (403) has no local trigger, mirroring `collection_not_allowed`/`relation_not_allowed`.
|
|
167
177
|
// TODO(PRD-673): distinguish disallowed from unknown when a separate exposure source exists.
|
|
168
|
-
|
|
178
|
+
const allowed = readModel.isActionAllowed(collection, actionName);
|
|
179
|
+
const refuseUnknownAction = () => {
|
|
169
180
|
throw (0, bff_local_errors_1.unknownAction)(`Unknown action: ${collection}.${actionName}`);
|
|
170
|
-
}
|
|
181
|
+
};
|
|
182
|
+
// The form is unaudited, so it keeps refusing before the body is read.
|
|
183
|
+
if (!allowed && verb !== EXECUTE_VERB)
|
|
184
|
+
refuseUnknownAction();
|
|
171
185
|
const body = (ctx.request.body ?? {});
|
|
172
186
|
assertKnownBodyKeys(body);
|
|
173
187
|
const recordIds = parseRecordIds(body.recordIds);
|
|
@@ -177,19 +191,40 @@ function createActionRoutesMiddleware({ store, transport, logger, createClient =
|
|
|
177
191
|
token,
|
|
178
192
|
actionEndpoints: readModel.getActionEndpoints(),
|
|
179
193
|
});
|
|
180
|
-
const
|
|
194
|
+
const loadAction = () => (0, agent_route_helpers_1.callAgent)(() => client.loadAction({
|
|
181
195
|
collection,
|
|
182
196
|
actionName,
|
|
183
197
|
recordIds,
|
|
184
198
|
timezone: ctx.state.timezone,
|
|
185
199
|
}), logger);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
200
|
+
// The form is not audited, mirroring mcp-server, whose get-action-form tool writes no log
|
|
201
|
+
// either: the record-touching event the trail records is the execution.
|
|
202
|
+
if (verb !== EXECUTE_VERB) {
|
|
203
|
+
const action = await loadAction();
|
|
204
|
+
await handleForm({ ctx, action, values, logger });
|
|
205
|
+
return;
|
|
192
206
|
}
|
|
207
|
+
// The whole sequence is audited, loadAction and setFields included, so the intent is recorded
|
|
208
|
+
// even when the attempt never reaches the agent's execute. The allow-list refusal is inside
|
|
209
|
+
// too, mirroring mcp-server, whose execute-action tool resolves the action within its own
|
|
210
|
+
// wrapper: an attempt on an action the caller may not trigger is exactly what the trail is
|
|
211
|
+
// for, and the record ids it names are only known once the body is read.
|
|
212
|
+
await activityLogs.record({
|
|
213
|
+
ctx,
|
|
214
|
+
action: 'action',
|
|
215
|
+
context: {
|
|
216
|
+
collectionName: collection,
|
|
217
|
+
recordIds,
|
|
218
|
+
label: `triggered the action "${actionName}"`,
|
|
219
|
+
},
|
|
220
|
+
isCompletedDespite: isApprovalRequest,
|
|
221
|
+
operation: async () => {
|
|
222
|
+
if (!allowed)
|
|
223
|
+
refuseUnknownAction();
|
|
224
|
+
const action = await loadAction();
|
|
225
|
+
await handleExecute({ ctx, action, values, logger });
|
|
226
|
+
},
|
|
227
|
+
});
|
|
193
228
|
};
|
|
194
229
|
}
|
|
195
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
230
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aW9uLXJvdXRlcy1taWRkbGV3YXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FjdGlvbi9hY3Rpb24tcm91dGVzLW1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUE0TUEsK0NBMkZDO0FBM1JELDREQUttQztBQUVuQyxtRUFBaUU7QUFDakUsNkRBQXFEO0FBQ3JELHdGQUFxRTtBQUNyRSw2RUFBeUY7QUFDekYsa0ZBQXdEO0FBQ3hELG1FQUEyRDtBQUMzRCxxRUFLcUM7QUFDckMsMkRBQXNEO0FBQ3RELCtEQU1rQztBQUVsQyxNQUFNLFlBQVksR0FBRywwREFBMEQsQ0FBQztBQUVoRixNQUFNLFlBQVksR0FBRyxTQUFTLENBQUM7QUFFL0I7Ozs7O0dBS0c7QUFDSCxTQUFTLGlCQUFpQixDQUFDLEtBQWM7SUFDdkMsT0FBTyxLQUFLLFlBQVksNkJBQVksSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLGdEQUE2QixDQUFDO0FBQ3ZGLENBQUM7QUFPRCxvR0FBb0c7QUFDcEcsK0ZBQStGO0FBQy9GLHdFQUF3RTtBQUN4RSxxR0FBcUc7QUFDckcscUdBQXFHO0FBQ3JHLFNBQVMsY0FBYyxDQUFDLEdBQVk7SUFDbEMsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUN4QixNQUFNLElBQUEsaUNBQWMsRUFBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQ3JFLENBQUM7SUFFRCxPQUFPLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUNuQyxDQUFDO0FBRUQsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLFdBQVcsRUFBRSxRQUFRLEVBQUUsVUFBVSxDQUFDLENBQUM7QUFFN0QsZ0dBQWdHO0FBQ2hHLDBFQUEwRTtBQUMxRSxTQUFT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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the audited requests and the status transitions they fire without `await`. Nothing else
|
|
3
|
+
* keeps the transitions alive: `server.close()` waits for connections, and one sent after the
|
|
4
|
+
* response is attached to none — without this, every deploy would leave entries stuck in `pending`.
|
|
5
|
+
*
|
|
6
|
+
* The requests are tracked too, and not only their transitions, for the embedded deployment: there
|
|
7
|
+
* the host owns the connections, so `stop()` returns while requests are still running and their
|
|
8
|
+
* transitions are not registered yet.
|
|
9
|
+
*/
|
|
10
|
+
export default class ActivityLogDrainer {
|
|
11
|
+
private readonly inFlight;
|
|
12
|
+
track<T>(operation: () => Promise<T>, description: string): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Loops rather than settling one snapshot: a transition is registered only once the request it
|
|
15
|
+
* audits has finished, so a single pass would return before the work that outlives it.
|
|
16
|
+
*
|
|
17
|
+
* `timeoutMs` is the shutdown deadline the caller shares: a stalled audit store would otherwise
|
|
18
|
+
* hold the process past the grace its orchestrator gives it, and be SIGKILLed mid-drain. Returns
|
|
19
|
+
* what the deadline left unfinished, empty when everything settled.
|
|
20
|
+
*/
|
|
21
|
+
drain(timeoutMs?: number): Promise<string[]>;
|
|
22
|
+
private settle;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=activity-log-drainer.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Holds the audited requests and the status transitions they fire without `await`. Nothing else
|
|
5
|
+
* keeps the transitions alive: `server.close()` waits for connections, and one sent after the
|
|
6
|
+
* response is attached to none — without this, every deploy would leave entries stuck in `pending`.
|
|
7
|
+
*
|
|
8
|
+
* The requests are tracked too, and not only their transitions, for the embedded deployment: there
|
|
9
|
+
* the host owns the connections, so `stop()` returns while requests are still running and their
|
|
10
|
+
* transitions are not registered yet.
|
|
11
|
+
*/
|
|
12
|
+
class ActivityLogDrainer {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.inFlight = new Set();
|
|
15
|
+
}
|
|
16
|
+
track(operation, description) {
|
|
17
|
+
const promise = operation();
|
|
18
|
+
const entry = { promise, description };
|
|
19
|
+
this.inFlight.add(entry);
|
|
20
|
+
promise.finally(() => this.inFlight.delete(entry)).catch(() => { });
|
|
21
|
+
return promise;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Loops rather than settling one snapshot: a transition is registered only once the request it
|
|
25
|
+
* audits has finished, so a single pass would return before the work that outlives it.
|
|
26
|
+
*
|
|
27
|
+
* `timeoutMs` is the shutdown deadline the caller shares: a stalled audit store would otherwise
|
|
28
|
+
* hold the process past the grace its orchestrator gives it, and be SIGKILLed mid-drain. Returns
|
|
29
|
+
* what the deadline left unfinished, empty when everything settled.
|
|
30
|
+
*/
|
|
31
|
+
async drain(timeoutMs) {
|
|
32
|
+
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
33
|
+
while (this.inFlight.size > 0) {
|
|
34
|
+
const remainingMs = deadline === undefined ? undefined : deadline - Date.now();
|
|
35
|
+
if (remainingMs !== undefined && remainingMs <= 0)
|
|
36
|
+
break;
|
|
37
|
+
// eslint-disable-next-line no-await-in-loop
|
|
38
|
+
await this.settle(remainingMs);
|
|
39
|
+
}
|
|
40
|
+
return [...this.inFlight].map(entry => entry.description);
|
|
41
|
+
}
|
|
42
|
+
async settle(timeoutMs) {
|
|
43
|
+
const settled = Promise.allSettled([...this.inFlight].map(entry => entry.promise));
|
|
44
|
+
if (timeoutMs === undefined) {
|
|
45
|
+
await settled;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let timer;
|
|
49
|
+
try {
|
|
50
|
+
await Promise.race([
|
|
51
|
+
settled,
|
|
52
|
+
new Promise(resolve => {
|
|
53
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.default = ActivityLogDrainer;
|
|
63
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aXZpdHktbG9nLWRyYWluZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWN0aXZpdHktbG9nL2FjdGl2aXR5LWxvZy1kcmFpbmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBTUE7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFxQixrQkFBa0I7SUFBdkM7UUFDbUIsYUFBUSxHQUFHLElBQUksR0FBRyxFQUFxQixDQUFDO0lBd0QzRCxDQUFDO0lBdERDLEtBQUssQ0FBSSxTQUEyQixFQUFFLFdBQW1CO1FBQ3ZELE1BQU0sT0FBTyxHQUFHLFNBQVMsRUFBRSxDQUFDO1FBQzVCLE1BQU0sS0FBSyxHQUFzQixFQUFFLE9BQU8sRUFBRSxXQUFXLEVBQUUsQ0FBQztRQUMxRCxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUN6QixPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxHQUFFLENBQUMsQ0FBQyxDQUFDO1FBRW5FLE9BQU8sT0FBTyxDQUFDO0lBQ2pCLENBQUM7SUFFRDs7Ozs7OztPQU9HO0lBQ0gsS0FBSyxDQUFDLEtBQUssQ0FBQyxTQUFrQjtRQUM1QixNQUFNLFFBQVEsR0FBRyxTQUFTLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxTQUFTLENBQUM7UUFFOUUsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUM5QixNQUFNLFdBQVcsR0FBRyxRQUFRLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7WUFFL0UsSUFBSSxXQUFXLEtBQUssU0FBUyxJQUFJLFdBQVcsSUFBSSxDQUFDO2dCQUFFLE1BQU07WUFFekQsNENBQTRDO1lBQzVDLE1BQU0sSUFBSSxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQztRQUNqQyxDQUFDO1FBRUQsT0FBTyxDQUFDLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUMsQ0FBQztJQUM1RCxDQUFDO0lBRU8sS0FBSyxDQUFDLE1BQU0sQ0FBQyxTQUFrQjtRQUNyQyxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7UUFFbkYsSUFBSSxTQUFTLEtBQUssU0FBUyxFQUFFLENBQUM7WUFDNUIsTUFBTSxPQUFPLENBQUM7WUFFZCxPQUFPO1FBQ1QsQ0FBQztRQUVELElBQUksS0FBaUMsQ0FBQztRQUV0QyxJQUFJLENBQUM7WUFDSCxNQUFNLE9BQU8sQ0FBQyxJQUFJLENBQUM7Z0JBQ2pCLE9BQU87Z0JBQ1AsSUFBSSxPQUFPLENBQU8sT0FBTyxDQUFDLEVBQUU7b0JBQzFCLEtBQUssR0FBRyxVQUFVLENBQUMsT0FBTyxFQUFFLFNBQVMsQ0FBQyxDQUFDO2dCQUN6QyxDQUFDLENBQUM7YUFDSCxDQUFDLENBQUM7UUFDTCxDQUFDO2dCQUFTLENBQUM7WUFDVCxZQUFZLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDdEIsQ0FBQztJQUNILENBQUM7Q0FDRjtBQXpERCxxQ0F5REMifQ==
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ActivityLogContext, BffActivityLogAction } from './activity-logs-creator';
|
|
2
|
+
import type { ActivityLogsWriter } from './activity-logs-service';
|
|
3
|
+
import type { Logger } from '../ports/logger-port';
|
|
4
|
+
import type { Context } from 'koa';
|
|
5
|
+
export interface RecordActivityLogOptions<T> {
|
|
6
|
+
ctx: Context;
|
|
7
|
+
action: BffActivityLogAction;
|
|
8
|
+
context?: ActivityLogContext;
|
|
9
|
+
operation: () => Promise<T>;
|
|
10
|
+
isCompletedDespite?: (error: unknown) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ActivityLogWriter {
|
|
13
|
+
record<T>(options: RecordActivityLogOptions<T>): Promise<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Waits for the audited requests still running and for the status transitions no connection
|
|
16
|
+
* holds. Called when the server stops, which shares its deadline through `timeoutMs`; returns
|
|
17
|
+
* the operations that deadline left unfinished.
|
|
18
|
+
*/
|
|
19
|
+
drain(timeoutMs?: number): Promise<string[]>;
|
|
20
|
+
}
|
|
21
|
+
export interface ActivityLogWriterOptions {
|
|
22
|
+
service: ActivityLogsWriter;
|
|
23
|
+
logger: Logger;
|
|
24
|
+
}
|
|
25
|
+
export default function createActivityLogWriter({ service, logger, }: ActivityLogWriterOptions): ActivityLogWriter;
|
|
26
|
+
//# sourceMappingURL=activity-log-writer.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.default = createActivityLogWriter;
|
|
7
|
+
const activity_log_drainer_1 = __importDefault(require("./activity-log-drainer"));
|
|
8
|
+
const with_activity_log_1 = __importDefault(require("./with-activity-log"));
|
|
9
|
+
function describeRequest(action, collectionName) {
|
|
10
|
+
return collectionName ? `'${action}' request on '${collectionName}'` : `'${action}' request`;
|
|
11
|
+
}
|
|
12
|
+
function createActivityLogWriter({ service, logger, }) {
|
|
13
|
+
const drainer = new activity_log_drainer_1.default();
|
|
14
|
+
return {
|
|
15
|
+
record(options) {
|
|
16
|
+
return drainer.track(() => (0, with_activity_log_1.default)({ ...options, service, drainer, logger }), describeRequest(options.action, options.context?.collectionName));
|
|
17
|
+
},
|
|
18
|
+
drain(timeoutMs) {
|
|
19
|
+
return drainer.drain(timeoutMs);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aXZpdHktbG9nLXdyaXRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hY3Rpdml0eS1sb2cvYWN0aXZpdHktbG9nLXdyaXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQW1DQSwwQ0FrQkM7QUFoREQsa0ZBQXdEO0FBQ3hELDRFQUFrRDtBQXlCbEQsU0FBUyxlQUFlLENBQUMsTUFBNEIsRUFBRSxjQUF1QjtJQUM1RSxPQUFPLGNBQWMsQ0FBQyxDQUFDLENBQUMsSUFBSSxNQUFNLGlCQUFpQixjQUFjLEdBQUcsQ0FBQyxDQUFDLENBQUMsSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUMvRixDQUFDO0FBRUQsU0FBd0IsdUJBQXVCLENBQUMsRUFDOUMsT0FBTyxFQUNQLE1BQU0sR0FDbUI7SUFDekIsTUFBTSxPQUFPLEdBQUcsSUFBSSw4QkFBa0IsRUFBRSxDQUFDO0lBRXpDLE9BQU87UUFDTCxNQUFNLENBQUksT0FBb0M7WUFDNUMsT0FBTyxPQUFPLENBQUMsS0FBSyxDQUNsQixHQUFHLEVBQUUsQ0FBQyxJQUFBLDJCQUFlLEVBQUMsRUFBRSxHQUFHLE9BQU8sRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQy9ELGVBQWUsQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxPQUFPLEVBQUUsY0FBYyxDQUFDLENBQ2pFLENBQUM7UUFDSixDQUFDO1FBRUQsS0FBSyxDQUFDLFNBQWtCO1lBQ3RCLE9BQU8sT0FBTyxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUNsQyxDQUFDO0tBQ0YsQ0FBQztBQUNKLENBQUMifQ==
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type ActivityLogDrainer from './activity-log-drainer';
|
|
2
|
+
import type { ActivityLogsWriter } from './activity-logs-service';
|
|
3
|
+
import type { Logger } from '../ports/logger-port';
|
|
4
|
+
import type { ActivityLogAction, ActivityLogResponse } from '@forestadmin/forestadmin-client';
|
|
5
|
+
import type { Context } from 'koa';
|
|
6
|
+
/** The actions the BFF writes: its data routes read, and its action route writes. */
|
|
7
|
+
export type BffActivityLogAction = Extract<ActivityLogAction, 'index' | 'search' | 'filter' | 'listRelatedData' | 'action'>;
|
|
8
|
+
export interface ActivityLogContext {
|
|
9
|
+
collectionName?: string;
|
|
10
|
+
recordId?: string | number;
|
|
11
|
+
recordIds?: string[] | number[];
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The token that created the log is kept for the status transition: the transition is fired after
|
|
16
|
+
* the response, when the session it came from may already be unreachable.
|
|
17
|
+
*/
|
|
18
|
+
export interface PendingActivityLog {
|
|
19
|
+
activityLog: ActivityLogResponse;
|
|
20
|
+
forestServerToken: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CreatePendingActivityLogOptions {
|
|
23
|
+
ctx: Context;
|
|
24
|
+
service: ActivityLogsWriter;
|
|
25
|
+
action: BffActivityLogAction;
|
|
26
|
+
context?: ActivityLogContext;
|
|
27
|
+
logger: Logger;
|
|
28
|
+
}
|
|
29
|
+
export default function createPendingActivityLog({ ctx, service, action, context, logger, }: CreatePendingActivityLogOptions): Promise<PendingActivityLog | null>;
|
|
30
|
+
export interface MarkActivityLogOptions {
|
|
31
|
+
service: ActivityLogsWriter;
|
|
32
|
+
drainer: ActivityLogDrainer;
|
|
33
|
+
pending: PendingActivityLog;
|
|
34
|
+
status: 'completed' | 'failed';
|
|
35
|
+
logger: Logger;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fire-and-forget on purpose: the caller's response must not wait for the audit store. The drainer
|
|
39
|
+
* holds the promise so a shutdown can wait for it instead.
|
|
40
|
+
*/
|
|
41
|
+
export declare function markActivityLog(options: MarkActivityLogOptions): void;
|
|
42
|
+
//# sourceMappingURL=activity-logs-creator.d.ts.map
|