@forestadmin/agent-bff 1.30.2 → 1.32.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 +28 -5
- 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/cors/origin.d.ts +1 -0
- package/dist/cors/origin.js +57 -6
- package/dist/cors/per-key-origin.js +5 -4
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ yarn start:dev # node --env-file=.env dist/cli.js
|
|
|
100
100
|
| `AGENT_URL` | yes | The customer agent base URL the BFF calls via agent-client. |
|
|
101
101
|
| `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
102
|
| `HTTP_PORT` | no | Server port, integer 0–65535. Defaults to `3450`. `0` binds an OS-assigned ephemeral port. |
|
|
103
|
-
| `BFF_ALLOWED_ORIGINS` | no | Comma-separated CORS allow-list of
|
|
103
|
+
| `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
104
|
| `BFF_DEFAULT_TIMEZONE` | no | Fallback IANA timezone used when a request carries neither an `X-Forest-Timezone` header nor a body `timezone`. |
|
|
105
105
|
| `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. |
|
|
106
106
|
| `BFF_AGENT_TIMEOUT_MS` | no | How long the BFF waits for the customer agent on any data or action call, in milliseconds. Defaults to `10000`. Past it the route answers `504 agent_timeout`; any outright transport failure — a refused connection, an unresolvable host, a connection reset, a TLS failure — answers `502 network_error` instead. Surrounding whitespace is trimmed, then a malformed value (non-integer, `0`, or above 2147483647) fails the boot; an unset, empty, or whitespace-only value counts as absent and takes the default. |
|
|
@@ -117,8 +117,11 @@ yarn start:dev # node --env-file=.env dist/cli.js
|
|
|
117
117
|
fails fast at boot: the process exits with a clear error and never echoes the offending value.
|
|
118
118
|
- A required var that is absent (or empty / whitespace-only) does not crash the server. It boots and
|
|
119
119
|
reports the gap through `/health` (503 `degraded`).
|
|
120
|
-
- Malformed `BFF_ALLOWED_ORIGINS` entries
|
|
121
|
-
|
|
120
|
+
- Malformed `BFF_ALLOWED_ORIGINS` entries are dropped and logged once at boot (`Warn`); they never
|
|
121
|
+
enter the allow-list. That covers a literal `*`, a `*` outside the leading host label
|
|
122
|
+
(`https://a.*.example.com`), several `*`, a host left with fewer than two non-empty labels
|
|
123
|
+
(`https://*.com`, `https://*.com.`, `https://*..com`, `https://*.localhost`), and a pattern on an
|
|
124
|
+
IP (`https://*.127.0.0.1`). Only the single leading-label form is served as a wildcard.
|
|
122
125
|
|
|
123
126
|
## Request edge (`/agent/*`)
|
|
124
127
|
|
|
@@ -158,8 +161,23 @@ a `POST /oauth/token` concern (`session_invalidated`, OAuth/RFC shape).
|
|
|
158
161
|
|
|
159
162
|
### CORS
|
|
160
163
|
|
|
161
|
-
Two layers, both driven by
|
|
162
|
-
|
|
164
|
+
Two layers, both driven by the same matcher (case-insensitive scheme/host, default ports normalized
|
|
165
|
+
away, no trailing slash). An entry with no `*` matches by strict equality. An entry whose leading
|
|
166
|
+
host label is `*` — `https://*.apps.zdusercontent.com` — matches exactly one DNS label in its place:
|
|
167
|
+
`https://1231469.apps.zdusercontent.com` passes, `https://a.b.apps.zdusercontent.com` and the apex
|
|
168
|
+
`https://apps.zdusercontent.com` do not. Scheme and port are never wildcarded and must match
|
|
169
|
+
exactly. The host left after `*.` must be at least two non-empty labels; every other `*` shape is
|
|
170
|
+
refused at parse time and warned about at boot.
|
|
171
|
+
|
|
172
|
+
The wildcard exists for hosts that serve an application under an opaque generated subdomain — an
|
|
173
|
+
installed Zendesk app is served from `https://<app-id>.apps.zdusercontent.com` — where naming the
|
|
174
|
+
exact origin means copying an id the operator does not choose. Understand what it grants: every app
|
|
175
|
+
of every publisher on that domain is inside the allow-list. That is acceptable here because CORS is
|
|
176
|
+
not the authentication boundary — a `bff_access` session or an API key is still required, and
|
|
177
|
+
another origin cannot read this app's tokens — but it is a real widening, so keep the suffix as
|
|
178
|
+
specific as the host allows.
|
|
179
|
+
|
|
180
|
+
The two layers:
|
|
163
181
|
|
|
164
182
|
- **Layer 1 (transport)** — the only layer that sets `Access-Control-Allow-Origin`. An origin in
|
|
165
183
|
`BFF_ALLOWED_ORIGINS` is echoed back exactly; any other `Origin` is refused outright with
|
|
@@ -175,6 +193,11 @@ normalized away, no trailing slash, no wildcard, no subdomain matching):
|
|
|
175
193
|
boundary, not the origin. An opaque `Origin: null` (sandboxed iframe, cross-origin redirect) is a
|
|
176
194
|
present origin and is rejected. An empty per-key list is a no-op.
|
|
177
195
|
|
|
196
|
+
A wildcard in a key's `allowedOrigins` (layer 2) is matched the same way. Those entries come from
|
|
197
|
+
the Forest SaaS and never pass through the boot-time parser, so an illegal pattern there gets no
|
|
198
|
+
warning of its own; it counts as an origin that can never pass, and the key-level `Warn` names the
|
|
199
|
+
key once when *every* one of its origins is outside `BFF_ALLOWED_ORIGINS` or illegal.
|
|
200
|
+
|
|
178
201
|
**Local development:** browsers still enforce CORS against `localhost`, so add your dev origin(s) to
|
|
179
202
|
`BFF_ALLOWED_ORIGINS` (e.g. `BFF_ALLOWED_ORIGINS=http://localhost:4200`) — there is no dev bypass.
|
|
180
203
|
|
|
@@ -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
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = createPendingActivityLog;
|
|
4
|
+
exports.markActivityLog = markActivityLog;
|
|
5
|
+
const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
|
|
6
|
+
const api_key_middleware_1 = require("../api-key/api-key-middleware");
|
|
7
|
+
const forest_server_token_middleware_1 = require("../auth/forest-server-token-middleware");
|
|
8
|
+
const bff_http_error_1 = require("../http/bff-http-error");
|
|
9
|
+
const bff_local_errors_1 = require("../http/bff-local-errors");
|
|
10
|
+
/**
|
|
11
|
+
* Fail policy for the audit trail, keyed by action type: a write whose activity log cannot be
|
|
12
|
+
* created is blocked (no unaudited side effect), while a read proceeds with a warning (an audit
|
|
13
|
+
* store outage must not take down the read surface).
|
|
14
|
+
*
|
|
15
|
+
* One case is arbitrated by the cause instead of the action type: an authorization refusal (403)
|
|
16
|
+
* propagates for reads too — the read itself is not authorized either.
|
|
17
|
+
*/
|
|
18
|
+
const ACTION_TO_TYPE = {
|
|
19
|
+
index: 'read',
|
|
20
|
+
search: 'read',
|
|
21
|
+
filter: 'read',
|
|
22
|
+
listRelatedData: 'read',
|
|
23
|
+
action: 'write',
|
|
24
|
+
};
|
|
25
|
+
const MAX_STATUS_ATTEMPTS = 5;
|
|
26
|
+
const STATUS_RETRY_DELAY_MS = 500;
|
|
27
|
+
const NO_RENDERING_MESSAGE = 'This request carries no usable rendering';
|
|
28
|
+
function describeCause(error) {
|
|
29
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
30
|
+
}
|
|
31
|
+
const FORBIDDEN = 403;
|
|
32
|
+
const UNAUTHORIZED = 401;
|
|
33
|
+
const SERVER_ERROR = 500;
|
|
34
|
+
/** Not found: the document may not be visible yet. 0: unreachable. 408: timeout. 429: throttled. */
|
|
35
|
+
const RETRYABLE_TRANSITION_STATUSES = new Set([0, 404, 408, 429]);
|
|
36
|
+
const AUDIT_ENDPOINT_ABSENT_STATUSES = new Set([404, 501]);
|
|
37
|
+
const NO_AUDIT_ENDPOINT_MESSAGE = 'The Forest server does not expose the endpoint the activity log is written through, so the ' +
|
|
38
|
+
'operation was not performed';
|
|
39
|
+
/**
|
|
40
|
+
* A 403 only. A 401 is not the caller being refused: the bearer the BFF audits with is minted by
|
|
41
|
+
* the Forest server and cached, so a 401 says that token expired — answering the caller with
|
|
42
|
+
* `audit_not_authorized` would refuse a read the fail-open policy lets through.
|
|
43
|
+
*/
|
|
44
|
+
function isAuthorizationRefusal(error) {
|
|
45
|
+
return error instanceof forestadmin_client_1.HttpError && error.status === FORBIDDEN;
|
|
46
|
+
}
|
|
47
|
+
function isExpiredAuditCredential(error) {
|
|
48
|
+
return error instanceof forestadmin_client_1.HttpError && error.status === UNAUTHORIZED;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A Forest server that does not serve the activity-log endpoint at all, rather than one failing to
|
|
52
|
+
* answer it. No retry can make the route appear, so the caller must not be handed a `Retry-After`
|
|
53
|
+
* it would keep honouring on every write.
|
|
54
|
+
*/
|
|
55
|
+
function isAuditEndpointAbsent(error) {
|
|
56
|
+
return error instanceof forestadmin_client_1.HttpError && AUDIT_ENDPOINT_ABSENT_STATUSES.has(error.status);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* What locates the failure for support, and nothing else: the credential, the record ids and the
|
|
60
|
+
* label are the payload this must never carry.
|
|
61
|
+
*/
|
|
62
|
+
function auditIdentifiers(ctx, context) {
|
|
63
|
+
const renderingId = (0, forest_server_token_middleware_1.resolveRenderingId)(ctx);
|
|
64
|
+
return {
|
|
65
|
+
...(renderingId === undefined ? {} : { renderingId }),
|
|
66
|
+
...(context?.collectionName === undefined ? {} : { collectionName: context.collectionName }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** An empty string is an answer, not a value: it locates no document, so it fails the guard. */
|
|
70
|
+
function isPresent(value) {
|
|
71
|
+
return value !== null && value !== undefined && value !== '';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The status transition reads both the id and the index, so an answer carrying only an id strands
|
|
75
|
+
* the entry `pending`: the fail-closed policy has to engage here, not asynchronously afterwards.
|
|
76
|
+
*/
|
|
77
|
+
function isTransitionable(activityLog) {
|
|
78
|
+
return isPresent(activityLog?.id) && isPresent(activityLog?.attributes?.index);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A credential this deployment never mints is a degradation the Forest server declares by sending
|
|
82
|
+
* none (`api-key/api-key-client.ts`), so it warns; only a resolution that actually failed is an
|
|
83
|
+
* error. The single report for either: `with-activity-log` states nothing of its own, which used to
|
|
84
|
+
* double every line of the read path.
|
|
85
|
+
*/
|
|
86
|
+
function reportUnresolvedCredentials({ ctx, action, context, logger, error, }) {
|
|
87
|
+
const identifiers = { ...auditIdentifiers(ctx, context), cause: describeCause(error) };
|
|
88
|
+
if ((0, bff_local_errors_1.isUnretryableAuditFailure)(error)) {
|
|
89
|
+
logger('Warn', `Activity log for '${action}' was not created: this deployment has no credential to write ` +
|
|
90
|
+
'it with', identifiers);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
logger('Error', `Activity log for '${action}' has no credentials to be created with`, identifiers);
|
|
94
|
+
}
|
|
95
|
+
async function resolveCredentials(ctx) {
|
|
96
|
+
const renderingId = (0, forest_server_token_middleware_1.resolveRenderingId)(ctx);
|
|
97
|
+
if (renderingId === undefined)
|
|
98
|
+
throw (0, bff_http_error_1.sessionExpired)(NO_RENDERING_MESSAGE);
|
|
99
|
+
return {
|
|
100
|
+
forestServerToken: await (0, forest_server_token_middleware_1.resolveForestServerToken)(ctx),
|
|
101
|
+
renderingId: String(renderingId),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function createPendingActivityLog({ ctx, service, action, context, logger, }) {
|
|
105
|
+
const type = ACTION_TO_TYPE[action];
|
|
106
|
+
let credentials;
|
|
107
|
+
try {
|
|
108
|
+
credentials = await resolveCredentials(ctx);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
reportUnresolvedCredentials({ ctx, action, context, logger, error });
|
|
112
|
+
if (type === 'write')
|
|
113
|
+
throw error;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const { forestServerToken, renderingId } = credentials;
|
|
117
|
+
let activityLog;
|
|
118
|
+
try {
|
|
119
|
+
activityLog = await service.createMcpActivityLog({
|
|
120
|
+
forestServerToken,
|
|
121
|
+
renderingId,
|
|
122
|
+
action,
|
|
123
|
+
type,
|
|
124
|
+
collectionName: context?.collectionName,
|
|
125
|
+
recordId: context?.recordId,
|
|
126
|
+
recordIds: context?.recordIds,
|
|
127
|
+
label: context?.label,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger('Error', `Activity log for '${action}' could not be created`, {
|
|
132
|
+
...auditIdentifiers(ctx, context),
|
|
133
|
+
cause: describeCause(error),
|
|
134
|
+
});
|
|
135
|
+
if (isAuthorizationRefusal(error))
|
|
136
|
+
throw (0, bff_local_errors_1.auditNotAuthorized)();
|
|
137
|
+
if (isExpiredAuditCredential(error))
|
|
138
|
+
(0, api_key_middleware_1.invalidateApiKeyIdentity)(ctx);
|
|
139
|
+
if (type === 'write') {
|
|
140
|
+
throw isAuditEndpointAbsent(error)
|
|
141
|
+
? (0, bff_local_errors_1.auditUnavailable)(undefined, NO_AUDIT_ENDPOINT_MESSAGE)
|
|
142
|
+
: (0, bff_local_errors_1.auditUnavailable)(bff_local_errors_1.AUDIT_RETRY_AFTER_SECONDS);
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (!isTransitionable(activityLog)) {
|
|
147
|
+
logger('Error', `Activity log for '${action}' could not be created: the server answered with no activity ` +
|
|
148
|
+
'log id or index, so the audit store dropped the write', auditIdentifiers(ctx, context));
|
|
149
|
+
if (type === 'write')
|
|
150
|
+
throw (0, bff_local_errors_1.auditUnavailable)(bff_local_errors_1.AUDIT_RETRY_AFTER_SECONDS);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return { activityLog, forestServerToken };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The transition is fired after the response, so nothing retries it downstream: an entry whose
|
|
157
|
+
* transition is dropped stays `pending` for good and skews the action-failure statistics. Retried
|
|
158
|
+
* are the failures a later attempt can land — the document not existing yet, a timeout, a throttle,
|
|
159
|
+
* an unreachable server (status 0) and any 5xx — plus a transport failure the client rethrows raw,
|
|
160
|
+
* which carries no status at all. A refusal or a malformed request is left to fail at once.
|
|
161
|
+
*/
|
|
162
|
+
function isRetryableTransitionFailure(error) {
|
|
163
|
+
if (!(error instanceof forestadmin_client_1.HttpError))
|
|
164
|
+
return true;
|
|
165
|
+
return error.status >= SERVER_ERROR || RETRYABLE_TRANSITION_STATUSES.has(error.status);
|
|
166
|
+
}
|
|
167
|
+
async function updateStatus(options, attempt = 1) {
|
|
168
|
+
const { service, pending, status, logger } = options;
|
|
169
|
+
try {
|
|
170
|
+
await service.updateActivityLogStatus({
|
|
171
|
+
forestServerToken: pending.forestServerToken,
|
|
172
|
+
activityLog: pending.activityLog,
|
|
173
|
+
status,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (isRetryableTransitionFailure(error) && attempt < MAX_STATUS_ATTEMPTS) {
|
|
178
|
+
logger('Debug', `Activity log status transition failed, retrying it`, {
|
|
179
|
+
attempt,
|
|
180
|
+
attempts: MAX_STATUS_ATTEMPTS,
|
|
181
|
+
cause: describeCause(error),
|
|
182
|
+
});
|
|
183
|
+
await new Promise(resolve => {
|
|
184
|
+
// Unreferenced: a pending retry must not outlive the shutdown grace the drainer enforces.
|
|
185
|
+
setTimeout(resolve, STATUS_RETRY_DELAY_MS).unref();
|
|
186
|
+
});
|
|
187
|
+
await updateStatus(options, attempt + 1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Fire-and-forget on purpose: the caller's response must not wait for the audit store. The drainer
|
|
195
|
+
* holds the promise so a shutdown can wait for it instead.
|
|
196
|
+
*/
|
|
197
|
+
function markActivityLog(options) {
|
|
198
|
+
const { drainer, status, logger } = options;
|
|
199
|
+
drainer
|
|
200
|
+
.track(() => updateStatus(options), `'${status}' transition of the activity log ${options.pending.activityLog.id}`)
|
|
201
|
+
.catch(error => {
|
|
202
|
+
logger('Error', `Failed to mark the activity log as '${status}'`, {
|
|
203
|
+
activityLogId: options.pending.activityLog.id,
|
|
204
|
+
index: options.pending.activityLog.attributes?.index,
|
|
205
|
+
cause: describeCause(error),
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aXZpdHktbG9ncy1jcmVhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FjdGl2aXR5LWxvZy9hY3Rpdml0eS1sb2dzLWNyZWF0b3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUF1TUEsMkNBb0VDO0FBMERELDBDQWVDO0FBMVVELHdFQUE0RDtBQUU1RCxzRUFBeUU7QUFDekUsMkZBR2dEO0FBQ2hELDJEQUF3RDtBQUN4RCwrREFLa0M7QUFRbEM7Ozs7Ozs7R0FPRztBQUNILE1BQU0sY0FBYyxHQUFrRDtJQUNwRSxLQUFLLEVBQUUsTUFBTTtJQUNiLE1BQU0sRUFBRSxNQUFNO0lBQ2QsTUFBTSxFQUFFLE1BQU07SUFDZCxlQUFlLEVBQUUsTUFBTTtJQUN2QixNQUFNLEVBQUUsT0FBTztDQUNoQixDQUFDO0FBRUYsTUFBTSxtQkFBbUIsR0FBRyxDQUFDLENBQUM7QUFDOUIsTUFBTSxxQkFBcUIsR0FBRyxHQUFHLENBQUM7QUFFbEMsTUFBTSxvQkFBb0IsR0FBRywwQ0FBMEMsQ0FBQztBQStCeEUsU0FBUyxhQUFhLENBQUMsS0FBYztJQUNuQyxPQUFPLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsS0FBSyxDQUFDLElBQUksS0FBSyxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwRixDQUFDO0FBRUQsTUFBTSxTQUFTLEdBQUcsR0FBRyxDQUFDO0FBQ3RCLE1BQU0sWUFBWSxHQUFHLEdBQUcsQ0FBQztBQUN6QixNQUFNLFlBQVksR0FBRyxHQUFHLENBQUM7QUFDekIsb0dBQW9HO0FBQ3BHLE1BQU0sNkJBQTZCLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDO0FBQ2xFLE1BQU0sOEJBQThCLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQztBQUUzRCxNQUFNLHlCQUF5QixHQUM3Qiw2RkFBNkY7SUFDN0YsNkJBQTZCLENBQUM7QUFFaEM7Ozs7R0FJRztBQUNILFNBQVMsc0JBQXNCLENBQUMsS0FBYztJQUM1QyxPQUFPLEtBQUssWUFBWSw4QkFBUyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssU0FBUyxDQUFDO0FBQ2xFLENBQUM7QUFFRCxTQUFTLHdCQUF3QixDQUFDLEtBQWM7SUFDOUMsT0FBTyxLQUFLLFlBQVksOEJBQVMsSUFBSSxLQUFLLENBQUMsTUFBTSxLQUFLLFlBQVksQ0FBQztBQUNyRSxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILFNBQVMscUJBQXFCLENBQUMsS0FBYztJQUMzQyxPQUFPLEtBQUssWUFBWSw4QkFBUyxJQUFJLDhCQUE4QixDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDeEYsQ0FBQztBQUVEOzs7R0FHRztBQUNILFNBQVMsZ0JBQWdCLENBQ3ZCLEdBQVksRUFDWixPQUE0QjtJQUU1QixNQUFNLFdBQVcsR0FBRyxJQUFBLG1EQUFrQixFQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRTVDLE9BQU87UUFDTCxHQUFHLENBQUMsV0FBVyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLFdBQVcsRUFBRSxDQUFDO1FBQ3JELEdBQUcsQ0FBQyxPQUFPLEVBQUUsY0FBYyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLGNBQWMsRUFBRSxPQUFPLENBQUMsY0FBYyxFQUFFLENBQUM7S0FDN0YsQ0FBQztBQUNKLENBQUM7QUFFRCxnR0FBZ0c7QUFDaEcsU0FBUyxTQUFTLENBQUMsS0FBZ0M7SUFDakQsT0FBTyxLQUFLLEtBQUssSUFBSSxJQUFJLEtBQUssS0FBSyxTQUFTLElBQUksS0FBSyxLQUFLLEVBQUUsQ0FBQztBQUMvRCxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsU0FBUyxnQkFBZ0IsQ0FBQyxXQUFnQztJQUN4RCxPQUFPLFNBQVMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLElBQUksU0FBUyxDQUFDLFdBQVcsRUFBRSxVQUFVLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFDakYsQ0FBQztBQVVEOzs7OztHQUtHO0FBQ0gsU0FBUywyQkFBMkIsQ0FBQyxFQUNuQyxHQUFHLEVBQ0gsTUFBTSxFQUNOLE9BQU8sRUFDUCxNQUFNLEVBQ04sS0FBSyxHQUN1QjtJQUM1QixNQUFNLFdBQVcsR0FBRyxFQUFFLEdBQUcsZ0JBQWdCLENBQUMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxFQUFFLEtBQUssRUFBRSxhQUFhLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztJQUV2RixJQUFJLElBQUEsNENBQXlCLEVBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNyQyxNQUFNLENBQ0osTUFBTSxFQUNOLHFCQUFxQixNQUFNLGdFQUFnRTtZQUN6RixTQUFTLEVBQ1gsV0FBVyxDQUNaLENBQUM7UUFFRixPQUFPO0lBQ1QsQ0FBQztJQUVELE1BQU0sQ0FDSixPQUFPLEVBQ1AscUJBQXFCLE1BQU0seUNBQXlDLEVBQ3BFLFdBQVcsQ0FDWixDQUFDO0FBQ0osQ0FBQztBQUVELEtBQUssVUFBVSxrQkFBa0IsQ0FBQyxHQUFZO0lBQzVDLE1BQU0sV0FBVyxHQUFHLElBQUEsbURBQWtCLEVBQUMsR0FBRyxDQUFDLENBQUM7SUFFNUMsSUFBSSxXQUFXLEtBQUssU0FBUztRQUFFLE1BQU0sSUFBQSwrQkFBYyxFQUFDLG9CQUFvQixDQUFDLENBQUM7SUFFMUUsT0FBTztRQUNMLGlCQUFpQixFQUFFLE1BQU0sSUFBQSx5REFBd0IsRUFBQyxHQUFHLENBQUM7UUFDdEQsV0FBVyxFQUFFLE1BQU0sQ0FBQyxXQUFXLENBQUM7S0FDakMsQ0FBQztBQUNKLENBQUM7QUFFYyxLQUFLLFVBQVUsd0JBQXdCLENBQUMsRUFDckQsR0FBRyxFQUNILE9BQU8sRUFDUCxNQUFNLEVBQ04sT0FBTyxFQUNQLE1BQU0sR0FDMEI7SUFDaEMsTUFBTSxJQUFJLEdBQUcsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRXBDLElBQUksV0FBNkIsQ0FBQztJQUVsQyxJQUFJLENBQUM7UUFDSCxXQUFXLEdBQUcsTUFBTSxrQkFBa0IsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUM5QyxDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLDJCQUEyQixDQUFDLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7UUFFckUsSUFBSSxJQUFJLEtBQUssT0FBTztZQUFFLE1BQU0sS0FBSyxDQUFDO1FBRWxDLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUVELE1BQU0sRUFBRSxpQkFBaUIsRUFBRSxXQUFXLEVBQUUsR0FBRyxXQUFXLENBQUM7SUFFdkQsSUFBSSxXQUFnQyxDQUFDO0lBRXJDLElBQUksQ0FBQztRQUNILFdBQVcsR0FBRyxNQUFNLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBQztZQUMvQyxpQkFBaUI7WUFDakIsV0FBVztZQUNYLE1BQU07WUFDTixJQUFJO1lBQ0osY0FBYyxFQUFFLE9BQU8sRUFBRSxjQUFjO1lBQ3ZDLFFBQVEsRUFBRSxPQUFPLEVBQUUsUUFBUTtZQUMzQixTQUFTLEVBQUUsT0FBTyxFQUFFLFNBQVM7WUFDN0IsS0FBSyxFQUFFLE9BQU8sRUFBRSxLQUFLO1NBQ3RCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsTUFBTSxDQUFDLE9BQU8sRUFBRSxxQkFBcUIsTUFBTSx3QkFBd0IsRUFBRTtZQUNuRSxHQUFHLGdCQUFnQixDQUFDLEdBQUcsRUFBRSxPQUFPLENBQUM7WUFDakMsS0FBSyxFQUFFLGFBQWEsQ0FBQyxLQUFLLENBQUM7U0FDNUIsQ0FBQyxDQUFDO1FBRUgsSUFBSSxzQkFBc0IsQ0FBQyxLQUFLLENBQUM7WUFBRSxNQUFNLElBQUEscUNBQWtCLEdBQUUsQ0FBQztRQUM5RCxJQUFJLHdCQUF3QixDQUFDLEtBQUssQ0FBQztZQUFFLElBQUEsNkNBQXdCLEVBQUMsR0FBRyxDQUFDLENBQUM7UUFFbkUsSUFBSSxJQUFJLEtBQUssT0FBTyxFQUFFLENBQUM7WUFDckIsTUFBTSxxQkFBcUIsQ0FBQyxLQUFLLENBQUM7Z0JBQ2hDLENBQUMsQ0FBQyxJQUFBLG1DQUFnQixFQUFDLFNBQVMsRUFBRSx5QkFBeUIsQ0FBQztnQkFDeEQsQ0FBQyxDQUFDLElBQUEsbUNBQWdCLEVBQUMsNENBQXlCLENBQUMsQ0FBQztRQUNsRCxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBRUQsSUFBSSxDQUFDLGdCQUFnQixDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUM7UUFDbkMsTUFBTSxDQUNKLE9BQU8sRUFDUCxxQkFBcUIsTUFBTSwrREFBK0Q7WUFDeEYsdURBQXVELEVBQ3pELGdCQUFnQixDQUFDLEdBQUcsRUFBRSxPQUFPLENBQUMsQ0FDL0IsQ0FBQztRQUVGLElBQUksSUFBSSxLQUFLLE9BQU87WUFBRSxNQUFNLElBQUEsbUNBQWdCLEVBQUMsNENBQXlCLENBQUMsQ0FBQztRQUV4RSxPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFFRCxPQUFPLEVBQUUsV0FBVyxFQUFFLGlCQUFpQixFQUFFLENBQUM7QUFDNUMsQ0FBQztBQVVEOzs7Ozs7R0FNRztBQUNILFNBQVMsNEJBQTRCLENBQUMsS0FBYztJQUNsRCxJQUFJLENBQUMsQ0FBQyxLQUFLLFlBQVksOEJBQVMsQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBRS9DLE9BQU8sS0FBSyxDQUFDLE1BQU0sSUFBSSxZQUFZLElBQUksNkJBQTZCLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUN6RixDQUFDO0FBRUQsS0FBSyxVQUFVLFlBQVksQ0FBQyxPQUErQixFQUFFLE9BQU8sR0FBRyxDQUFDO0lBQ3RFLE1BQU0sRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsR0FBRyxPQUFPLENBQUM7SUFFckQsSUFBSSxDQUFDO1FBQ0gsTUFBTSxPQUFPLENBQUMsdUJBQXVCLENBQUM7WUFDcEMsaUJBQWlCLEVBQUUsT0FBTyxDQUFDLGlCQUFpQjtZQUM1QyxXQUFXLEVBQUUsT0FBTyxDQUFDLFdBQVc7WUFDaEMsTUFBTTtTQUNQLENBQUMsQ0FBQztJQUNMLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsSUFBSSw0QkFBNEIsQ0FBQyxLQUFLLENBQUMsSUFBSSxPQUFPLEdBQUcsbUJBQW1CLEVBQUUsQ0FBQztZQUN6RSxNQUFNLENBQUMsT0FBTyxFQUFFLG9EQUFvRCxFQUFFO2dCQUNwRSxPQUFPO2dCQUNQLFFBQVEsRUFBRSxtQkFBbUI7Z0JBQzdCLEtBQUssRUFBRSxhQUFhLENBQUMsS0FBSyxDQUFDO2FBQzVCLENBQUMsQ0FBQztZQUVILE1BQU0sSUFBSSxPQUFPLENBQU8sT0FBTyxDQUFDLEVBQUU7Z0JBQ2hDLDBGQUEwRjtnQkFDMUYsVUFBVSxDQUFDLE9BQU8sRUFBRSxxQkFBcUIsQ0FBQyxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3JELENBQUMsQ0FBQyxDQUFDO1lBRUgsTUFBTSxZQUFZLENBQUMsT0FBTyxFQUFFLE9BQU8sR0FBRyxDQUFDLENBQUMsQ0FBQztZQUV6QyxPQUFPO1FBQ1QsQ0FBQztRQUVELE1BQU0sS0FBSyxDQUFDO0lBQ2QsQ0FBQztBQUNILENBQUM7QUFFRDs7O0dBR0c7QUFDSCxTQUFnQixlQUFlLENBQUMsT0FBK0I7SUFDN0QsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBRTVDLE9BQU87U0FDSixLQUFLLENBQ0osR0FBRyxFQUFFLENBQUMsWUFBWSxDQUFDLE9BQU8sQ0FBQyxFQUMzQixJQUFJLE1BQU0sb0NBQW9DLE9BQU8sQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLEVBQUUsRUFBRSxDQUMvRTtTQUNBLEtBQUssQ0FBQyxLQUFLLENBQUMsRUFBRTtRQUNiLE1BQU0sQ0FBQyxPQUFPLEVBQUUsdUNBQXVDLE1BQU0sR0FBRyxFQUFFO1lBQ2hFLGFBQWEsRUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxFQUFFO1lBQzdDLEtBQUssRUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxVQUFVLEVBQUUsS0FBSztZQUNwRCxLQUFLLEVBQUUsYUFBYSxDQUFDLEtBQUssQ0FBQztTQUM1QixDQUFDLENBQUM7SUFDTCxDQUFDLENBQUMsQ0FBQztBQUNQLENBQUMifQ==
|