@lossless.org/objectstorage 10.7.0 → 11.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 +12 -10
- package/dist_rust/ruststorage_linux_amd64 +0 -0
- package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_amd64_musl +0 -0
- package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_arm64 +0 -0
- package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_arm64_musl +0 -0
- package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_macos_amd64 +0 -0
- package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_macos_arm64 +0 -0
- package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +125 -0
- package/dist_ts/index.js +147 -8
- package/package.json +9 -12
- package/readme.md +543 -30
- package/third-party-notices.md +1 -1
- package/third_party_licenses/inventory.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +311 -13
package/third-party-notices.md
CHANGED
|
@@ -109,7 +109,7 @@ root, run:
|
|
|
109
109
|
|
|
110
110
|
```sh
|
|
111
111
|
node scripts/generate-native-notices.mjs
|
|
112
|
-
pnpm exec
|
|
112
|
+
pnpm exec tsrepo test test/test.notices.node.ts --verbose --logfile
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
The generator uses Cargo.lock and checksum-pinned local or Git license sources
|
|
@@ -2116,7 +2116,7 @@
|
|
|
2116
2116
|
"zerocopy-derive@0.8.47": "third_party_licenses/zerocopy-derive-0.8.47-license.txt"
|
|
2117
2117
|
},
|
|
2118
2118
|
"files": {
|
|
2119
|
-
".smartconfig.json": "
|
|
2119
|
+
".smartconfig.json": "36557e7545b45c38432fa291e11551c20cc0e54fdc6436a0797429e32d1e4d8d",
|
|
2120
2120
|
"license.md": "bd1cbee3bf4a1ec7f4bb842e315cb0c296744e93050493607cb32f1570f1e878",
|
|
2121
2121
|
"rust-toolchain.toml": "a721b38eb6b02c7b6cfba956549c18282a3cd28347954021fc1e508c7b27d15b",
|
|
2122
2122
|
"rust/Cargo.lock": "4b1fb196bca6bec6c326dd822db4d5ca5eb0740fea0544100f9e9d8984187762",
|
|
@@ -2125,7 +2125,7 @@
|
|
|
2125
2125
|
"rust/about.toml": "dc5dbae4e4b746e12c3a810cdc2473c380e7b9b3c0ecdd095d02693dbb8876b2",
|
|
2126
2126
|
"rust/notices.sources.json": "7c98fe57f606700bb8372a890f8e49a1240385224680ec0877c3c623ab9cf598",
|
|
2127
2127
|
"scripts/generate-native-notices.mjs": "e654c52e292778ca72f09e96420b18a49271a7c6612cdd2e2f4dbcd01dec1bcb",
|
|
2128
|
-
"third-party-notices.md": "
|
|
2128
|
+
"third-party-notices.md": "7e733804670e866e563b999ea600ddff72b0f8dee63552f223516ed88dc9afdd",
|
|
2129
2129
|
"third_party_licenses/aho-corasick-1.1.4-license.txt": "0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f",
|
|
2130
2130
|
"third_party_licenses/base64-0.22.1/LICENSE-MIT": "0dd882e53de11566d50f8e8e2d5a651bcf3fabee4987d70f306233cf39094ba7",
|
|
2131
2131
|
"third_party_licenses/cargo-dependencies.html": "8a007780ac193d1fc51ebe8ef7d302dd39e2426a887219049c4ac392786fbed0",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@lossless.org/objectstorage',
|
|
6
|
-
version: '
|
|
6
|
+
version: '11.0.0',
|
|
7
7
|
description: 'A Node.js TypeScript package to create a local S3-compatible storage server using mapped local directories for development and testing purposes.'
|
|
8
8
|
}
|
package/ts/index.ts
CHANGED
|
@@ -62,6 +62,11 @@ export interface ILimitsConfig {
|
|
|
62
62
|
export interface IMultipartConfig {
|
|
63
63
|
expirationDays?: number;
|
|
64
64
|
cleanupIntervalMinutes?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Open multipart uploads that the prefix credentials of one scope (bucket
|
|
67
|
+
* and prefix) may hold together (default 100). Counted in memory.
|
|
68
|
+
*/
|
|
69
|
+
maxUploadsPerPrefixScope?: number;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -118,6 +123,11 @@ export interface IStorageConfig {
|
|
|
118
123
|
export interface IErasureConfig {
|
|
119
124
|
dataShards?: number;
|
|
120
125
|
parityShards?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Bytes per erasure-coded chunk, between 1 and 64 MiB (67,108,864 bytes);
|
|
128
|
+
* startup refuses a value outside that range, because every manifest
|
|
129
|
+
* refuses a larger chunk. Default 4,194,304 (4 MiB).
|
|
130
|
+
*/
|
|
121
131
|
chunkSizeBytes?: number;
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -126,6 +136,13 @@ export interface IErasureConfig {
|
|
|
126
136
|
*/
|
|
127
137
|
export interface IDriveConfig {
|
|
128
138
|
paths: string[];
|
|
139
|
+
/**
|
|
140
|
+
* Accept drive roots that share a device (`st_dev`). Each drive root counts
|
|
141
|
+
* as one failure domain, so by default startup refuses two drive roots on
|
|
142
|
+
* one device. Set this only for dev and test clusters whose drive roots are
|
|
143
|
+
* directories on one filesystem. Default false.
|
|
144
|
+
*/
|
|
145
|
+
allowSharedDevice?: boolean;
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
/**
|
|
@@ -140,6 +157,16 @@ export interface IClusterConfig {
|
|
|
140
157
|
drives?: IDriveConfig;
|
|
141
158
|
heartbeatIntervalMs?: number;
|
|
142
159
|
heartbeatTimeoutMs?: number;
|
|
160
|
+
/**
|
|
161
|
+
* Longest a single GET may run, in milliseconds, from its start to its last
|
|
162
|
+
* byte, including the client's transfer time. A longer GET is aborted.
|
|
163
|
+
* Superseded and deleted object generations keep their shards for this
|
|
164
|
+
* deadline plus the smaller of the deadline and one hour, then cleanup frees
|
|
165
|
+
* them. Must be the same on every node: a mismatch can abort streams but
|
|
166
|
+
* never serves wrong bytes. Default 21,600,000 (6 hours); allowed from
|
|
167
|
+
* 60,000 (1 minute) to 604,800,000 (7 days).
|
|
168
|
+
*/
|
|
169
|
+
getDeadlineMs?: number;
|
|
143
170
|
}
|
|
144
171
|
|
|
145
172
|
/**
|
|
@@ -341,6 +368,35 @@ export interface IBucketTenantMetadata {
|
|
|
341
368
|
retention?: IBucketRetentionReceipt;
|
|
342
369
|
}
|
|
343
370
|
|
|
371
|
+
export interface IPrefixCredentialInput {
|
|
372
|
+
bucketName: string;
|
|
373
|
+
/** Unreserved characters (RFC 3986) and `/`; ends with `/`, no leading `/`. */
|
|
374
|
+
prefix: string;
|
|
375
|
+
/** First node: lifetime in seconds, 1..43200. */
|
|
376
|
+
ttlSeconds?: number;
|
|
377
|
+
/** Fan-out to further nodes: the first node's expiresAt (epoch ms). */
|
|
378
|
+
expiresAt?: number;
|
|
379
|
+
/** Supply both together with expiresAt to install an existing credential on another node. */
|
|
380
|
+
accessKeyId?: string;
|
|
381
|
+
secretAccessKey?: string;
|
|
382
|
+
region?: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface IPrefixCredentialMetadata {
|
|
386
|
+
accessKeyId: string;
|
|
387
|
+
bucketName: string;
|
|
388
|
+
prefix: string;
|
|
389
|
+
region: string;
|
|
390
|
+
/** Epoch ms. */
|
|
391
|
+
createdAt: number;
|
|
392
|
+
/** Epoch ms; requests are refused from this instant and admitted ones end at it. */
|
|
393
|
+
expiresAt: number;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface IPrefixCredential extends IPrefixCredentialMetadata {
|
|
397
|
+
secretAccessKey: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
344
400
|
export interface IBucketRetentionPolicyInput {
|
|
345
401
|
mode: 'compliance';
|
|
346
402
|
policyId: string;
|
|
@@ -998,6 +1054,10 @@ export interface IObjectStorageHealth {
|
|
|
998
1054
|
enabled: boolean;
|
|
999
1055
|
credentialCount: number;
|
|
1000
1056
|
tenantCredentialCount: number;
|
|
1057
|
+
prefixCredentialCount: number;
|
|
1058
|
+
prefixCredentialTombstoneCount: number;
|
|
1059
|
+
/** False when the prefix credential file is unreadable: every prefix credential is refused. */
|
|
1060
|
+
prefixCredentialStoreAvailable: boolean;
|
|
1001
1061
|
};
|
|
1002
1062
|
bucketCount: number;
|
|
1003
1063
|
objectCount: number;
|
|
@@ -1062,6 +1122,55 @@ const normalizeResourceFenceError = (errorArg: unknown): Error => {
|
|
|
1062
1122
|
);
|
|
1063
1123
|
};
|
|
1064
1124
|
|
|
1125
|
+
export const objectStorageCredentialErrorCodes = [
|
|
1126
|
+
'ECREDENTIAL_AUTH_DISABLED',
|
|
1127
|
+
'ECREDENTIAL_INVALID_PREFIX',
|
|
1128
|
+
'ECREDENTIAL_INVALID_TTL',
|
|
1129
|
+
'ECREDENTIAL_INVALID_EXPIRY',
|
|
1130
|
+
'ECREDENTIAL_INVALID_KEY',
|
|
1131
|
+
'ECREDENTIAL_NO_SUCH_BUCKET',
|
|
1132
|
+
'ECREDENTIAL_BUCKET_UNAVAILABLE',
|
|
1133
|
+
'ECREDENTIAL_CONFLICT',
|
|
1134
|
+
'ECREDENTIAL_REVOKED',
|
|
1135
|
+
'ECREDENTIAL_NOT_PREFIX',
|
|
1136
|
+
'ECREDENTIAL_LIMIT',
|
|
1137
|
+
'ECREDENTIAL_PERSISTENCE',
|
|
1138
|
+
'ECREDENTIAL_STORE_UNAVAILABLE',
|
|
1139
|
+
] as const;
|
|
1140
|
+
|
|
1141
|
+
export type TObjectStorageCredentialErrorCode =
|
|
1142
|
+
(typeof objectStorageCredentialErrorCodes)[number];
|
|
1143
|
+
|
|
1144
|
+
export class ObjectStorageCredentialError extends Error {
|
|
1145
|
+
public readonly code: TObjectStorageCredentialErrorCode;
|
|
1146
|
+
|
|
1147
|
+
public constructor(
|
|
1148
|
+
codeArg: TObjectStorageCredentialErrorCode,
|
|
1149
|
+
messageArg: string,
|
|
1150
|
+
causeArg?: unknown,
|
|
1151
|
+
) {
|
|
1152
|
+
super(`${codeArg}: ${messageArg}`, { cause: causeArg });
|
|
1153
|
+
this.name = 'ObjectStorageCredentialError';
|
|
1154
|
+
this.code = codeArg;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const normalizeCredentialError = (errorArg: unknown): Error => {
|
|
1159
|
+
const error = errorArg instanceof Error ? errorArg : new Error(String(errorArg));
|
|
1160
|
+
const match = /(ECREDENTIAL_[A-Z_]+):\s*(.*)$/s.exec(error.message);
|
|
1161
|
+
if (
|
|
1162
|
+
!match ||
|
|
1163
|
+
!objectStorageCredentialErrorCodes.includes(match[1] as TObjectStorageCredentialErrorCode)
|
|
1164
|
+
) {
|
|
1165
|
+
return error;
|
|
1166
|
+
}
|
|
1167
|
+
return new ObjectStorageCredentialError(
|
|
1168
|
+
match[1] as TObjectStorageCredentialErrorCode,
|
|
1169
|
+
match[2],
|
|
1170
|
+
error,
|
|
1171
|
+
);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1065
1174
|
export interface IObjectStorageMetrics {
|
|
1066
1175
|
bucketCount: number;
|
|
1067
1176
|
objectCount: number;
|
|
@@ -1119,6 +1228,8 @@ export interface IClusterRepairHealth {
|
|
|
1119
1228
|
lastDurationMs?: number;
|
|
1120
1229
|
shardsChecked?: number;
|
|
1121
1230
|
shardsHealed?: number;
|
|
1231
|
+
/** Pending shards (writes that failed at publication) the last scan left pending. */
|
|
1232
|
+
shardsPending?: number;
|
|
1122
1233
|
failed?: number;
|
|
1123
1234
|
lastError?: string;
|
|
1124
1235
|
}
|
|
@@ -1140,6 +1251,35 @@ export interface IClusterHealth {
|
|
|
1140
1251
|
repairs?: IClusterRepairHealth;
|
|
1141
1252
|
}
|
|
1142
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* The result of one cluster healing scan, as `ObjectStorage.runHealingScan()`
|
|
1256
|
+
* returns it. Times are milliseconds since the Unix epoch.
|
|
1257
|
+
*/
|
|
1258
|
+
export interface IClusterHealingScanResult {
|
|
1259
|
+
/** When the scan started; always after the request arrived. */
|
|
1260
|
+
startedAt: number;
|
|
1261
|
+
completedAt: number;
|
|
1262
|
+
durationMs: number;
|
|
1263
|
+
/** Shard placements checked at their drives (placements on offline nodes are not). */
|
|
1264
|
+
shardsChecked: number;
|
|
1265
|
+
/** Shards written: missing placements rebuilt and pending shards completed. */
|
|
1266
|
+
shardsHealed: number;
|
|
1267
|
+
/** Pending shards (writes that failed at publication) this scan left pending. */
|
|
1268
|
+
shardsPending: number;
|
|
1269
|
+
/** Failed checks, rebuilds, writes and manifest stores. */
|
|
1270
|
+
failed: number;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export interface IRunHealingScanOptions {
|
|
1274
|
+
/**
|
|
1275
|
+
* How long to wait for the scan, in milliseconds. Defaults to the bridge's
|
|
1276
|
+
* request timeout (300,000). The engine cannot cancel a scan: one that
|
|
1277
|
+
* outlasts this keeps running, its result is discarded, and a later call
|
|
1278
|
+
* joins the next scan that starts after it.
|
|
1279
|
+
*/
|
|
1280
|
+
timeoutMs?: number;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1143
1283
|
/**
|
|
1144
1284
|
* Default configuration values
|
|
1145
1285
|
*/
|
|
@@ -1185,6 +1325,7 @@ const DEFAULT_CONFIG: IObjectStorageConfig = {
|
|
|
1185
1325
|
multipart: {
|
|
1186
1326
|
expirationDays: 7,
|
|
1187
1327
|
cleanupIntervalMinutes: 60,
|
|
1328
|
+
maxUploadsPerPrefixScope: 100,
|
|
1188
1329
|
},
|
|
1189
1330
|
};
|
|
1190
1331
|
|
|
@@ -1313,6 +1454,10 @@ function createAccessKeyId(): string {
|
|
|
1313
1454
|
return `SS${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
|
|
1314
1455
|
}
|
|
1315
1456
|
|
|
1457
|
+
function createPrefixAccessKeyId(): string {
|
|
1458
|
+
return `SP${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1316
1461
|
function createSecretAccessKey(): string {
|
|
1317
1462
|
return plugins.crypto.randomBytes(32).toString('hex');
|
|
1318
1463
|
}
|
|
@@ -1441,7 +1586,29 @@ type TRustStorageCommands = {
|
|
|
1441
1586
|
listBucketSummaries: { params: {}; result: IBucketSummary[] };
|
|
1442
1587
|
listCredentials: { params: {}; result: IStorageCredentialMetadata[] };
|
|
1443
1588
|
replaceCredentials: { params: { credentials: IStorageCredential[] }; result: {} };
|
|
1589
|
+
createPrefixCredential: {
|
|
1590
|
+
params: {
|
|
1591
|
+
bucketName: string;
|
|
1592
|
+
prefix: string;
|
|
1593
|
+
accessKeyId: string;
|
|
1594
|
+
secretAccessKey: string;
|
|
1595
|
+
region?: string;
|
|
1596
|
+
ttlSeconds?: number;
|
|
1597
|
+
expiresAt?: number;
|
|
1598
|
+
};
|
|
1599
|
+
result: IPrefixCredentialMetadata;
|
|
1600
|
+
};
|
|
1601
|
+
revokePrefixCredential: { params: { accessKeyId: string }; result: { revoked: boolean } };
|
|
1602
|
+
listPrefixCredentials: {
|
|
1603
|
+
params: { bucketName?: string };
|
|
1604
|
+
result: IPrefixCredentialMetadata[];
|
|
1605
|
+
};
|
|
1606
|
+
getPrefixCredentialStatus: {
|
|
1607
|
+
params: {};
|
|
1608
|
+
result: { available: boolean; credentialCount: number; tombstoneCount: number };
|
|
1609
|
+
};
|
|
1444
1610
|
getClusterHealth: { params: {}; result: IClusterHealth };
|
|
1611
|
+
runHealingScan: { params: {}; result: IClusterHealingScanResult };
|
|
1445
1612
|
};
|
|
1446
1613
|
|
|
1447
1614
|
/**
|
|
@@ -1730,7 +1897,7 @@ export class ObjectStorage {
|
|
|
1730
1897
|
});
|
|
1731
1898
|
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1732
1899
|
} catch (error) {
|
|
1733
|
-
throw normalizeResourceFenceError(error);
|
|
1900
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1734
1901
|
}
|
|
1735
1902
|
}
|
|
1736
1903
|
|
|
@@ -1745,7 +1912,7 @@ export class ObjectStorage {
|
|
|
1745
1912
|
});
|
|
1746
1913
|
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1747
1914
|
} catch (error) {
|
|
1748
|
-
throw normalizeResourceFenceError(error);
|
|
1915
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1749
1916
|
}
|
|
1750
1917
|
}
|
|
1751
1918
|
|
|
@@ -1754,7 +1921,7 @@ export class ObjectStorage {
|
|
|
1754
1921
|
try {
|
|
1755
1922
|
await this.bridge.sendCommand('deleteBucketTenant', tenantArg);
|
|
1756
1923
|
} catch (error) {
|
|
1757
|
-
throw normalizeResourceFenceError(error);
|
|
1924
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1758
1925
|
}
|
|
1759
1926
|
}
|
|
1760
1927
|
|
|
@@ -1771,7 +1938,7 @@ export class ObjectStorage {
|
|
|
1771
1938
|
});
|
|
1772
1939
|
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1773
1940
|
} catch (error) {
|
|
1774
|
-
throw normalizeResourceFenceError(error);
|
|
1941
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1775
1942
|
}
|
|
1776
1943
|
}
|
|
1777
1944
|
|
|
@@ -1790,6 +1957,95 @@ export class ObjectStorage {
|
|
|
1790
1957
|
return this.buildBucketTenantDescriptor(credential, optionsArg.bucketName);
|
|
1791
1958
|
}
|
|
1792
1959
|
|
|
1960
|
+
/**
|
|
1961
|
+
* Creates a credential confined to one key prefix of one bucket until its
|
|
1962
|
+
* expiry. The first node takes `ttlSeconds`; further nodes of a fan-out take
|
|
1963
|
+
* the first result's `accessKeyId`, `secretAccessKey` and `expiresAt`. The
|
|
1964
|
+
* secret is returned only here and never kept by this instance.
|
|
1965
|
+
*/
|
|
1966
|
+
public async createPrefixCredential(
|
|
1967
|
+
inputArg: IPrefixCredentialInput,
|
|
1968
|
+
): Promise<IPrefixCredential> {
|
|
1969
|
+
const hasTtl = inputArg.ttlSeconds !== undefined;
|
|
1970
|
+
const hasExpiresAt = inputArg.expiresAt !== undefined;
|
|
1971
|
+
if (hasTtl === hasExpiresAt) {
|
|
1972
|
+
throw new ObjectStorageCredentialError(
|
|
1973
|
+
'ECREDENTIAL_INVALID_TTL',
|
|
1974
|
+
'Exactly one of ttlSeconds and expiresAt is required.',
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
if (hasTtl && (!Number.isSafeInteger(inputArg.ttlSeconds) || inputArg.ttlSeconds! < 1)) {
|
|
1978
|
+
throw new ObjectStorageCredentialError(
|
|
1979
|
+
'ECREDENTIAL_INVALID_TTL',
|
|
1980
|
+
'ttlSeconds must be an integer from 1 to 43200.',
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
if (hasExpiresAt && !Number.isSafeInteger(inputArg.expiresAt)) {
|
|
1984
|
+
throw new ObjectStorageCredentialError(
|
|
1985
|
+
'ECREDENTIAL_INVALID_EXPIRY',
|
|
1986
|
+
'expiresAt must be an integer epoch millisecond.',
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
const hasAccessKeyId = inputArg.accessKeyId !== undefined;
|
|
1990
|
+
const hasSecretAccessKey = inputArg.secretAccessKey !== undefined;
|
|
1991
|
+
if (hasAccessKeyId !== hasSecretAccessKey) {
|
|
1992
|
+
throw new ObjectStorageCredentialError(
|
|
1993
|
+
'ECREDENTIAL_INVALID_KEY',
|
|
1994
|
+
'accessKeyId and secretAccessKey must be supplied together.',
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
if (hasAccessKeyId && !hasExpiresAt) {
|
|
1998
|
+
throw new ObjectStorageCredentialError(
|
|
1999
|
+
'ECREDENTIAL_INVALID_KEY',
|
|
2000
|
+
'Key material is accepted only with expiresAt, to install an existing credential.',
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
const secretAccessKey = inputArg.secretAccessKey ?? createSecretAccessKey();
|
|
2004
|
+
try {
|
|
2005
|
+
const metadata = await this.bridge.sendCommand('createPrefixCredential', {
|
|
2006
|
+
bucketName: inputArg.bucketName,
|
|
2007
|
+
prefix: inputArg.prefix,
|
|
2008
|
+
accessKeyId: inputArg.accessKeyId ?? createPrefixAccessKeyId(),
|
|
2009
|
+
secretAccessKey,
|
|
2010
|
+
region: inputArg.region || this.config.server.region,
|
|
2011
|
+
...(hasTtl ? { ttlSeconds: inputArg.ttlSeconds } : { expiresAt: inputArg.expiresAt }),
|
|
2012
|
+
});
|
|
2013
|
+
return { ...metadata, secretAccessKey };
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
throw normalizeCredentialError(error);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Revokes a prefix credential on this node. An unknown ID is recorded as
|
|
2021
|
+
* revoked too, so a later create for it is refused; `revoked` is true only
|
|
2022
|
+
* when a live credential was removed.
|
|
2023
|
+
*/
|
|
2024
|
+
public async revokePrefixCredential(optionsArg: {
|
|
2025
|
+
accessKeyId: string;
|
|
2026
|
+
}): Promise<{ revoked: boolean }> {
|
|
2027
|
+
try {
|
|
2028
|
+
return await this.bridge.sendCommand('revokePrefixCredential', {
|
|
2029
|
+
accessKeyId: optionsArg.accessKeyId,
|
|
2030
|
+
});
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
throw normalizeCredentialError(error);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/** Lists this node's live prefix credentials, without secrets. */
|
|
2037
|
+
public async listPrefixCredentials(
|
|
2038
|
+
optionsArg: { bucketName?: string } = {},
|
|
2039
|
+
): Promise<IPrefixCredentialMetadata[]> {
|
|
2040
|
+
try {
|
|
2041
|
+
return await this.bridge.sendCommand('listPrefixCredentials', {
|
|
2042
|
+
...(optionsArg.bucketName !== undefined ? { bucketName: optionsArg.bucketName } : {}),
|
|
2043
|
+
});
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
throw normalizeCredentialError(error);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1793
2049
|
public async getBucketRetentionCapability(): Promise<IBucketRetentionCapability> {
|
|
1794
2050
|
return this.bridge.sendCommand('getBucketRetentionCapability', {});
|
|
1795
2051
|
}
|
|
@@ -1864,7 +2120,7 @@ export class ObjectStorage {
|
|
|
1864
2120
|
try {
|
|
1865
2121
|
return await this.bridge.sendCommand('deleteBucket', optionsArg);
|
|
1866
2122
|
} catch (error) {
|
|
1867
|
-
throw normalizeResourceFenceError(error);
|
|
2123
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1868
2124
|
}
|
|
1869
2125
|
}
|
|
1870
2126
|
|
|
@@ -1914,7 +2170,11 @@ export class ObjectStorage {
|
|
|
1914
2170
|
* if this override introduces ownership or credential-material drift.
|
|
1915
2171
|
*/
|
|
1916
2172
|
public async replaceCredentials(credentials: IStorageCredential[]): Promise<void> {
|
|
1917
|
-
|
|
2173
|
+
try {
|
|
2174
|
+
await this.bridge.sendCommand('replaceCredentials', { credentials });
|
|
2175
|
+
} catch (error) {
|
|
2176
|
+
throw normalizeCredentialError(error);
|
|
2177
|
+
}
|
|
1918
2178
|
this.config.auth.credentials = credentials.map((credential) => ({ ...credential }));
|
|
1919
2179
|
}
|
|
1920
2180
|
|
|
@@ -1922,6 +2182,27 @@ export class ObjectStorage {
|
|
|
1922
2182
|
return this.bridge.sendCommand('getClusterHealth', {});
|
|
1923
2183
|
}
|
|
1924
2184
|
|
|
2185
|
+
/**
|
|
2186
|
+
* Runs a healing scan on this cluster node and resolves with its result.
|
|
2187
|
+
* The scan covers the manifests this node coordinated. The request joins
|
|
2188
|
+
* the next scan that starts after it arrives, never one already running,
|
|
2189
|
+
* and requests that arrive together share one scan. It rejects in
|
|
2190
|
+
* standalone mode, when the scan fails (for example without a majority of
|
|
2191
|
+
* the cluster, or when a bucket's manifests cannot be listed), when 16
|
|
2192
|
+
* requests already wait for a scan to start, and when the engine shuts down
|
|
2193
|
+
* before the scan completes. The engine answers other management calls
|
|
2194
|
+
* while the scan runs.
|
|
2195
|
+
*/
|
|
2196
|
+
public async runHealingScan(
|
|
2197
|
+
optionsArg: IRunHealingScanOptions = {},
|
|
2198
|
+
): Promise<IClusterHealingScanResult> {
|
|
2199
|
+
return this.bridge.sendCommand(
|
|
2200
|
+
'runHealingScan',
|
|
2201
|
+
{},
|
|
2202
|
+
optionsArg.timeoutMs === undefined ? undefined : { timeoutMs: optionsArg.timeoutMs },
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
1925
2206
|
public async getHealth(): Promise<IObjectStorageHealth> {
|
|
1926
2207
|
if (!this.running) {
|
|
1927
2208
|
return {
|
|
@@ -1933,6 +2214,9 @@ export class ObjectStorage {
|
|
|
1933
2214
|
enabled: this.config.auth.enabled,
|
|
1934
2215
|
credentialCount: this.config.auth.credentials.length,
|
|
1935
2216
|
tenantCredentialCount: 0,
|
|
2217
|
+
prefixCredentialCount: 0,
|
|
2218
|
+
prefixCredentialTombstoneCount: 0,
|
|
2219
|
+
prefixCredentialStoreAvailable: false,
|
|
1936
2220
|
},
|
|
1937
2221
|
bucketCount: 0,
|
|
1938
2222
|
objectCount: 0,
|
|
@@ -1947,13 +2231,15 @@ export class ObjectStorage {
|
|
|
1947
2231
|
};
|
|
1948
2232
|
}
|
|
1949
2233
|
|
|
1950
|
-
const [stats, credentials, tenants, cluster, resourceFencing] =
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2234
|
+
const [stats, credentials, tenants, prefixCredentials, cluster, resourceFencing] =
|
|
2235
|
+
await Promise.all([
|
|
2236
|
+
this.getStorageStats(),
|
|
2237
|
+
this.listCredentials(),
|
|
2238
|
+
this.config.auth.enabled ? this.listBucketTenants() : Promise.resolve([]),
|
|
2239
|
+
this.bridge.sendCommand('getPrefixCredentialStatus', {}),
|
|
2240
|
+
this.getClusterHealth(),
|
|
2241
|
+
this.getResourceFencingCapability(),
|
|
2242
|
+
]);
|
|
1957
2243
|
return {
|
|
1958
2244
|
ok: true,
|
|
1959
2245
|
running: true,
|
|
@@ -1963,6 +2249,9 @@ export class ObjectStorage {
|
|
|
1963
2249
|
enabled: this.config.auth.enabled,
|
|
1964
2250
|
credentialCount: credentials.length,
|
|
1965
2251
|
tenantCredentialCount: tenants.length,
|
|
2252
|
+
prefixCredentialCount: prefixCredentials.credentialCount,
|
|
2253
|
+
prefixCredentialTombstoneCount: prefixCredentials.tombstoneCount,
|
|
2254
|
+
prefixCredentialStoreAvailable: prefixCredentials.available,
|
|
1966
2255
|
},
|
|
1967
2256
|
bucketCount: stats.bucketCount,
|
|
1968
2257
|
objectCount: stats.totalObjectCount,
|
|
@@ -2002,6 +2291,15 @@ export class ObjectStorage {
|
|
|
2002
2291
|
'# HELP objectstorage_tenant_credentials_total Scoped bucket tenant credential count.',
|
|
2003
2292
|
'# TYPE objectstorage_tenant_credentials_total gauge',
|
|
2004
2293
|
`objectstorage_tenant_credentials_total ${health.auth.tenantCredentialCount}`,
|
|
2294
|
+
'# HELP objectstorage_prefix_credentials_total Live prefix credential count.',
|
|
2295
|
+
'# TYPE objectstorage_prefix_credentials_total gauge',
|
|
2296
|
+
`objectstorage_prefix_credentials_total ${health.auth.prefixCredentialCount}`,
|
|
2297
|
+
'# HELP objectstorage_prefix_credential_tombstones_total Unexpired prefix credential revocations.',
|
|
2298
|
+
'# TYPE objectstorage_prefix_credential_tombstones_total gauge',
|
|
2299
|
+
`objectstorage_prefix_credential_tombstones_total ${health.auth.prefixCredentialTombstoneCount}`,
|
|
2300
|
+
'# HELP objectstorage_prefix_credential_store_available Prefix credential store readable.',
|
|
2301
|
+
'# TYPE objectstorage_prefix_credential_store_available gauge',
|
|
2302
|
+
`objectstorage_prefix_credential_store_available ${health.auth.prefixCredentialStoreAvailable ? 1 : 0}`,
|
|
2005
2303
|
'# HELP objectstorage_cluster_enabled Cluster mode enabled.',
|
|
2006
2304
|
'# TYPE objectstorage_cluster_enabled gauge',
|
|
2007
2305
|
`objectstorage_cluster_enabled ${clusterEnabled ? 1 : 0}`,
|