openclaw-db9-audit 0.1.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/index.ts +22 -0
- package/openclaw.plugin.json +28 -0
- package/package.json +40 -0
- package/src/bootstrap.ts +277 -0
- package/src/cli.ts +362 -0
- package/src/config.ts +53 -0
- package/src/control-plane.ts +170 -0
- package/src/event-log-sync.ts +166 -0
- package/src/fs-client.ts +317 -0
- package/src/postgres.ts +430 -0
- package/src/redact.ts +37 -0
- package/src/run-tracker.ts +158 -0
- package/src/service.ts +356 -0
- package/src/session-store.ts +150 -0
- package/src/state-store.ts +165 -0
- package/src/transcript-sync.ts +514 -0
- package/src/types.ts +215 -0
- package/src/utils.ts +277 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export type Db9AuditRedactConfig = {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
maxFieldBytes: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type Db9AuditPluginConfig = {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
apiBase: string;
|
|
9
|
+
databaseName: string;
|
|
10
|
+
databaseRegion?: string | undefined;
|
|
11
|
+
schema: string;
|
|
12
|
+
logRoot: string;
|
|
13
|
+
batchSize: number;
|
|
14
|
+
flushIntervalMs: number;
|
|
15
|
+
backfillOnStart: boolean;
|
|
16
|
+
redact: Db9AuditRedactConfig;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type Db9AuditCustomerState = {
|
|
20
|
+
id: string;
|
|
21
|
+
token: string;
|
|
22
|
+
tokenExpiresAt?: string | undefined;
|
|
23
|
+
isAnonymous: boolean;
|
|
24
|
+
anonymousId?: string | undefined;
|
|
25
|
+
anonymousSecret?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type Db9AuditDatabaseState = {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
state: string;
|
|
32
|
+
adminUser: string;
|
|
33
|
+
adminPassword: string;
|
|
34
|
+
connectionString: string;
|
|
35
|
+
host: string;
|
|
36
|
+
port: number;
|
|
37
|
+
database: string;
|
|
38
|
+
region?: string | undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type Db9AuditFsState = {
|
|
42
|
+
wsUrl: string;
|
|
43
|
+
username: string;
|
|
44
|
+
password: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type Db9AuditPluginState = {
|
|
48
|
+
schema: string;
|
|
49
|
+
logRoot: string;
|
|
50
|
+
initializedAt: string;
|
|
51
|
+
lastBootstrapAt: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type Db9AuditState = {
|
|
55
|
+
version: 1;
|
|
56
|
+
apiBase: string;
|
|
57
|
+
customer: Db9AuditCustomerState;
|
|
58
|
+
database: Db9AuditDatabaseState;
|
|
59
|
+
fs: Db9AuditFsState;
|
|
60
|
+
plugin: Db9AuditPluginState;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type Db9AnonymousRegisterResponse = {
|
|
64
|
+
token: string;
|
|
65
|
+
expires_at: string;
|
|
66
|
+
is_anonymous: boolean;
|
|
67
|
+
anonymous_id: string;
|
|
68
|
+
anonymous_secret: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type Db9AnonymousRefreshResponse = {
|
|
72
|
+
token: string;
|
|
73
|
+
expires_at: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type Db9CustomerResponse = {
|
|
77
|
+
id: string;
|
|
78
|
+
email: string;
|
|
79
|
+
created_at: string;
|
|
80
|
+
status: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type Db9DatabaseResponse = {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
state: string;
|
|
87
|
+
region?: string;
|
|
88
|
+
admin_user?: string;
|
|
89
|
+
admin_password?: string;
|
|
90
|
+
connection_string?: string;
|
|
91
|
+
created_at: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type Db9DatabaseStatusResponse = Db9DatabaseResponse & {
|
|
95
|
+
endpoints?: Array<{
|
|
96
|
+
host: string;
|
|
97
|
+
port: number;
|
|
98
|
+
type?: string;
|
|
99
|
+
priority?: number;
|
|
100
|
+
description?: string;
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type Db9DatabaseCredentialsResponse = {
|
|
106
|
+
admin_user: string;
|
|
107
|
+
admin_password: string;
|
|
108
|
+
connection_string: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type ParsedConnectionString = {
|
|
112
|
+
raw: string;
|
|
113
|
+
host: string;
|
|
114
|
+
port: number;
|
|
115
|
+
username: string;
|
|
116
|
+
password: string;
|
|
117
|
+
database: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type TranscriptSessionHeader = {
|
|
121
|
+
type: "session";
|
|
122
|
+
version?: string | number;
|
|
123
|
+
id?: string;
|
|
124
|
+
timestamp?: string;
|
|
125
|
+
cwd?: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type SessionLookupResult = {
|
|
129
|
+
agentId: string;
|
|
130
|
+
sessionId?: string;
|
|
131
|
+
sessionKey?: string;
|
|
132
|
+
sessionStorePath?: string;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type SessionStoreEntry = {
|
|
136
|
+
sessionId?: string;
|
|
137
|
+
sessionFile?: string;
|
|
138
|
+
updatedAt?: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type TranscriptMessageRecord = {
|
|
142
|
+
sessionKey: string;
|
|
143
|
+
agentId: string;
|
|
144
|
+
sessionFile: string;
|
|
145
|
+
lineNo: number;
|
|
146
|
+
runId?: string;
|
|
147
|
+
role?: string;
|
|
148
|
+
source: string;
|
|
149
|
+
toolName?: string;
|
|
150
|
+
toolCallId?: string;
|
|
151
|
+
contentText?: string;
|
|
152
|
+
contentJson?: unknown;
|
|
153
|
+
rawJson: Record<string, unknown>;
|
|
154
|
+
createdAt?: string;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type TranscriptSyncSessionMeta = {
|
|
158
|
+
sessionKey: string;
|
|
159
|
+
agentId: string;
|
|
160
|
+
sessionId?: string;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type TranscriptFileOffset = {
|
|
164
|
+
sessionFile: string;
|
|
165
|
+
agentId?: string | undefined;
|
|
166
|
+
sessionKey?: string | undefined;
|
|
167
|
+
lastOffset: number;
|
|
168
|
+
lastLineNo: number;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type TranscriptSyncBatch = {
|
|
172
|
+
sessionFile: string;
|
|
173
|
+
sessionMeta: TranscriptSyncSessionMeta;
|
|
174
|
+
messages: TranscriptMessageRecord[];
|
|
175
|
+
lastOffset: number;
|
|
176
|
+
lastLineNo: number;
|
|
177
|
+
resetExistingFile: boolean;
|
|
178
|
+
sessionHeader?: TranscriptSessionHeader | undefined;
|
|
179
|
+
sessionMetaJson?: Record<string, unknown> | undefined;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type TranscriptSyncResult = {
|
|
183
|
+
insertedMessages: number;
|
|
184
|
+
lastOffset: number;
|
|
185
|
+
lastLineNo: number;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type AuditRunRecord = {
|
|
189
|
+
runId: string;
|
|
190
|
+
sessionKey?: string | undefined;
|
|
191
|
+
agentId?: string | undefined;
|
|
192
|
+
startedAt?: string | undefined;
|
|
193
|
+
endedAt?: string | undefined;
|
|
194
|
+
success?: boolean | undefined;
|
|
195
|
+
durationMs?: number | undefined;
|
|
196
|
+
error?: string | undefined;
|
|
197
|
+
trigger?: string | undefined;
|
|
198
|
+
messageProvider?: string | undefined;
|
|
199
|
+
stats: Record<string, unknown>;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type Db9AuditDiagnosticEvent = {
|
|
203
|
+
stream?: string | undefined;
|
|
204
|
+
sessionKey?: string | undefined;
|
|
205
|
+
agentId?: string | undefined;
|
|
206
|
+
runId?: string | undefined;
|
|
207
|
+
data: Record<string, unknown>;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export type Db9AuditStatusReport = {
|
|
211
|
+
statePath: string;
|
|
212
|
+
hasState: boolean;
|
|
213
|
+
stateSummary?: Record<string, unknown> | undefined;
|
|
214
|
+
checks: Record<string, { ok: boolean; detail?: string | undefined }>;
|
|
215
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ParsedConnectionString } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DB9_API_BASE = "https://db9.ai/api";
|
|
6
|
+
const DEFAULT_DB9_FS_PORT = 5480;
|
|
7
|
+
const DEFAULT_DB9_PG_PORT = 5433;
|
|
8
|
+
const DEFAULT_AGENT_ID = "main";
|
|
9
|
+
|
|
10
|
+
export function defaultDb9ApiBase(): string {
|
|
11
|
+
return DEFAULT_DB9_API_BASE;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function defaultDb9FsPort(): number {
|
|
15
|
+
return DEFAULT_DB9_FS_PORT;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatError(error: unknown): string {
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
return error.stack ?? error.message;
|
|
21
|
+
}
|
|
22
|
+
if (typeof error === "string") {
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(error);
|
|
27
|
+
} catch {
|
|
28
|
+
return String(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
37
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function maskSecret(value: string | undefined, opts?: { keepStart?: number }): string {
|
|
41
|
+
if (!value) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
const keepStart = Math.max(0, opts?.keepStart ?? 6);
|
|
45
|
+
if (value.length <= keepStart) {
|
|
46
|
+
return "*".repeat(value.length);
|
|
47
|
+
}
|
|
48
|
+
return `${value.slice(0, keepStart)}***`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function truncateUtf8Bytes(input: string, maxBytes: number): string {
|
|
52
|
+
const limit = Math.max(1, maxBytes);
|
|
53
|
+
if (Buffer.byteLength(input, "utf8") <= limit) {
|
|
54
|
+
return input;
|
|
55
|
+
}
|
|
56
|
+
let output = "";
|
|
57
|
+
for (const char of input) {
|
|
58
|
+
const candidate = output + char;
|
|
59
|
+
if (Buffer.byteLength(candidate, "utf8") > limit) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
output = candidate;
|
|
63
|
+
}
|
|
64
|
+
return `${output}...<truncated>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sanitizeDatabaseName(raw: string): string {
|
|
68
|
+
const normalized = raw
|
|
69
|
+
.trim()
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
72
|
+
.replace(/^-+/g, "")
|
|
73
|
+
.replace(/-+$/g, "")
|
|
74
|
+
.replace(/-{2,}/g, "-");
|
|
75
|
+
const safe = normalized || "unknown-host";
|
|
76
|
+
return safe.slice(0, 48);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveDefaultDatabaseName(hostname: string = os.hostname()): string {
|
|
80
|
+
return `openclaw-audit-${sanitizeDatabaseName(hostname)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeApiBase(value: string | undefined): string {
|
|
84
|
+
const trimmed = value?.trim();
|
|
85
|
+
return (trimmed && trimmed.replace(/\/+$/g, "")) || defaultDb9ApiBase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function normalizeLogRoot(value: string | undefined): string {
|
|
89
|
+
const trimmed = value?.trim();
|
|
90
|
+
if (!trimmed) {
|
|
91
|
+
return "/logs";
|
|
92
|
+
}
|
|
93
|
+
const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
94
|
+
return prefixed.replace(/\/+$/g, "") || "/logs";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function normalizeSchemaName(value: string | undefined): string {
|
|
98
|
+
const trimmed = value?.trim().toLowerCase();
|
|
99
|
+
const normalized = trimmed?.replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
100
|
+
return normalized || "openclaw_audit";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseConnectionString(connectionString: string): ParsedConnectionString {
|
|
104
|
+
let url: URL;
|
|
105
|
+
try {
|
|
106
|
+
url = new URL(connectionString);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`Invalid connection string: ${formatError(error)}`);
|
|
109
|
+
}
|
|
110
|
+
if (url.protocol !== "postgresql:" && url.protocol !== "postgres:") {
|
|
111
|
+
throw new Error(`Unsupported connection string protocol: ${url.protocol}`);
|
|
112
|
+
}
|
|
113
|
+
const username = decodeURIComponent(url.username);
|
|
114
|
+
const password = decodeURIComponent(url.password);
|
|
115
|
+
if (!username) {
|
|
116
|
+
throw new Error("Connection string is missing username");
|
|
117
|
+
}
|
|
118
|
+
if (!password) {
|
|
119
|
+
throw new Error("Connection string is missing password");
|
|
120
|
+
}
|
|
121
|
+
const host = url.hostname;
|
|
122
|
+
if (!host) {
|
|
123
|
+
throw new Error("Connection string is missing host");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
raw: connectionString,
|
|
127
|
+
host,
|
|
128
|
+
port: url.port ? Number.parseInt(url.port, 10) : DEFAULT_DB9_PG_PORT,
|
|
129
|
+
username,
|
|
130
|
+
password,
|
|
131
|
+
database: url.pathname.replace(/^\/+/, "") || "postgres",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveFsWsUrl(host: string, port: number = defaultDb9FsPort()): string {
|
|
136
|
+
return `wss://${host}:${port}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function joinPosixPath(...parts: string[]): string {
|
|
140
|
+
const joined = path.posix.join(...parts.map((part) => part.trim()).filter(Boolean));
|
|
141
|
+
return joined.startsWith("/") ? joined : `/${joined}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function dirnamePosix(filePath: string): string {
|
|
145
|
+
const normalized = filePath.trim() || "/";
|
|
146
|
+
return path.posix.dirname(normalized);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sleep(ms: number): Promise<void> {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
setTimeout(resolve, ms);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function toIsoString(timestamp: number = Date.now()): string {
|
|
156
|
+
return new Date(timestamp).toISOString();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parseInteger(value: unknown, fallback: number, min: number): number {
|
|
160
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
return Math.max(min, Math.floor(value));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function parseOptionalString(value: unknown): string | undefined {
|
|
167
|
+
return isNonEmptyString(value) ? value.trim() : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function normalizeSessionPath(pathValue: string): string {
|
|
171
|
+
return path.resolve(pathValue.trim());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function safeJsonParse(value: string): unknown {
|
|
175
|
+
return JSON.parse(value) as unknown;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function safeStringifyJson(value: unknown): string {
|
|
179
|
+
return JSON.stringify(value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | null): string {
|
|
183
|
+
const raw = (sessionKey ?? "").trim();
|
|
184
|
+
if (!raw) {
|
|
185
|
+
return DEFAULT_AGENT_ID;
|
|
186
|
+
}
|
|
187
|
+
const lower = raw.toLowerCase();
|
|
188
|
+
if (!lower.startsWith("agent:")) {
|
|
189
|
+
return DEFAULT_AGENT_ID;
|
|
190
|
+
}
|
|
191
|
+
const parts = raw.split(":");
|
|
192
|
+
return sanitizeAgentId(parts[1]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function sanitizeAgentId(value: string | undefined | null): string {
|
|
196
|
+
const trimmed = (value ?? "").trim().toLowerCase();
|
|
197
|
+
if (!trimmed) {
|
|
198
|
+
return DEFAULT_AGENT_ID;
|
|
199
|
+
}
|
|
200
|
+
const sanitized = trimmed
|
|
201
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
202
|
+
.replace(/^-+/g, "")
|
|
203
|
+
.replace(/-+$/g, "");
|
|
204
|
+
return sanitized || DEFAULT_AGENT_ID;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function resolveAgentIdFromSessionPath(sessionFile: string): string | undefined {
|
|
208
|
+
const normalized = path.resolve(sessionFile);
|
|
209
|
+
const parts = normalized.split(path.sep).filter(Boolean);
|
|
210
|
+
const sessionsIndex = parts.lastIndexOf("sessions");
|
|
211
|
+
if (sessionsIndex < 2 || parts[sessionsIndex - 2] !== "agents") {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const candidate = parts[sessionsIndex - 1];
|
|
215
|
+
return candidate ? sanitizeAgentId(candidate) : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function resolveSessionIdFromSessionPath(sessionFile: string): string | undefined {
|
|
219
|
+
const base = path.basename(sessionFile.trim());
|
|
220
|
+
if (!base) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : base;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function toDatePartition(timestampMs: number): string {
|
|
227
|
+
return new Date(timestampMs).toISOString().slice(0, 10);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function coerceIsoTimestamp(value: unknown): string | undefined {
|
|
231
|
+
if (typeof value === "string") {
|
|
232
|
+
const parsed = new Date(value);
|
|
233
|
+
return Number.isNaN(parsed.valueOf()) ? undefined : parsed.toISOString();
|
|
234
|
+
}
|
|
235
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
236
|
+
return new Date(value).toISOString();
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function extractErrorMessage(value: unknown): string | undefined {
|
|
242
|
+
if (value instanceof Error) {
|
|
243
|
+
return value.message;
|
|
244
|
+
}
|
|
245
|
+
if (typeof value === "string") {
|
|
246
|
+
const trimmed = value.trim();
|
|
247
|
+
return trimmed || undefined;
|
|
248
|
+
}
|
|
249
|
+
if (isRecord(value)) {
|
|
250
|
+
const message = parseOptionalString(value.message);
|
|
251
|
+
if (message) {
|
|
252
|
+
return message;
|
|
253
|
+
}
|
|
254
|
+
const nested = value.error;
|
|
255
|
+
if (typeof nested === "string") {
|
|
256
|
+
return parseOptionalString(nested);
|
|
257
|
+
}
|
|
258
|
+
if (isRecord(nested)) {
|
|
259
|
+
return parseOptionalString(nested.message) ?? parseOptionalString(nested.code);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function coerceInteger(value: unknown): number | undefined {
|
|
266
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
267
|
+
return Math.trunc(value);
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function asArray<T>(value: T | T[] | undefined): T[] {
|
|
273
|
+
if (value === undefined) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
return Array.isArray(value) ? value : [value];
|
|
277
|
+
}
|