@lossless.org/objectstorage 11.0.0 → 11.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.
Files changed (51) hide show
  1. package/.smartconfig.json +19 -1
  2. package/dist_rust/ruststorage_linux_amd64 +0 -0
  3. package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +4 -4
  4. package/dist_rust/ruststorage_linux_amd64_musl +0 -0
  5. package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +4 -4
  6. package/dist_rust/ruststorage_linux_arm64 +0 -0
  7. package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +4 -4
  8. package/dist_rust/ruststorage_linux_arm64_musl +0 -0
  9. package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +4 -4
  10. package/dist_rust/ruststorage_macos_amd64 +0 -0
  11. package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +4 -4
  12. package/dist_rust/ruststorage_macos_arm64 +0 -0
  13. package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +4 -4
  14. package/dist_ts/00_commitinfo_data.js +1 -1
  15. package/dist_ts/classes.dashboard.d.ts +49 -0
  16. package/dist_ts/classes.dashboard.js +234 -0
  17. package/dist_ts/dashboard.bundled.d.ts +4 -0
  18. package/dist_ts/dashboard.bundled.js +20 -0
  19. package/dist_ts/dashboard.interfaces.d.ts +132 -0
  20. package/dist_ts/dashboard.interfaces.js +2 -0
  21. package/dist_ts/dashboard.plugins.d.ts +5 -0
  22. package/dist_ts/dashboard.plugins.js +10 -0
  23. package/dist_ts/index.d.ts +94 -1
  24. package/dist_ts/index.js +63 -1
  25. package/package.json +11 -7
  26. package/readme.md +192 -9
  27. package/third-party-notices.md +20 -4
  28. package/third_party_licenses/borsh-1.8.1-license.txt +23 -0
  29. package/third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt +201 -0
  30. package/third_party_licenses/cargo-dependencies.html +420 -0
  31. package/third_party_licenses/dyn-clone-1.0.20-license.txt +23 -0
  32. package/third_party_licenses/equivalent-1.0.3-license.txt +25 -0
  33. package/third_party_licenses/hashbrown-0.17.1-license.txt +25 -0
  34. package/third_party_licenses/indexmap-2.14.2-license.txt +25 -0
  35. package/third_party_licenses/inventory.json +310 -8
  36. package/third_party_licenses/proc-macro-crate-3.5.0-license.txt +23 -0
  37. package/third_party_licenses/ref-cast-1.0.27-license.txt +23 -0
  38. package/third_party_licenses/ref-cast-impl-1.0.27-license.txt +23 -0
  39. package/third_party_licenses/schemars-1.2.3-license.txt +21 -0
  40. package/third_party_licenses/syn-3.0.9-license.txt +23 -0
  41. package/third_party_licenses/toml_datetime-1.1.2-license.txt +19 -0
  42. package/third_party_licenses/toml_edit-0.25.17-license.txt +19 -0
  43. package/third_party_licenses/toml_parser-1.1.5-license.txt +19 -0
  44. package/third_party_licenses/winnow-1.0.4-license.txt +18 -0
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.dashboard.ts +280 -0
  47. package/ts/dashboard.bundled.ts.third-party-notices.json +1741 -0
  48. package/ts/dashboard.bundled.ts.third-party-notices.txt +5015 -0
  49. package/ts/dashboard.interfaces.ts +148 -0
  50. package/ts/dashboard.plugins.ts +13 -0
  51. package/ts/index.ts +162 -1
