@forestadmin/agent-bff 1.31.0 → 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/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/package.json +1 -1
|
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aXZpdHktbG9ncy1jcmVhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FjdGl2aXR5LWxvZy9hY3Rpdml0eS1sb2dzLWNyZWF0b3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUF1TUEsMkNBb0VDO0FBMERELDBDQWVDO0FBMVVELHdFQUE0RDtBQUU1RCxzRUFBeUU7QUFDekUsMkZBR2dEO0FBQ2hELDJEQUF3RDtBQUN4RCwrREFLa0M7QUFRbEM7Ozs7Ozs7R0FPRztBQUNILE1BQU0sY0FBYyxHQUFrRDtJQUNwRSxLQUFLLEVBQUUsTUFBTTtJQUNiLE1BQU0sRUFBRSxNQUFNO0lBQ2QsTUFBTSxFQUFFLE1BQU07SUFDZCxlQUFlLEVBQUUsTUFBTTtJQUN2QixNQUFNLEVBQUUsT0FBTztDQUNoQixDQUFDO0FBRUYsTUFBTSxtQkFBbUIsR0FBRyxDQUFDLENBQUM7QUFDOUIsTUFBTSxxQkFBcUIsR0FBRyxHQUFHLENBQUM7QUFFbEMsTUFBTSxvQkFBb0IsR0FBRywwQ0FBMEMsQ0FBQztBQStCeEUsU0FBUyxhQUFhLENBQUMsS0FBYztJQUNuQyxPQUFPLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsS0FBSyxDQUFDLElBQUksS0FBSyxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwRixDQUFDO0FBRUQsTUFBTSxTQUFTLEdBQUcsR0FBRyxDQUFDO0FBQ3RCLE1BQU0sWUFBWSxHQUFHLEdBQUcsQ0FBQztBQUN6QixNQUFNLFlBQVksR0FBRyxHQUFHLENBQUM7QUFDekIsb0dBQW9HO0FBQ3BHLE1BQU0sNkJBQTZCLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDO0FBQ2xFLE1BQU0sOEJBQThCLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFH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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ActivityLogResponse, CreateActivityLogParams, UpdateActivityLogStatusParams } from '@forestadmin/forestadmin-client';
|
|
2
|
+
export declare const APPLICATION_SOURCE_HEADER = "Forest-Application-Source";
|
|
3
|
+
export declare const BFF_APPLICATION_SOURCE = "BFF";
|
|
4
|
+
/**
|
|
5
|
+
* The slice of `ActivityLogsService` the BFF uses. Named so a fake can stand in for the two calls
|
|
6
|
+
* without carrying the rest of the Forest client.
|
|
7
|
+
*/
|
|
8
|
+
export interface ActivityLogsWriter {
|
|
9
|
+
createMcpActivityLog(params: CreateActivityLogParams): Promise<ActivityLogResponse>;
|
|
10
|
+
updateActivityLogStatus(params: UpdateActivityLogStatusParams): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Its own instance rather than the client `oauth/forest-server-client.ts` already holds:
|
|
14
|
+
* `ForestAdminClientOptions` carries no `headers`, so that one cannot tell the server which channel
|
|
15
|
+
* wrote the log.
|
|
16
|
+
*/
|
|
17
|
+
export default function createBffActivityLogsService(forestServerUrl: string): ActivityLogsWriter;
|
|
18
|
+
//# sourceMappingURL=activity-logs-service.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BFF_APPLICATION_SOURCE = exports.APPLICATION_SOURCE_HEADER = void 0;
|
|
4
|
+
exports.default = createBffActivityLogsService;
|
|
5
|
+
const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
|
|
6
|
+
exports.APPLICATION_SOURCE_HEADER = 'Forest-Application-Source';
|
|
7
|
+
exports.BFF_APPLICATION_SOURCE = 'BFF';
|
|
8
|
+
/**
|
|
9
|
+
* Its own instance rather than the client `oauth/forest-server-client.ts` already holds:
|
|
10
|
+
* `ForestAdminClientOptions` carries no `headers`, so that one cannot tell the server which channel
|
|
11
|
+
* wrote the log.
|
|
12
|
+
*/
|
|
13
|
+
function createBffActivityLogsService(forestServerUrl) {
|
|
14
|
+
return new forestadmin_client_1.ActivityLogsService(new forestadmin_client_1.ForestHttpApi(), {
|
|
15
|
+
forestServerUrl,
|
|
16
|
+
headers: { [exports.APPLICATION_SOURCE_HEADER]: exports.BFF_APPLICATION_SOURCE },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aXZpdHktbG9ncy1zZXJ2aWNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FjdGl2aXR5LWxvZy9hY3Rpdml0eS1sb2dzLXNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBeUJBLCtDQUtDO0FBeEJELHdFQUFxRjtBQUV4RSxRQUFBLHlCQUF5QixHQUFHLDJCQUEyQixDQUFDO0FBQ3hELFFBQUEsc0JBQXNCLEdBQUcsS0FBSyxDQUFDO0FBVzVDOzs7O0dBSUc7QUFDSCxTQUF3Qiw0QkFBNEIsQ0FBQyxlQUF1QjtJQUMxRSxPQUFPLElBQUksd0NBQW1CLENBQUMsSUFBSSxrQ0FBYSxFQUFFLEVBQUU7UUFDbEQsZUFBZTtRQUNmLE9BQU8sRUFBRSxFQUFFLENBQUMsaUNBQXlCLENBQUMsRUFBRSw4QkFBc0IsRUFBRTtLQUNqRSxDQUFDLENBQUM7QUFDTCxDQUFDIn0=
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type ActivityLogDrainer from './activity-log-drainer';
|
|
2
|
+
import type { ActivityLogContext, BffActivityLogAction } from './activity-logs-creator';
|
|
3
|
+
import type { ActivityLogsWriter } from './activity-logs-service';
|
|
4
|
+
import type { Logger } from '../ports/logger-port';
|
|
5
|
+
import type { Context } from 'koa';
|
|
6
|
+
export interface WithActivityLogOptions<T> {
|
|
7
|
+
ctx: Context;
|
|
8
|
+
service: ActivityLogsWriter;
|
|
9
|
+
drainer: ActivityLogDrainer;
|
|
10
|
+
action: BffActivityLogAction;
|
|
11
|
+
context?: ActivityLogContext;
|
|
12
|
+
logger: Logger;
|
|
13
|
+
operation: () => Promise<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Errors the log records as `completed` rather than `failed` — the operation reached a business
|
|
16
|
+
* outcome the BFF answers with an error status.
|
|
17
|
+
*/
|
|
18
|
+
isCompletedDespite?: (error: unknown) => boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs an operation under an activity log: the pending log is awaited before the operation starts,
|
|
22
|
+
* so nothing runs unaudited, and the status transition is fired without `await` afterwards.
|
|
23
|
+
*/
|
|
24
|
+
export default function withActivityLog<T>(options: WithActivityLogOptions<T>): Promise<T>;
|
|
25
|
+
//# sourceMappingURL=with-activity-log.d.ts.map
|