@lossless.org/objectstorage 9.0.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/.smartconfig.json +71 -0
- package/dist_rust/ruststorage_linux_amd64 +0 -0
- package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +14 -0
- package/dist_rust/ruststorage_linux_arm64 +0 -0
- package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +14 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/index.d.ts +741 -0
- package/dist_ts/index.js +885 -0
- package/dist_ts/paths.d.ts +4 -0
- package/dist_ts/paths.js +6 -0
- package/dist_ts/plugins.d.ts +9 -0
- package/dist_ts/plugins.js +13 -0
- package/license.md +21 -0
- package/package.json +77 -0
- package/readme.md +1430 -0
- package/third_party_licenses/base64-0.22.1/LICENSE-MIT +21 -0
- package/third_party_licenses/crc32fast-1.5.0/LICENSE-MIT +21 -0
- package/third_party_licenses/ring-0.17.14/INDEX.md +15 -0
- package/third_party_licenses/ring-0.17.14/LICENSE +9 -0
- package/third_party_licenses/ring-0.17.14/LICENSE-BoringSSL +272 -0
- package/third_party_licenses/ring-0.17.14/LICENSE-fiat +15 -0
- package/third_party_licenses/ring-0.17.14/LICENSE-once_cell-APACHE +201 -0
- package/third_party_licenses/ring-0.17.14/LICENSE-once_cell-MIT +23 -0
- package/third_party_licenses/ring-0.17.14/LICENSE-other-bits +13 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +1932 -0
- package/ts/paths.ts +11 -0
- package/ts/plugins.ts +17 -0
package/ts/index.ts
ADDED
|
@@ -0,0 +1,1932 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as paths from './paths.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authentication configuration
|
|
6
|
+
*/
|
|
7
|
+
export interface IStorageCredential {
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
bucketName?: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IStorageCredentialMetadata {
|
|
15
|
+
accessKeyId: string;
|
|
16
|
+
bucketName?: string;
|
|
17
|
+
region?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Authentication configuration
|
|
22
|
+
*/
|
|
23
|
+
export interface IAuthConfig {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
credentials: IStorageCredential[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CORS configuration
|
|
30
|
+
*/
|
|
31
|
+
export interface ICorsConfig {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
allowedOrigins?: string[];
|
|
34
|
+
allowedMethods?: string[];
|
|
35
|
+
allowedHeaders?: string[];
|
|
36
|
+
exposedHeaders?: string[];
|
|
37
|
+
maxAge?: number;
|
|
38
|
+
allowCredentials?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Logging configuration
|
|
43
|
+
*/
|
|
44
|
+
export interface ILoggingConfig {
|
|
45
|
+
level?: 'error' | 'warn' | 'info' | 'debug';
|
|
46
|
+
format?: 'text' | 'json';
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Request limits configuration
|
|
52
|
+
*/
|
|
53
|
+
export interface ILimitsConfig {
|
|
54
|
+
maxObjectSize?: number;
|
|
55
|
+
maxMetadataSize?: number;
|
|
56
|
+
requestTimeout?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Multipart upload configuration
|
|
61
|
+
*/
|
|
62
|
+
export interface IMultipartConfig {
|
|
63
|
+
expirationDays?: number;
|
|
64
|
+
cleanupIntervalMinutes?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Server configuration
|
|
69
|
+
*/
|
|
70
|
+
export interface IServerConfig {
|
|
71
|
+
port?: number;
|
|
72
|
+
address?: string;
|
|
73
|
+
silent?: boolean;
|
|
74
|
+
region?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Storage configuration
|
|
79
|
+
*/
|
|
80
|
+
export interface ILocalFilesystemStoragePoolBackend {
|
|
81
|
+
kind: 'localFs';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IMountedFilesystemStoragePoolBackend {
|
|
85
|
+
kind: 'mountedFs';
|
|
86
|
+
expectedFilesystemType: 'nfs' | 'smb';
|
|
87
|
+
expectedSource: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type TStoragePoolBackend =
|
|
91
|
+
| ILocalFilesystemStoragePoolBackend
|
|
92
|
+
| IMountedFilesystemStoragePoolBackend;
|
|
93
|
+
|
|
94
|
+
export interface IStoragePoolConfig {
|
|
95
|
+
id: string;
|
|
96
|
+
directory: string;
|
|
97
|
+
backend: TStoragePoolBackend;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface IMountedStoragePoolConfig extends IStoragePoolConfig {
|
|
101
|
+
backend: IMountedFilesystemStoragePoolBackend;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface IStorageConfig {
|
|
105
|
+
directory?: string;
|
|
106
|
+
cleanSlate?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* One ObjectStorage process serves one pool. `directory` remains as a
|
|
109
|
+
* compatibility alias and must resolve to the same path when both are set.
|
|
110
|
+
*/
|
|
111
|
+
pool?: IStoragePoolConfig;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Erasure coding configuration
|
|
116
|
+
*/
|
|
117
|
+
export interface IErasureConfig {
|
|
118
|
+
dataShards?: number;
|
|
119
|
+
parityShards?: number;
|
|
120
|
+
chunkSizeBytes?: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Drive configuration for multi-drive support
|
|
125
|
+
*/
|
|
126
|
+
export interface IDriveConfig {
|
|
127
|
+
paths: string[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Cluster configuration for distributed mode
|
|
132
|
+
*/
|
|
133
|
+
export interface IClusterConfig {
|
|
134
|
+
enabled: boolean;
|
|
135
|
+
nodeId?: string;
|
|
136
|
+
quicPort?: number;
|
|
137
|
+
seedNodes?: string[];
|
|
138
|
+
erasure?: IErasureConfig;
|
|
139
|
+
drives?: IDriveConfig;
|
|
140
|
+
heartbeatIntervalMs?: number;
|
|
141
|
+
heartbeatTimeoutMs?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Complete objectstorage configuration
|
|
146
|
+
*/
|
|
147
|
+
export interface IObjectStorageConfig {
|
|
148
|
+
server?: IServerConfig;
|
|
149
|
+
storage?: IStorageConfig;
|
|
150
|
+
auth?: IAuthConfig;
|
|
151
|
+
cors?: ICorsConfig;
|
|
152
|
+
logging?: ILoggingConfig;
|
|
153
|
+
limits?: ILimitsConfig;
|
|
154
|
+
multipart?: IMultipartConfig;
|
|
155
|
+
cluster?: IClusterConfig;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Logical bucket stats maintained by the Rust runtime.
|
|
160
|
+
* Values are initialized from native storage on startup and updated on objectstorage mutations.
|
|
161
|
+
*/
|
|
162
|
+
export interface IBucketSummary {
|
|
163
|
+
name: string;
|
|
164
|
+
objectCount: number;
|
|
165
|
+
totalSizeBytes: number;
|
|
166
|
+
creationDate?: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Filesystem-level capacity snapshot for the storage directory or configured drive path.
|
|
171
|
+
*/
|
|
172
|
+
export interface IStorageLocationSummary {
|
|
173
|
+
path: string;
|
|
174
|
+
totalBytes?: number;
|
|
175
|
+
availableBytes?: number;
|
|
176
|
+
usedBytes?: number;
|
|
177
|
+
pool?: IStoragePoolObservation;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type TStoragePoolCheckStatus =
|
|
181
|
+
| 'passed'
|
|
182
|
+
| 'failed'
|
|
183
|
+
| 'notRun'
|
|
184
|
+
| 'notApplicable';
|
|
185
|
+
|
|
186
|
+
export interface IStoragePoolCheck {
|
|
187
|
+
status: TStoragePoolCheckStatus;
|
|
188
|
+
reason?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface IStoragePoolChecks {
|
|
192
|
+
directoryExists: IStoragePoolCheck;
|
|
193
|
+
exactMount: IStoragePoolCheck;
|
|
194
|
+
filesystemType: IStoragePoolCheck;
|
|
195
|
+
source: IStoragePoolCheck;
|
|
196
|
+
symlinkConfinement: IStoragePoolCheck;
|
|
197
|
+
remoteLocking: IStoragePoolCheck;
|
|
198
|
+
rootOwnership: IStoragePoolCheck;
|
|
199
|
+
sameFilesystem: IStoragePoolCheck;
|
|
200
|
+
lockContention: IStoragePoolCheck;
|
|
201
|
+
atomicDirectoryExchange: IStoragePoolCheck;
|
|
202
|
+
directoryFsync: IStoragePoolCheck;
|
|
203
|
+
providerIdentity: IStoragePoolCheck;
|
|
204
|
+
mountStable: IStoragePoolCheck;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface IStoragePoolFilesystemObservation {
|
|
208
|
+
filesystemType?: string;
|
|
209
|
+
source?: string;
|
|
210
|
+
mountPoint?: string;
|
|
211
|
+
mountId?: number;
|
|
212
|
+
device?: string;
|
|
213
|
+
networkBacked: boolean;
|
|
214
|
+
blockSizeBytes?: number;
|
|
215
|
+
readSizeBytes?: number;
|
|
216
|
+
writeSizeBytes?: number;
|
|
217
|
+
symlinkFollowingDisabled?: boolean;
|
|
218
|
+
remoteLockingMode?:
|
|
219
|
+
| 'serverCoordinated'
|
|
220
|
+
| 'disabled'
|
|
221
|
+
| 'localOnly'
|
|
222
|
+
| 'unrecognized'
|
|
223
|
+
| 'notApplicable';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type TStoragePoolFailureCode =
|
|
227
|
+
| 'pathUnavailable'
|
|
228
|
+
| 'mountInfoInvalid'
|
|
229
|
+
| 'exactMountRequired'
|
|
230
|
+
| 'filesystemTypeMismatch'
|
|
231
|
+
| 'sourceMismatch'
|
|
232
|
+
| 'mountChanged'
|
|
233
|
+
| 'descriptorMismatch'
|
|
234
|
+
| 'filesystemMagicMismatch'
|
|
235
|
+
| 'symlinkFollowingAllowed'
|
|
236
|
+
| 'lockingModeUnsafe'
|
|
237
|
+
| 'activationPoisoned';
|
|
238
|
+
|
|
239
|
+
export interface IStoragePoolObservation {
|
|
240
|
+
schemaVersion: 1;
|
|
241
|
+
poolId: string;
|
|
242
|
+
directory: string;
|
|
243
|
+
role: 'controlAndBlob';
|
|
244
|
+
backendKind: 'localFs' | 'mountedFs';
|
|
245
|
+
filesystem: IStoragePoolFilesystemObservation;
|
|
246
|
+
checks: IStoragePoolChecks;
|
|
247
|
+
supported: boolean | null;
|
|
248
|
+
productionEligible: boolean;
|
|
249
|
+
activationValid?: boolean;
|
|
250
|
+
poisoned?: boolean;
|
|
251
|
+
poisonReason?: string;
|
|
252
|
+
failure?: {
|
|
253
|
+
code: TStoragePoolFailureCode;
|
|
254
|
+
message: string;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Runtime storage stats served by the Rust core without issuing S3 list calls.
|
|
260
|
+
*/
|
|
261
|
+
export interface IStorageStats {
|
|
262
|
+
bucketCount: number;
|
|
263
|
+
totalObjectCount: number;
|
|
264
|
+
totalStorageBytes: number;
|
|
265
|
+
buckets: IBucketSummary[];
|
|
266
|
+
storageDirectory: string;
|
|
267
|
+
storageLocations?: IStorageLocationSummary[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface IStorageStopResult {
|
|
271
|
+
complete: true;
|
|
272
|
+
terminalError?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface IBucketTenantInput {
|
|
276
|
+
bucketName: string;
|
|
277
|
+
accessKeyId?: string;
|
|
278
|
+
secretAccessKey?: string;
|
|
279
|
+
region?: string;
|
|
280
|
+
retention?: IBucketRetentionPolicyInput;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface IEnsureBucketTenantInput {
|
|
284
|
+
bucketName: string;
|
|
285
|
+
accessKeyId: string;
|
|
286
|
+
secretAccessKey: string;
|
|
287
|
+
region?: string;
|
|
288
|
+
retention?: IBucketRetentionPolicyInput;
|
|
289
|
+
fence?: IObjectStorageResourceFence;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface IDeleteBucketTenantInput {
|
|
293
|
+
bucketName: string;
|
|
294
|
+
accessKeyId?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface IBucketTenantMetadata {
|
|
298
|
+
bucketName: string;
|
|
299
|
+
accessKeyId: string;
|
|
300
|
+
region?: string;
|
|
301
|
+
retention?: IBucketRetentionReceipt;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface IBucketRetentionPolicyInput {
|
|
305
|
+
mode: 'compliance';
|
|
306
|
+
policyId: string;
|
|
307
|
+
retentionDurationSeconds: number;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface IObjectRetentionReceipt {
|
|
311
|
+
mode: 'compliance';
|
|
312
|
+
policyId: string;
|
|
313
|
+
retentionDurationSeconds: number;
|
|
314
|
+
createdAt: number;
|
|
315
|
+
retainUntil: number;
|
|
316
|
+
payloadSha256: string;
|
|
317
|
+
metadataSha256: string;
|
|
318
|
+
etag: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface IBucketRetentionReceipt extends IBucketRetentionPolicyInput {
|
|
322
|
+
bucketName: string;
|
|
323
|
+
configuredAt: number;
|
|
324
|
+
sentinelKey: string;
|
|
325
|
+
sentinel: IObjectRetentionReceipt;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface IBucketRetentionCapability {
|
|
329
|
+
version: 1;
|
|
330
|
+
supported: boolean;
|
|
331
|
+
backend: 'standalone' | 'clustered';
|
|
332
|
+
modes: 'compliance'[];
|
|
333
|
+
atomicCreateOnly: boolean;
|
|
334
|
+
retainedMultipartSupported: false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface IObjectStorageMigrationBinding {
|
|
338
|
+
migrationId: string;
|
|
339
|
+
authorityId: string;
|
|
340
|
+
logicalBucket: string;
|
|
341
|
+
sourceProviderIdentitySha256: string;
|
|
342
|
+
destinationProviderIdentitySha256: string;
|
|
343
|
+
destinationReceiptPublicKeyEd25519: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export type TObjectStorageSourceMigrationPhase =
|
|
347
|
+
| 'active'
|
|
348
|
+
| 'sealed'
|
|
349
|
+
| 'cutoverCommitted'
|
|
350
|
+
| 'retired'
|
|
351
|
+
| 'released';
|
|
352
|
+
|
|
353
|
+
export type TObjectStorageDestinationMigrationPhase =
|
|
354
|
+
| 'holding'
|
|
355
|
+
| 'verified'
|
|
356
|
+
| 'cutoverAuthorized'
|
|
357
|
+
| 'published'
|
|
358
|
+
| 'aborted';
|
|
359
|
+
|
|
360
|
+
export type TObjectStorageMigrationPhase =
|
|
361
|
+
| { role: 'source'; phase: TObjectStorageSourceMigrationPhase }
|
|
362
|
+
| { role: 'destination'; phase: TObjectStorageDestinationMigrationPhase };
|
|
363
|
+
|
|
364
|
+
export interface IObjectStorageMigrationCreateInput {
|
|
365
|
+
binding: IObjectStorageMigrationBinding;
|
|
366
|
+
fenceToken: number;
|
|
367
|
+
opaqueCapability: string;
|
|
368
|
+
/** Required for source creation; omitted for destination creation. */
|
|
369
|
+
sourceCleanupAccessKeyId?: string;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface IObjectStorageMigrationTerminalReceipt {
|
|
373
|
+
version: 1;
|
|
374
|
+
kind: 'smartstorage.bucket-migration.terminal-receipt.standalone-v1';
|
|
375
|
+
binding: IObjectStorageMigrationBinding;
|
|
376
|
+
providerIdentitySha256: string;
|
|
377
|
+
fenceToken: number;
|
|
378
|
+
requestDigestSha256: string;
|
|
379
|
+
phase: TObjectStorageMigrationPhase;
|
|
380
|
+
manifestDigestSha256?: string;
|
|
381
|
+
counterpartTerminalReceiptSha256?: string;
|
|
382
|
+
signingPublicKeyEd25519: string;
|
|
383
|
+
signatureEd25519: string;
|
|
384
|
+
receiptSha256: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface IObjectStorageMigrationTransitionReceipt {
|
|
388
|
+
version: 1;
|
|
389
|
+
kind: 'smartstorage.bucket-migration.transition-receipt.standalone-v1';
|
|
390
|
+
binding: IObjectStorageMigrationBinding;
|
|
391
|
+
providerIdentitySha256: string;
|
|
392
|
+
fenceToken: number;
|
|
393
|
+
requestDigestSha256: string;
|
|
394
|
+
phase: TObjectStorageMigrationPhase;
|
|
395
|
+
manifestDigestSha256?: string;
|
|
396
|
+
terminalReceiptSha256?: string;
|
|
397
|
+
receiptSha256: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface IObjectStorageSourceMigrationTransitionInput {
|
|
401
|
+
binding: IObjectStorageMigrationBinding;
|
|
402
|
+
fenceToken: number;
|
|
403
|
+
opaqueCapability: string;
|
|
404
|
+
targetPhase: Exclude<TObjectStorageSourceMigrationPhase, 'active'>;
|
|
405
|
+
manifestDigestSha256?: string;
|
|
406
|
+
destinationAbortReceipt?: IObjectStorageMigrationTerminalReceipt;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface IObjectStorageDestinationMigrationTransitionInput {
|
|
410
|
+
binding: IObjectStorageMigrationBinding;
|
|
411
|
+
fenceToken: number;
|
|
412
|
+
opaqueCapability: string;
|
|
413
|
+
targetPhase: Exclude<TObjectStorageDestinationMigrationPhase, 'holding'>;
|
|
414
|
+
manifestDigestSha256?: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface IObjectStorageSourceMigrationCleanupInput {
|
|
418
|
+
binding: IObjectStorageMigrationBinding;
|
|
419
|
+
cleanupFenceToken: number;
|
|
420
|
+
opaqueCapability: string;
|
|
421
|
+
sourceRetiredReceiptSha256: string;
|
|
422
|
+
destinationPublishedReceipt: IObjectStorageMigrationTerminalReceipt;
|
|
423
|
+
resourceFence: IObjectStorageResourceFence;
|
|
424
|
+
accessKeyId: string;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface IObjectStorageSourceMigrationCleanupReceipt {
|
|
428
|
+
version: 1;
|
|
429
|
+
kind: 'smartstorage.bucket-migration.source-cleanup-receipt.standalone-v1';
|
|
430
|
+
binding: IObjectStorageMigrationBinding;
|
|
431
|
+
providerIdentitySha256: string;
|
|
432
|
+
cleanupFenceToken: number;
|
|
433
|
+
requestDigestSha256: string;
|
|
434
|
+
sourceRetiredReceiptSha256: string;
|
|
435
|
+
destinationPublishedReceiptSha256: string;
|
|
436
|
+
resourceFenceScopeId: string;
|
|
437
|
+
resourceFenceToken: number;
|
|
438
|
+
resourceFenceMutationId: string;
|
|
439
|
+
resourceFencePayloadSha256: string;
|
|
440
|
+
accessKeyIdSha256: string;
|
|
441
|
+
deleteResultSha256: string;
|
|
442
|
+
receiptSha256: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface IObjectStorageSourceMigrationCleanupInspection {
|
|
446
|
+
phase: 'requested' | 'completed';
|
|
447
|
+
cleanupFenceToken: number;
|
|
448
|
+
requestDigestSha256: string;
|
|
449
|
+
sourceRetiredReceiptSha256: string;
|
|
450
|
+
destinationPublishedReceipt: IObjectStorageMigrationTerminalReceipt;
|
|
451
|
+
resourceFence: IObjectStorageResourceFence;
|
|
452
|
+
accessKeyIdSha256: string;
|
|
453
|
+
deleteResultSha256?: string;
|
|
454
|
+
receipt?: IObjectStorageSourceMigrationCleanupReceipt;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export interface IObjectStorageMigrationInspection {
|
|
458
|
+
binding: IObjectStorageMigrationBinding;
|
|
459
|
+
providerIdentitySha256: string;
|
|
460
|
+
highestFenceToken: number;
|
|
461
|
+
requestDigestSha256: string;
|
|
462
|
+
phase: TObjectStorageMigrationPhase;
|
|
463
|
+
manifestDigestSha256?: string;
|
|
464
|
+
boundDestinationAbortReceipt?: IObjectStorageMigrationTerminalReceipt;
|
|
465
|
+
terminalReceipt?: IObjectStorageMigrationTerminalReceipt;
|
|
466
|
+
lastReceipt: IObjectStorageMigrationTransitionReceipt;
|
|
467
|
+
sourceCleanupAccessKeyIdSha256?: string;
|
|
468
|
+
sourceCleanup?: IObjectStorageSourceMigrationCleanupInspection;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export interface IObjectStorageBucketMigrationCapability {
|
|
472
|
+
version: 1;
|
|
473
|
+
supported: boolean;
|
|
474
|
+
backend: 'standalone' | 'clustered';
|
|
475
|
+
providerIdentitySha256?: string;
|
|
476
|
+
receiptPublicKeyEd25519?: string;
|
|
477
|
+
sourceSealDrainsMutations: boolean;
|
|
478
|
+
sourceReadsRemainAvailableWhileSealed: boolean;
|
|
479
|
+
destinationPublicationRequiresControllerCredentialHold: true;
|
|
480
|
+
sourceRetirementCleanupSupported: boolean;
|
|
481
|
+
sourceRetirementCleanupControllerRetryRequired: true;
|
|
482
|
+
sourceRetirementCleanupAutomaticStartupDeletion: false;
|
|
483
|
+
reason?: string;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export interface IBucketTenantDescriptor extends plugins.tsclass.storage.IS3Descriptor {
|
|
487
|
+
endpoint: string;
|
|
488
|
+
port: number;
|
|
489
|
+
region: string;
|
|
490
|
+
bucket: string;
|
|
491
|
+
bucketName: string;
|
|
492
|
+
accessKeyId: string;
|
|
493
|
+
secretAccessKey: string;
|
|
494
|
+
accessKey: string;
|
|
495
|
+
accessSecret: string;
|
|
496
|
+
useSsl: boolean;
|
|
497
|
+
ssl: boolean;
|
|
498
|
+
env: Record<string, string>;
|
|
499
|
+
/** Present when ensureBucketTenant completed under a resource fence. */
|
|
500
|
+
resourceFence?: IObjectStorageResourceFenceReceipt;
|
|
501
|
+
/** Engine-verified immutable-retention evidence when configured for the bucket. */
|
|
502
|
+
retention?: IBucketRetentionReceipt;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export interface IBucketExportObject {
|
|
506
|
+
key: string;
|
|
507
|
+
size: number;
|
|
508
|
+
md5: string;
|
|
509
|
+
metadata: Record<string, string>;
|
|
510
|
+
dataHex: string;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export interface IBucketExport {
|
|
514
|
+
format: 'smartstorage.bucket.v1';
|
|
515
|
+
bucketName: string;
|
|
516
|
+
exportedAt: number;
|
|
517
|
+
objects: IBucketExportObject[];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export interface IObjectStorageReplaceEffectivePayloadV1 {
|
|
521
|
+
version: 1;
|
|
522
|
+
kind: 'smartstorage.bucket.replace.v1';
|
|
523
|
+
bucketName: string;
|
|
524
|
+
accessKeyId?: string;
|
|
525
|
+
/** Request a durable write-publication barrier after provider replacement. */
|
|
526
|
+
holdPublication?: boolean;
|
|
527
|
+
source: IBucketExport;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export interface IObjectStorageExactDeleteEffectivePayloadV1 {
|
|
531
|
+
version: 1;
|
|
532
|
+
kind: 'smartstorage.bucket.delete.v1';
|
|
533
|
+
bucketName: string;
|
|
534
|
+
accessKeyId: string;
|
|
535
|
+
/** Request a durable write-publication barrier after provider deletion. */
|
|
536
|
+
holdPublication?: boolean;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface IObjectStorageEnsureEffectivePayloadV1 {
|
|
540
|
+
version: 1;
|
|
541
|
+
kind: 'smartstorage.bucket.ensure.v1';
|
|
542
|
+
bucketName: string;
|
|
543
|
+
accessKeyId: string;
|
|
544
|
+
/** Hashed before it is incorporated into the effective payload digest. */
|
|
545
|
+
secretAccessKey: string;
|
|
546
|
+
/** The effective region applied by the provider. */
|
|
547
|
+
region: string;
|
|
548
|
+
/** Optional immutable-retention intent applied as part of the fenced ensure. */
|
|
549
|
+
retention?: IBucketRetentionPolicyInput;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export type TObjectStorageResourceFenceEffectivePayloadV1 =
|
|
553
|
+
| IObjectStorageReplaceEffectivePayloadV1
|
|
554
|
+
| IObjectStorageExactDeleteEffectivePayloadV1
|
|
555
|
+
| IObjectStorageEnsureEffectivePayloadV1;
|
|
556
|
+
|
|
557
|
+
const resourceFenceV1Limits = {
|
|
558
|
+
maximumObjects: 10_000,
|
|
559
|
+
maximumObjectBytes: 64 * 1024 * 1024,
|
|
560
|
+
maximumImportBytes: 64 * 1024 * 1024,
|
|
561
|
+
maximumKeyBytes: 1024,
|
|
562
|
+
maximumMetadataEntries: 64,
|
|
563
|
+
maximumMetadataBytes: 8 * 1024,
|
|
564
|
+
maximumTotalKeyBytes: 1024 * 1024,
|
|
565
|
+
maximumTotalMetadataBytes: 1024 * 1024,
|
|
566
|
+
maximumIdentifierBytes: 256,
|
|
567
|
+
} as const;
|
|
568
|
+
|
|
569
|
+
const resourceFenceV1Uint64 = (valueArg: number): Buffer => {
|
|
570
|
+
if (!Number.isSafeInteger(valueArg) || valueArg < 0) {
|
|
571
|
+
throw new Error('ObjectStorage resource fence v1 integers must be non-negative safe integers.');
|
|
572
|
+
}
|
|
573
|
+
const encoded = Buffer.alloc(8);
|
|
574
|
+
encoded.writeBigUInt64BE(BigInt(valueArg));
|
|
575
|
+
return encoded;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const resourceFenceV1Utf8 = (valueArg: string, fieldArg: string): Buffer => {
|
|
579
|
+
if (typeof valueArg !== 'string') {
|
|
580
|
+
throw new Error(`${fieldArg} must be a string.`);
|
|
581
|
+
}
|
|
582
|
+
const encoded = Buffer.from(valueArg, 'utf8');
|
|
583
|
+
if (encoded.toString('utf8') !== valueArg) {
|
|
584
|
+
throw new Error(`${fieldArg} must contain valid Unicode scalar values.`);
|
|
585
|
+
}
|
|
586
|
+
return encoded;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const compareResourceFenceV1Utf8 = (leftArg: string, rightArg: string): number =>
|
|
590
|
+
Buffer.compare(Buffer.from(leftArg, 'utf8'), Buffer.from(rightArg, 'utf8'));
|
|
591
|
+
|
|
592
|
+
const assertResourceFenceV1BucketName = (bucketNameArg: string): void => {
|
|
593
|
+
const encoded = resourceFenceV1Utf8(bucketNameArg, 'bucketName');
|
|
594
|
+
const ipv4Parts = bucketNameArg.split('.');
|
|
595
|
+
const isIpv4Address = ipv4Parts.length === 4 && ipv4Parts.every((part) =>
|
|
596
|
+
/^[0-9]+$/.test(part) && Number(part) <= 255
|
|
597
|
+
);
|
|
598
|
+
if (
|
|
599
|
+
encoded.length < 3
|
|
600
|
+
|| encoded.length > 63
|
|
601
|
+
|| !/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(bucketNameArg)
|
|
602
|
+
|| bucketNameArg.includes('..')
|
|
603
|
+
|| isIpv4Address
|
|
604
|
+
) {
|
|
605
|
+
throw new Error('bucketName is not a canonical S3 bucket name.');
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const assertResourceFenceV1Identifier = (valueArg: string, fieldArg: string): void => {
|
|
610
|
+
const encoded = resourceFenceV1Utf8(valueArg, fieldArg);
|
|
611
|
+
if (
|
|
612
|
+
encoded.length === 0
|
|
613
|
+
|| encoded.length > resourceFenceV1Limits.maximumIdentifierBytes
|
|
614
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/.test(valueArg)
|
|
615
|
+
) {
|
|
616
|
+
throw new Error(`${fieldArg} is not a canonical identifier.`);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const canonicalResourceFenceV1Metadata = (
|
|
621
|
+
metadataArg: Record<string, string>,
|
|
622
|
+
objectKeyArg: string,
|
|
623
|
+
): Buffer => {
|
|
624
|
+
if (!metadataArg || typeof metadataArg !== 'object' || Array.isArray(metadataArg)) {
|
|
625
|
+
throw new Error(`Object '${objectKeyArg}' metadata must be an object.`);
|
|
626
|
+
}
|
|
627
|
+
const entries = Object.entries(metadataArg);
|
|
628
|
+
if (entries.length > resourceFenceV1Limits.maximumMetadataEntries) {
|
|
629
|
+
throw new Error(`Object '${objectKeyArg}' has too many metadata entries.`);
|
|
630
|
+
}
|
|
631
|
+
for (const [key, value] of entries) {
|
|
632
|
+
const keyBytes = resourceFenceV1Utf8(key, `Object '${objectKeyArg}' metadata key`);
|
|
633
|
+
const valueBytes = resourceFenceV1Utf8(value, `Object '${objectKeyArg}' metadata value`);
|
|
634
|
+
if (
|
|
635
|
+
keyBytes.length === 0
|
|
636
|
+
|| keyBytes.length > 128
|
|
637
|
+
|| valueBytes.length > 2048
|
|
638
|
+
|| keyBytes.some((byte) => byte <= 0x1f || byte === 0x7f)
|
|
639
|
+
|| valueBytes.some((byte) => (byte <= 0x1f && byte !== 0x09) || byte === 0x7f)
|
|
640
|
+
) {
|
|
641
|
+
throw new Error(`Object '${objectKeyArg}' metadata is invalid.`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
entries.sort(([left], [right]) => compareResourceFenceV1Utf8(left, right));
|
|
645
|
+
const encoded = Buffer.from(
|
|
646
|
+
`{${entries.map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)}`).join(',')}}`,
|
|
647
|
+
'utf8',
|
|
648
|
+
);
|
|
649
|
+
if (encoded.length > resourceFenceV1Limits.maximumMetadataBytes) {
|
|
650
|
+
throw new Error(`Object '${objectKeyArg}' metadata exceeds its size limit.`);
|
|
651
|
+
}
|
|
652
|
+
return encoded;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Computes the canonical provider-side effective payload digest for resource
|
|
657
|
+
* fencing v1. This intentionally excludes backup timestamps and source bucket
|
|
658
|
+
* names, matching the Rust engine's replacement and exact-delete profiles.
|
|
659
|
+
*/
|
|
660
|
+
export const computeObjectStorageResourceFenceEffectivePayloadSha256V1 = (
|
|
661
|
+
payloadArg: TObjectStorageResourceFenceEffectivePayloadV1,
|
|
662
|
+
): string => {
|
|
663
|
+
if (payloadArg.version !== 1) {
|
|
664
|
+
throw new Error('ObjectStorage resource fence effective payload version is unsupported.');
|
|
665
|
+
}
|
|
666
|
+
assertResourceFenceV1BucketName(payloadArg.bucketName);
|
|
667
|
+
const hash = plugins.crypto.createHash('sha256');
|
|
668
|
+
const digestField = (valueArg: string | Buffer): void => {
|
|
669
|
+
const value = typeof valueArg === 'string'
|
|
670
|
+
? resourceFenceV1Utf8(valueArg, 'resource fence digest field')
|
|
671
|
+
: valueArg;
|
|
672
|
+
hash.update(resourceFenceV1Uint64(value.length));
|
|
673
|
+
hash.update(value);
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
if (payloadArg.kind === 'smartstorage.bucket.delete.v1') {
|
|
677
|
+
assertResourceFenceV1Identifier(payloadArg.accessKeyId, 'accessKeyId');
|
|
678
|
+
digestField(payloadArg.kind);
|
|
679
|
+
digestField(payloadArg.bucketName);
|
|
680
|
+
digestField(payloadArg.accessKeyId);
|
|
681
|
+
if (payloadArg.holdPublication === true) {
|
|
682
|
+
digestField('smartstorage.resource-fence.hold-publication.v1');
|
|
683
|
+
digestField(Buffer.from([1]));
|
|
684
|
+
}
|
|
685
|
+
return hash.digest('hex');
|
|
686
|
+
}
|
|
687
|
+
if (payloadArg.kind === 'smartstorage.bucket.ensure.v1') {
|
|
688
|
+
assertResourceFenceV1Identifier(payloadArg.accessKeyId, 'accessKeyId');
|
|
689
|
+
const secretAccessKey = resourceFenceV1Utf8(
|
|
690
|
+
payloadArg.secretAccessKey,
|
|
691
|
+
'secretAccessKey',
|
|
692
|
+
);
|
|
693
|
+
if (!payloadArg.secretAccessKey.trim()) {
|
|
694
|
+
throw new Error('secretAccessKey must not be empty.');
|
|
695
|
+
}
|
|
696
|
+
const region = resourceFenceV1Utf8(payloadArg.region, 'region');
|
|
697
|
+
if (!payloadArg.region.trim()) {
|
|
698
|
+
throw new Error('region must not be empty.');
|
|
699
|
+
}
|
|
700
|
+
digestField(payloadArg.kind);
|
|
701
|
+
digestField(payloadArg.bucketName);
|
|
702
|
+
digestField(payloadArg.accessKeyId);
|
|
703
|
+
digestField(plugins.crypto.createHash('sha256').update(secretAccessKey).digest('hex'));
|
|
704
|
+
digestField(region);
|
|
705
|
+
if (payloadArg.retention) {
|
|
706
|
+
if (
|
|
707
|
+
payloadArg.retention.mode !== 'compliance'
|
|
708
|
+
|| !/^[A-Za-z0-9._-]{1,96}$/.test(payloadArg.retention.policyId)
|
|
709
|
+
|| !Number.isSafeInteger(payloadArg.retention.retentionDurationSeconds)
|
|
710
|
+
|| payloadArg.retention.retentionDurationSeconds <= 0
|
|
711
|
+
|| payloadArg.retention.retentionDurationSeconds > 100 * 366 * 24 * 60 * 60
|
|
712
|
+
) {
|
|
713
|
+
throw new Error('retention must contain canonical compliance-retention intent.');
|
|
714
|
+
}
|
|
715
|
+
digestField('smartstorage.bucket.retention.v1');
|
|
716
|
+
digestField(payloadArg.retention.mode);
|
|
717
|
+
digestField(payloadArg.retention.policyId);
|
|
718
|
+
digestField(resourceFenceV1Uint64(payloadArg.retention.retentionDurationSeconds));
|
|
719
|
+
}
|
|
720
|
+
return hash.digest('hex');
|
|
721
|
+
}
|
|
722
|
+
if (payloadArg.kind !== 'smartstorage.bucket.replace.v1') {
|
|
723
|
+
throw new Error('ObjectStorage resource fence effective payload kind is unsupported.');
|
|
724
|
+
}
|
|
725
|
+
if (payloadArg.accessKeyId !== undefined) {
|
|
726
|
+
assertResourceFenceV1Identifier(payloadArg.accessKeyId, 'accessKeyId');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const source = payloadArg.source;
|
|
730
|
+
if (source.format !== 'smartstorage.bucket.v1') {
|
|
731
|
+
throw new Error('source format must be smartstorage.bucket.v1.');
|
|
732
|
+
}
|
|
733
|
+
assertResourceFenceV1BucketName(source.bucketName);
|
|
734
|
+
if (!Number.isSafeInteger(source.exportedAt) || source.exportedAt < 0) {
|
|
735
|
+
throw new Error('source exportedAt must be a non-negative safe integer.');
|
|
736
|
+
}
|
|
737
|
+
if (!Array.isArray(source.objects) || source.objects.length > resourceFenceV1Limits.maximumObjects) {
|
|
738
|
+
throw new Error(`source exceeds the ${resourceFenceV1Limits.maximumObjects}-object limit.`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const logicalKeys = new Set<string>();
|
|
742
|
+
let totalBytes = 0;
|
|
743
|
+
let totalKeyBytes = 0;
|
|
744
|
+
let totalMetadataBytes = 0;
|
|
745
|
+
const prepared = source.objects.map((object) => {
|
|
746
|
+
const keyBytes = resourceFenceV1Utf8(object.key, 'object key');
|
|
747
|
+
if (
|
|
748
|
+
keyBytes.length === 0
|
|
749
|
+
|| keyBytes.length > resourceFenceV1Limits.maximumKeyBytes
|
|
750
|
+
) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`Object key '${object.key}' must contain between 1 and ${resourceFenceV1Limits.maximumKeyBytes} UTF-8 bytes.`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (logicalKeys.has(object.key)) {
|
|
756
|
+
throw new Error(`source contains duplicate object key '${object.key}'.`);
|
|
757
|
+
}
|
|
758
|
+
logicalKeys.add(object.key);
|
|
759
|
+
totalKeyBytes += keyBytes.length;
|
|
760
|
+
if (totalKeyBytes > resourceFenceV1Limits.maximumTotalKeyBytes) {
|
|
761
|
+
throw new Error(`source exceeds the ${resourceFenceV1Limits.maximumTotalKeyBytes}-byte aggregate key limit.`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (
|
|
765
|
+
!Number.isSafeInteger(object.size)
|
|
766
|
+
|| object.size < 0
|
|
767
|
+
|| object.size > resourceFenceV1Limits.maximumObjectBytes
|
|
768
|
+
|| object.dataHex.length !== object.size * 2
|
|
769
|
+
|| !/^[0-9a-f]*$/.test(object.dataHex)
|
|
770
|
+
) {
|
|
771
|
+
throw new Error(`Object '${object.key}' dataHex or size is not canonical.`);
|
|
772
|
+
}
|
|
773
|
+
const data = Buffer.from(object.dataHex, 'hex');
|
|
774
|
+
if (data.length !== object.size) {
|
|
775
|
+
throw new Error(`Object '${object.key}' size does not match dataHex.`);
|
|
776
|
+
}
|
|
777
|
+
if (!/^[0-9a-f]{32}$/.test(object.md5)) {
|
|
778
|
+
throw new Error(`Object '${object.key}' md5 is not canonical.`);
|
|
779
|
+
}
|
|
780
|
+
const actualMd5 = plugins.crypto.createHash('md5').update(data).digest('hex');
|
|
781
|
+
if (actualMd5 !== object.md5) {
|
|
782
|
+
throw new Error(`Object '${object.key}' md5 does not match payload.`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const metadata = canonicalResourceFenceV1Metadata(object.metadata, object.key);
|
|
786
|
+
totalMetadataBytes += metadata.length;
|
|
787
|
+
if (totalMetadataBytes > resourceFenceV1Limits.maximumTotalMetadataBytes) {
|
|
788
|
+
throw new Error(`source exceeds the ${resourceFenceV1Limits.maximumTotalMetadataBytes}-byte aggregate metadata limit.`);
|
|
789
|
+
}
|
|
790
|
+
totalBytes += object.size;
|
|
791
|
+
if (totalBytes > resourceFenceV1Limits.maximumImportBytes) {
|
|
792
|
+
throw new Error(`source exceeds the ${resourceFenceV1Limits.maximumImportBytes}-byte import limit.`);
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
key: object.key,
|
|
796
|
+
size: object.size,
|
|
797
|
+
md5: object.md5,
|
|
798
|
+
metadata,
|
|
799
|
+
dataSha256: plugins.crypto.createHash('sha256').update(data).digest('hex'),
|
|
800
|
+
};
|
|
801
|
+
});
|
|
802
|
+
prepared.sort((left, right) => compareResourceFenceV1Utf8(left.key, right.key));
|
|
803
|
+
|
|
804
|
+
digestField(payloadArg.kind);
|
|
805
|
+
digestField(payloadArg.bucketName);
|
|
806
|
+
if (payloadArg.accessKeyId !== undefined) {
|
|
807
|
+
digestField(payloadArg.accessKeyId);
|
|
808
|
+
}
|
|
809
|
+
digestField(resourceFenceV1Uint64(prepared.length));
|
|
810
|
+
for (const object of prepared) {
|
|
811
|
+
digestField(object.key);
|
|
812
|
+
digestField(resourceFenceV1Uint64(object.size));
|
|
813
|
+
digestField(object.md5);
|
|
814
|
+
digestField(object.metadata);
|
|
815
|
+
digestField(object.dataSha256);
|
|
816
|
+
}
|
|
817
|
+
if (payloadArg.holdPublication === true) {
|
|
818
|
+
digestField('smartstorage.resource-fence.hold-publication.v1');
|
|
819
|
+
digestField(Buffer.from([1]));
|
|
820
|
+
}
|
|
821
|
+
return hash.digest('hex');
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
export interface IExportBucketInput {
|
|
825
|
+
bucketName: string;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export interface IImportBucketInput {
|
|
829
|
+
bucketName: string;
|
|
830
|
+
source: IBucketExport;
|
|
831
|
+
/** Exact exclusive bucket credential required by the fenced replacement. */
|
|
832
|
+
accessKeyId?: string;
|
|
833
|
+
fence?: IObjectStorageResourceFence;
|
|
834
|
+
/** Keep target writes closed until commitResourcePublication succeeds. */
|
|
835
|
+
holdPublication?: boolean;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export interface IDeleteBucketInput {
|
|
839
|
+
bucketName: string;
|
|
840
|
+
/**
|
|
841
|
+
* Exact bucket-scoped credential to revoke. When supplied, the access key
|
|
842
|
+
* is included in the durable mutation identity and retries treat an already
|
|
843
|
+
* absent key as a successful idempotent revocation. Omitting it retains the
|
|
844
|
+
* legacy data-only deletion behavior that preserves credentials and policy.
|
|
845
|
+
*/
|
|
846
|
+
accessKeyId?: string;
|
|
847
|
+
fence: IObjectStorageResourceFence;
|
|
848
|
+
/** Keep target writes closed until commitResourcePublication succeeds. */
|
|
849
|
+
holdPublication?: boolean;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export interface IObjectStorageResourceFence {
|
|
853
|
+
version: 1;
|
|
854
|
+
scopeId: string;
|
|
855
|
+
token: number;
|
|
856
|
+
mutationId: string;
|
|
857
|
+
payloadSha256: string;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export interface IObjectStorageResourceFenceReceipt
|
|
861
|
+
extends IObjectStorageResourceFence {
|
|
862
|
+
kind:
|
|
863
|
+
| 'smartstorage.bucket.replace.v1'
|
|
864
|
+
| 'smartstorage.bucket.delete.v1'
|
|
865
|
+
| 'smartstorage.bucket.ensure.v1';
|
|
866
|
+
effectivePayloadSha256: string;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export interface IObjectStorageResourcePublicationBarrier {
|
|
870
|
+
version: 1;
|
|
871
|
+
kind: 'smartstorage.bucket.write-publication.v1';
|
|
872
|
+
providerRootIdentitySha256: string;
|
|
873
|
+
bucketName: string;
|
|
874
|
+
resourceIdSha256: string;
|
|
875
|
+
scopeId: string;
|
|
876
|
+
token: number;
|
|
877
|
+
mutationId: string;
|
|
878
|
+
mutationKind: 'smartstorage.bucket.replace.v1' | 'smartstorage.bucket.delete.v1';
|
|
879
|
+
payloadSha256: string;
|
|
880
|
+
effectivePayloadSha256: string;
|
|
881
|
+
mutationReceiptSha256: string;
|
|
882
|
+
/** Opaque CSPRNG capability. Never log or persist outside protected control state. */
|
|
883
|
+
commitCapability: string;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export interface IObjectStorageResourcePublicationReleaseReceipt {
|
|
887
|
+
version: 1;
|
|
888
|
+
kind: 'smartstorage.bucket.write-publication.release.v1';
|
|
889
|
+
providerRootIdentitySha256: string;
|
|
890
|
+
bucketName: string;
|
|
891
|
+
resourceIdSha256: string;
|
|
892
|
+
scopeId: string;
|
|
893
|
+
token: number;
|
|
894
|
+
mutationId: string;
|
|
895
|
+
mutationKind: 'smartstorage.bucket.replace.v1' | 'smartstorage.bucket.delete.v1';
|
|
896
|
+
payloadSha256: string;
|
|
897
|
+
effectivePayloadSha256: string;
|
|
898
|
+
mutationReceiptSha256: string;
|
|
899
|
+
barrierSha256: string;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export interface ICommitObjectStorageResourcePublicationInput {
|
|
903
|
+
barrier: IObjectStorageResourcePublicationBarrier;
|
|
904
|
+
resourceFenceReceipt: IObjectStorageResourceFenceReceipt;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export interface IObjectStorageBucketMutationResult {
|
|
908
|
+
bucketName: string;
|
|
909
|
+
objects?: number;
|
|
910
|
+
totalBytes?: number;
|
|
911
|
+
bucketDeleted?: boolean;
|
|
912
|
+
/**
|
|
913
|
+
* Credentials removed during the completing attempt. A crash-recovery
|
|
914
|
+
* retry can report zero after the exact credential was already persisted
|
|
915
|
+
* as absent.
|
|
916
|
+
*/
|
|
917
|
+
deletedCredentials?: number;
|
|
918
|
+
credentialsPreserved?: boolean;
|
|
919
|
+
policyPreserved?: boolean;
|
|
920
|
+
resourceFence?: IObjectStorageResourceFenceReceipt;
|
|
921
|
+
resourcePublicationBarrier?: IObjectStorageResourcePublicationBarrier;
|
|
922
|
+
resourcePublicationRelease?: IObjectStorageResourcePublicationReleaseReceipt;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export interface IObjectStorageResourceFencingCapability {
|
|
926
|
+
version: 1;
|
|
927
|
+
supported: boolean;
|
|
928
|
+
requiresDrain: true;
|
|
929
|
+
publicationHoldVersion: 1;
|
|
930
|
+
publicationHoldSupported: boolean;
|
|
931
|
+
publicationHoldRequiresExternalDrain: false;
|
|
932
|
+
reason?: string;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export interface IObjectStorageHealth {
|
|
936
|
+
ok: boolean;
|
|
937
|
+
running: boolean;
|
|
938
|
+
storageDirectory: string;
|
|
939
|
+
storageLocations?: IStorageLocationSummary[];
|
|
940
|
+
auth: {
|
|
941
|
+
enabled: boolean;
|
|
942
|
+
credentialCount: number;
|
|
943
|
+
tenantCredentialCount: number;
|
|
944
|
+
};
|
|
945
|
+
bucketCount: number;
|
|
946
|
+
objectCount: number;
|
|
947
|
+
totalBytes: number;
|
|
948
|
+
cluster: IClusterHealth;
|
|
949
|
+
resourceFencingVersion: 1;
|
|
950
|
+
resourceFencingSupported: boolean;
|
|
951
|
+
resourceFencingRequiresDrain: true;
|
|
952
|
+
resourcePublicationHoldVersion: 1;
|
|
953
|
+
resourcePublicationHoldSupported: boolean;
|
|
954
|
+
resourcePublicationHoldRequiresExternalDrain: false;
|
|
955
|
+
resourceFencingReason?: string;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export const objectStorageResourceFenceErrorCodes = [
|
|
959
|
+
'EFENCE_REQUIRED',
|
|
960
|
+
'EFENCE_STALE',
|
|
961
|
+
'EFENCE_SCOPE_MISMATCH',
|
|
962
|
+
'EFENCE_UNSUPPORTED',
|
|
963
|
+
'EFENCE_MUTATION_MISMATCH',
|
|
964
|
+
'EFENCE_STATE_CORRUPT',
|
|
965
|
+
'EFENCE_RECEIPT_LIMIT',
|
|
966
|
+
'EFENCE_BUSY',
|
|
967
|
+
'EFENCE_OWNERSHIP_MISMATCH',
|
|
968
|
+
'EFENCE_PUBLICATION_HELD',
|
|
969
|
+
'EFENCE_PUBLICATION_MISMATCH',
|
|
970
|
+
'EFENCE_PUBLICATION_CAPABILITY',
|
|
971
|
+
] as const;
|
|
972
|
+
|
|
973
|
+
export type TObjectStorageResourceFenceErrorCode =
|
|
974
|
+
(typeof objectStorageResourceFenceErrorCodes)[number];
|
|
975
|
+
|
|
976
|
+
export class ObjectStorageResourceFenceError extends Error {
|
|
977
|
+
public readonly code: TObjectStorageResourceFenceErrorCode;
|
|
978
|
+
|
|
979
|
+
public constructor(
|
|
980
|
+
codeArg: TObjectStorageResourceFenceErrorCode,
|
|
981
|
+
messageArg: string,
|
|
982
|
+
causeArg?: unknown,
|
|
983
|
+
) {
|
|
984
|
+
super(`${codeArg}: ${messageArg}`, { cause: causeArg });
|
|
985
|
+
this.name = 'ObjectStorageResourceFenceError';
|
|
986
|
+
this.code = codeArg;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const normalizeResourceFenceError = (errorArg: unknown): Error => {
|
|
991
|
+
const error = errorArg instanceof Error ? errorArg : new Error(String(errorArg));
|
|
992
|
+
const match = /(EFENCE_[A-Z_]+):\s*(.*)$/s.exec(error.message);
|
|
993
|
+
if (
|
|
994
|
+
!match ||
|
|
995
|
+
!objectStorageResourceFenceErrorCodes.includes(
|
|
996
|
+
match[1] as TObjectStorageResourceFenceErrorCode,
|
|
997
|
+
)
|
|
998
|
+
) {
|
|
999
|
+
return error;
|
|
1000
|
+
}
|
|
1001
|
+
return new ObjectStorageResourceFenceError(
|
|
1002
|
+
match[1] as TObjectStorageResourceFenceErrorCode,
|
|
1003
|
+
match[2],
|
|
1004
|
+
error,
|
|
1005
|
+
);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
export interface IObjectStorageMetrics {
|
|
1009
|
+
bucketCount: number;
|
|
1010
|
+
objectCount: number;
|
|
1011
|
+
totalBytes: number;
|
|
1012
|
+
authCredentialCount: number;
|
|
1013
|
+
tenantCredentialCount: number;
|
|
1014
|
+
clusterEnabled: boolean;
|
|
1015
|
+
prometheusText: string;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Known peer status from the local node's current cluster view.
|
|
1020
|
+
*/
|
|
1021
|
+
export interface IClusterPeerHealth {
|
|
1022
|
+
nodeId: string;
|
|
1023
|
+
status: 'online' | 'suspect' | 'offline';
|
|
1024
|
+
quicAddress?: string;
|
|
1025
|
+
s3Address?: string;
|
|
1026
|
+
driveCount?: number;
|
|
1027
|
+
lastHeartbeat?: number;
|
|
1028
|
+
missedHeartbeats?: number;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Local drive health as measured by objectstorage's runtime probes.
|
|
1033
|
+
*/
|
|
1034
|
+
export interface IClusterDriveHealth {
|
|
1035
|
+
index: number;
|
|
1036
|
+
path: string;
|
|
1037
|
+
status: 'online' | 'degraded' | 'offline' | 'healing';
|
|
1038
|
+
totalBytes?: number;
|
|
1039
|
+
usedBytes?: number;
|
|
1040
|
+
availableBytes?: number;
|
|
1041
|
+
errorCount?: number;
|
|
1042
|
+
lastError?: string;
|
|
1043
|
+
lastCheck?: number;
|
|
1044
|
+
erasureSetId?: number;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export interface IClusterErasureHealth {
|
|
1048
|
+
dataShards: number;
|
|
1049
|
+
parityShards: number;
|
|
1050
|
+
chunkSizeBytes: number;
|
|
1051
|
+
totalShards: number;
|
|
1052
|
+
readQuorum: number;
|
|
1053
|
+
writeQuorum: number;
|
|
1054
|
+
erasureSetCount: number;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export interface IClusterRepairHealth {
|
|
1058
|
+
active: boolean;
|
|
1059
|
+
scanIntervalMs?: number;
|
|
1060
|
+
lastRunStartedAt?: number;
|
|
1061
|
+
lastRunCompletedAt?: number;
|
|
1062
|
+
lastDurationMs?: number;
|
|
1063
|
+
shardsChecked?: number;
|
|
1064
|
+
shardsHealed?: number;
|
|
1065
|
+
failed?: number;
|
|
1066
|
+
lastError?: string;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Cluster runtime health from the Rust core.
|
|
1071
|
+
* When clustering is disabled, the response is `{ enabled: false }`.
|
|
1072
|
+
*/
|
|
1073
|
+
export interface IClusterHealth {
|
|
1074
|
+
enabled: boolean;
|
|
1075
|
+
nodeId?: string;
|
|
1076
|
+
quorumHealthy?: boolean;
|
|
1077
|
+
majorityHealthy?: boolean;
|
|
1078
|
+
peers?: IClusterPeerHealth[];
|
|
1079
|
+
drives?: IClusterDriveHealth[];
|
|
1080
|
+
erasure?: IClusterErasureHealth;
|
|
1081
|
+
repairs?: IClusterRepairHealth;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Default configuration values
|
|
1086
|
+
*/
|
|
1087
|
+
const DEFAULT_CONFIG: IObjectStorageConfig = {
|
|
1088
|
+
server: {
|
|
1089
|
+
port: 3000,
|
|
1090
|
+
address: '0.0.0.0',
|
|
1091
|
+
silent: false,
|
|
1092
|
+
region: 'us-east-1',
|
|
1093
|
+
},
|
|
1094
|
+
storage: {
|
|
1095
|
+
directory: paths.bucketsDir,
|
|
1096
|
+
cleanSlate: false,
|
|
1097
|
+
},
|
|
1098
|
+
auth: {
|
|
1099
|
+
enabled: false,
|
|
1100
|
+
credentials: [
|
|
1101
|
+
{
|
|
1102
|
+
accessKeyId: 'STORAGE',
|
|
1103
|
+
secretAccessKey: 'STORAGE',
|
|
1104
|
+
},
|
|
1105
|
+
],
|
|
1106
|
+
},
|
|
1107
|
+
cors: {
|
|
1108
|
+
enabled: false,
|
|
1109
|
+
allowedOrigins: ['*'],
|
|
1110
|
+
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
1111
|
+
allowedHeaders: ['*'],
|
|
1112
|
+
exposedHeaders: ['ETag', 'x-amz-request-id', 'x-amz-version-id'],
|
|
1113
|
+
maxAge: 86400,
|
|
1114
|
+
allowCredentials: false,
|
|
1115
|
+
},
|
|
1116
|
+
logging: {
|
|
1117
|
+
level: 'info',
|
|
1118
|
+
format: 'text',
|
|
1119
|
+
enabled: true,
|
|
1120
|
+
},
|
|
1121
|
+
limits: {
|
|
1122
|
+
maxObjectSize: 5 * 1024 * 1024 * 1024, // 5GB
|
|
1123
|
+
maxMetadataSize: 2048,
|
|
1124
|
+
requestTimeout: 300000, // 5 minutes
|
|
1125
|
+
},
|
|
1126
|
+
multipart: {
|
|
1127
|
+
expirationDays: 7,
|
|
1128
|
+
cleanupIntervalMinutes: 60,
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
// A fenced import may contain 64 MiB of raw object data encoded as hexadecimal.
|
|
1133
|
+
// Keep this aligned with the bounded Rust management-line reader.
|
|
1134
|
+
const MAXIMUM_MANAGEMENT_PAYLOAD_BYTES = 144 * 1024 * 1024;
|
|
1135
|
+
|
|
1136
|
+
function assertExactMountedDirectory(directoryArg: string, fieldArg: string): void {
|
|
1137
|
+
if (
|
|
1138
|
+
directoryArg.length === 0 ||
|
|
1139
|
+
!plugins.path.isAbsolute(directoryArg) ||
|
|
1140
|
+
plugins.path.normalize(directoryArg) !== directoryArg ||
|
|
1141
|
+
plugins.path.resolve(directoryArg) !== directoryArg
|
|
1142
|
+
) {
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`${fieldArg} must be a non-empty absolute lexically exact mountpoint without '.', '..', duplicate separators, or a trailing separator.`,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Merge user config with defaults (deep merge)
|
|
1151
|
+
*/
|
|
1152
|
+
function mergeConfig(userConfig: IObjectStorageConfig): Required<IObjectStorageConfig> {
|
|
1153
|
+
const userStorage = userConfig.storage || {};
|
|
1154
|
+
const defaultDirectory = DEFAULT_CONFIG.storage!.directory!;
|
|
1155
|
+
const legacyDirectory = userStorage.directory;
|
|
1156
|
+
const configuredPool = userStorage.pool;
|
|
1157
|
+
const configuredMountedPool = configuredPool?.backend.kind === 'mountedFs';
|
|
1158
|
+
|
|
1159
|
+
if (configuredMountedPool) {
|
|
1160
|
+
assertExactMountedDirectory(
|
|
1161
|
+
configuredPool.directory,
|
|
1162
|
+
'storage.pool.directory',
|
|
1163
|
+
);
|
|
1164
|
+
if (legacyDirectory !== undefined) {
|
|
1165
|
+
assertExactMountedDirectory(legacyDirectory, 'storage.directory');
|
|
1166
|
+
if (legacyDirectory !== configuredPool.directory) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
'storage.directory and storage.pool.directory must be the same exact mountedFs path.',
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const effectiveDirectory =
|
|
1175
|
+
configuredPool?.directory || legacyDirectory || defaultDirectory;
|
|
1176
|
+
|
|
1177
|
+
if (
|
|
1178
|
+
configuredPool &&
|
|
1179
|
+
!configuredMountedPool &&
|
|
1180
|
+
legacyDirectory &&
|
|
1181
|
+
plugins.path.resolve(configuredPool.directory) !==
|
|
1182
|
+
plugins.path.resolve(legacyDirectory)
|
|
1183
|
+
) {
|
|
1184
|
+
throw new Error(
|
|
1185
|
+
'storage.directory and storage.pool.directory must resolve to the same path.',
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const pool: IStoragePoolConfig = configuredPool
|
|
1190
|
+
? {
|
|
1191
|
+
...configuredPool,
|
|
1192
|
+
directory: plugins.path.resolve(effectiveDirectory),
|
|
1193
|
+
backend: { ...configuredPool.backend },
|
|
1194
|
+
}
|
|
1195
|
+
: {
|
|
1196
|
+
id: 'default',
|
|
1197
|
+
directory: plugins.path.resolve(effectiveDirectory),
|
|
1198
|
+
backend: { kind: 'localFs' },
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(pool.id)) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
'storage.pool.id must start with a lowercase letter and contain at most 63 lowercase letters, digits, or hyphens.',
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
if (
|
|
1207
|
+
pool.backend.kind === 'mountedFs' &&
|
|
1208
|
+
pool.backend.expectedSource.trim().length === 0
|
|
1209
|
+
) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
'storage.pool.backend.expectedSource must not be empty for mountedFs.',
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
if (pool.backend.kind === 'mountedFs' && userStorage.cleanSlate) {
|
|
1215
|
+
throw new Error('storage.cleanSlate is not permitted for mountedFs pools.');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return {
|
|
1219
|
+
server: {
|
|
1220
|
+
...DEFAULT_CONFIG.server!,
|
|
1221
|
+
...(userConfig.server || {}),
|
|
1222
|
+
},
|
|
1223
|
+
storage: {
|
|
1224
|
+
...DEFAULT_CONFIG.storage!,
|
|
1225
|
+
...userStorage,
|
|
1226
|
+
directory: pool.directory,
|
|
1227
|
+
pool,
|
|
1228
|
+
},
|
|
1229
|
+
auth: {
|
|
1230
|
+
...DEFAULT_CONFIG.auth!,
|
|
1231
|
+
...(userConfig.auth || {}),
|
|
1232
|
+
},
|
|
1233
|
+
cors: {
|
|
1234
|
+
...DEFAULT_CONFIG.cors!,
|
|
1235
|
+
...(userConfig.cors || {}),
|
|
1236
|
+
},
|
|
1237
|
+
logging: {
|
|
1238
|
+
...DEFAULT_CONFIG.logging!,
|
|
1239
|
+
...(userConfig.logging || {}),
|
|
1240
|
+
},
|
|
1241
|
+
limits: {
|
|
1242
|
+
...DEFAULT_CONFIG.limits!,
|
|
1243
|
+
...(userConfig.limits || {}),
|
|
1244
|
+
},
|
|
1245
|
+
multipart: {
|
|
1246
|
+
...DEFAULT_CONFIG.multipart!,
|
|
1247
|
+
...(userConfig.multipart || {}),
|
|
1248
|
+
},
|
|
1249
|
+
...(userConfig.cluster ? { cluster: userConfig.cluster } : {}),
|
|
1250
|
+
} as Required<IObjectStorageConfig>;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function createAccessKeyId(): string {
|
|
1254
|
+
return `SS${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function createSecretAccessKey(): string {
|
|
1258
|
+
return plugins.crypto.randomBytes(32).toString('hex');
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function getRustStorageBinaryNames(): string[] {
|
|
1262
|
+
const platformTargetMap: Record<string, string> = {
|
|
1263
|
+
'linux:x64': 'linux_amd64',
|
|
1264
|
+
'linux:arm64': 'linux_arm64',
|
|
1265
|
+
'darwin:x64': 'macos_amd64',
|
|
1266
|
+
'darwin:arm64': 'macos_arm64',
|
|
1267
|
+
};
|
|
1268
|
+
const platformTarget = platformTargetMap[`${process.platform}:${process.arch}`];
|
|
1269
|
+
return [platformTarget ? `ruststorage_${platformTarget}` : null, 'ruststorage']
|
|
1270
|
+
.filter((binaryNameArg): binaryNameArg is string => Boolean(binaryNameArg));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* IPC command type map for RustBridge
|
|
1275
|
+
*/
|
|
1276
|
+
type TRustStorageCommands = {
|
|
1277
|
+
probeStoragePool: {
|
|
1278
|
+
params: { pool: IMountedStoragePoolConfig };
|
|
1279
|
+
result: IStoragePoolObservation;
|
|
1280
|
+
};
|
|
1281
|
+
getStoragePoolDiagnostic: {
|
|
1282
|
+
params: {};
|
|
1283
|
+
result: IStoragePoolObservation | null;
|
|
1284
|
+
};
|
|
1285
|
+
start: { params: { config: Required<IObjectStorageConfig> }; result: {} };
|
|
1286
|
+
stop: { params: {}; result: IStorageStopResult };
|
|
1287
|
+
createBucket: { params: { name: string }; result: {} };
|
|
1288
|
+
createBucketTenant: {
|
|
1289
|
+
params: {
|
|
1290
|
+
bucketName: string;
|
|
1291
|
+
accessKeyId: string;
|
|
1292
|
+
secretAccessKey: string;
|
|
1293
|
+
region?: string;
|
|
1294
|
+
retention?: IBucketRetentionPolicyInput;
|
|
1295
|
+
};
|
|
1296
|
+
result: IStorageCredential & { retention?: IBucketRetentionReceipt };
|
|
1297
|
+
};
|
|
1298
|
+
ensureBucketTenant: {
|
|
1299
|
+
params: IEnsureBucketTenantInput;
|
|
1300
|
+
result: IStorageCredential & {
|
|
1301
|
+
resourceFence?: IObjectStorageResourceFenceReceipt;
|
|
1302
|
+
retention?: IBucketRetentionReceipt;
|
|
1303
|
+
};
|
|
1304
|
+
};
|
|
1305
|
+
deleteBucketTenant: {
|
|
1306
|
+
params: { bucketName: string; accessKeyId?: string };
|
|
1307
|
+
result: {};
|
|
1308
|
+
};
|
|
1309
|
+
rotateBucketTenantCredentials: {
|
|
1310
|
+
params: {
|
|
1311
|
+
bucketName: string;
|
|
1312
|
+
accessKeyId: string;
|
|
1313
|
+
secretAccessKey: string;
|
|
1314
|
+
region?: string;
|
|
1315
|
+
};
|
|
1316
|
+
result: IStorageCredential & { retention?: IBucketRetentionReceipt };
|
|
1317
|
+
};
|
|
1318
|
+
listBucketTenants: { params: {}; result: IBucketTenantMetadata[] };
|
|
1319
|
+
getBucketTenantCredential: {
|
|
1320
|
+
params: { bucketName: string };
|
|
1321
|
+
result: IStorageCredential & { retention?: IBucketRetentionReceipt };
|
|
1322
|
+
};
|
|
1323
|
+
getBucketRetentionCapability: { params: {}; result: IBucketRetentionCapability };
|
|
1324
|
+
getBucketRetentionReceipt: {
|
|
1325
|
+
params: { bucketName: string };
|
|
1326
|
+
result: IBucketRetentionReceipt | null;
|
|
1327
|
+
};
|
|
1328
|
+
getBucketMigrationCapability: {
|
|
1329
|
+
params: {};
|
|
1330
|
+
result: IObjectStorageBucketMigrationCapability;
|
|
1331
|
+
};
|
|
1332
|
+
createSourceMigration: {
|
|
1333
|
+
params: IObjectStorageMigrationCreateInput;
|
|
1334
|
+
result: IObjectStorageMigrationTransitionReceipt;
|
|
1335
|
+
};
|
|
1336
|
+
createDestinationMigration: {
|
|
1337
|
+
params: IObjectStorageMigrationCreateInput;
|
|
1338
|
+
result: IObjectStorageMigrationTransitionReceipt;
|
|
1339
|
+
};
|
|
1340
|
+
transitionSourceMigration: {
|
|
1341
|
+
params: IObjectStorageSourceMigrationTransitionInput;
|
|
1342
|
+
result: IObjectStorageMigrationTransitionReceipt;
|
|
1343
|
+
};
|
|
1344
|
+
transitionDestinationMigration: {
|
|
1345
|
+
params: IObjectStorageDestinationMigrationTransitionInput;
|
|
1346
|
+
result: IObjectStorageMigrationTransitionReceipt;
|
|
1347
|
+
};
|
|
1348
|
+
cleanupSourceMigration: {
|
|
1349
|
+
params: IObjectStorageSourceMigrationCleanupInput;
|
|
1350
|
+
result: IObjectStorageSourceMigrationCleanupReceipt;
|
|
1351
|
+
};
|
|
1352
|
+
inspectBucketMigration: {
|
|
1353
|
+
params: { migrationId: string };
|
|
1354
|
+
result: IObjectStorageMigrationInspection | null;
|
|
1355
|
+
};
|
|
1356
|
+
exportBucket: { params: { bucketName: string }; result: IBucketExport };
|
|
1357
|
+
importBucket: {
|
|
1358
|
+
params: IImportBucketInput;
|
|
1359
|
+
result: IObjectStorageBucketMutationResult;
|
|
1360
|
+
};
|
|
1361
|
+
deleteBucket: {
|
|
1362
|
+
params: IDeleteBucketInput;
|
|
1363
|
+
result: IObjectStorageBucketMutationResult;
|
|
1364
|
+
};
|
|
1365
|
+
commitResourcePublication: {
|
|
1366
|
+
params: ICommitObjectStorageResourcePublicationInput;
|
|
1367
|
+
result: IObjectStorageResourcePublicationReleaseReceipt;
|
|
1368
|
+
};
|
|
1369
|
+
getResourceFencingCapability: {
|
|
1370
|
+
params: {};
|
|
1371
|
+
result: IObjectStorageResourceFencingCapability;
|
|
1372
|
+
};
|
|
1373
|
+
getStorageStats: { params: {}; result: IStorageStats };
|
|
1374
|
+
listBucketSummaries: { params: {}; result: IBucketSummary[] };
|
|
1375
|
+
listCredentials: { params: {}; result: IStorageCredentialMetadata[] };
|
|
1376
|
+
replaceCredentials: { params: { credentials: IStorageCredential[] }; result: {} };
|
|
1377
|
+
getClusterHealth: { params: {}; result: IClusterHealth };
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Main ObjectStorage class - production-ready S3-compatible storage server
|
|
1382
|
+
*/
|
|
1383
|
+
export class ObjectStorage {
|
|
1384
|
+
// STATIC
|
|
1385
|
+
public static async createAndStart(configArg: IObjectStorageConfig = {}) {
|
|
1386
|
+
const objectStorageInstance = new ObjectStorage(configArg);
|
|
1387
|
+
await objectStorageInstance.start();
|
|
1388
|
+
return objectStorageInstance;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
public static async probeStoragePool(
|
|
1392
|
+
poolArg: IMountedStoragePoolConfig,
|
|
1393
|
+
): Promise<IStoragePoolObservation> {
|
|
1394
|
+
const objectStorageInstance = new ObjectStorage({
|
|
1395
|
+
server: { silent: true },
|
|
1396
|
+
storage: {
|
|
1397
|
+
directory: poolArg.directory,
|
|
1398
|
+
cleanSlate: false,
|
|
1399
|
+
pool: poolArg,
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
await objectStorageInstance.ensureLocalBinariesExecutable();
|
|
1403
|
+
const spawned = await objectStorageInstance.bridge.spawn();
|
|
1404
|
+
if (!spawned) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
'Failed to spawn ruststorage binary. Make sure it is compiled (pnpm build).',
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
try {
|
|
1410
|
+
return await objectStorageInstance.bridge.sendCommand('probeStoragePool', {
|
|
1411
|
+
pool: objectStorageInstance.config.storage
|
|
1412
|
+
.pool as IMountedStoragePoolConfig,
|
|
1413
|
+
});
|
|
1414
|
+
} finally {
|
|
1415
|
+
await objectStorageInstance.bridge.terminate();
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// INSTANCE
|
|
1420
|
+
public config: Required<IObjectStorageConfig>;
|
|
1421
|
+
private localBinaryPaths: string[];
|
|
1422
|
+
private bridge: InstanceType<typeof plugins.RustBridge<TRustStorageCommands>>;
|
|
1423
|
+
private running = false;
|
|
1424
|
+
private completedStopResult: IStorageStopResult | undefined;
|
|
1425
|
+
private startPromise: Promise<void> | undefined;
|
|
1426
|
+
private stopPromise: Promise<void> | undefined;
|
|
1427
|
+
private bridgeOwned = false;
|
|
1428
|
+
private stopRequested = false;
|
|
1429
|
+
private stopCompleted = false;
|
|
1430
|
+
|
|
1431
|
+
constructor(configArg: IObjectStorageConfig = {}) {
|
|
1432
|
+
this.config = mergeConfig(configArg);
|
|
1433
|
+
this.localBinaryPaths = getRustStorageBinaryNames().map((binaryNameArg) =>
|
|
1434
|
+
plugins.path.join(paths.packageDir, 'dist_rust', binaryNameArg),
|
|
1435
|
+
);
|
|
1436
|
+
this.bridge = new plugins.RustBridge<TRustStorageCommands>({
|
|
1437
|
+
binaryName: 'ruststorage',
|
|
1438
|
+
localPaths: this.localBinaryPaths,
|
|
1439
|
+
readyTimeoutMs: 30000,
|
|
1440
|
+
requestTimeoutMs: 300000,
|
|
1441
|
+
maxPayloadSize: MAXIMUM_MANAGEMENT_PAYLOAD_BYTES,
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
private async ensureLocalBinariesExecutable() {
|
|
1446
|
+
for (const localBinaryPath of this.localBinaryPaths) {
|
|
1447
|
+
const binaryStat = await plugins.fs.stat(localBinaryPath).catch((errorArg: NodeJS.ErrnoException) => {
|
|
1448
|
+
if (errorArg.code !== 'ENOENT') {
|
|
1449
|
+
throw errorArg;
|
|
1450
|
+
}
|
|
1451
|
+
return null;
|
|
1452
|
+
});
|
|
1453
|
+
if (!binaryStat || (binaryStat.mode & 0o111) !== 0) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
await plugins.fs.chmod(localBinaryPath, binaryStat.mode | 0o755);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
public start(): Promise<void> {
|
|
1461
|
+
if (this.stopRequested && !this.stopCompleted) {
|
|
1462
|
+
return Promise.reject(new Error('Storage shutdown must complete before restarting.'));
|
|
1463
|
+
}
|
|
1464
|
+
if (this.startPromise) {
|
|
1465
|
+
return this.startPromise;
|
|
1466
|
+
}
|
|
1467
|
+
if (this.running) {
|
|
1468
|
+
return Promise.resolve();
|
|
1469
|
+
}
|
|
1470
|
+
if (this.bridgeOwned) {
|
|
1471
|
+
return Promise.reject(new Error('Storage startup cleanup must complete before restarting.'));
|
|
1472
|
+
}
|
|
1473
|
+
this.stopRequested = false;
|
|
1474
|
+
this.stopCompleted = false;
|
|
1475
|
+
this.stopPromise = undefined;
|
|
1476
|
+
this.completedStopResult = undefined;
|
|
1477
|
+
const start = this.startInternal();
|
|
1478
|
+
this.startPromise = start;
|
|
1479
|
+
const releaseStart = () => {
|
|
1480
|
+
if (this.startPromise === start) {
|
|
1481
|
+
this.startPromise = undefined;
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
start.then(releaseStart, releaseStart);
|
|
1485
|
+
return start;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
private async startInternal(): Promise<void> {
|
|
1489
|
+
try {
|
|
1490
|
+
await this.ensureLocalBinariesExecutable();
|
|
1491
|
+
// A failed spawn can still own a transport whose termination needs retrying.
|
|
1492
|
+
this.bridgeOwned = true;
|
|
1493
|
+
const spawned = await this.bridge.spawn();
|
|
1494
|
+
if (!spawned) {
|
|
1495
|
+
throw new Error('Failed to spawn ruststorage binary. Make sure it is compiled (pnpm build).');
|
|
1496
|
+
}
|
|
1497
|
+
await this.bridge.sendCommand('start', { config: this.config });
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
try {
|
|
1500
|
+
if (this.bridgeOwned) {
|
|
1501
|
+
await this.bridge.terminate();
|
|
1502
|
+
this.bridgeOwned = false;
|
|
1503
|
+
}
|
|
1504
|
+
} catch (terminationError) {
|
|
1505
|
+
throw new AggregateError(
|
|
1506
|
+
[error, terminationError],
|
|
1507
|
+
'Storage startup failed and Rust bridge termination could not be confirmed.',
|
|
1508
|
+
{ cause: error },
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
throw error;
|
|
1512
|
+
}
|
|
1513
|
+
this.running = true;
|
|
1514
|
+
|
|
1515
|
+
if (!this.config.server.silent) {
|
|
1516
|
+
console.log('storage server is running');
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
public async getStorageDescriptor(
|
|
1521
|
+
optionsArg?: Partial<plugins.tsclass.storage.IS3Descriptor>,
|
|
1522
|
+
): Promise<plugins.tsclass.storage.IS3Descriptor> {
|
|
1523
|
+
const cred = this.config.auth.credentials[0] || {
|
|
1524
|
+
accessKeyId: 'STORAGE',
|
|
1525
|
+
secretAccessKey: 'STORAGE',
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const descriptor: plugins.tsclass.storage.IS3Descriptor = {
|
|
1529
|
+
endpoint: this.config.server.address === '0.0.0.0' ? 'localhost' : this.config.server.address!,
|
|
1530
|
+
port: this.config.server.port!,
|
|
1531
|
+
useSsl: false,
|
|
1532
|
+
accessKey: cred.accessKeyId,
|
|
1533
|
+
accessSecret: cred.secretAccessKey,
|
|
1534
|
+
bucketName: '',
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
return {
|
|
1538
|
+
...descriptor,
|
|
1539
|
+
...(optionsArg ? optionsArg : {}),
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
private getEndpoint(): string {
|
|
1544
|
+
return this.config.server.address === '0.0.0.0' ? 'localhost' : this.config.server.address!;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
private buildBucketTenantDescriptor(
|
|
1548
|
+
credential: IStorageCredential & {
|
|
1549
|
+
resourceFence?: IObjectStorageResourceFenceReceipt;
|
|
1550
|
+
retention?: IBucketRetentionReceipt;
|
|
1551
|
+
},
|
|
1552
|
+
bucketNameArg: string,
|
|
1553
|
+
): IBucketTenantDescriptor {
|
|
1554
|
+
const bucketName = credential.bucketName || bucketNameArg;
|
|
1555
|
+
const region = credential.region || this.config.server.region || 'us-east-1';
|
|
1556
|
+
const endpoint = this.getEndpoint();
|
|
1557
|
+
const port = this.config.server.port!;
|
|
1558
|
+
const useSsl = false;
|
|
1559
|
+
|
|
1560
|
+
const descriptor: IBucketTenantDescriptor = {
|
|
1561
|
+
endpoint,
|
|
1562
|
+
port,
|
|
1563
|
+
region,
|
|
1564
|
+
bucket: bucketName,
|
|
1565
|
+
bucketName,
|
|
1566
|
+
accessKeyId: credential.accessKeyId,
|
|
1567
|
+
secretAccessKey: credential.secretAccessKey,
|
|
1568
|
+
accessKey: credential.accessKeyId,
|
|
1569
|
+
accessSecret: credential.secretAccessKey,
|
|
1570
|
+
useSsl,
|
|
1571
|
+
ssl: useSsl,
|
|
1572
|
+
env: {
|
|
1573
|
+
S3_ENDPOINT: endpoint,
|
|
1574
|
+
S3_PORT: String(port),
|
|
1575
|
+
S3_REGION: region,
|
|
1576
|
+
S3_BUCKET: bucketName,
|
|
1577
|
+
S3_ACCESS_KEY_ID: credential.accessKeyId,
|
|
1578
|
+
S3_SECRET_ACCESS_KEY: credential.secretAccessKey,
|
|
1579
|
+
S3_USE_SSL: String(useSsl),
|
|
1580
|
+
AWS_ACCESS_KEY_ID: credential.accessKeyId,
|
|
1581
|
+
AWS_SECRET_ACCESS_KEY: credential.secretAccessKey,
|
|
1582
|
+
AWS_REGION: region,
|
|
1583
|
+
},
|
|
1584
|
+
};
|
|
1585
|
+
if (credential.resourceFence) {
|
|
1586
|
+
descriptor.resourceFence = credential.resourceFence;
|
|
1587
|
+
}
|
|
1588
|
+
if (credential.retention) {
|
|
1589
|
+
descriptor.retention = credential.retention;
|
|
1590
|
+
}
|
|
1591
|
+
return descriptor;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
private assertTenantAuthEnabled(): void {
|
|
1595
|
+
if (!this.config.auth.enabled) {
|
|
1596
|
+
throw new Error('Bucket tenant APIs require auth.enabled=true.');
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
public async createBucket(bucketNameArg: string) {
|
|
1601
|
+
try {
|
|
1602
|
+
await this.bridge.sendCommand('createBucket', { name: bucketNameArg });
|
|
1603
|
+
return { name: bucketNameArg };
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
throw normalizeResourceFenceError(error);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
public async createBucketTenant(
|
|
1610
|
+
tenantArg: IBucketTenantInput,
|
|
1611
|
+
): Promise<IBucketTenantDescriptor> {
|
|
1612
|
+
this.assertTenantAuthEnabled();
|
|
1613
|
+
try {
|
|
1614
|
+
const credential = await this.bridge.sendCommand('createBucketTenant', {
|
|
1615
|
+
bucketName: tenantArg.bucketName,
|
|
1616
|
+
accessKeyId: tenantArg.accessKeyId || createAccessKeyId(),
|
|
1617
|
+
secretAccessKey: tenantArg.secretAccessKey || createSecretAccessKey(),
|
|
1618
|
+
region: tenantArg.region || this.config.server.region,
|
|
1619
|
+
...(tenantArg.retention ? { retention: tenantArg.retention } : {}),
|
|
1620
|
+
});
|
|
1621
|
+
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
throw normalizeResourceFenceError(error);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
public async ensureBucketTenant(
|
|
1628
|
+
tenantArg: IEnsureBucketTenantInput,
|
|
1629
|
+
): Promise<IBucketTenantDescriptor> {
|
|
1630
|
+
this.assertTenantAuthEnabled();
|
|
1631
|
+
try {
|
|
1632
|
+
const credential = await this.bridge.sendCommand('ensureBucketTenant', {
|
|
1633
|
+
...tenantArg,
|
|
1634
|
+
region: tenantArg.region || this.config.server.region,
|
|
1635
|
+
});
|
|
1636
|
+
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
throw normalizeResourceFenceError(error);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
public async deleteBucketTenant(tenantArg: IDeleteBucketTenantInput): Promise<void> {
|
|
1643
|
+
this.assertTenantAuthEnabled();
|
|
1644
|
+
try {
|
|
1645
|
+
await this.bridge.sendCommand('deleteBucketTenant', tenantArg);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
throw normalizeResourceFenceError(error);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
public async rotateBucketTenantCredentials(
|
|
1652
|
+
tenantArg: IBucketTenantInput,
|
|
1653
|
+
): Promise<IBucketTenantDescriptor> {
|
|
1654
|
+
this.assertTenantAuthEnabled();
|
|
1655
|
+
try {
|
|
1656
|
+
const credential = await this.bridge.sendCommand('rotateBucketTenantCredentials', {
|
|
1657
|
+
bucketName: tenantArg.bucketName,
|
|
1658
|
+
accessKeyId: tenantArg.accessKeyId || createAccessKeyId(),
|
|
1659
|
+
secretAccessKey: tenantArg.secretAccessKey || createSecretAccessKey(),
|
|
1660
|
+
region: tenantArg.region || this.config.server.region,
|
|
1661
|
+
});
|
|
1662
|
+
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
throw normalizeResourceFenceError(error);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
public async listBucketTenants(): Promise<IBucketTenantMetadata[]> {
|
|
1669
|
+
this.assertTenantAuthEnabled();
|
|
1670
|
+
return this.bridge.sendCommand('listBucketTenants', {});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
public async getBucketTenantDescriptor(optionsArg: {
|
|
1674
|
+
bucketName: string;
|
|
1675
|
+
}): Promise<IBucketTenantDescriptor> {
|
|
1676
|
+
this.assertTenantAuthEnabled();
|
|
1677
|
+
const credential = await this.bridge.sendCommand('getBucketTenantCredential', {
|
|
1678
|
+
bucketName: optionsArg.bucketName,
|
|
1679
|
+
});
|
|
1680
|
+
return this.buildBucketTenantDescriptor(credential, optionsArg.bucketName);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
public async getBucketRetentionCapability(): Promise<IBucketRetentionCapability> {
|
|
1684
|
+
return this.bridge.sendCommand('getBucketRetentionCapability', {});
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
public async getBucketRetentionReceipt(optionsArg: {
|
|
1688
|
+
bucketName: string;
|
|
1689
|
+
}): Promise<IBucketRetentionReceipt | null> {
|
|
1690
|
+
return this.bridge.sendCommand('getBucketRetentionReceipt', optionsArg);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
public async getBucketMigrationCapability(): Promise<IObjectStorageBucketMigrationCapability> {
|
|
1694
|
+
return this.bridge.sendCommand('getBucketMigrationCapability', {});
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
public async createSourceMigration(
|
|
1698
|
+
optionsArg: IObjectStorageMigrationCreateInput,
|
|
1699
|
+
): Promise<IObjectStorageMigrationTransitionReceipt> {
|
|
1700
|
+
return this.bridge.sendCommand('createSourceMigration', optionsArg);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
public async createDestinationMigration(
|
|
1704
|
+
optionsArg: IObjectStorageMigrationCreateInput,
|
|
1705
|
+
): Promise<IObjectStorageMigrationTransitionReceipt> {
|
|
1706
|
+
return this.bridge.sendCommand('createDestinationMigration', optionsArg);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
public async transitionSourceMigration(
|
|
1710
|
+
optionsArg: IObjectStorageSourceMigrationTransitionInput,
|
|
1711
|
+
): Promise<IObjectStorageMigrationTransitionReceipt> {
|
|
1712
|
+
return this.bridge.sendCommand('transitionSourceMigration', optionsArg);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
public async transitionDestinationMigration(
|
|
1716
|
+
optionsArg: IObjectStorageDestinationMigrationTransitionInput,
|
|
1717
|
+
): Promise<IObjectStorageMigrationTransitionReceipt> {
|
|
1718
|
+
return this.bridge.sendCommand('transitionDestinationMigration', optionsArg);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
public async cleanupSourceMigration(
|
|
1722
|
+
optionsArg: IObjectStorageSourceMigrationCleanupInput,
|
|
1723
|
+
): Promise<IObjectStorageSourceMigrationCleanupReceipt> {
|
|
1724
|
+
try {
|
|
1725
|
+
return await this.bridge.sendCommand('cleanupSourceMigration', optionsArg);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
throw normalizeResourceFenceError(error);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
public async inspectBucketMigration(optionsArg: {
|
|
1732
|
+
migrationId: string;
|
|
1733
|
+
}): Promise<IObjectStorageMigrationInspection | null> {
|
|
1734
|
+
return this.bridge.sendCommand('inspectBucketMigration', optionsArg);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
public async exportBucket(optionsArg: IExportBucketInput): Promise<IBucketExport> {
|
|
1738
|
+
return this.bridge.sendCommand('exportBucket', { bucketName: optionsArg.bucketName });
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
public async importBucket(
|
|
1742
|
+
optionsArg: IImportBucketInput,
|
|
1743
|
+
): Promise<IObjectStorageBucketMutationResult> {
|
|
1744
|
+
try {
|
|
1745
|
+
return await this.bridge.sendCommand('importBucket', optionsArg);
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
throw normalizeResourceFenceError(error);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
public async deleteBucket(
|
|
1752
|
+
optionsArg: IDeleteBucketInput,
|
|
1753
|
+
): Promise<IObjectStorageBucketMutationResult> {
|
|
1754
|
+
try {
|
|
1755
|
+
return await this.bridge.sendCommand('deleteBucket', optionsArg);
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
throw normalizeResourceFenceError(error);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
public async commitResourcePublication(
|
|
1762
|
+
optionsArg: ICommitObjectStorageResourcePublicationInput,
|
|
1763
|
+
): Promise<IObjectStorageResourcePublicationReleaseReceipt> {
|
|
1764
|
+
try {
|
|
1765
|
+
return await this.bridge.sendCommand('commitResourcePublication', optionsArg);
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
throw normalizeResourceFenceError(error);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
public async getResourceFencingCapability(): Promise<IObjectStorageResourceFencingCapability> {
|
|
1772
|
+
return this.bridge.sendCommand('getResourceFencingCapability', {});
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
public async getStorageStats(): Promise<IStorageStats> {
|
|
1776
|
+
return this.bridge.sendCommand('getStorageStats', {});
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
public async getStoragePoolDiagnostic(): Promise<IStoragePoolObservation | null> {
|
|
1780
|
+
return this.bridge.sendCommand('getStoragePoolDiagnostic', {});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
public async listBucketSummaries(): Promise<IBucketSummary[]> {
|
|
1784
|
+
return this.bridge.sendCommand('listBucketSummaries', {});
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
public async listCredentials(): Promise<IStorageCredentialMetadata[]> {
|
|
1788
|
+
return this.bridge.sendCommand('listCredentials', {});
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Trusted process-local administrative override. Resource lifecycle code
|
|
1793
|
+
* must use fenced ensure/delete instead; completed exact receipts fail closed
|
|
1794
|
+
* if this override introduces ownership or credential-material drift.
|
|
1795
|
+
*/
|
|
1796
|
+
public async replaceCredentials(credentials: IStorageCredential[]): Promise<void> {
|
|
1797
|
+
await this.bridge.sendCommand('replaceCredentials', { credentials });
|
|
1798
|
+
this.config.auth.credentials = credentials.map((credential) => ({ ...credential }));
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
public async getClusterHealth(): Promise<IClusterHealth> {
|
|
1802
|
+
return this.bridge.sendCommand('getClusterHealth', {});
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
public async getHealth(): Promise<IObjectStorageHealth> {
|
|
1806
|
+
if (!this.running) {
|
|
1807
|
+
return {
|
|
1808
|
+
ok: false,
|
|
1809
|
+
running: false,
|
|
1810
|
+
storageDirectory: this.config.storage.directory || paths.bucketsDir,
|
|
1811
|
+
storageLocations: [],
|
|
1812
|
+
auth: {
|
|
1813
|
+
enabled: this.config.auth.enabled,
|
|
1814
|
+
credentialCount: this.config.auth.credentials.length,
|
|
1815
|
+
tenantCredentialCount: 0,
|
|
1816
|
+
},
|
|
1817
|
+
bucketCount: 0,
|
|
1818
|
+
objectCount: 0,
|
|
1819
|
+
totalBytes: 0,
|
|
1820
|
+
cluster: { enabled: false },
|
|
1821
|
+
resourceFencingVersion: 1,
|
|
1822
|
+
resourceFencingSupported: false,
|
|
1823
|
+
resourceFencingRequiresDrain: true,
|
|
1824
|
+
resourcePublicationHoldVersion: 1,
|
|
1825
|
+
resourcePublicationHoldSupported: false,
|
|
1826
|
+
resourcePublicationHoldRequiresExternalDrain: false,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
const [stats, credentials, tenants, cluster, resourceFencing] = await Promise.all([
|
|
1831
|
+
this.getStorageStats(),
|
|
1832
|
+
this.listCredentials(),
|
|
1833
|
+
this.config.auth.enabled ? this.listBucketTenants() : Promise.resolve([]),
|
|
1834
|
+
this.getClusterHealth(),
|
|
1835
|
+
this.getResourceFencingCapability(),
|
|
1836
|
+
]);
|
|
1837
|
+
return {
|
|
1838
|
+
ok: true,
|
|
1839
|
+
running: true,
|
|
1840
|
+
storageDirectory: stats.storageDirectory,
|
|
1841
|
+
storageLocations: stats.storageLocations || [],
|
|
1842
|
+
auth: {
|
|
1843
|
+
enabled: this.config.auth.enabled,
|
|
1844
|
+
credentialCount: credentials.length,
|
|
1845
|
+
tenantCredentialCount: tenants.length,
|
|
1846
|
+
},
|
|
1847
|
+
bucketCount: stats.bucketCount,
|
|
1848
|
+
objectCount: stats.totalObjectCount,
|
|
1849
|
+
totalBytes: stats.totalStorageBytes,
|
|
1850
|
+
cluster,
|
|
1851
|
+
resourceFencingVersion: resourceFencing.version,
|
|
1852
|
+
resourceFencingSupported: resourceFencing.supported,
|
|
1853
|
+
resourceFencingRequiresDrain: resourceFencing.requiresDrain,
|
|
1854
|
+
resourcePublicationHoldVersion: resourceFencing.publicationHoldVersion,
|
|
1855
|
+
resourcePublicationHoldSupported: resourceFencing.publicationHoldSupported,
|
|
1856
|
+
resourcePublicationHoldRequiresExternalDrain:
|
|
1857
|
+
resourceFencing.publicationHoldRequiresExternalDrain,
|
|
1858
|
+
resourceFencingReason: resourceFencing.reason,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
public async getMetrics(): Promise<IObjectStorageMetrics> {
|
|
1863
|
+
const health = await this.getHealth();
|
|
1864
|
+
const clusterEnabled = health.cluster.enabled;
|
|
1865
|
+
return {
|
|
1866
|
+
bucketCount: health.bucketCount,
|
|
1867
|
+
objectCount: health.objectCount,
|
|
1868
|
+
totalBytes: health.totalBytes,
|
|
1869
|
+
authCredentialCount: health.auth.credentialCount,
|
|
1870
|
+
tenantCredentialCount: health.auth.tenantCredentialCount,
|
|
1871
|
+
clusterEnabled,
|
|
1872
|
+
prometheusText: [
|
|
1873
|
+
'# HELP objectstorage_buckets_total Runtime bucket count.',
|
|
1874
|
+
'# TYPE objectstorage_buckets_total gauge',
|
|
1875
|
+
`objectstorage_buckets_total ${health.bucketCount}`,
|
|
1876
|
+
'# HELP objectstorage_objects_total Runtime object count.',
|
|
1877
|
+
'# TYPE objectstorage_objects_total gauge',
|
|
1878
|
+
`objectstorage_objects_total ${health.objectCount}`,
|
|
1879
|
+
'# HELP objectstorage_storage_bytes_total Runtime storage bytes.',
|
|
1880
|
+
'# TYPE objectstorage_storage_bytes_total gauge',
|
|
1881
|
+
`objectstorage_storage_bytes_total ${health.totalBytes}`,
|
|
1882
|
+
'# HELP objectstorage_tenant_credentials_total Scoped bucket tenant credential count.',
|
|
1883
|
+
'# TYPE objectstorage_tenant_credentials_total gauge',
|
|
1884
|
+
`objectstorage_tenant_credentials_total ${health.auth.tenantCredentialCount}`,
|
|
1885
|
+
'# HELP objectstorage_cluster_enabled Cluster mode enabled.',
|
|
1886
|
+
'# TYPE objectstorage_cluster_enabled gauge',
|
|
1887
|
+
`objectstorage_cluster_enabled ${clusterEnabled ? 1 : 0}`,
|
|
1888
|
+
].join('\n'),
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Retains the Rust bridge when shutdown is incomplete so a later call can
|
|
1894
|
+
* retry ownership draining. Completed terminal errors kill the bridge before
|
|
1895
|
+
* they are rethrown.
|
|
1896
|
+
*/
|
|
1897
|
+
public stop(): Promise<void> {
|
|
1898
|
+
if (this.stopPromise) {
|
|
1899
|
+
return this.stopPromise;
|
|
1900
|
+
}
|
|
1901
|
+
this.stopRequested = true;
|
|
1902
|
+
const stop = this.stopInternal();
|
|
1903
|
+
this.stopPromise = stop;
|
|
1904
|
+
stop.then(undefined, () => {
|
|
1905
|
+
if (!this.stopCompleted && this.stopPromise === stop) {
|
|
1906
|
+
this.stopPromise = undefined;
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
return stop;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
private async stopInternal(): Promise<void> {
|
|
1913
|
+
if (this.startPromise) {
|
|
1914
|
+
// Startup keeps its own rejection; stop joins only its resource ownership.
|
|
1915
|
+
await Promise.allSettled([this.startPromise]);
|
|
1916
|
+
}
|
|
1917
|
+
if (this.bridgeOwned) {
|
|
1918
|
+
if (this.running) {
|
|
1919
|
+
this.completedStopResult ??= await this.bridge.sendCommand('stop', {});
|
|
1920
|
+
}
|
|
1921
|
+
await this.bridge.terminate();
|
|
1922
|
+
this.bridgeOwned = false;
|
|
1923
|
+
this.running = false;
|
|
1924
|
+
}
|
|
1925
|
+
this.stopCompleted = true;
|
|
1926
|
+
if (this.completedStopResult?.terminalError) {
|
|
1927
|
+
throw new Error(
|
|
1928
|
+
`Storage server shutdown completed with terminal errors: ${this.completedStopResult.terminalError}`,
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|