@@ -0,0 +1,148 @@
1
+ import type { ITypedRequest, implementsTR } from '@api.global/typedrequest-interfaces';
2
+ import type { ITypedSocketEvent } from '@api.global/typedsocket';
3
+
4
+ /**
5
+ * Engine snapshot shapes, as `ObjectStorage.getClusterSnapshot()` returns
6
+ * them. The engine builds them from memory only: the drive states the last
7
+ * heartbeat probe left, runtime stats, healing state, S3 listener counters
8
+ * and a bounded ring of recent events. Times are milliseconds since the Unix
9
+ * epoch; absent values are `null`.
10
+ */
11
+ export interface IObjectStorageTelemetryEvent {
12
+ /** Numbered per node from 1; a gap means older events left the ring. */
13
+ seq: number;
14
+ at: number;
15
+ kind: 'engine' | 'peer' | 'drive' | 'healing';
16
+ severity: 'info' | 'warning' | 'error';
17
+ message: string;
18
+ }
19
+
20
+ /** S3 listener counters since the engine started; bytes include HTTP headers. */
21
+ export interface IObjectStorageTrafficCounters {
22
+ requestsTotal: number;
23
+ errorResponses: number;
24
+ bytesIn: number;
25
+ bytesOut: number;
26
+ }
27
+
28
+ export interface IObjectStorageStorageTotals {
29
+ bucketCount: number;
30
+ objectCount: number;
31
+ totalBytes: number;
32
+ }
33
+
34
+ export interface IObjectStorageDriveSnapshot {
35
+ index: number;
36
+ path: string;
37
+ status: 'online' | 'degraded' | 'offline' | 'healing';
38
+ totalBytes: number;
39
+ usedBytes: number;
40
+ availableBytes: number;
41
+ errorCount: number;
42
+ lastError: string | null;
43
+ lastCheck: number | null;
44
+ erasureSetId: number | null;
45
+ }
46
+
47
+ export interface IObjectStorageHealingSnapshot {
48
+ active: boolean;
49
+ scanIntervalMs: number;
50
+ lastRunStartedAt: number | null;
51
+ lastRunCompletedAt: number | null;
52
+ lastDurationMs: number | null;
53
+ shardsChecked: number | null;
54
+ shardsHealed: number | null;
55
+ shardsPending: number | null;
56
+ failed: number | null;
57
+ lastError: string | null;
58
+ }
59
+
60
+ /** What one node reports about itself; local and remote nodes share this shape. */
61
+ export interface IObjectStorageNodeSnapshot {
62
+ /** Absent in standalone mode. */
63
+ nodeId: string | null;
64
+ mode: 'standalone' | 'cluster';
65
+ startedAt: number;
66
+ capturedAt: number;
67
+ traffic: IObjectStorageTrafficCounters;
68
+ storage: IObjectStorageStorageTotals;
69
+ /** Local drives (cluster mode); empty in standalone mode. */
70
+ drives: IObjectStorageDriveSnapshot[];
71
+ healing: IObjectStorageHealingSnapshot | null;
72
+ /** Recent events, oldest first. */
73
+ events: IObjectStorageTelemetryEvent[];
74
+ }
75
+
76
+ export interface IObjectStoragePeerSnapshot {
77
+ nodeId: string;
78
+ status: 'online' | 'suspect' | 'offline';
79
+ quicAddress: string;
80
+ s3Address: string;
81
+ driveCount: number;
82
+ lastHeartbeat: number;
83
+ missedHeartbeats: number;
84
+ /** The peer's own snapshot, when it answered within a second. */
85
+ snapshot: IObjectStorageNodeSnapshot | null;
86
+ unavailableReason: string | null;
87
+ }
88
+
89
+ export interface IObjectStorageErasureSnapshot {
90
+ dataShards: number;
91
+ parityShards: number;
92
+ chunkSizeBytes: number;
93
+ totalShards: number;
94
+ readQuorum: number;
95
+ writeQuorum: number;
96
+ erasureSetCount: number;
97
+ }
98
+
99
+ export interface IObjectStorageClusterViewSnapshot {
100
+ nodeId: string;
101
+ quorumHealthy: boolean;
102
+ majorityHealthy: boolean;
103
+ erasure: IObjectStorageErasureSnapshot;
104
+ peers: IObjectStoragePeerSnapshot[];
105
+ }
106
+
107
+ export interface IObjectStorageClusterSnapshot {
108
+ capturedAt: number;
109
+ clusterEnabled: boolean;
110
+ local: IObjectStorageNodeSnapshot;
111
+ /** Absent in standalone mode. */
112
+ cluster: IObjectStorageClusterViewSnapshot | null;
113
+ }
114
+
115
+ /**
116
+ * Dashboard configuration. The dashboard is off unless `enabled` is true.
117
+ */
118
+ export interface IObjectStorageDashboardConfig {
119
+ enabled: boolean;
120
+ /** Interface the dashboard listens on. Default `127.0.0.1`. */
121
+ address?: string;
122
+ /** TCP port; `0` asks the OS for one. Default 3001; must differ from the S3 port. */
123
+ port?: number;
124
+ }
125
+
126
+ /** One dashboard sample, as pushed to every connected dashboard. */
127
+ export interface IObjectStorageDashboardSample {
128
+ sampledAt: number;
129
+ /** The snapshot, or `null` when the engine could not answer. */
130
+ snapshot: IObjectStorageClusterSnapshot | null;
131
+ error: string | null;
132
+ /** Events were left out of peer snapshots to keep the sample within the push limit. */
133
+ eventsTruncated: boolean;
134
+ }
135
+
136
+ /** Answers with the latest sample, taking one when none exists yet. */
137
+ export interface IReq_ObjectStorageDashboardGetSample
138
+ extends implementsTR<ITypedRequest, IReq_ObjectStorageDashboardGetSample> {
139
+ method: 'objectStorageDashboardGetSample';
140
+ request: Record<string, never>;
141
+ response: IObjectStorageDashboardSample;
142
+ }
143
+
144
+ /** Pushed every two seconds while at least one dashboard is connected. */
145
+ export interface IEvent_ObjectStorageDashboardSample
146
+ extends ITypedSocketEvent<IObjectStorageDashboardSample> {
147
+ name: 'objectStorageDashboardSample';
148
+ }
@@ -0,0 +1,13 @@
1
+ // Loaded only by the dashboard module, which `ObjectStorage` imports
2
+ // dynamically when the dashboard is enabled.
3
+
4
+ // @api.global scope
5
+ import * as typedrequest from '@api.global/typedrequest';
6
+ import * as typedsocket from '@api.global/typedsocket';
7
+
8
+ export { typedrequest, typedsocket };
9
+
10
+ // @push.rocks scope
11
+ import * as smartserve from '@push.rocks/smartserve';
12
+
13
+ export { smartserve };
package/ts/index.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import * as paths from './paths.js';
3
+ import type { ObjectStorageDashboard } from './classes.dashboard.js';
4
+ import type {
5
+ IObjectStorageClusterSnapshot,
6
+ IObjectStorageDashboardConfig,
7
+ } from './dashboard.interfaces.js';
8
+
9
+ export * from './dashboard.interfaces.js';
3
10
 
