doover-js 0.6.0 → 0.6.2
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/apis/agents-api.d.ts +8 -0
- package/dist/apis/agents-api.js +55 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/react/useMultiAgentChannelMessages.d.ts +24 -1
- package/dist/react/useMultiAgentChannelMessages.js +30 -4
- package/dist/test/apis.test.js +76 -0
- package/dist/types/audit.d.ts +53 -0
- package/dist/types/audit.js +14 -0
- package/package.json +1 -1
|
@@ -32,6 +32,14 @@ export declare class AgentsApi {
|
|
|
32
32
|
private readonly controlApiUrl?;
|
|
33
33
|
constructor(rest: RestClient, controlApiUrl?: string | undefined);
|
|
34
34
|
listAgents(options?: GetAgentsOptions): Promise<AgentsResponse>;
|
|
35
|
+
/**
|
|
36
|
+
* Batch-fetch recent messages for many agents. Auto-chunks `agent_id`
|
|
37
|
+
* (and the parallel `agent_before` cursors, if given) at MULTI_AGENT_CHUNK_SIZE
|
|
38
|
+
* so each request's URL stays under CloudFront's 8,192-byte quota. Chunks
|
|
39
|
+
* are fetched in parallel and merged: `results`/`count` concat-and-sum,
|
|
40
|
+
* `next_cursors`/`at_limit_agent_ids` union, legacy `next` becomes the
|
|
41
|
+
* lexically-highest cursor across at-limit agents.
|
|
42
|
+
*/
|
|
35
43
|
getMultiAgentMessages(channelName: string, params: MultiAgentMessagesParams): Promise<BatchMessagesResponse>;
|
|
36
44
|
/**
|
|
37
45
|
* Batch-fetch aggregates for many agents. Auto-chunks `agent_id` so each
|
package/dist/apis/agents-api.js
CHANGED
|
@@ -144,12 +144,65 @@ class AgentsApi {
|
|
|
144
144
|
}
|
|
145
145
|
return { ...raw, agents: merged, results: merged, count: merged.length };
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Batch-fetch recent messages for many agents. Auto-chunks `agent_id`
|
|
149
|
+
* (and the parallel `agent_before` cursors, if given) at MULTI_AGENT_CHUNK_SIZE
|
|
150
|
+
* so each request's URL stays under CloudFront's 8,192-byte quota. Chunks
|
|
151
|
+
* are fetched in parallel and merged: `results`/`count` concat-and-sum,
|
|
152
|
+
* `next_cursors`/`at_limit_agent_ids` union, legacy `next` becomes the
|
|
153
|
+
* lexically-highest cursor across at-limit agents.
|
|
154
|
+
*/
|
|
147
155
|
async getMultiAgentMessages(channelName, params) {
|
|
148
|
-
const
|
|
149
|
-
|
|
156
|
+
const path = `/agents/channels/${channelName}/messages`;
|
|
157
|
+
const { agent_id, agent_before, ...rest } = params;
|
|
158
|
+
const stampTimestamps = (response) => ({
|
|
150
159
|
...response,
|
|
151
160
|
results: response.results.map((message) => "timestamp" in message ? message : (0, snowflake_1.addTimestampToMessage)(message)),
|
|
161
|
+
});
|
|
162
|
+
if (agent_id.length <= MULTI_AGENT_CHUNK_SIZE) {
|
|
163
|
+
const response = await this.rest.get(path, params);
|
|
164
|
+
return stampTimestamps(response);
|
|
165
|
+
}
|
|
166
|
+
if (agent_before && agent_before.length !== agent_id.length) {
|
|
167
|
+
throw new Error("agent_before must be the same length as agent_id when set");
|
|
168
|
+
}
|
|
169
|
+
const chunks = [];
|
|
170
|
+
for (let i = 0; i < agent_id.length; i += MULTI_AGENT_CHUNK_SIZE) {
|
|
171
|
+
chunks.push({
|
|
172
|
+
agent_id: agent_id.slice(i, i + MULTI_AGENT_CHUNK_SIZE),
|
|
173
|
+
...(agent_before
|
|
174
|
+
? { agent_before: agent_before.slice(i, i + MULTI_AGENT_CHUNK_SIZE) }
|
|
175
|
+
: {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const responses = await Promise.all(chunks.map((chunk) => this.rest
|
|
179
|
+
.get(path, { ...rest, ...chunk })
|
|
180
|
+
.then(stampTimestamps)));
|
|
181
|
+
// Merge: results/count concat-and-sum; next_cursors merge; at_limit_agent_ids
|
|
182
|
+
// concat (both keyed by agent id, unique across chunks). Legacy `next` is the
|
|
183
|
+
// lexically-highest oldest-cursor across at-limit agents — snowflakes sort
|
|
184
|
+
// lexically since they're equal-width digit strings.
|
|
185
|
+
const merged = {
|
|
186
|
+
results: responses.flatMap((r) => r.results),
|
|
187
|
+
count: responses.reduce((acc, r) => acc + r.count, 0),
|
|
152
188
|
};
|
|
189
|
+
const nexts = responses
|
|
190
|
+
.map((r) => r.next)
|
|
191
|
+
.filter((n) => typeof n === "string" && n.length > 0);
|
|
192
|
+
if (nexts.length > 0) {
|
|
193
|
+
merged.next = nexts.reduce((a, b) => (a > b ? a : b));
|
|
194
|
+
}
|
|
195
|
+
const cursors = {};
|
|
196
|
+
for (const r of responses) {
|
|
197
|
+
if (r.next_cursors)
|
|
198
|
+
Object.assign(cursors, r.next_cursors);
|
|
199
|
+
}
|
|
200
|
+
if (Object.keys(cursors).length > 0)
|
|
201
|
+
merged.next_cursors = cursors;
|
|
202
|
+
const atLimit = responses.flatMap((r) => r.at_limit_agent_ids ?? []);
|
|
203
|
+
if (atLimit.length > 0)
|
|
204
|
+
merged.at_limit_agent_ids = atLimit;
|
|
205
|
+
return merged;
|
|
153
206
|
}
|
|
154
207
|
/**
|
|
155
208
|
* Batch-fetch aggregates for many agents. Auto-chunks `agent_id` so each
|
package/dist/index.d.ts
CHANGED
|
@@ -47,4 +47,6 @@ export type * from "./types/common";
|
|
|
47
47
|
export type * from "./types/connection";
|
|
48
48
|
export type * from "./types/openapi";
|
|
49
49
|
export type * from "./types/viewer";
|
|
50
|
+
export type * from "./types/audit";
|
|
51
|
+
export { DV_AUDIT_CHANNEL } from "./types/audit";
|
|
50
52
|
export { addTimestampToMessage, extractSnowflakeId, generateSnowflakeIdAtTime, } from "./utils/snowflake";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateSnowflakeIdAtTime = exports.extractSnowflakeId = exports.addTimestampToMessage = exports.getIdentifierFromPath = exports.DooverDataProvider = exports.DooverValidationError = exports.DooverGatewayError = exports.DooverApiError = exports.RestClient = exports.GatewayClient = exports.buildAuth = exports.DooverAuthError = exports.AuthProfile = exports.DooverTokenAuth = exports.CookieAuth = exports.DooverAuth = exports.AmbiguousWriteError = exports.UnsupportedCapabilityError = exports.ALL_CAPABILITIES = exports.DooverRpcError = exports.RpcDispatcher = exports.UsersApi = exports.TurnApi = exports.ProcessorsApi = exports.PermissionsApi = exports.NotificationsApi = exports.MessagesApi = exports.ConnectionsApi = exports.ChannelsApi = exports.AlarmsApi = exports.AggregatesApi = exports.AgentsApi = exports.DooverStatsCollector = exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.MultiplexClient = exports.LocalAgentClient = exports.DooverClient = void 0;
|
|
3
|
+
exports.generateSnowflakeIdAtTime = exports.extractSnowflakeId = exports.addTimestampToMessage = exports.DV_AUDIT_CHANNEL = exports.getIdentifierFromPath = exports.DooverDataProvider = exports.DooverValidationError = exports.DooverGatewayError = exports.DooverApiError = exports.RestClient = exports.GatewayClient = exports.buildAuth = exports.DooverAuthError = exports.AuthProfile = exports.DooverTokenAuth = exports.CookieAuth = exports.DooverAuth = exports.AmbiguousWriteError = exports.UnsupportedCapabilityError = exports.ALL_CAPABILITIES = exports.DooverRpcError = exports.RpcDispatcher = exports.UsersApi = exports.TurnApi = exports.ProcessorsApi = exports.PermissionsApi = exports.NotificationsApi = exports.MessagesApi = exports.ConnectionsApi = exports.ChannelsApi = exports.AlarmsApi = exports.AggregatesApi = exports.AgentsApi = exports.DooverStatsCollector = exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.MultiplexClient = exports.LocalAgentClient = exports.DooverClient = void 0;
|
|
4
4
|
var doover_client_1 = require("./client/doover-client");
|
|
5
5
|
Object.defineProperty(exports, "DooverClient", { enumerable: true, get: function () { return doover_client_1.DooverClient; } });
|
|
6
6
|
var local_agent_client_1 = require("./client/local-agent-client");
|
|
@@ -68,6 +68,8 @@ var doover_data_provider_1 = require("./viewer/doover-data-provider");
|
|
|
68
68
|
Object.defineProperty(exports, "DooverDataProvider", { enumerable: true, get: function () { return doover_data_provider_1.DooverDataProvider; } });
|
|
69
69
|
var path_parsing_1 = require("./viewer/path-parsing");
|
|
70
70
|
Object.defineProperty(exports, "getIdentifierFromPath", { enumerable: true, get: function () { return path_parsing_1.getIdentifierFromPath; } });
|
|
71
|
+
var audit_1 = require("./types/audit");
|
|
72
|
+
Object.defineProperty(exports, "DV_AUDIT_CHANNEL", { enumerable: true, get: function () { return audit_1.DV_AUDIT_CHANNEL; } });
|
|
71
73
|
var snowflake_1 = require("./utils/snowflake");
|
|
72
74
|
Object.defineProperty(exports, "addTimestampToMessage", { enumerable: true, get: function () { return snowflake_1.addTimestampToMessage; } });
|
|
73
75
|
Object.defineProperty(exports, "extractSnowflakeId", { enumerable: true, get: function () { return snowflake_1.extractSnowflakeId; } });
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { type InfiniteData, type UseInfiniteQueryResult } from "@tanstack/react-query";
|
|
2
2
|
import type { MessageStructure } from "../types/common";
|
|
3
|
-
export declare function multiAgentChannelMessagesQueryKey(channelName: string, agentIds: string[], sources?: string[]
|
|
3
|
+
export declare function multiAgentChannelMessagesQueryKey(channelName: string, agentIds: string[], sources?: string[], scope?: {
|
|
4
|
+
after?: string;
|
|
5
|
+
fields?: readonly string[];
|
|
6
|
+
}): readonly ["doover", "channel", string, "messages", string, "src", string] | readonly ["doover", "channel", string, "messages", string, "src", string, Record<string, unknown>];
|
|
4
7
|
export interface UseMultiAgentChannelMessagesOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Global cap on the total number of messages returned across all agents
|
|
10
|
+
* for a single request. The server distributes this budget across agents
|
|
11
|
+
* in newest-first order, so a noisy agent can starve quieter ones.
|
|
12
|
+
* Prefer `agentMessageLimit` when you want a fair per-agent slice.
|
|
13
|
+
*/
|
|
5
14
|
limit?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Per-agent cap on messages returned. Forwarded as `agent_message_limit`.
|
|
17
|
+
* Use this when fanning out across many agents that have wildly different
|
|
18
|
+
* message rates and you want each agent's window represented.
|
|
19
|
+
*/
|
|
20
|
+
agentMessageLimit?: number;
|
|
6
21
|
/** If false, skip live subscriptions per agent. Defaults true. */
|
|
7
22
|
liveUpdates?: boolean;
|
|
8
23
|
/**
|
|
@@ -12,6 +27,14 @@ export interface UseMultiAgentChannelMessagesOptions {
|
|
|
12
27
|
fields?: string[];
|
|
13
28
|
/** Optional first-page `before` cursor (snowflake id). */
|
|
14
29
|
initialBefore?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional lower-bound snowflake id. Forwarded server-side so each
|
|
32
|
+
* agent's pagination stops on its own once it walks past the bound —
|
|
33
|
+
* mirrors `useChannelMessages`'s `after`. Use this to fetch a bounded
|
|
34
|
+
* time window (e.g. last 24h) across many agents without filtering
|
|
35
|
+
* on the client.
|
|
36
|
+
*/
|
|
37
|
+
after?: string;
|
|
15
38
|
/**
|
|
16
39
|
* Restrict to these source ids on a `MultiplexClient`. Ignored for a plain
|
|
17
40
|
* `DooverClient` or `LocalAgentClient`. When set, the query key is
|
|
@@ -5,9 +5,9 @@ exports.useMultiAgentChannelMessages = useMultiAgentChannelMessages;
|
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const react_query_1 = require("@tanstack/react-query");
|
|
7
7
|
const context_1 = require("./context");
|
|
8
|
-
function multiAgentChannelMessagesQueryKey(channelName, agentIds, sources) {
|
|
8
|
+
function multiAgentChannelMessagesQueryKey(channelName, agentIds, sources, scope) {
|
|
9
9
|
const sourceDim = sources && sources.length ? [...sources].sort().join(",") : "*";
|
|
10
|
-
|
|
10
|
+
const base = [
|
|
11
11
|
"doover",
|
|
12
12
|
"channel",
|
|
13
13
|
channelName,
|
|
@@ -16,16 +16,34 @@ function multiAgentChannelMessagesQueryKey(channelName, agentIds, sources) {
|
|
|
16
16
|
"src",
|
|
17
17
|
sourceDim,
|
|
18
18
|
];
|
|
19
|
+
// Pagination cursors come from the first page's response and are scoped
|
|
20
|
+
// to *that* response's `after`/`fields`. Mixing them under one cache
|
|
21
|
+
// entry would cause `getNextPageParam` to anchor on an unrelated page,
|
|
22
|
+
// so a differently-bounded request must live under its own key.
|
|
23
|
+
const scopeKey = {};
|
|
24
|
+
if (scope?.after)
|
|
25
|
+
scopeKey.after = scope.after;
|
|
26
|
+
if (scope?.fields && scope.fields.length > 0) {
|
|
27
|
+
scopeKey.fields = [...scope.fields].sort();
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(scopeKey).length > 0
|
|
30
|
+
? [...base, scopeKey]
|
|
31
|
+
: base;
|
|
19
32
|
}
|
|
20
33
|
function useMultiAgentChannelMessages(channelName, agentIds, options) {
|
|
21
34
|
const client = (0, context_1.useDooverClient)();
|
|
22
35
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
23
36
|
const limit = options?.limit;
|
|
37
|
+
const agentMessageLimit = options?.agentMessageLimit;
|
|
24
38
|
const liveUpdates = options?.liveUpdates ?? true;
|
|
25
39
|
const fields = options?.fields;
|
|
26
40
|
const initialBefore = options?.initialBefore;
|
|
27
41
|
const sources = options?.sources;
|
|
28
|
-
const
|
|
42
|
+
const after = options?.after;
|
|
43
|
+
const key = multiAgentChannelMessagesQueryKey(channelName, agentIds, sources, {
|
|
44
|
+
after,
|
|
45
|
+
fields,
|
|
46
|
+
});
|
|
29
47
|
const prependMessage = (0, react_1.useCallback)((message) => {
|
|
30
48
|
queryClient.setQueryData(key, (current) => {
|
|
31
49
|
if (!current)
|
|
@@ -42,8 +60,12 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
|
|
|
42
60
|
};
|
|
43
61
|
});
|
|
44
62
|
},
|
|
63
|
+
// `key` captures every scope dim (agentIds + sources + after + fields);
|
|
64
|
+
// serialise the whole thing so the closure refreshes when any dim
|
|
65
|
+
// changes — listing dims by hand drops live updates whenever the list
|
|
66
|
+
// grows (we used to omit `after`/`fields`).
|
|
45
67
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
-
[queryClient,
|
|
68
|
+
[queryClient, JSON.stringify(key)]);
|
|
47
69
|
(0, react_1.useEffect)(() => {
|
|
48
70
|
if (!liveUpdates || agentIds.length === 0)
|
|
49
71
|
return;
|
|
@@ -71,7 +93,11 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
|
|
|
71
93
|
agent_id: agentIds,
|
|
72
94
|
...(typeof pageParam === "string" ? { before: pageParam } : {}),
|
|
73
95
|
...(limit !== undefined ? { limit } : {}),
|
|
96
|
+
...(agentMessageLimit !== undefined
|
|
97
|
+
? { agent_message_limit: agentMessageLimit }
|
|
98
|
+
: {}),
|
|
74
99
|
...(fields && fields.length > 0 ? { field_name: fields } : {}),
|
|
100
|
+
...(after !== undefined ? { after } : {}),
|
|
75
101
|
};
|
|
76
102
|
// Pass `{ sources }` as a trailing bag only when set — cast through
|
|
77
103
|
// `never` since the TypeScript overloads don't declare it.
|
package/dist/test/apis.test.js
CHANGED
|
@@ -82,6 +82,82 @@ function setupRest(responseFactory) {
|
|
|
82
82
|
const returnedIds = aggregates.results.map((r) => r.agent_id).sort();
|
|
83
83
|
(0, chai_1.expect)(returnedIds).to.deep.equal([...agentIds].sort());
|
|
84
84
|
});
|
|
85
|
+
(0, mocha_1.it)("chunks getMultiAgentMessages over the per-request agent cap", async () => {
|
|
86
|
+
const baseTs = new Date("2026-01-01T00:00:00Z");
|
|
87
|
+
const { rest, fetchMock } = setupRest((url) => {
|
|
88
|
+
const ids = [...new URL(url).searchParams.getAll("agent_id")];
|
|
89
|
+
return (0, helpers_1.createJsonResponse)({
|
|
90
|
+
results: ids.map((id, i) => ({
|
|
91
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date(baseTs.getTime() + i)),
|
|
92
|
+
author_id: "u",
|
|
93
|
+
channel: { agent_id: id, name: "c1" },
|
|
94
|
+
data: {},
|
|
95
|
+
attachments: [],
|
|
96
|
+
})),
|
|
97
|
+
count: ids.length,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const api = new agents_api_1.AgentsApi(rest);
|
|
101
|
+
// 600 agents → 3 chunks of 250/250/100.
|
|
102
|
+
const agentIds = Array.from({ length: 600 }, (_, i) => `a${i}`);
|
|
103
|
+
const messages = await api.getMultiAgentMessages("c1", {
|
|
104
|
+
agent_id: agentIds,
|
|
105
|
+
agent_message_limit: 50,
|
|
106
|
+
});
|
|
107
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(3);
|
|
108
|
+
(0, chai_1.expect)(messages.results).to.have.length(600);
|
|
109
|
+
(0, chai_1.expect)(messages.count).to.equal(600);
|
|
110
|
+
const returnedIds = messages.results
|
|
111
|
+
.map((m) => m.channel.agent_id)
|
|
112
|
+
.sort();
|
|
113
|
+
(0, chai_1.expect)(returnedIds).to.deep.equal([...agentIds].sort());
|
|
114
|
+
// Per-request param survives chunking.
|
|
115
|
+
for (let i = 0; i < 3; i++) {
|
|
116
|
+
const url = fetchMock.getCall(i).args[0];
|
|
117
|
+
(0, chai_1.expect)(url).to.include("agent_message_limit=50");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
(0, mocha_1.it)("merges next_cursors and at_limit_agent_ids across chunks", async () => {
|
|
121
|
+
let call = 0;
|
|
122
|
+
const cursors = [
|
|
123
|
+
{ "a0": "100", "a1": "200" },
|
|
124
|
+
{ "a250": "150" },
|
|
125
|
+
];
|
|
126
|
+
const atLimit = [["a0", "a1"], ["a250"]];
|
|
127
|
+
const { rest } = setupRest((url) => {
|
|
128
|
+
const idx = call++;
|
|
129
|
+
const ids = [...new URL(url).searchParams.getAll("agent_id")];
|
|
130
|
+
return (0, helpers_1.createJsonResponse)({
|
|
131
|
+
results: ids.map((id) => ({
|
|
132
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00Z")),
|
|
133
|
+
author_id: "u",
|
|
134
|
+
channel: { agent_id: id, name: "c1" },
|
|
135
|
+
data: {},
|
|
136
|
+
attachments: [],
|
|
137
|
+
})),
|
|
138
|
+
count: ids.length,
|
|
139
|
+
next: idx === 0 ? "200" : "150",
|
|
140
|
+
next_cursors: cursors[idx],
|
|
141
|
+
at_limit_agent_ids: atLimit[idx],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
const api = new agents_api_1.AgentsApi(rest);
|
|
145
|
+
const agentIds = Array.from({ length: 300 }, (_, i) => `a${i}`);
|
|
146
|
+
const messages = await api.getMultiAgentMessages("c1", {
|
|
147
|
+
agent_id: agentIds,
|
|
148
|
+
});
|
|
149
|
+
(0, chai_1.expect)(messages.next).to.equal("200"); // max across chunks
|
|
150
|
+
(0, chai_1.expect)(messages.next_cursors).to.deep.equal({
|
|
151
|
+
a0: "100",
|
|
152
|
+
a1: "200",
|
|
153
|
+
a250: "150",
|
|
154
|
+
});
|
|
155
|
+
(0, chai_1.expect)(messages.at_limit_agent_ids?.sort()).to.deep.equal([
|
|
156
|
+
"a0",
|
|
157
|
+
"a1",
|
|
158
|
+
"a250",
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
85
161
|
(0, mocha_1.it)("covers channels methods", async () => {
|
|
86
162
|
const { rest, fetchMock } = setupRest(() => (0, helpers_1.createJsonResponse)([]));
|
|
87
163
|
const api = new channels_api_1.ChannelsApi(rest);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit event types — mirror of the records doover-control writes to the
|
|
3
|
+
* `dv-audit` channel (see ``doover-control/doover_control/events/audit.py``
|
|
4
|
+
* for the source of truth).
|
|
5
|
+
*
|
|
6
|
+
* Each record is read back via the standard channel-messages APIs: it lands
|
|
7
|
+
* inside a ``MessageStructure<AuditEvent>`` envelope (with ``id``, ``timestamp``,
|
|
8
|
+
* ``channel``, etc.), and the record itself is on ``message.data``.
|
|
9
|
+
*/
|
|
10
|
+
/** Channel name on which audit records live in doover-data. */
|
|
11
|
+
export declare const DV_AUDIT_CHANNEL = "dv-audit";
|
|
12
|
+
/** The role this party plays in the record. */
|
|
13
|
+
export type AuditEntityType = "device" | "user" | "organisation" | "Tunnel" | "ApplicationInstallation" | "SolutionInstallation" | "ApplicationDeployment" | "ObjectGroup" | "PendingUser" | "User" | "fusionauth" | "system";
|
|
14
|
+
export interface AuditEntity {
|
|
15
|
+
type: AuditEntityType;
|
|
16
|
+
/** Stringified id (User pk, Device pk, FA UUID, etc.). May be absent for `system`. */
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Display name. May be an email for users / pending invites. */
|
|
19
|
+
name?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AuditOrganisation {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AuditRequestContext {
|
|
26
|
+
ip_address?: string;
|
|
27
|
+
user_agent?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Origin system for the event. */
|
|
30
|
+
export type AuditSource = "control" | "auth" | "data" | "tunnels";
|
|
31
|
+
/**
|
|
32
|
+
* String-literal union of every wired audit action — kept in lock-step with
|
|
33
|
+
* ``doover_control/events/types.py``'s ``EventType`` enum. Future event types
|
|
34
|
+
* declared in Python but not yet emitted are intentionally included so the
|
|
35
|
+
* frontend mapping (icons, labels) can be filled in ahead of the wire-up.
|
|
36
|
+
*/
|
|
37
|
+
export type AuditAction = "user.logged_in" | "user.login.failed" | "user.signed_up" | "user.created" | "user.updated" | "user.deleted" | "user.deactivated" | "user.reactivated" | "user.password_changed" | "user.password_reset" | "user.password_breached" | "user.mfa_changed" | "user.email_verified" | "user.email_changed" | "user.idp_linked" | "user.idp_unlinked" | "user.token_revoked" | "user.invited" | "user.removed" | "user.role_changed" | "app.installed" | "app.deployed" | "app.config_changed" | "app.uninstalled" | "solution.config_changed" | "group.created" | "group.edited" | "group.deleted" | "device.created" | "device.updated" | "device.config_changed" | "device.archived" | "device.unarchived" | "device.deleted" | "device.opened" | "tunnel.created" | "tunnel.deleted" | "tunnel.opened" | "tunnel.closed" | "tunnel.accessed" | "notification.sent" | "report.created" | "command.received";
|
|
38
|
+
/**
|
|
39
|
+
* The audit record payload as written by ``publish_audit`` — i.e. what you
|
|
40
|
+
* find on ``MessageStructure<AuditEvent>.data`` after reading the channel.
|
|
41
|
+
*
|
|
42
|
+
* The doover-data ``MessageStructure`` envelope provides ``id`` (snowflake)
|
|
43
|
+
* and ``timestamp`` (epoch ms) for ordering; this struct is the body only.
|
|
44
|
+
*/
|
|
45
|
+
export interface AuditEvent {
|
|
46
|
+
action: AuditAction;
|
|
47
|
+
actor: AuditEntity | null;
|
|
48
|
+
subject: AuditEntity | null;
|
|
49
|
+
organisation: AuditOrganisation | null;
|
|
50
|
+
source: AuditSource;
|
|
51
|
+
metadata: Record<string, unknown>;
|
|
52
|
+
request_context: AuditRequestContext | null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Audit event types — mirror of the records doover-control writes to the
|
|
4
|
+
* `dv-audit` channel (see ``doover-control/doover_control/events/audit.py``
|
|
5
|
+
* for the source of truth).
|
|
6
|
+
*
|
|
7
|
+
* Each record is read back via the standard channel-messages APIs: it lands
|
|
8
|
+
* inside a ``MessageStructure<AuditEvent>`` envelope (with ``id``, ``timestamp``,
|
|
9
|
+
* ``channel``, etc.), and the record itself is on ``message.data``.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DV_AUDIT_CHANNEL = void 0;
|
|
13
|
+
/** Channel name on which audit records live in doover-data. */
|
|
14
|
+
exports.DV_AUDIT_CHANNEL = "dv-audit";
|