@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.
Files changed (45) hide show
  1. package/README.md +107 -1
  2. package/dist/action/action-routes-middleware.d.ts +3 -1
  3. package/dist/action/action-routes-middleware.js +48 -13
  4. package/dist/activity-log/activity-log-drainer.d.ts +24 -0
  5. package/dist/activity-log/activity-log-drainer.js +63 -0
  6. package/dist/activity-log/activity-log-writer.d.ts +26 -0
  7. package/dist/activity-log/activity-log-writer.js +23 -0
  8. package/dist/activity-log/activity-logs-creator.d.ts +42 -0
  9. package/dist/activity-log/activity-logs-creator.js +209 -0
  10. package/dist/activity-log/activity-logs-service.d.ts +18 -0
  11. package/dist/activity-log/activity-logs-service.js +19 -0
  12. package/dist/activity-log/with-activity-log.d.ts +25 -0
  13. package/dist/activity-log/with-activity-log.js +63 -0
  14. package/dist/api-key/api-key-authenticator.d.ts +11 -0
  15. package/dist/api-key/api-key-authenticator.js +12 -2
  16. package/dist/api-key/api-key-client.d.ts +6 -0
  17. package/dist/api-key/api-key-client.js +2 -1
  18. package/dist/api-key/api-key-middleware.d.ts +10 -1
  19. package/dist/api-key/api-key-middleware.js +18 -1
  20. package/dist/api-key/resolve-cache.d.ts +12 -0
  21. package/dist/api-key/resolve-cache.js +38 -6
  22. package/dist/auth/auth-mode.d.ts +1 -0
  23. package/dist/auth/auth-mode.js +10 -3
  24. package/dist/auth/forest-server-token-middleware.d.ts +25 -0
  25. package/dist/auth/forest-server-token-middleware.js +90 -0
  26. package/dist/build-bff.d.ts +8 -0
  27. package/dist/build-bff.js +29 -10
  28. package/dist/cli-core.d.ts +9 -0
  29. package/dist/cli-core.js +38 -3
  30. package/dist/data/agent-query.d.ts +12 -0
  31. package/dist/data/agent-query.js +19 -1
  32. package/dist/data/data-routes-middleware.d.ts +3 -1
  33. package/dist/data/data-routes-middleware.js +41 -4
  34. package/dist/http/bff-http-server.d.ts +22 -0
  35. package/dist/http/bff-http-server.js +50 -5
  36. package/dist/http/bff-local-errors.d.ts +15 -0
  37. package/dist/http/bff-local-errors.js +29 -3
  38. package/dist/openapi/openapi-document.js +3 -3
  39. package/dist/tracing-handle.d.ts +26 -0
  40. package/dist/tracing-handle.js +40 -0
  41. package/dist/tracing-preload.d.ts +2 -0
  42. package/dist/tracing-preload.js +27 -0
  43. package/dist/tracing.d.ts +79 -0
  44. package/dist/tracing.js +168 -0
  45. 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`. The
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
- if (!readModel.isActionAllowed(collection, actionName)) {
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 action = await (0, agent_route_helpers_1.callAgent)(() => client.loadAction({
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
- const handlerArgs = { ctx, action, values, logger };
187
- if (verb === 'execute') {
188
- await handleExecute(handlerArgs);
189
- }
190
- else {
191
- await handleForm(handlerArgs);
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aW9uLXJvdXRlcy1taWRkbGV3YXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FjdGlvbi9hY3Rpb24tcm91dGVzLW1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUE0TEEsK0NBNERDO0FBN09ELDREQUttQztBQUVuQyxtRUFBaUU7QUFDakUsNkRBQXFEO0FBQ3JELHdGQUFxRTtBQUNyRSw2RUFBeUY7QUFDekYsa0ZBQXdEO0FBQ3hELG1FQUEyRDtBQUMzRCxxRUFLcUM7QUFDckMsK0RBS2tDO0FBRWxDLE1BQU0sWUFBWSxHQUFHLDBEQUEwRCxDQUFDO0FBT2hGLG9HQUFvRztBQUNwRywrRkFBK0Y7QUFDL0Ysd0VBQXdFO0FBQ3hFLHFHQUFxRztBQUNyRyxxR0FBcUc7QUFDckcsU0FBUyxjQUFjLENBQUMsR0FBWTtJQUNsQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3hCLE1BQU0sSUFBQSxpQ0FBYyxFQUFDLDRDQUE0QyxDQUFDLENBQUM7SUFDckUsQ0FBQztJQUVELE9BQU8sR0FBRyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBQ25DLENBQUM7QUFFRCxNQUFNLGdCQUFnQixHQUFHLENBQUMsV0FBVyxFQUFFLFFBQVEsRUFBRSxVQUFVLENBQUMsQ0FBQztBQUU3RCxnR0FBZ0c7QUFDaEcsMEVBQTBFO0FBQzFFLFNBQVMsbUJBQW1CLENBQUMsSUFBNkI7SUFDeEQsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDLGdCQUFnQixDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBRTdFLElB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
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