4
11
  /**
5
12
  * Authentication configuration
@@ -145,11 +152,34 @@ export interface IDriveConfig {
145
152
  allowSharedDevice?: boolean;
146
153
  }
147
154
 
155
+ /**
156
+ * Mutual TLS for the cluster transport. Every file is read once at startup and
157
+ * must lie outside the control and drive roots, at an absolute path without
158
+ * symbolic links; a restart reloads them.
159
+ */
160
+ export interface IClusterTlsConfig {
161
+ /** PEM file with one or two CA certificates (two only during a CA rotation). */
162
+ caFile: string;
163
+ /**
164
+ * PEM file with this node's certificate, optionally followed by intermediates. It has one DNS SAN,
165
+ * equal to nodeId, and is valid for server and client authentication: an extended key usage
166
+ * extension listing serverAuth and clientAuth, or no extended key usage extension.
167
+ */
168
+ certFile: string;
169
+ /** PEM private key of certFile; owned by the engine's user, mode 0600 or 0400. */
170
+ keyFile: string;
171
+ }
172
+
148
173
  /**
149
174
  * Cluster configuration for distributed mode
150
175
  */
151
176
  export interface IClusterConfig {
152
177
  enabled: boolean;
178
+ /**
179
+ * This node's id. Generated and persisted when omitted, except with `tls`:
180
+ * then it is required and must be a lowercase DNS name equal to the node
181
+ * certificate's DNS subject alternative name.
182
+ */
153
183
  nodeId?: string;
154
184
  quicPort?: number;
155
185
  seedNodes?: string[];
@@ -167,12 +197,20 @@ export interface IClusterConfig {
167
197
  * 60,000 (1 minute) to 604,800,000 (7 days).
168
198
  */
169
199
  getDeadlineMs?: number;
200
+ /**
201
+ * Mutual TLS for the cluster transport. Without it the transport is
202
+ * encrypted but authenticates no peer. Every node of a cluster sets it, or
203
+ * none does: enabling it is a full stop of every node, not a rolling change.
204
+ */
205
+ tls?: IClusterTlsConfig;
170
206
  }
171
207
 
172
208
  /**
173
209
  * Configuration as the engine receives it: every section resolved.
174
210
  */
175
- export type TObjectStorageResolvedConfig = Required<Omit<IObjectStorageConfig, 'binaryPath'>>;
211
+ export type TObjectStorageResolvedConfig = Required<
212
+ Omit<IObjectStorageConfig, 'binaryPath' | 'dashboard'>
213
+ >;
176
214
 
177
215
  /**
178
216
  * Complete objectstorage configuration
@@ -193,6 +231,11 @@ export interface IObjectStorageConfig {
193
231
  limits?: ILimitsConfig;
194
232
  multipart?: IMultipartConfig;
195
233
  cluster?: IClusterConfig;
234
+ /**
235
+ * The read-only visibility dashboard, served by this process on its own
236
+ * port. Off unless `enabled` is true; never sent to the engine.
237
+ */
238
+ dashboard?: IObjectStorageDashboardConfig;
196
239
  }
