@hearthlog/runtime 0.0.0-stage → 0.1.1
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/LICENSE +235 -0
- package/package.json +51 -3
- package/src/access/session.ts +223 -0
- package/src/access/user-session.ts +61 -0
- package/src/ai/decisions.ts +89 -0
- package/src/calendar/draft.ts +84 -0
- package/src/calendar/feeds.ts +91 -0
- package/src/calendar/groups.ts +198 -0
- package/src/calendar/logic.ts +141 -0
- package/src/calendar/preferences.ts +40 -0
- package/src/calendar/weeks.ts +56 -0
- package/src/commands/index.ts +184 -0
- package/src/commands/local-commit.ts +214 -0
- package/src/commands/standard.ts +8 -0
- package/src/commands/validation.ts +206 -0
- package/src/db/bootstrap.ts +72 -0
- package/src/db/calendar.ts +638 -0
- package/src/db/dynamic-ddl.ts +127 -0
- package/src/db/entities.ts +208 -0
- package/src/db/history.ts +194 -0
- package/src/db/index.ts +1240 -0
- package/src/db/kv.ts +33 -0
- package/src/db/local-statistics.ts +106 -0
- package/src/db/module-projections.ts +46 -0
- package/src/db/raw-browser.ts +361 -0
- package/src/db/read-repositories.ts +29 -0
- package/src/db/read-session.ts +47 -0
- package/src/db/schema-version.ts +76 -0
- package/src/db/session-context.ts +33 -0
- package/src/event/code-export.ts +65 -0
- package/src/event/dynamic-entities.ts +503 -0
- package/src/event/dynamic-materializer.ts +288 -0
- package/src/event/handlers.ts +490 -0
- package/src/event/index.ts +53 -0
- package/src/event/meta-entity-types.ts +165 -0
- package/src/event/projection-builder.ts +384 -0
- package/src/event/queries.ts +126 -0
- package/src/event/rebuild.ts +176 -0
- package/src/event/reference-lookup.ts +93 -0
- package/src/event/registry.ts +158 -0
- package/src/event/schema-effects.ts +45 -0
- package/src/event/types.ts +20 -0
- package/src/history/action-boundary.ts +18 -0
- package/src/history/action-detail.ts +23 -0
- package/src/history/change-comparison.ts +29 -0
- package/src/history/change-display.ts +41 -0
- package/src/history/contracts.ts +63 -0
- package/src/history/history.ts +372 -0
- package/src/history/legacy-replay.ts +69 -0
- package/src/history/legacy-url.ts +9 -0
- package/src/history/lineage.ts +123 -0
- package/src/history/log-format.ts +17 -0
- package/src/history/log-map.ts +110 -0
- package/src/history/navigation.ts +14 -0
- package/src/history/point-snapshot.ts +229 -0
- package/src/history/timeline.ts +215 -0
- package/src/history/url.ts +18 -0
- package/src/ledger/README.md +118 -0
- package/src/ledger/admin-client.ts +616 -0
- package/src/ledger/client.ts +10 -0
- package/src/ledger/config.ts +117 -0
- package/src/ledger/entity-id.ts +115 -0
- package/src/ledger/errors.ts +144 -0
- package/src/ledger/http-client.ts +590 -0
- package/src/ledger/index.ts +67 -0
- package/src/ledger/socket.ts +400 -0
- package/src/ledger/types.ts +202 -0
- package/src/ledger/version-tracker-singleton.ts +12 -0
- package/src/ledger/version-tracker.ts +259 -0
- package/src/log-management.ts +123 -0
- package/src/log-validation.ts +19 -0
- package/src/native/reminders.ts +2 -0
- package/src/native/shell.svelte.ts +136 -0
- package/src/native/shell.ts +102 -0
- package/src/pwa/registration.ts +169 -0
- package/src/recovery/local-recovery.ts +59 -0
- package/src/recovery/reset-sequence.ts +13 -0
- package/src/runtime/clock.ts +43 -0
- package/src/runtime/kept-page-data.svelte.ts +42 -0
- package/src/runtime/page-data.svelte.ts +160 -0
- package/src/runtime/phone-header.svelte.ts +17 -0
- package/src/runtime/read-coordinator.ts +103 -0
- package/src/runtime/refresh.ts +15 -0
- package/src/runtime/start-page.ts +56 -0
- package/src/runtime/view.ts +83 -0
- package/src/runtime/workspace.ts +62 -0
- package/src/storage/indexeddb-statistics.ts +153 -0
- package/src/store/attention.svelte.ts +51 -0
- package/src/store/conflict-merge.ts +169 -0
- package/src/store/entity-sync-state.svelte.ts +36 -0
- package/src/store/event-log.svelte.ts +109 -0
- package/src/store/events.svelte.ts +14 -0
- package/src/store/local-context.ts +93 -0
- package/src/store/local-store.svelte.ts +58 -0
- package/src/store/log-version.svelte.ts +35 -0
- package/src/store/logs.svelte.ts +60 -0
- package/src/store/moved.svelte.ts +43 -0
- package/src/store/offline-readiness.svelte.ts +188 -0
- package/src/store/outbox-request.ts +61 -0
- package/src/store/outbox.ts +426 -0
- package/src/store/pending-work-transfer.ts +49 -0
- package/src/store/pending-work.ts +143 -0
- package/src/store/sync-coordinator.ts +170 -0
- package/src/store/sync-invalidation.ts +10 -0
- package/src/store/sync-issues.ts +406 -0
- package/src/store/sync-progress.ts +54 -0
- package/src/store/sync-state.ts +127 -0
- package/src/store/sync.svelte.ts +1010 -0
- package/src/store/tab-coordination.ts +164 -0
- package/src/testing/e2e-seed.ts +40 -0
- package/src/testing/local-database.ts +120 -0
- package/src/testing/projection-database.ts +105 -0
- package/src/testing/test-entities.ts +28 -0
- package/src/theme/appearance.svelte.ts +111 -0
- package/src/utils/address.ts +5 -0
- package/src/utils/async.ts +30 -0
- package/src/utils/long-press.ts +70 -0
- package/src/utils/perf-dataset.ts +405 -0
- package/src/utils/perf.ts +627 -0
- package/README.md +0 -3
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import type { StoredEvent } from "./types.js";
|
|
2
|
+
import type { AdminConnectionsResponse } from "@hearthlog/contracts/ledger/admin";
|
|
3
|
+
export type {
|
|
4
|
+
AdminConnection,
|
|
5
|
+
AdminConnectionsResponse,
|
|
6
|
+
AdminRecentConnection,
|
|
7
|
+
} from "@hearthlog/contracts/ledger/admin";
|
|
8
|
+
import {
|
|
9
|
+
clearAccessReauthenticationAttempts,
|
|
10
|
+
isAccessSessionExpiredResponse,
|
|
11
|
+
requestAccessReauthentication,
|
|
12
|
+
withCloudflareAccessAjaxHeader,
|
|
13
|
+
} from "../access/session.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ADMIN_BASE_URL = "/api/ledger/admin/v1";
|
|
16
|
+
|
|
17
|
+
export interface AdminSession {
|
|
18
|
+
email: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AdminAppSummary {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
revokedAt: string | null;
|
|
26
|
+
activeTokenCreatedAt: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AdminAppListResponse {
|
|
30
|
+
apps: AdminAppSummary[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AdminAppTokenResponse {
|
|
34
|
+
app: AdminAppSummary;
|
|
35
|
+
token: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AdminLogSummary {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
headVersion: number;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
lastUpdatedAt: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AdminLogListResponse {
|
|
47
|
+
logs: AdminLogSummary[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AdminMetrics {
|
|
51
|
+
sampledAt: string;
|
|
52
|
+
logCount: number;
|
|
53
|
+
eventCount: number;
|
|
54
|
+
entityCount: number;
|
|
55
|
+
databaseBytes: number;
|
|
56
|
+
connectedClients: number;
|
|
57
|
+
lastUpdatedAt: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AdminEventPage {
|
|
61
|
+
events: StoredEvent[];
|
|
62
|
+
headVersion: number;
|
|
63
|
+
nextBeforeVersion: number | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AdminExportedLog extends AdminLogSummary {
|
|
67
|
+
events: StoredEvent[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AdminApplicationExport {
|
|
71
|
+
format: "ledger-application-export";
|
|
72
|
+
version: 1;
|
|
73
|
+
exportedAt: string;
|
|
74
|
+
application: AdminAppSummary;
|
|
75
|
+
logs: AdminExportedLog[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AdminErrorResponse {
|
|
79
|
+
error: string;
|
|
80
|
+
code: string;
|
|
81
|
+
message?: string | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class LedgerAdminError extends Error {
|
|
85
|
+
constructor(
|
|
86
|
+
message: string,
|
|
87
|
+
public readonly status: number | null,
|
|
88
|
+
public readonly code: string,
|
|
89
|
+
public readonly outcomeUnknown = false,
|
|
90
|
+
) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "LedgerAdminError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class LedgerAdminUnauthorizedError extends LedgerAdminError {
|
|
97
|
+
constructor() {
|
|
98
|
+
super(
|
|
99
|
+
"You do not have Ledger administrator access.",
|
|
100
|
+
403,
|
|
101
|
+
"ledger_admin_forbidden",
|
|
102
|
+
);
|
|
103
|
+
this.name = "LedgerAdminUnauthorizedError";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class LedgerAdminClient {
|
|
108
|
+
private readonly baseUrl: string;
|
|
109
|
+
|
|
110
|
+
constructor(
|
|
111
|
+
baseUrl = DEFAULT_ADMIN_BASE_URL,
|
|
112
|
+
private readonly fetchRequest: typeof fetch = (...args) =>
|
|
113
|
+
globalThis.fetch(...args),
|
|
114
|
+
) {
|
|
115
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getSession(): Promise<AdminSession> {
|
|
119
|
+
return this.request("GET", "/session", undefined, parseAdminSession);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async listApps(): Promise<AdminAppSummary[]> {
|
|
123
|
+
const response = await this.request(
|
|
124
|
+
"GET",
|
|
125
|
+
"/apps",
|
|
126
|
+
undefined,
|
|
127
|
+
parseAppList,
|
|
128
|
+
);
|
|
129
|
+
return response.apps;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async listAppLogs(appId: string): Promise<AdminLogSummary[]> {
|
|
133
|
+
const response = await this.request(
|
|
134
|
+
"GET",
|
|
135
|
+
`/apps/${encodeURIComponent(appId)}/logs`,
|
|
136
|
+
undefined,
|
|
137
|
+
parseLogList,
|
|
138
|
+
);
|
|
139
|
+
return response.logs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getAppExport(appId: string): Promise<AdminApplicationExport> {
|
|
143
|
+
return this.request(
|
|
144
|
+
"GET",
|
|
145
|
+
`/apps/${encodeURIComponent(appId)}/export`,
|
|
146
|
+
undefined,
|
|
147
|
+
parseApplicationExport,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getAppMetrics(appId: string): Promise<AdminMetrics> {
|
|
152
|
+
return this.request(
|
|
153
|
+
"GET",
|
|
154
|
+
`/apps/${encodeURIComponent(appId)}/metrics`,
|
|
155
|
+
undefined,
|
|
156
|
+
parseMetrics,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
listConnections(
|
|
161
|
+
appId: string,
|
|
162
|
+
afterId?: string,
|
|
163
|
+
): Promise<AdminConnectionsResponse> {
|
|
164
|
+
const query = afterId ? `?${new URLSearchParams({ afterId })}` : "";
|
|
165
|
+
return this.request(
|
|
166
|
+
"GET",
|
|
167
|
+
`/apps/${encodeURIComponent(appId)}/connections${query}`,
|
|
168
|
+
undefined,
|
|
169
|
+
parseConnections,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getSqliteExport(
|
|
174
|
+
appId: string | null,
|
|
175
|
+
signal?: AbortSignal,
|
|
176
|
+
): Promise<Response> {
|
|
177
|
+
const path =
|
|
178
|
+
appId === null
|
|
179
|
+
? "/registry/export/sqlite"
|
|
180
|
+
: `/apps/${encodeURIComponent(appId)}/export/sqlite`;
|
|
181
|
+
const response = await this.fetchResponse(
|
|
182
|
+
"GET",
|
|
183
|
+
path,
|
|
184
|
+
undefined,
|
|
185
|
+
signal,
|
|
186
|
+
);
|
|
187
|
+
if (
|
|
188
|
+
!response.ok ||
|
|
189
|
+
!response.body ||
|
|
190
|
+
!response.headers.get("Content-Type")?.startsWith("application/sql")
|
|
191
|
+
) {
|
|
192
|
+
throw new LedgerAdminError(
|
|
193
|
+
`Database export could not be started (${response.status}).`,
|
|
194
|
+
response.status,
|
|
195
|
+
"ledger_export_failed",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return response;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
listLogEvents(
|
|
202
|
+
appId: string,
|
|
203
|
+
logId: string,
|
|
204
|
+
options: { beforeVersion?: number; search?: string } = {},
|
|
205
|
+
): Promise<AdminEventPage> {
|
|
206
|
+
const query = new URLSearchParams({ limit: "50" });
|
|
207
|
+
if (options.beforeVersion !== undefined)
|
|
208
|
+
query.set("beforeVersion", String(options.beforeVersion));
|
|
209
|
+
if (options.search) query.set("search", options.search);
|
|
210
|
+
return this.request(
|
|
211
|
+
"GET",
|
|
212
|
+
`/apps/${encodeURIComponent(appId)}/logs/${encodeURIComponent(logId)}/events?${query}`,
|
|
213
|
+
undefined,
|
|
214
|
+
parseEventPage,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
createApp(name: string): Promise<AdminAppTokenResponse> {
|
|
219
|
+
return this.request("POST", "/apps", { name }, parseAppTokenResponse);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
rotateAppToken(appId: string): Promise<AdminAppTokenResponse> {
|
|
223
|
+
return this.request(
|
|
224
|
+
"POST",
|
|
225
|
+
`/apps/${encodeURIComponent(appId)}/rotate-token`,
|
|
226
|
+
undefined,
|
|
227
|
+
parseAppTokenResponse,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
revokeApp(appId: string): Promise<AdminAppSummary> {
|
|
232
|
+
return this.request(
|
|
233
|
+
"POST",
|
|
234
|
+
`/apps/${encodeURIComponent(appId)}/revoke`,
|
|
235
|
+
undefined,
|
|
236
|
+
parseAppSummary,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async request<T>(
|
|
241
|
+
method: "GET" | "POST",
|
|
242
|
+
path: string,
|
|
243
|
+
body: unknown,
|
|
244
|
+
parse: (value: unknown) => T,
|
|
245
|
+
): Promise<T> {
|
|
246
|
+
const mutation = method !== "GET";
|
|
247
|
+
const response = await this.fetchResponse(method, path, body);
|
|
248
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
249
|
+
const isJson = contentType.toLowerCase().includes("application/json");
|
|
250
|
+
let payload: unknown = null;
|
|
251
|
+
if (isJson) {
|
|
252
|
+
try {
|
|
253
|
+
payload = await response.json();
|
|
254
|
+
} catch {
|
|
255
|
+
payload = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const error = parseAdminError(payload);
|
|
260
|
+
throw new LedgerAdminError(
|
|
261
|
+
error?.message ??
|
|
262
|
+
error?.error ??
|
|
263
|
+
`Ledger administrator request failed (${response.status})`,
|
|
264
|
+
response.status,
|
|
265
|
+
error?.code ?? "ledger_admin_request_failed",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return parse(payload);
|
|
270
|
+
} catch {
|
|
271
|
+
throw new LedgerAdminError(
|
|
272
|
+
mutation
|
|
273
|
+
? "The Ledger response could not be verified. Refresh the application list before trying another administrator action."
|
|
274
|
+
: "The Ledger administrator response was invalid.",
|
|
275
|
+
response.status,
|
|
276
|
+
"ledger_admin_invalid_response",
|
|
277
|
+
mutation,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async fetchResponse(
|
|
283
|
+
method: "GET" | "POST",
|
|
284
|
+
path: string,
|
|
285
|
+
body: unknown,
|
|
286
|
+
signal?: AbortSignal,
|
|
287
|
+
): Promise<Response> {
|
|
288
|
+
const mutation = method !== "GET";
|
|
289
|
+
let response: Response;
|
|
290
|
+
try {
|
|
291
|
+
response = await this.fetchRequest(`${this.baseUrl}${path}`, {
|
|
292
|
+
method,
|
|
293
|
+
cache: "no-store",
|
|
294
|
+
credentials: "same-origin",
|
|
295
|
+
redirect: "manual",
|
|
296
|
+
signal,
|
|
297
|
+
headers: withCloudflareAccessAjaxHeader({
|
|
298
|
+
Accept: "application/json",
|
|
299
|
+
...(body === undefined
|
|
300
|
+
? {}
|
|
301
|
+
: { "Content-Type": "application/json" }),
|
|
302
|
+
}),
|
|
303
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
304
|
+
});
|
|
305
|
+
} catch {
|
|
306
|
+
throw new LedgerAdminError(
|
|
307
|
+
mutation
|
|
308
|
+
? "The request outcome is unknown. Refresh the application list before trying another administrator action."
|
|
309
|
+
: "Ledger administrator access could not be reached.",
|
|
310
|
+
null,
|
|
311
|
+
"ledger_admin_network_error",
|
|
312
|
+
mutation,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (isAccessSessionExpiredResponse(response)) {
|
|
317
|
+
requestAccessReauthentication();
|
|
318
|
+
throw new LedgerAdminUnauthorizedError();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
clearAccessReauthenticationAttempts();
|
|
322
|
+
|
|
323
|
+
if (response.status === 403) {
|
|
324
|
+
throw new LedgerAdminUnauthorizedError();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return response;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseConnections(value: unknown): AdminConnectionsResponse {
|
|
332
|
+
const record = objectValue(value);
|
|
333
|
+
// Ledger releases before session history omit `recent`.
|
|
334
|
+
const recent = record.recent ?? [];
|
|
335
|
+
if (!Array.isArray(recent)) throw new TypeError();
|
|
336
|
+
for (const item of recent) {
|
|
337
|
+
const session = objectValue(item);
|
|
338
|
+
for (const field of ["id", "logId", "principalId", "connectedAt"])
|
|
339
|
+
if (typeof session[field] !== "string") throw new TypeError();
|
|
340
|
+
for (const field of ["disconnectedAt", "lastSentAt", "logName"])
|
|
341
|
+
if (!nullableString(session[field])) throw new TypeError();
|
|
342
|
+
if (
|
|
343
|
+
!positiveSafeInteger(session.generation) ||
|
|
344
|
+
!Number.isSafeInteger(session.cursor) ||
|
|
345
|
+
(session.cursor as number) < 0 ||
|
|
346
|
+
!(
|
|
347
|
+
session.closeCode === null ||
|
|
348
|
+
(Number.isSafeInteger(session.closeCode) &&
|
|
349
|
+
(session.closeCode as number) >= 0)
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
throw new TypeError();
|
|
353
|
+
const client = objectValue(session.client);
|
|
354
|
+
for (const field of ["ip", "userAgent", "country", "colo"])
|
|
355
|
+
if (!nullableString(client[field])) throw new TypeError();
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
typeof record.sampledAt !== "string" ||
|
|
359
|
+
!Number.isSafeInteger(record.total) ||
|
|
360
|
+
(record.total as number) < 0 ||
|
|
361
|
+
!Number.isSafeInteger(record.unavailable) ||
|
|
362
|
+
(record.unavailable as number) < 0 ||
|
|
363
|
+
!nullableString(record.nextAfterId) ||
|
|
364
|
+
!Array.isArray(record.connections) ||
|
|
365
|
+
record.connections.length > 100
|
|
366
|
+
)
|
|
367
|
+
throw new TypeError();
|
|
368
|
+
for (const item of record.connections) {
|
|
369
|
+
const connection = objectValue(item);
|
|
370
|
+
for (const field of ["id", "logId", "principalId"])
|
|
371
|
+
if (typeof connection[field] !== "string") throw new TypeError();
|
|
372
|
+
for (const field of ["connectedAt", "lastSentAt", "logName"])
|
|
373
|
+
if (!nullableString(connection[field])) throw new TypeError();
|
|
374
|
+
if (
|
|
375
|
+
!positiveSafeInteger(connection.generation) ||
|
|
376
|
+
!Number.isSafeInteger(connection.cursor) ||
|
|
377
|
+
(connection.cursor as number) < 0 ||
|
|
378
|
+
!(
|
|
379
|
+
connection.headVersion === null ||
|
|
380
|
+
(Number.isSafeInteger(connection.headVersion) &&
|
|
381
|
+
(connection.headVersion as number) >= 0)
|
|
382
|
+
) ||
|
|
383
|
+
!["connecting", "open", "closing", "closed"].includes(
|
|
384
|
+
String(connection.state),
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
throw new TypeError();
|
|
388
|
+
const client = objectValue(connection.client);
|
|
389
|
+
for (const field of ["ip", "userAgent", "country", "colo"])
|
|
390
|
+
if (!nullableString(client[field])) throw new TypeError();
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
record.nextAfterId !== null &&
|
|
394
|
+
record.nextAfterId !== record.connections.at(-1)?.id
|
|
395
|
+
)
|
|
396
|
+
throw new TypeError();
|
|
397
|
+
return { ...(value as AdminConnectionsResponse), recent };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseAdminSession(value: unknown): AdminSession {
|
|
401
|
+
const record = objectValue(value);
|
|
402
|
+
if (typeof record.email !== "string" || !record.email)
|
|
403
|
+
throw new TypeError();
|
|
404
|
+
return { email: record.email };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function parseMetrics(value: unknown): AdminMetrics {
|
|
408
|
+
const record = objectValue(value);
|
|
409
|
+
for (const key of [
|
|
410
|
+
"logCount",
|
|
411
|
+
"eventCount",
|
|
412
|
+
"entityCount",
|
|
413
|
+
"databaseBytes",
|
|
414
|
+
"connectedClients",
|
|
415
|
+
]) {
|
|
416
|
+
if (!Number.isSafeInteger(record[key]) || (record[key] as number) < 0)
|
|
417
|
+
throw new TypeError();
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
typeof record.sampledAt !== "string" ||
|
|
421
|
+
!nullableString(record.lastUpdatedAt)
|
|
422
|
+
)
|
|
423
|
+
throw new TypeError();
|
|
424
|
+
return {
|
|
425
|
+
sampledAt: record.sampledAt,
|
|
426
|
+
lastUpdatedAt: record.lastUpdatedAt,
|
|
427
|
+
logCount: record.logCount as number,
|
|
428
|
+
eventCount: record.eventCount as number,
|
|
429
|
+
entityCount: record.entityCount as number,
|
|
430
|
+
databaseBytes: record.databaseBytes as number,
|
|
431
|
+
connectedClients: record.connectedClients as number,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function parseEventPage(value: unknown): AdminEventPage {
|
|
436
|
+
const record = objectValue(value);
|
|
437
|
+
if (
|
|
438
|
+
!Array.isArray(record.events) ||
|
|
439
|
+
record.events.length > 50 ||
|
|
440
|
+
!Number.isSafeInteger(record.headVersion) ||
|
|
441
|
+
(record.headVersion as number) < 0 ||
|
|
442
|
+
!(
|
|
443
|
+
record.nextBeforeVersion === null ||
|
|
444
|
+
positiveSafeInteger(record.nextBeforeVersion)
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
throw new TypeError();
|
|
448
|
+
const events = record.events.map(parseStoredEvent);
|
|
449
|
+
if (
|
|
450
|
+
events.some(
|
|
451
|
+
(event, index) =>
|
|
452
|
+
event.version > (record.headVersion as number) ||
|
|
453
|
+
(index > 0 && event.version >= events[index - 1].version),
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
throw new TypeError();
|
|
457
|
+
if (
|
|
458
|
+
record.nextBeforeVersion !== null &&
|
|
459
|
+
record.nextBeforeVersion !== events.at(-1)?.version
|
|
460
|
+
)
|
|
461
|
+
throw new TypeError();
|
|
462
|
+
return {
|
|
463
|
+
events,
|
|
464
|
+
headVersion: record.headVersion as number,
|
|
465
|
+
nextBeforeVersion: record.nextBeforeVersion,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parseAppList(value: unknown): AdminAppListResponse {
|
|
470
|
+
const record = objectValue(value);
|
|
471
|
+
if (!Array.isArray(record.apps)) throw new TypeError();
|
|
472
|
+
return { apps: record.apps.map(parseAppSummary) };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function parseAppTokenResponse(value: unknown): AdminAppTokenResponse {
|
|
476
|
+
const record = objectValue(value);
|
|
477
|
+
if (typeof record.token !== "string" || !record.token)
|
|
478
|
+
throw new TypeError();
|
|
479
|
+
return {
|
|
480
|
+
app: parseAppSummary(record.app),
|
|
481
|
+
token: record.token,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function parseLogList(value: unknown): AdminLogListResponse {
|
|
486
|
+
const record = objectValue(value);
|
|
487
|
+
if (!Array.isArray(record.logs)) throw new TypeError();
|
|
488
|
+
return { logs: record.logs.map(parseLogSummary) };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseApplicationExport(value: unknown): AdminApplicationExport {
|
|
492
|
+
const record = objectValue(value);
|
|
493
|
+
if (
|
|
494
|
+
record.format !== "ledger-application-export" ||
|
|
495
|
+
record.version !== 1 ||
|
|
496
|
+
typeof record.exportedAt !== "string" ||
|
|
497
|
+
!Array.isArray(record.logs)
|
|
498
|
+
) {
|
|
499
|
+
throw new TypeError();
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
format: record.format,
|
|
503
|
+
version: record.version,
|
|
504
|
+
exportedAt: record.exportedAt,
|
|
505
|
+
application: parseAppSummary(record.application),
|
|
506
|
+
logs: record.logs.map(parseExportedLog),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function parseExportedLog(value: unknown): AdminExportedLog {
|
|
511
|
+
const record = objectValue(value);
|
|
512
|
+
if (!Array.isArray(record.events)) throw new TypeError();
|
|
513
|
+
return {
|
|
514
|
+
...parseLogSummary(record),
|
|
515
|
+
events: record.events.map(parseStoredEvent),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function parseStoredEvent(value: unknown): StoredEvent {
|
|
520
|
+
const record = objectValue(value);
|
|
521
|
+
if (
|
|
522
|
+
!positiveSafeInteger(record.version) ||
|
|
523
|
+
typeof record.id !== "string" ||
|
|
524
|
+
typeof record.type !== "string" ||
|
|
525
|
+
typeof record.entityId !== "string" ||
|
|
526
|
+
!positiveSafeInteger(record.entityVersion) ||
|
|
527
|
+
!Number.isSafeInteger(record.timestamp) ||
|
|
528
|
+
!record.payload ||
|
|
529
|
+
typeof record.payload !== "object" ||
|
|
530
|
+
Array.isArray(record.payload)
|
|
531
|
+
) {
|
|
532
|
+
throw new TypeError();
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
version: record.version,
|
|
536
|
+
id: record.id,
|
|
537
|
+
type: record.type,
|
|
538
|
+
entityId: record.entityId,
|
|
539
|
+
entityVersion: record.entityVersion,
|
|
540
|
+
timestamp: record.timestamp as number,
|
|
541
|
+
payload: record.payload as Record<string, unknown>,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parseLogSummary(value: unknown): AdminLogSummary {
|
|
546
|
+
const record = objectValue(value);
|
|
547
|
+
if (
|
|
548
|
+
typeof record.id !== "string" ||
|
|
549
|
+
typeof record.name !== "string" ||
|
|
550
|
+
!Number.isSafeInteger(record.headVersion) ||
|
|
551
|
+
(record.headVersion as number) < 0 ||
|
|
552
|
+
typeof record.createdAt !== "string" ||
|
|
553
|
+
!nullableString(record.lastUpdatedAt)
|
|
554
|
+
) {
|
|
555
|
+
throw new TypeError();
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
id: record.id,
|
|
559
|
+
name: record.name,
|
|
560
|
+
headVersion: record.headVersion as number,
|
|
561
|
+
createdAt: record.createdAt,
|
|
562
|
+
lastUpdatedAt: record.lastUpdatedAt,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function parseAppSummary(value: unknown): AdminAppSummary {
|
|
567
|
+
const record = objectValue(value);
|
|
568
|
+
if (
|
|
569
|
+
typeof record.id !== "string" ||
|
|
570
|
+
typeof record.name !== "string" ||
|
|
571
|
+
typeof record.createdAt !== "string" ||
|
|
572
|
+
!nullableString(record.revokedAt) ||
|
|
573
|
+
!nullableString(record.activeTokenCreatedAt)
|
|
574
|
+
) {
|
|
575
|
+
throw new TypeError();
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
id: record.id,
|
|
579
|
+
name: record.name,
|
|
580
|
+
createdAt: record.createdAt,
|
|
581
|
+
revokedAt: record.revokedAt,
|
|
582
|
+
activeTokenCreatedAt: record.activeTokenCreatedAt,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function parseAdminError(value: unknown): AdminErrorResponse | null {
|
|
587
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
588
|
+
return null;
|
|
589
|
+
const record = value as Record<string, unknown>;
|
|
590
|
+
if (typeof record.error !== "string" || typeof record.code !== "string") {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
error: record.error,
|
|
595
|
+
code: record.code,
|
|
596
|
+
message:
|
|
597
|
+
typeof record.message === "string" || record.message === null
|
|
598
|
+
? record.message
|
|
599
|
+
: undefined,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function objectValue(value: unknown): Record<string, unknown> {
|
|
604
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
605
|
+
throw new TypeError();
|
|
606
|
+
}
|
|
607
|
+
return value as Record<string, unknown>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function nullableString(value: unknown): value is string | null {
|
|
611
|
+
return value === null || typeof value === "string";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function positiveSafeInteger(value: unknown): value is number {
|
|
615
|
+
return Number.isSafeInteger(value) && (value as number) > 0;
|
|
616
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadLedgerConfig } from './config.js';
|
|
2
|
+
import { LedgerClient } from './http-client.js';
|
|
3
|
+
|
|
4
|
+
let client: LedgerClient | null = null;
|
|
5
|
+
|
|
6
|
+
/** The configured transport shared by this tab's default Ledger consumers. */
|
|
7
|
+
export function getLedgerClient(): LedgerClient {
|
|
8
|
+
client ??= new LedgerClient(loadLedgerConfig());
|
|
9
|
+
return client;
|
|
10
|
+
}
|