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/cli.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { OpenClawPluginApi, PluginLogger } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { bootstrapFreshState, recoverDb9AuditState } from "./bootstrap.js";
|
|
3
|
+
import { loadDb9AuditConfig } from "./config.js";
|
|
4
|
+
import { Db9ControlPlaneClient, Db9ControlPlaneError } from "./control-plane.js";
|
|
5
|
+
import { Db9FsClient } from "./fs-client.js";
|
|
6
|
+
import { Db9AuditPostgres } from "./postgres.js";
|
|
7
|
+
import { Db9AuditSessionLookup, discoverTranscriptFiles } from "./session-store.js";
|
|
8
|
+
import { Db9AuditStateStore, summarizeDb9AuditState } from "./state-store.js";
|
|
9
|
+
import { Db9AuditTranscriptSync } from "./transcript-sync.js";
|
|
10
|
+
import type { Db9AuditPluginConfig, Db9AuditState, Db9AuditStatusReport } from "./types.js";
|
|
11
|
+
import { extractErrorMessage, parseConnectionString } from "./utils.js";
|
|
12
|
+
|
|
13
|
+
async function readState(params: {
|
|
14
|
+
stateStore: Db9AuditStateStore;
|
|
15
|
+
}): Promise<Db9AuditState | null> {
|
|
16
|
+
return await params.stateStore.read();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readStateSafely(params: {
|
|
20
|
+
stateStore: Db9AuditStateStore;
|
|
21
|
+
}): Promise<{ state: Db9AuditState | null; error?: string | undefined }> {
|
|
22
|
+
try {
|
|
23
|
+
return { state: await readState(params) };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
state: null,
|
|
27
|
+
error: extractErrorMessage(error) ?? String(error),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function buildStatusReport(params: {
|
|
33
|
+
config: Db9AuditPluginConfig;
|
|
34
|
+
stateStore: Db9AuditStateStore;
|
|
35
|
+
logger: PluginLogger;
|
|
36
|
+
}): Promise<Db9AuditStatusReport> {
|
|
37
|
+
const stateResult = await readStateSafely({ stateStore: params.stateStore });
|
|
38
|
+
const state = stateResult.state;
|
|
39
|
+
const report: Db9AuditStatusReport = {
|
|
40
|
+
statePath: params.stateStore.stateFilePath,
|
|
41
|
+
hasState: Boolean(state),
|
|
42
|
+
...(state ? { stateSummary: summarizeDb9AuditState(state) } : {}),
|
|
43
|
+
checks: {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!state) {
|
|
47
|
+
report.checks.state = { ok: false, detail: stateResult.error ?? "state file not found" };
|
|
48
|
+
return report;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
report.checks.state = { ok: true };
|
|
52
|
+
|
|
53
|
+
const controlPlane = new Db9ControlPlaneClient(params.config.apiBase);
|
|
54
|
+
try {
|
|
55
|
+
await controlPlane.getMe(state.customer.token);
|
|
56
|
+
report.checks.controlPlane = { ok: true };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
report.checks.controlPlane = {
|
|
59
|
+
ok: false,
|
|
60
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let pg: Db9AuditPostgres | null = null;
|
|
65
|
+
try {
|
|
66
|
+
pg = new Db9AuditPostgres({
|
|
67
|
+
connectionString: state.database.connectionString,
|
|
68
|
+
schema: params.config.schema,
|
|
69
|
+
logger: params.logger,
|
|
70
|
+
});
|
|
71
|
+
await pg.ping();
|
|
72
|
+
report.checks.postgres = { ok: true };
|
|
73
|
+
report.checks.schema = { ok: await pg.hasSchemaVersion() };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
report.checks.postgres = {
|
|
76
|
+
ok: false,
|
|
77
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
78
|
+
};
|
|
79
|
+
report.checks.schema = { ok: false, detail: "postgres unavailable" };
|
|
80
|
+
} finally {
|
|
81
|
+
await pg?.close().catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let fsClient: Db9FsClient | null = null;
|
|
85
|
+
try {
|
|
86
|
+
fsClient = new Db9FsClient({
|
|
87
|
+
wsUrl: state.fs.wsUrl,
|
|
88
|
+
username: state.fs.username,
|
|
89
|
+
password: state.fs.password,
|
|
90
|
+
logger: params.logger,
|
|
91
|
+
});
|
|
92
|
+
await fsClient.connect();
|
|
93
|
+
report.checks.fs = { ok: true };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
report.checks.fs = {
|
|
96
|
+
ok: false,
|
|
97
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
98
|
+
};
|
|
99
|
+
} finally {
|
|
100
|
+
await fsClient?.close().catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return report;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runDoctor(params: {
|
|
107
|
+
config: Db9AuditPluginConfig;
|
|
108
|
+
stateStore: Db9AuditStateStore;
|
|
109
|
+
logger: PluginLogger;
|
|
110
|
+
}): Promise<Record<string, unknown>> {
|
|
111
|
+
const stateResult = await readStateSafely({ stateStore: params.stateStore });
|
|
112
|
+
const state = stateResult.state;
|
|
113
|
+
const controlPlane = new Db9ControlPlaneClient(params.config.apiBase);
|
|
114
|
+
if (!state) {
|
|
115
|
+
return {
|
|
116
|
+
state: { ok: false, detail: stateResult.error ?? "state file not found" },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const report: Record<string, unknown> = {
|
|
121
|
+
state: { ok: true, path: params.stateStore.stateFilePath },
|
|
122
|
+
customer: {
|
|
123
|
+
id: state.customer.id,
|
|
124
|
+
anonymous: state.customer.isAnonymous,
|
|
125
|
+
},
|
|
126
|
+
database: {
|
|
127
|
+
id: state.database.id,
|
|
128
|
+
name: state.database.name,
|
|
129
|
+
host: state.database.host,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
let refreshedState: Db9AuditState | null = null;
|
|
134
|
+
try {
|
|
135
|
+
await controlPlane.getMe(state.customer.token);
|
|
136
|
+
report.token = { ok: true };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
report.token = {
|
|
139
|
+
ok: false,
|
|
140
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
141
|
+
};
|
|
142
|
+
if (error instanceof Db9ControlPlaneError && error.status === 401) {
|
|
143
|
+
try {
|
|
144
|
+
refreshedState = await recoverDb9AuditState({
|
|
145
|
+
client: controlPlane,
|
|
146
|
+
config: params.config,
|
|
147
|
+
state,
|
|
148
|
+
logger: params.logger,
|
|
149
|
+
});
|
|
150
|
+
report.recovery = { ok: true };
|
|
151
|
+
} catch (recoveryError) {
|
|
152
|
+
report.recovery = {
|
|
153
|
+
ok: false,
|
|
154
|
+
detail: extractErrorMessage(recoveryError) ?? String(recoveryError),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const effectiveState = refreshedState ?? state;
|
|
161
|
+
try {
|
|
162
|
+
const credentials = await controlPlane.getCredentials(effectiveState.customer.token, effectiveState.database.id);
|
|
163
|
+
try {
|
|
164
|
+
const parsed = parseConnectionString(credentials.connection_string);
|
|
165
|
+
report.credentials = {
|
|
166
|
+
ok: true,
|
|
167
|
+
host: parsed.host,
|
|
168
|
+
port: parsed.port,
|
|
169
|
+
database: parsed.database,
|
|
170
|
+
username: parsed.username,
|
|
171
|
+
};
|
|
172
|
+
} catch (parseError) {
|
|
173
|
+
report.credentials = {
|
|
174
|
+
ok: true,
|
|
175
|
+
detail: `failed to parse connection string: ${extractErrorMessage(parseError) ?? String(parseError)}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
report.credentials = {
|
|
180
|
+
ok: false,
|
|
181
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let pg: Db9AuditPostgres | null = null;
|
|
186
|
+
try {
|
|
187
|
+
pg = new Db9AuditPostgres({
|
|
188
|
+
connectionString: effectiveState.database.connectionString,
|
|
189
|
+
schema: params.config.schema,
|
|
190
|
+
logger: params.logger,
|
|
191
|
+
});
|
|
192
|
+
await pg.ping();
|
|
193
|
+
report.postgres = { ok: true };
|
|
194
|
+
report.schema = { ok: await pg.hasSchemaVersion() };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
report.postgres = {
|
|
197
|
+
ok: false,
|
|
198
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
199
|
+
};
|
|
200
|
+
report.schema = { ok: false, detail: "postgres unavailable" };
|
|
201
|
+
} finally {
|
|
202
|
+
await pg?.close().catch(() => undefined);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let fsClient: Db9FsClient | null = null;
|
|
206
|
+
try {
|
|
207
|
+
fsClient = new Db9FsClient({
|
|
208
|
+
wsUrl: effectiveState.fs.wsUrl,
|
|
209
|
+
username: effectiveState.fs.username,
|
|
210
|
+
password: effectiveState.fs.password,
|
|
211
|
+
logger: params.logger,
|
|
212
|
+
});
|
|
213
|
+
await fsClient.connect();
|
|
214
|
+
report.fs = { ok: true };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
report.fs = {
|
|
217
|
+
ok: false,
|
|
218
|
+
detail: extractErrorMessage(error) ?? String(error),
|
|
219
|
+
};
|
|
220
|
+
} finally {
|
|
221
|
+
await fsClient?.close().catch(() => undefined);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return report;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function bootstrapState(params: {
|
|
228
|
+
config: Db9AuditPluginConfig;
|
|
229
|
+
stateStore: Db9AuditStateStore;
|
|
230
|
+
logger: PluginLogger;
|
|
231
|
+
force: boolean;
|
|
232
|
+
}): Promise<Db9AuditState> {
|
|
233
|
+
const stateResult = await readStateSafely({ stateStore: params.stateStore });
|
|
234
|
+
if (stateResult.error && !params.force) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Existing state is unreadable. Re-run with --force to rebuild it. (${stateResult.error})`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const existing = stateResult.state;
|
|
240
|
+
if (existing && !params.force) {
|
|
241
|
+
return existing;
|
|
242
|
+
}
|
|
243
|
+
const controlPlane = new Db9ControlPlaneClient(params.config.apiBase);
|
|
244
|
+
const state = await bootstrapFreshState({
|
|
245
|
+
client: controlPlane,
|
|
246
|
+
config: params.config,
|
|
247
|
+
logger: params.logger,
|
|
248
|
+
});
|
|
249
|
+
await params.stateStore.write(state);
|
|
250
|
+
return state;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function printJson(value: unknown): void {
|
|
254
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function registerDb9AuditCli(api: OpenClawPluginApi, config: Db9AuditPluginConfig): void {
|
|
258
|
+
api.registerCli(
|
|
259
|
+
({ program, logger }) => {
|
|
260
|
+
const resolvedConfig = loadDb9AuditConfig(config);
|
|
261
|
+
const stateStore = new Db9AuditStateStore(api.runtime.state.resolveStateDir());
|
|
262
|
+
const root = program.command("db9-audit").description("DB9 audit plugin commands");
|
|
263
|
+
|
|
264
|
+
root.command("status").description("Show db9-audit status").action(async () => {
|
|
265
|
+
const report = await buildStatusReport({
|
|
266
|
+
config: resolvedConfig,
|
|
267
|
+
stateStore,
|
|
268
|
+
logger,
|
|
269
|
+
});
|
|
270
|
+
printJson(report);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
root
|
|
274
|
+
.command("init")
|
|
275
|
+
.description("Bootstrap db9-audit state")
|
|
276
|
+
.option("--force", "overwrite existing state by creating a fresh DB9 bootstrap")
|
|
277
|
+
.action(async (options: { force?: boolean }) => {
|
|
278
|
+
const state = await bootstrapState({
|
|
279
|
+
config: resolvedConfig,
|
|
280
|
+
stateStore,
|
|
281
|
+
logger,
|
|
282
|
+
force: options.force === true,
|
|
283
|
+
});
|
|
284
|
+
printJson({
|
|
285
|
+
statePath: stateStore.stateFilePath,
|
|
286
|
+
summary: summarizeDb9AuditState(state),
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
root.command("doctor").description("Run db9-audit diagnostics").action(async () => {
|
|
291
|
+
const report = await runDoctor({
|
|
292
|
+
config: resolvedConfig,
|
|
293
|
+
stateStore,
|
|
294
|
+
logger,
|
|
295
|
+
});
|
|
296
|
+
printJson(report);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
root
|
|
300
|
+
.command("backfill")
|
|
301
|
+
.description("Backfill local transcripts into DB9")
|
|
302
|
+
.option("--agent <id>", "limit to one agent id")
|
|
303
|
+
.option("--since <date>", "only include transcript files modified on/after this date")
|
|
304
|
+
.option("--reset-offsets", "clear stored offsets/messages for each session file before syncing")
|
|
305
|
+
.action(async (options: { agent?: string; since?: string; resetOffsets?: boolean }) => {
|
|
306
|
+
const state = await bootstrapState({
|
|
307
|
+
config: resolvedConfig,
|
|
308
|
+
stateStore,
|
|
309
|
+
logger,
|
|
310
|
+
force: false,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const pg = new Db9AuditPostgres({
|
|
314
|
+
connectionString: state.database.connectionString,
|
|
315
|
+
schema: resolvedConfig.schema,
|
|
316
|
+
logger,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await pg.ensureSchema();
|
|
321
|
+
|
|
322
|
+
const sessionLookup = new Db9AuditSessionLookup();
|
|
323
|
+
const transcriptSync = new Db9AuditTranscriptSync({
|
|
324
|
+
sessionLookup,
|
|
325
|
+
redactConfig: resolvedConfig.redact,
|
|
326
|
+
batchSize: resolvedConfig.batchSize,
|
|
327
|
+
logger,
|
|
328
|
+
getStore: async () => ({
|
|
329
|
+
getOffset: (sessionFile) => pg.getOffset(sessionFile),
|
|
330
|
+
syncTranscriptBatch: (batch) => pg.syncTranscriptBatch(batch),
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const since = options.since ? new Date(options.since) : undefined;
|
|
335
|
+
const files = await discoverTranscriptFiles({
|
|
336
|
+
stateDir: api.runtime.state.resolveStateDir(),
|
|
337
|
+
...(options.agent ? { agentId: options.agent } : {}),
|
|
338
|
+
...(since && !Number.isNaN(since.valueOf()) ? { since } : {}),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
let syncedFiles = 0;
|
|
342
|
+
let insertedMessages = 0;
|
|
343
|
+
for (const filePath of files) {
|
|
344
|
+
const result = await transcriptSync.syncSessionFile(filePath, {
|
|
345
|
+
resetOffsets: options.resetOffsets === true,
|
|
346
|
+
});
|
|
347
|
+
syncedFiles += 1;
|
|
348
|
+
insertedMessages += result.insertedMessages;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
printJson({
|
|
352
|
+
syncedFiles,
|
|
353
|
+
insertedMessages,
|
|
354
|
+
});
|
|
355
|
+
} finally {
|
|
356
|
+
await pg.close().catch(() => undefined);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
{ commands: ["db9-audit"] },
|
|
361
|
+
);
|
|
362
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolveDefaultDatabaseName, normalizeApiBase, normalizeLogRoot, normalizeSchemaName, parseInteger, parseOptionalString } from "./utils.js";
|
|
2
|
+
import type { Db9AuditPluginConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const db9AuditConfigSchema = {
|
|
5
|
+
type: "object",
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
properties: {
|
|
8
|
+
enabled: { type: "boolean" },
|
|
9
|
+
apiBase: { type: "string", minLength: 1 },
|
|
10
|
+
databaseName: { type: "string", minLength: 1 },
|
|
11
|
+
databaseRegion: { type: "string", minLength: 1 },
|
|
12
|
+
schema: { type: "string", minLength: 1 },
|
|
13
|
+
logRoot: { type: "string", minLength: 1 },
|
|
14
|
+
batchSize: { type: "integer", minimum: 1 },
|
|
15
|
+
flushIntervalMs: { type: "integer", minimum: 100 },
|
|
16
|
+
backfillOnStart: { type: "boolean" },
|
|
17
|
+
redact: {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
enabled: { type: "boolean" },
|
|
22
|
+
maxFieldBytes: { type: "integer", minimum: 1024 },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export function loadDb9AuditConfig(input: unknown): Db9AuditPluginConfig {
|
|
29
|
+
const raw = typeof input === "object" && input !== null ? (input as Record<string, unknown>) : {};
|
|
30
|
+
const redact = typeof raw.redact === "object" && raw.redact !== null
|
|
31
|
+
? (raw.redact as Record<string, unknown>)
|
|
32
|
+
: {};
|
|
33
|
+
|
|
34
|
+
const databaseName = parseOptionalString(raw.databaseName) ?? resolveDefaultDatabaseName();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
enabled: raw.enabled !== false,
|
|
38
|
+
apiBase: normalizeApiBase(parseOptionalString(raw.apiBase)),
|
|
39
|
+
databaseName,
|
|
40
|
+
...(parseOptionalString(raw.databaseRegion)
|
|
41
|
+
? { databaseRegion: parseOptionalString(raw.databaseRegion) }
|
|
42
|
+
: {}),
|
|
43
|
+
schema: normalizeSchemaName(parseOptionalString(raw.schema)),
|
|
44
|
+
logRoot: normalizeLogRoot(parseOptionalString(raw.logRoot)),
|
|
45
|
+
batchSize: parseInteger(raw.batchSize, 100, 1),
|
|
46
|
+
flushIntervalMs: parseInteger(raw.flushIntervalMs, 1_000, 100),
|
|
47
|
+
backfillOnStart: raw.backfillOnStart !== false,
|
|
48
|
+
redact: {
|
|
49
|
+
enabled: redact.enabled !== false,
|
|
50
|
+
maxFieldBytes: parseInteger(redact.maxFieldBytes, 65_536, 1_024),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Db9AnonymousRefreshResponse,
|
|
3
|
+
Db9AnonymousRegisterResponse,
|
|
4
|
+
Db9CustomerResponse,
|
|
5
|
+
Db9DatabaseCredentialsResponse,
|
|
6
|
+
Db9DatabaseResponse,
|
|
7
|
+
Db9DatabaseStatusResponse,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { extractErrorMessage, formatError, isRecord, parseOptionalString } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
export class Db9ControlPlaneError extends Error {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly code?: string | undefined;
|
|
14
|
+
readonly body?: unknown;
|
|
15
|
+
|
|
16
|
+
constructor(params: {
|
|
17
|
+
message: string;
|
|
18
|
+
status: number;
|
|
19
|
+
code?: string | undefined;
|
|
20
|
+
body?: unknown;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
}) {
|
|
23
|
+
super(params.message, params.cause !== undefined ? { cause: params.cause } : undefined);
|
|
24
|
+
this.name = "Db9ControlPlaneError";
|
|
25
|
+
this.status = params.status;
|
|
26
|
+
this.code = params.code;
|
|
27
|
+
this.body = params.body;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseErrorCode(payload: unknown): string | undefined {
|
|
32
|
+
if (!isRecord(payload)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const error = payload.error;
|
|
36
|
+
if (typeof error === "string") {
|
|
37
|
+
return parseOptionalString(error);
|
|
38
|
+
}
|
|
39
|
+
if (!isRecord(error)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return parseOptionalString(error.code);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseJsonObject(value: unknown): Record<string, unknown> {
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
|
+
throw new Error("DB9 API returned a non-object JSON payload");
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Db9ControlPlaneClient {
|
|
53
|
+
readonly apiBase: string;
|
|
54
|
+
|
|
55
|
+
constructor(apiBase: string) {
|
|
56
|
+
this.apiBase = apiBase;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async anonymousRegister(): Promise<Db9AnonymousRegisterResponse> {
|
|
60
|
+
return await this.requestJson<Db9AnonymousRegisterResponse>("POST", "/customer/anonymous-register", {
|
|
61
|
+
body: {},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async anonymousRefresh(params: {
|
|
66
|
+
anonymousId: string;
|
|
67
|
+
anonymousSecret: string;
|
|
68
|
+
}): Promise<Db9AnonymousRefreshResponse> {
|
|
69
|
+
return await this.requestJson<Db9AnonymousRefreshResponse>("POST", "/customer/anonymous-refresh", {
|
|
70
|
+
body: {
|
|
71
|
+
anonymous_id: params.anonymousId,
|
|
72
|
+
anonymous_secret: params.anonymousSecret,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getMe(token: string): Promise<Db9CustomerResponse> {
|
|
78
|
+
return await this.requestJson<Db9CustomerResponse>("GET", "/customer/me", { token });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async createDatabase(
|
|
82
|
+
token: string,
|
|
83
|
+
params: { name: string; region?: string | undefined },
|
|
84
|
+
): Promise<Db9DatabaseResponse> {
|
|
85
|
+
return await this.requestJson<Db9DatabaseResponse>("POST", "/customer/databases", {
|
|
86
|
+
token,
|
|
87
|
+
body: {
|
|
88
|
+
name: params.name,
|
|
89
|
+
...(params.region ? { region: params.region } : {}),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getDatabase(token: string, databaseId: string): Promise<Db9DatabaseStatusResponse> {
|
|
95
|
+
return await this.requestJson<Db9DatabaseStatusResponse>(
|
|
96
|
+
"GET",
|
|
97
|
+
`/customer/databases/${encodeURIComponent(databaseId)}`,
|
|
98
|
+
{ token },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getCredentials(token: string, databaseId: string): Promise<Db9DatabaseCredentialsResponse> {
|
|
103
|
+
return await this.requestJson<Db9DatabaseCredentialsResponse>(
|
|
104
|
+
"GET",
|
|
105
|
+
`/customer/databases/${encodeURIComponent(databaseId)}/credentials`,
|
|
106
|
+
{ token },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async requestJson<T>(
|
|
111
|
+
method: string,
|
|
112
|
+
pathname: string,
|
|
113
|
+
options: {
|
|
114
|
+
token?: string | undefined;
|
|
115
|
+
body?: Record<string, unknown> | null | undefined;
|
|
116
|
+
} = {},
|
|
117
|
+
): Promise<T> {
|
|
118
|
+
const url = `${this.apiBase}${pathname}`;
|
|
119
|
+
let response: Response;
|
|
120
|
+
try {
|
|
121
|
+
const init: RequestInit = {
|
|
122
|
+
method,
|
|
123
|
+
headers: {
|
|
124
|
+
accept: "application/json",
|
|
125
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
126
|
+
...(options.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
127
|
+
},
|
|
128
|
+
...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
|
|
129
|
+
};
|
|
130
|
+
response = await fetch(url, init);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new Db9ControlPlaneError({
|
|
133
|
+
status: 0,
|
|
134
|
+
message: `DB9 API request failed: ${formatError(error)}`,
|
|
135
|
+
cause: error,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const payload = await this.readJsonResponse(response);
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const message =
|
|
142
|
+
extractErrorMessage(payload) ??
|
|
143
|
+
`DB9 API request failed with status ${response.status} ${response.statusText}`;
|
|
144
|
+
throw new Db9ControlPlaneError({
|
|
145
|
+
status: response.status,
|
|
146
|
+
code: parseErrorCode(payload),
|
|
147
|
+
body: payload,
|
|
148
|
+
message,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return payload as T;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async readJsonResponse(response: Response): Promise<unknown> {
|
|
156
|
+
const raw = await response.text();
|
|
157
|
+
if (!raw.trim()) {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return parseJsonObject(JSON.parse(raw));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new Db9ControlPlaneError({
|
|
164
|
+
status: response.status,
|
|
165
|
+
message: `DB9 API returned invalid JSON: ${formatError(error)}`,
|
|
166
|
+
cause: error,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|