197
240
 
198
241
  /**
@@ -1194,6 +1237,35 @@ export interface IClusterPeerHealth {
1194
1237
  missedHeartbeats?: number;
1195
1238
  }
1196
1239
 
1240
+ /**
1241
+ * A drive's medium: `'rotational'` and `'flash'` (network volumes included),
1242
+ * `'smr'` for drive-managed SMR, which reports itself as rotational and is
1243
+ * never detected, and `'zoned'` for host-managed or host-aware SMR and ZNS.
1244
+ */
1245
+ export type TDriveMedia = 'rotational' | 'smr' | 'flash' | 'zoned';
1246
+
1247
+ /**
1248
+ * A drive's media.
1249
+ */
1250
+ export interface IClusterDriveMedia {
1251
+ /**
1252
+ * The media the configuration declares for the drive. Not reported yet:
1253
+ * drives cannot declare their media.
1254
+ */
1255
+ declared?: TDriveMedia;
1256
+ /**
1257
+ * What the engine detected once at startup, from the whole disk behind the
1258
+ * drive root's filesystem (Linux sysfs `queue/zoned` and `queue/rotational`;
1259
+ * a partition counts as its disk). `'unknown'` for filesystems without a
1260
+ * block device of their own (tmpfs, overlay, network filesystems),
1261
+ * device-mapper, md, loop and other virtual block devices, and on every
1262
+ * platform but Linux. Virtual and network block devices report the
1263
+ * rotational flag their driver or hypervisor sets (QEMU disks show
1264
+ * `'rotational'` unless configured with `rotation_rate=1`).
1265
+ */
1266
+ detected: TDriveMedia | 'unknown';
1267
+ }
1268
+
1197
1269
  /**
1198
1270
  * Local drive health as measured by objectstorage's runtime probes.
1199
1271
  */
@@ -1208,6 +1280,7 @@ export interface IClusterDriveHealth {
1208
1280
  lastError?: string;
1209
1281
  lastCheck?: number;
1210
1282
  erasureSetId?: number;
1283
+ media: IClusterDriveMedia;
1211
1284
  }
1212
1285
 
1213
1286
  export interface IClusterErasureHealth {
@@ -1234,6 +1307,25 @@ export interface IClusterRepairHealth {
1234
1307
  lastError?: string;
1235
1308
  }
1236
1309
 
1310
+ export interface IClusterTransportCertificate {
1311
+ /** SHA-256 of the DER certificate, lowercase hex. */
1312
+ sha256: string;
1313
+ /** Milliseconds since the Unix epoch. */
1314
+ notAfter: number;
1315
+ }
1316
+
1317
+ /**
1318
+ * How the cluster transport authenticates peers, and the certificates it
1319
+ * loaded at startup.
1320
+ */
1321
+ export interface IClusterTransportHealth {
1322
+ mode: 'mutualTls' | 'unauthenticated';
1323
+ /** This node's certificate; only in mode 'mutualTls'. */
1324
+ certificate?: IClusterTransportCertificate;
1325
+ /** The loaded CA bundle in file order; only in mode 'mutualTls'. */
1326
+ caCertificates?: IClusterTransportCertificate[];
1327
+ }
1328
+
1237
1329
  /**
1238
1330
  * Cluster runtime health from the Rust core.
1239
1331
  * When clustering is disabled, the response is `{ enabled: false }`.
@@ -1249,6 +1341,7 @@ export interface IClusterHealth {
1249
1341
  drives?: IClusterDriveHealth[];
1250
1342
  erasure?: IClusterErasureHealth;
1251
1343
  repairs?: IClusterRepairHealth;
1344
+ transport?: IClusterTransportHealth;
1252
1345
  }
1253
1346
 
1254
1347
  /**
@@ -1450,6 +1543,33 @@ function mergeConfig(userConfig: IObjectStorageConfig): TObjectStorageResolvedCo
1450
1543
  } as TObjectStorageResolvedConfig;
1451
1544
  }
1452
1545
 
1546
+ const DEFAULT_DASHBOARD_ADDRESS = '127.0.0.1';
1547
+ const DEFAULT_DASHBOARD_PORT = 3001;
1548
+
1549
+ function resolveDashboardConfig(
1550
+ dashboardArg: IObjectStorageDashboardConfig | undefined,
1551
+ s3PortArg: number,
1552
+ ): Required<IObjectStorageDashboardConfig> | undefined {
1553
+ if (dashboardArg === undefined) {
1554
+ return undefined;
1555
+ }
1556
+ if (typeof dashboardArg.enabled !== 'boolean') {
1557
+ throw new Error('dashboard.enabled must be a boolean.');
1558
+ }
1559
+ const address = dashboardArg.address ?? DEFAULT_DASHBOARD_ADDRESS;
1560
+ if (typeof address !== 'string' || address.length === 0) {
1561
+ throw new Error('dashboard.address must be a non-empty string.');
1562
+ }
1563
+ const port = dashboardArg.port ?? DEFAULT_DASHBOARD_PORT;
1564
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
1565
+ throw new Error('dashboard.port must be an integer from 0 to 65535.');
1566
+ }
1567
+ if (dashboardArg.enabled && port !== 0 && port === s3PortArg) {
1568
+ throw new Error(`dashboard.port ${port} is the S3 port; the dashboard needs its own port.`);
1569
+ }
1570
+ return { enabled: dashboardArg.enabled, address, port };
1571
+ }
1572
+
1453
1573
  function createAccessKeyId(): string {
1454
1574
  return `SS${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
1455
1575
  }
@@ -1608,6 +1728,7 @@ type TRustStorageCommands = {
1608
1728
  result: { available: boolean; credentialCount: number; tombstoneCount: number };
1609
1729
  };
1610
1730
  getClusterHealth: { params: {}; result: IClusterHealth };
1731
+ getClusterSnapshot: { params: {}; result: IObjectStorageClusterSnapshot };
1611
1732
  runHealingScan: { params: {}; result: IClusterHealingScanResult };
1612
1733
  };
1613
1734
 
@@ -1692,6 +1813,8 @@ export class ObjectStorage {
1692
1813
  private bridgeOwned = false;
1693
1814
  private stopRequested = false;
1694
1815
  private stopCompleted = false;
1816
+ private readonly dashboardConfig: Required<IObjectStorageDashboardConfig> | undefined;
1817
+ private dashboard: ObjectStorageDashboard | undefined;
1695
1818
 
1696
1819
  constructor(configArg: IObjectStorageConfig = {}) {
1697
1820
  const binaryPath = configArg.binaryPath;
@@ -1702,6 +1825,7 @@ export class ObjectStorage {
1702
1825
  throw new Error('binaryPath must be a non-empty executable path.');
1703
1826
  }
1704
1827
  this.config = mergeConfig(configArg);
1828
+ this.dashboardConfig = resolveDashboardConfig(configArg.dashboard, this.config.server.port!);
1705
1829
  this.bridge = new plugins.RustBridge<TRustStorageCommands>({
1706
1830
  binaryName: 'ruststorage',
1707
1831
  ...(binaryPath !== undefined
@@ -1767,6 +1891,23 @@ export class ObjectStorage {
1767
1891
  throw new Error('Storage engine did not report a valid bound TCP port.');
1768
1892
  }
1769
1893
  this.boundPort = result.port;
1894
+ if (this.dashboardConfig?.enabled) {
1895
+ // Enabled but unable to start (a port in use, say) fails startup:
1896
+ // the catch below stops the engine.
1897
+ const { ObjectStorageDashboard } = await import('./classes.dashboard.js');
1898
+ const dashboard = new ObjectStorageDashboard({
1899
+ address: this.dashboardConfig.address,
1900
+ port: this.dashboardConfig.port,
1901
+ readSnapshot: () => this.getClusterSnapshot(),
1902
+ });
1903
+ try {
1904
+ await dashboard.start();
1905
+ } catch (dashboardError) {
1906
+ await dashboard.stop();
1907
+ throw dashboardError;
1908
+ }
1909
+ this.dashboard = dashboard;
1910
+ }
1770
1911
  } catch (error) {
1771
1912
  try {
1772
1913
  if (this.bridgeOwned) {
@@ -1794,6 +1935,11 @@ export class ObjectStorage {
1794
1935
  return this.running ? this.boundPort : this.config.server.port!;
1795
1936
  }
1796
1937
 
1938
+ /** The dashboard's bound TCP port while it runs; `undefined` otherwise. */
1939
+ public get dashboardPort(): number | undefined {
1940
+ return this.dashboard?.port;
1941
+ }
1942
+
1797
1943
  public async getStorageDescriptor(
1798
1944
  optionsArg?: Partial<plugins.tsclass.storage.IS3Descriptor>,
1799
1945
  ): Promise<plugins.tsclass.storage.IS3Descriptor> {
@@ -2182,6 +2328,17 @@ export class ObjectStorage {
2182
2328
  return this.bridge.sendCommand('getClusterHealth', {});
2183
2329
  }
2184
2330
 
2331
+ /**
2332
+ * The dashboard's snapshot, built by the engine from memory only: it never
2333
+ * probes a drive, syncs or walks storage. A cluster node also asks each
2334
+ * online peer for its own snapshot, waiting up to a second per peer, in
2335
+ * parallel; a peer that does not answer is reported unavailable. One
2336
+ * snapshot runs at a time: a call while another runs rejects.
2337
+ */
2338
+ public async getClusterSnapshot(): Promise<IObjectStorageClusterSnapshot> {
2339
+ return this.bridge.sendCommand('getClusterSnapshot', {});
2340
+ }
2341
+
2185
2342
  /**
2186
2343
  * Runs a healing scan on this cluster node and resolves with its result.
2187
2344
  * The scan covers the manifests this node coordinated. The request joins
@@ -2332,6 +2489,10 @@ export class ObjectStorage {
2332
2489
  // Startup keeps its own rejection; stop joins only its resource ownership.
2333
2490
  await Promise.allSettled([this.startPromise]);
2334
2491
  }
2492
+ if (this.dashboard) {
2493
+ await this.dashboard.stop();
2494
+ this.dashboard = undefined;
2495
+ }
2335
2496
  if (this.bridgeOwned) {
2336
2497
  if (this.running) {
2337
2498
  this.completedStopResult ??= await this.bridge.sendCommand('stop', {});