@lossless.org/objectstorage 11.0.0 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.smartconfig.json +19 -1
  2. package/dist_rust/ruststorage_linux_amd64 +0 -0
  3. package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +4 -4
  4. package/dist_rust/ruststorage_linux_amd64_musl +0 -0
  5. package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +4 -4
  6. package/dist_rust/ruststorage_linux_arm64 +0 -0
  7. package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +4 -4
  8. package/dist_rust/ruststorage_linux_arm64_musl +0 -0
  9. package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +4 -4
  10. package/dist_rust/ruststorage_macos_amd64 +0 -0
  11. package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +4 -4
  12. package/dist_rust/ruststorage_macos_arm64 +0 -0
  13. package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +4 -4
  14. package/dist_ts/00_commitinfo_data.js +1 -1
  15. package/dist_ts/classes.dashboard.d.ts +49 -0
  16. package/dist_ts/classes.dashboard.js +234 -0
  17. package/dist_ts/dashboard.bundled.d.ts +4 -0
  18. package/dist_ts/dashboard.bundled.js +20 -0
  19. package/dist_ts/dashboard.interfaces.d.ts +132 -0
  20. package/dist_ts/dashboard.interfaces.js +2 -0
  21. package/dist_ts/dashboard.plugins.d.ts +5 -0
  22. package/dist_ts/dashboard.plugins.js +10 -0
  23. package/dist_ts/index.d.ts +116 -1
  24. package/dist_ts/index.js +90 -2
  25. package/package.json +11 -7
  26. package/readme.md +400 -22
  27. package/third-party-notices.md +20 -4
  28. package/third_party_licenses/borsh-1.8.1-license.txt +23 -0
  29. package/third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt +201 -0
  30. package/third_party_licenses/cargo-dependencies.html +420 -0
  31. package/third_party_licenses/dyn-clone-1.0.20-license.txt +23 -0
  32. package/third_party_licenses/equivalent-1.0.3-license.txt +25 -0
  33. package/third_party_licenses/hashbrown-0.17.1-license.txt +25 -0
  34. package/third_party_licenses/indexmap-2.14.2-license.txt +25 -0
  35. package/third_party_licenses/inventory.json +310 -8
  36. package/third_party_licenses/proc-macro-crate-3.5.0-license.txt +23 -0
  37. package/third_party_licenses/ref-cast-1.0.27-license.txt +23 -0
  38. package/third_party_licenses/ref-cast-impl-1.0.27-license.txt +23 -0
  39. package/third_party_licenses/schemars-1.2.3-license.txt +21 -0
  40. package/third_party_licenses/syn-3.0.9-license.txt +23 -0
  41. package/third_party_licenses/toml_datetime-1.1.2-license.txt +19 -0
  42. package/third_party_licenses/toml_edit-0.25.17-license.txt +19 -0
  43. package/third_party_licenses/toml_parser-1.1.5-license.txt +19 -0
  44. package/third_party_licenses/winnow-1.0.4-license.txt +18 -0
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.dashboard.ts +280 -0
  47. package/ts/dashboard.bundled.ts.third-party-notices.json +1741 -0
  48. package/ts/dashboard.bundled.ts.third-party-notices.txt +5015 -0
  49. package/ts/dashboard.interfaces.ts +148 -0
  50. package/ts/dashboard.plugins.ts +13 -0
  51. package/ts/index.ts +217 -2
package/readme.md CHANGED
@@ -69,7 +69,7 @@ described below.
69
69
  ### Clustering Features
70
70
 
71
71
  - 🔗 **Erasure coding** — Reed-Solomon (configurable k data + m parity shards) for storage efficiency and fault tolerance
72
- - 🚄 **QUIC transport** — multiplexed inter-node communication via `quinn` with zero head-of-line blocking. Encrypted but not authenticated: run it only on a trusted network (see Cluster transport trust).
72
+ - 🚄 **QUIC transport** — multiplexed inter-node communication via `quinn` with zero head-of-line blocking. Mutual TLS with a cluster CA when `cluster.tls` is set; without it, encrypted but not authenticated, for trusted networks only (see Cluster transport trust).
73
73
  - 💽 **Multi-drive awareness** — each node manages multiple independent storage paths with health monitoring
74
74
  - 🩺 **Cluster health introspection** — query native node, drive, quorum, and healing status for product dashboards
75
75
  - 🤝 **Cluster membership** — static seed config + runtime join, heartbeat-based failure detection
@@ -184,8 +184,9 @@ Cluster mode requires the control root and every drive root to exist before
184
184
  startup. The process user must own them, no path component may be a symlink,
185
185
  and group/world write permission must be disabled.
186
186
 
187
- The cluster transport does not authenticate nodes: run cluster mode only on a
188
- network where every host that can reach `quicPort` is a cluster node (see
187
+ Without `cluster.tls` the cluster transport does not authenticate nodes: run
188
+ cluster mode only on a network where every host that can reach `quicPort` is a
189
+ cluster node, or set `cluster.tls` on every node (see
189
190
  [Cluster transport trust](#cluster-transport-trust)).
190
191
 
191
192
  ```typescript
@@ -286,7 +287,7 @@ const config: IObjectStorageConfig = {
286
287
  },
287
288
  cluster: { // Optional — omit for standalone mode
288
289
  enabled: true,
289
- nodeId: 'node-1', // Auto-generated UUID if omitted
290
+ nodeId: 'node-1', // Auto-generated UUID if omitted; required with tls
290
291
  quicPort: 4000, // Default: 4000
291
292
  seedNodes: [], // Addresses of existing cluster members
292
293
  erasure: {
@@ -301,6 +302,16 @@ const config: IObjectStorageConfig = {
301
302
  heartbeatIntervalMs: 5000, // Default: 5000
302
303
  heartbeatTimeoutMs: 30000, // Default: 30000
303
304
  getDeadlineMs: 21600000, // Default: 6 h; 60000 (1 min) to 604800000 (7 d)
305
+ tls: { // Optional mutual TLS; set on every node or on none
306
+ caFile: '/etc/objectstorage/tls/ca.pem',
307
+ certFile: '/etc/objectstorage/tls/node-1.pem', // its DNS SAN is nodeId
308
+ keyFile: '/etc/objectstorage/tls/node-1.key', // mode 0600 or 0400
309
+ },
310
+ },
311
+ dashboard: { // Optional read-only dashboard; off unless enabled
312
+ enabled: true,
313
+ address: '127.0.0.1', // Default: '127.0.0.1'
314
+ port: 3001, // Default: 3001; 0 allocates an ephemeral TCP port; never the S3 port
304
315
  },
305
316
  };
306
317
 
@@ -617,7 +628,8 @@ to the prefix (its folder marker) is covered.
617
628
  (see [Supported S3 Operations](#supported-s3-operations)), so a parameter
618
629
  the operation does not implement gets `NotImplemented` (501).
619
630
  - **DeleteObjects.** Every member is checked on its own: covered keys are
620
- deleted, and the others get a per-key `AccessDenied`.
631
+ deleted, and the others get a per-key `AccessDenied`, as does every member
632
+ that names a `VersionId`.
621
633
  - **Listings.** ListObjects V1, ListObjectsV2, ListObjectVersions and
622
634
  ListMultipartUploads need a `prefix` parameter that is covered. A missing or
623
635
  wider prefix is refused, never clamped.
@@ -625,7 +637,8 @@ to the prefix (its folder marker) is covered.
625
637
  - ListBuckets returns only the credential's bucket.
626
638
  - HeadBucket and `GET /{bucket}?location` work on that bucket.
627
639
  - Any other bucket-level query (`?acl`, `?versioning`, `?lifecycle`, …) is
628
- refused with `AccessDenied`, and so is any repeated query parameter.
640
+ refused with `AccessDenied`, and so is any repeated query parameter, so a
641
+ prefix credential can neither read nor change a bucket's versioning.
629
642
  - Creating or deleting buckets and bucket policy operations are refused.
630
643
  - **Policies.** The scope check runs before bucket policies, so a policy Allow
631
644
  (including `Principal: "*"`) never widens a prefix credential. A policy Deny
@@ -924,8 +937,10 @@ Absent-bucket fenced ensure removes any residual policy before creating the new
924
937
  resource; existing-bucket ensure preserves policy and object data. Once a bucket
925
938
  has durable fence history, legacy `createBucket()`, `createBucketTenant()`,
926
939
  `rotateBucketTenantCredentials()`, `deleteBucketTenant()`, and unfenced
927
- `importBucket()` mutations fail with `EFENCE_REQUIRED`. Use fenced ensure,
928
- exact import, and exact delete for its lifecycle.
940
+ `importBucket()` mutations fail with `EFENCE_REQUIRED`. S3 `CreateBucket` and
941
+ `DeleteBucket` on such a bucket get `InvalidBucketState` (`409`), whose message
942
+ is the `EFENCE_REQUIRED: …` text. Use fenced ensure, exact import, and exact
943
+ delete for its lifecycle.
929
944
 
930
945
  `replaceCredentials()` is a trusted process-local administrative override, not
931
946
  a fenced lifecycle API. If it changes exact-owned credential material, completed
@@ -941,7 +956,8 @@ const metrics = await storage.getMetrics();
941
956
 
942
957
  - `getHealth()` reports running state, storage directory and pool observation, auth enabled state, credential counts, bucket count, object count, total bytes, and cluster health.
943
958
  - `getHealth().auth` also reports `prefixCredentialCount` (live prefix credentials), `prefixCredentialTombstoneCount` (unexpired revocations) and `prefixCredentialStoreAvailable` (`false` while the prefix credential file is unreadable and every prefix credential is refused).
944
- - `getMetrics()` returns numeric counters and a Prometheus text snippet for bucket, object, byte, tenant credential, prefix credential (`objectstorage_prefix_credentials_total`, `objectstorage_prefix_credential_tombstones_total`, `objectstorage_prefix_credential_store_available`), and cluster-enabled metrics.
959
+ - `getMetrics()` returns numeric counters and a Prometheus text snippet for bucket, object, byte, tenant credential, prefix credential (`objectstorage_prefix_credentials_total`, `objectstorage_prefix_credential_tombstones_total`, `objectstorage_prefix_credential_store_available`), version (`objectstorage_noncurrent_versions_total`, `objectstorage_noncurrent_bytes_total`, `objectstorage_delete_markers_total`) and cluster-enabled metrics.
960
+ - `getHealth().stampClockAheadOfWallMs` is present while version commit times run ahead of the wall clock (a wall clock that went backwards, see [Bucket versioning](#bucket-versioning)).
945
961
  - `getHealth()` reads the prefix credential figures through the `getPrefixCredentialStatus` IPC command (no TypeScript method of its own), and `getMetrics()` derives its prefix metrics from `getHealth()`. The command takes no parameters and returns `{ available, credentialCount, tombstoneCount }`: counts and the store state only, never an access key ID, secret, bucket or prefix.
946
962
 
947
963
  ## Runtime Stats
@@ -962,6 +978,11 @@ interface IBucketSummary {
962
978
  objectCount: number;
963
979
  totalSizeBytes: number;
964
980
  creationDate?: number;
981
+ // Versioned buckets only:
982
+ versioning?: 'Enabled' | 'Suspended';
983
+ noncurrentVersionCount?: number;
984
+ noncurrentBytes?: number;
985
+ deleteMarkerCount?: number;
965
986
  }
966
987
 
967
988
  interface IStorageLocationSummary {
@@ -979,9 +1000,22 @@ interface IStorageStats {
979
1000
  buckets: IBucketSummary[];
980
1001
  storageDirectory: string;
981
1002
  storageLocations?: IStorageLocationSummary[];
1003
+ // With at least one versioned bucket:
1004
+ totalNoncurrentVersionCount?: number;
1005
+ totalNoncurrentBytes?: number;
1006
+ totalDeleteMarkerCount?: number;
1007
+ stampClockAheadOfWallMs?: number;
982
1008
  }
983
1009
  ```
984
1010
 
1011
+ - Object counts and bytes count current objects only. A versioned bucket also
1012
+ reports its noncurrent data versions and their payload bytes
1013
+ (`noncurrentVersionCount`, `noncurrentBytes`) and its delete markers, current
1014
+ and noncurrent (`deleteMarkerCount`); the stats totals add them up, and
1015
+ `getMetrics()` exports them as `objectstorage_noncurrent_versions_total`,
1016
+ `objectstorage_noncurrent_bytes_total` and `objectstorage_delete_markers_total`.
1017
+ Stored capacity is current plus noncurrent bytes.
1018
+
985
1019
  - `bucketCount`, `totalObjectCount`, `totalStorageBytes`, and per-bucket totals are logical object stats maintained by the Rust runtime. They count object payload bytes, not sidecar files or erasure-coded shard overhead.
986
1020
  - objectstorage initializes these values from native on-disk state at startup, then keeps them in memory and updates them when bucket/object mutations succeed. Stats reads do not issue S3 `ListObjects` or rescan every object.
987
1021
  - Values are exact for mutations performed through objectstorage after startup. Direct filesystem edits outside objectstorage are not watched; restart the server to resync.
@@ -1011,6 +1045,23 @@ interface IClusterHealth {
1011
1045
  drives?: IClusterDriveHealth[];
1012
1046
  erasure?: IClusterErasureHealth;
1013
1047
  repairs?: IClusterRepairHealth;
1048
+ transport?: IClusterTransportHealth;
1049
+ }
1050
+
1051
+ interface IClusterTransportHealth {
1052
+ mode: 'mutualTls' | 'unauthenticated';
1053
+ certificate?: { sha256: string; notAfter: number }; // only with mutualTls
1054
+ caCertificates?: { sha256: string; notAfter: number }[]; // file order; only with mutualTls
1055
+ }
1056
+
1057
+ type TDriveMedia = 'rotational' | 'smr' | 'flash' | 'zoned';
1058
+
1059
+ interface IClusterDriveHealth {
1060
+ // index, path, status, capacity, error and erasure set fields, plus:
1061
+ media: {
1062
+ declared?: TDriveMedia; // not reported yet: drives cannot declare their media
1063
+ detected: TDriveMedia | 'unknown'; // detected once at startup
1064
+ };
1014
1065
  }
1015
1066
  ```
1016
1067
 
@@ -1018,10 +1069,88 @@ interface IClusterHealth {
1018
1069
  - Standalone mode returns `{ enabled: false }`.
1019
1070
  - Peer status is the local node's current view of cluster membership and heartbeats, so it is best-effort and may lag real network state.
1020
1071
  - Drive health is based on live native probe checks on the configured local drive paths. Capacity values are cheap filesystem snapshots.
1072
+ - `drives[].media.detected` is the medium the engine detected for each drive once at startup, never per health call. On Linux it follows the drive root's filesystem to the whole disk behind it (`st_dev` through `/sys/dev/block`; a partition counts as its disk) and reads the disk's `queue/zoned` and `queue/rotational`: `zoned` for host-managed or host-aware devices (SMR or ZNS), otherwise `rotational` or `flash`, as the disk's driver sets `queue/rotational`. Virtual and network block devices report the flag their driver or hypervisor sets: QEMU disks show `rotational` unless they are configured with `rotation_rate=1`, and NVMe volumes show `flash`. Declaring a drive's media, which is planned, is what will correct such a reading. It is `unknown`, never a guess, for filesystems without a block device of their own (tmpfs, overlay, network and FUSE filesystems, btrfs subvolumes), for device-mapper, md, loop and other virtual block devices (LVM, dm-crypt and software RAID included), when sysfs gives no usable answer, and on every platform but Linux. Drive-managed SMR reports itself as rotational and cannot be detected (`smr` can only ever be declared). The detected media change no behaviour today.
1021
1073
  - `quorumHealthy` means the local node currently sees majority quorum and enough available placements in every erasure set to satisfy the configured write quorum.
1074
+ - `transport.mode` is `mutualTls` when `cluster.tls` is set and `unauthenticated` otherwise. With mutual TLS, `certificate` and `caCertificates` describe the files the engine loaded at startup: the SHA-256 of each DER certificate (lowercase hex) and its `notAfter` in milliseconds since the Unix epoch. The files are read only at startup, so during a certificate or CA rotation these show what each node actually runs.
1022
1075
  - Repair fields expose the background healer's currently available runtime state. They are best-effort and limited to what the engine tracks today, such as whether a scan is active, the last completed run, and the last error.
1023
1076
  - `repairs.shardsChecked`, `shardsHealed` and `failed` count the last completed scan's placement checks, shards written and failures. `repairs.shardsPending` counts the pending shards (see [Self-Healing](#self-healing)) that scan left pending because their drive was out of reach, refused the write, or their object was busy. While a scan runs, `lastRunStartedAt` is the running scan's start and the counts are still the previous scan's; `runHealingScan()` returns one scan's start and counts together.
1024
1077
 
1078
+ ## Dashboard
1079
+
1080
+ Every node can serve a small read-only dashboard for quick visibility: the
1081
+ cluster's nodes, drives, erasure layout and quorum, object and byte totals,
1082
+ request, error and byte rates, healing state and recent engine events. It is
1083
+ off by default:
1084
+
1085
+ ```typescript
1086
+ const storage = await ObjectStorage.createAndStart({
1087
+ // ...
1088
+ dashboard: { enabled: true, address: '10.0.0.5', port: 3001 },
1089
+ });
1090
+ console.log(storage.dashboardPort); // the bound port; undefined while off
1091
+ ```
1092
+
1093
+ Open `http://<address>:<port>/` in a browser. The page is bundled into the
1094
+ package (`ts_web/`, built into `ts/dashboard.bundled.ts`), so nothing is
1095
+ fetched from elsewhere, and it updates itself over one TypedSocket connection
1096
+ to the same origin.
1097
+
1098
+ - **Configuration.** `dashboard.enabled` must be set. `address` defaults to
1099
+ `127.0.0.1` and `port` to `3001`; `port: 0` asks the OS for a port. A port
1100
+ equal to `server.port` is refused at construction. A dashboard that is
1101
+ enabled but cannot bind fails `start()`, and the engine is stopped: it never
1102
+ runs silently without it. The `dashboard` section is not sent to the engine.
1103
+ - **What it shows.** Nodes (status and last heartbeat in this node's view,
1104
+ start time); local drives of every node (status, usage, errors,
1105
+ last probe); the erasure layout and quorum; objects and bytes per node and
1106
+ summed; request, error and byte rates per node and summed, with two-minute
1107
+ sparklines kept in the browser; healing per node (last scan, checked,
1108
+ healed, pending shards, failures); and the newest events of each node (peer
1109
+ status changes, drive status changes, healing scan results, engine start).
1110
+ A standalone node shows its totals, rates and events. Rates are derived
1111
+ from counters of the S3 listener; byte counts are taken at the TCP stream
1112
+ and include HTTP headers. Nothing is stored: a reload starts the sparklines
1113
+ again.
1114
+ - **Where the data comes from.** One management command, `getClusterSnapshot`
1115
+ (also `ObjectStorage.getClusterSnapshot()`), built by the engine from memory
1116
+ only: the drive states the last heartbeat probe left, the in-memory runtime
1117
+ stats, healing state, S3 listener counters and a ring of the newest 64
1118
+ events. It never probes a drive, syncs, walks a directory or reads the
1119
+ control store. A cluster node asks each online peer for that peer's own
1120
+ snapshot over QUIC (`NodeSnapshot`), in parallel, with one second per peer;
1121
+ a peer that is not online or does not answer in time shows as unavailable in
1122
+ that sample. Any node with the dashboard on therefore shows the whole
1123
+ cluster, and the other nodes need no dashboard of their own.
1124
+ - **Cost.** With no dashboard connected there is no timer, no IPC and no QUIC
1125
+ traffic. While at least one is connected, one shared sampler takes a
1126
+ snapshot every two seconds, keeps at most one in flight and skips ticks
1127
+ while one runs, and pushes the same sample (a few KB per node) to every
1128
+ connected dashboard. At most 16 dashboard sockets are admitted at once; the
1129
+ 17th is refused with `503`. On the engine side, request and byte counting is
1130
+ a relaxed atomic add per response and per socket read or write. An enabled
1131
+ dashboard with nobody watching runs no timer and sends no traffic; on a
1132
+ three-node cluster against 11.0.0 its CPU cost per request was within 0.4 %,
1133
+ and the engine's memory was unchanged. Its server modules and bundled page
1134
+ add about 50 MiB to the Node.js process of each node once, at start, and
1135
+ that does not grow with time or stored objects. With 16 dashboards open on
1136
+ every node, large uploads ran about 1.5 % slower and the p99 latency was
1137
+ about 5 % higher.
1138
+ - **Security.** The dashboard is read-only by construction: its socket
1139
+ registers one read handler and nothing else, so it cannot start a healing
1140
+ scan, change configuration or touch S3 traffic or stored data. It has no
1141
+ authentication: protect it with the bind address and a firewall, for
1142
+ example by binding it to a private interface only. It shows node IDs,
1143
+ addresses, drive paths, counts and event messages, never credentials or
1144
+ object keys.
1145
+ - **Versions.** Run every node on the same version, as clusters already
1146
+ require. An engine that predates `NodeSnapshot` cannot decode the request,
1147
+ logs a stream error and closes the stream, and the asking node shows it as
1148
+ unavailable.
1149
+
1150
+ `getClusterHealth()` probes every local drive (a write, sync and read of a
1151
+ probe file) on each call; poll `getClusterSnapshot()` instead when you need a
1152
+ cheap periodic view.
1153
+
1025
1154
  ## Supported S3 Operations
1026
1155
 
1027
1156
  The router fails closed, in standalone and cluster mode alike. A request
@@ -1044,7 +1173,7 @@ current object, and `GET ?versioning` answered with an object listing.
1044
1173
  | ListObjectVersions | `GET /{bucket}?versions` | `prefix`, `delimiter`, `max-keys`, `key-marker`, `version-id-marker`, `encoding-type` |
1045
1174
  | ListMultipartUploads | `GET /{bucket}?uploads` | `prefix`, `encoding-type` |
1046
1175
  | GetBucketLocation | `GET /{bucket}?location` | none |
1047
- | GetBucketVersioning | `GET /{bucket}?versioning` | none |
1176
+ | GetBucketVersioning, PutBucketVersioning | `GET`, `PUT /{bucket}?versioning` | none (PUT: standalone only, `NotImplemented` in cluster mode) |
1048
1177
  | GetBucketLifecycleConfiguration | `GET /{bucket}?lifecycle` | none |
1049
1178
  | GetBucketPolicy, PutBucketPolicy, DeleteBucketPolicy | `GET`, `PUT`, `DELETE /{bucket}?policy` | none |
1050
1179
  | DeleteObjects | `POST /{bucket}?delete` | none |
@@ -1082,9 +1211,129 @@ override such as `response-content-type=text/html` would let a public object
1082
1211
  serve script from the storage origin; a presigned URL is such a request. With
1083
1212
  auth disabled the overrides apply.
1084
1213
 
1085
- Versioning is never enabled on a bucket:
1214
+ ### Bucket versioning
1215
+
1216
+ Standalone buckets support S3 versioning exactly as S3 defines it: a bucket is
1217
+ unversioned until `PutBucketVersioning` sets `Enabled` or `Suspended`, and it
1218
+ never goes back to unversioned. New buckets are unversioned. A bucket that was
1219
+ never versioned behaves exactly as before versioning existed, on disk and on
1220
+ the wire: no version ids, no `x-amz-version-id` header, `null` as its only
1221
+ version id. In cluster mode `PUT ?versioning` answers `NotImplemented` (501),
1222
+ so every cluster bucket stays unversioned for now.
1223
+
1224
+ - **Version ids.** A version written while versioning is `Enabled` gets a
1225
+ lowercase UUID as its version id. Versions written before versioning was
1226
+ enabled, and every version written while it is `Suspended`, are the key's
1227
+ `null` version; a key has at most one.
1228
+ - **Enabled.** PUT, CopyObject and CompleteMultipartUpload add a new current
1229
+ version and keep the previous one as noncurrent; the response carries
1230
+ `x-amz-version-id`. A DELETE without a version id adds a delete marker as
1231
+ the current version (also for a key with no object, as S3 does): GET and
1232
+ HEAD then answer `NoSuchKey` (404) with `x-amz-delete-marker: true` and the
1233
+ marker's `x-amz-version-id`, and ListObjects no longer lists the key. The
1234
+ DELETE response carries `x-amz-delete-marker: true` and the marker's id.
1235
+ - **Suspended.** Writes create the `null` version and replace any previous
1236
+ `null` version of the key, wherever it is in the history; versions with ids
1237
+ stay. A DELETE replaces the `null` version with a `null` delete marker.
1238
+ Responses carry `x-amz-version-id: null`.
1239
+ - **By version id.** GET, HEAD and DELETE take `?versionId=` (an id or
1240
+ `null`). Reading a delete marker by its id answers `MethodNotAllowed` (405)
1241
+ with `x-amz-delete-marker: true`, its id and its `Last-Modified`. DELETE with
1242
+ a version id removes that version for good; when it was the current one, the
1243
+ next newest version becomes current again (a delete marker, or the object it
1244
+ covered). A well-formed id that names no version of the key is
1245
+ `NoSuchVersion` (404), also on DELETE; a malformed one is `InvalidArgument`.
1246
+ DeleteObjects accepts a `VersionId` per object and reports `VersionId`,
1247
+ `DeleteMarker` and `DeleteMarkerVersionId` per deleted entry.
1248
+ - **Copies.** A copy source may name a version (`?versionId=` after the
1249
+ source); `x-amz-copy-source-if-*` apply to that version, and the response
1250
+ carries `x-amz-copy-source-version-id` (and `x-amz-version-id` for the new
1251
+ version in a versioned bucket). A delete marker as a copy source is
1252
+ `InvalidRequest` (400); a source whose current version is a delete marker is
1253
+ `NoSuchKey`. UploadPartCopy works the same way.
1254
+ - **Conditional requests.** `If-None-Match: *` on PUT and
1255
+ CompleteMultipartUpload succeeds when the key has no current version or its
1256
+ current version is a delete marker. `If-Match` on DELETE is evaluated against
1257
+ the current version, also with a version id; a current delete marker fails
1258
+ it (412).
1259
+ - **ListObjectVersions** lists every version and delete marker in key order,
1260
+ each key newest first, with `IsLatest` on the current one; versions and
1261
+ delete markers, and common prefixes, all count against `max-keys`. A
1262
+ truncated page carries `NextKeyMarker` and, after a version,
1263
+ `NextVersionIdMarker`; `key-marker` alone resumes after every version of
1264
+ that key, with `version-id-marker` after that version of it. A
1265
+ `version-id-marker` naming a version that was deleted between the pages
1266
+ resumes at that key's newest remaining version: no version of the key is
1267
+ skipped, but versions the previous page already listed are listed again.
1268
+ `Last-Modified` of a version is its commit time.
1269
+ - **Permissions.** `PutBucketVersioning` is authorized as
1270
+ `s3:PutBucketVersioning`, `GetBucketVersioning` as `s3:GetBucketVersioning`,
1271
+ ListObjectVersions as `s3:ListBucketVersions`; a `versionId` (also `null`,
1272
+ also in DeleteObjects) as `s3:GetObjectVersion` or `s3:DeleteObjectVersion`.
1273
+ Only root credentials may change a bucket's versioning: bucket tenant
1274
+ credentials get `AccessDenied` for `PutBucketVersioning` (they may read it
1275
+ with `GetBucketVersioning`, list versions, and read and delete versions in
1276
+ their bucket), because the operator's bucket flows (export, import, fenced
1277
+ replacement and deletion, migrations) do not carry version history and
1278
+ refuse versioned buckets. This lifts once a format that carries history
1279
+ exists. Prefix credentials never name a version and cannot change
1280
+ versioning; they can list versions inside their prefix, and their DELETEs
1281
+ add delete markers.
1282
+ - **Not combined with.** A bucket with engine-enforced immutable retention
1283
+ refuses versioning, and a versioned bucket refuses retention
1284
+ (`InvalidBucketState`, 409). Bucket export and import, fenced bucket
1285
+ replacement and deletion, and bucket migrations refuse versioned buckets,
1286
+ because none of them carries version history; a migration whose source
1287
+ became versioned after it was created cannot be sealed
1288
+ (`EMIGRATION_VERSIONED`, the source stays active). `MfaDelete` is not
1289
+ implemented (501). A versioned bucket is empty for DeleteBucket only once
1290
+ every version and delete marker is deleted (`BucketNotEmpty` before);
1291
+ deleting it forgets its versioning, so a new bucket of the same name starts
1292
+ unversioned. `deleteBucketTenant` deletes every version with the bucket.
1293
+ - **Requests by version wait for writes.** Every request whose answer depends
1294
+ on a bucket's versioning state reads that state, and the history, under the
1295
+ bucket's gate: DELETE and each DeleteObjects member, GET and HEAD with a
1296
+ version id (also `null` on a bucket that was never versioned), a copy or
1297
+ UploadPartCopy source with a version id, the delete-marker answer of a key
1298
+ without a current object, and ListObjectVersions. A request that arrives
1299
+ while `PutBucketVersioning` or a write to the bucket is in progress is
1300
+ therefore served by the state that change leaves. A write holds the gate
1301
+ while it receives its body, so these requests, like every listing, wait
1302
+ for uploads to the same bucket that are in progress: measured with one
1303
+ 1 GiB upload into the bucket, a GET by version id issued during it
1304
+ answered once the upload had finished, after 2.6 s at full loopback speed
1305
+ and after 20.3 s at 50 MB/s, while GETs without a version id stayed at a
1306
+ 1.6 ms median (3.5 ms p99). GET and HEAD without a version id never wait.
1307
+ - **Startup.** A standalone engine checks every version chain once when it
1308
+ starts, reading every version's record and opening its payload (and
1309
+ repairing what a crash left), and takes the bucket counters from that
1310
+ check. Startup time therefore grows with the number of stored versions.
1311
+ Measured with 1,000,000 versions (100,000 keys × 10) on 8 CPUs with a
1312
+ 12 GiB memory limit, on a KVM host whose page cache did not keep the root
1313
+ between starts: the first start after writing them took 346 s; six clean
1314
+ starts took 127 to 264 s (127, 145, 150, 193, 203 and 264 s, median about
1315
+ 170 s); three starts after a SIGKILL during writes by 32 concurrent
1316
+ writers took 215 to 256 s. A root on which no bucket was ever versioned
1317
+ pays nothing for this and starts exactly as before. `start()` waits for as
1318
+ long as startup takes; once recovery passes 10 s, the engine logs its
1319
+ progress (chains, versions, elapsed time) to the host's stderr every 10 s.
1320
+ - **Commit times.** Version commit times come from one stamp clock per
1321
+ engine: `max(wall clock, last stamp + monotonic time since)`, persisted to
1322
+ `.smartstorage/versioning/stamp-clock.json` by every `PutBucketVersioning`
1323
+ that changes a bucket's state (before the state itself, and before the
1324
+ response), every 60 seconds and on a graceful stop. Within a process, a wall clock
1325
+ that jumps back never produces a commit time earlier than true time. After
1326
+ a restart with the wall clock behind, commit times can lag true time by at
1327
+ most the downtime plus 60 seconds (a suspended host counts as downtime). A
1328
+ forward jump that is corrected later keeps commit times ahead by the jump
1329
+ for the life of the process. While commit times run ahead of the wall clock,
1330
+ `getStorageStats()` and `getHealth()` report `stampClockAheadOfWallMs`.
1331
+ Within a key, commit times never go backwards, and a version becomes
1332
+ noncurrent at its successor's commit time.
1333
+
1334
+ The reads of an unversioned bucket keep their 11.0.0 semantics:
1086
1335
  - `GET ?versioning` returns the empty `<VersioningConfiguration/>` of a bucket
1087
- that was never versioned. `PUT ?versioning` is `NotImplemented`.
1336
+ that was never versioned.
1088
1337
  - `GET ?lifecycle` returns `NoSuchLifecycleConfiguration` (404). `PUT` and
1089
1338
  `DELETE ?lifecycle` are `NotImplemented`.
1090
1339
  - `GET ?versions` lists every current object as its S3 null version
@@ -1092,11 +1341,12 @@ Versioning is never enabled on a bucket:
1092
1341
  `key-marker` and `version-id-marker`. A truncated page returns the last key
1093
1342
  or common prefix as `NextKeyMarker`, plus `NextVersionIdMarker` `null` after a
1094
1343
  key. A `version-id-marker` needs a `key-marker`.
1095
- - `versionId` on GET, HEAD and DELETE, and a `?versionId=` after the copy source
1096
- of CopyObject and UploadPartCopy: `null` is the current object. A
1097
- well-formed id (a lowercase UUID) names no version and returns
1098
- `NoSuchVersion` (404; `NoSuchBucket` when the bucket is missing). Anything
1099
- else, the empty string included, returns `InvalidArgument` (400). A `?`
1344
+ - `versionId` on GET, HEAD and DELETE, a `VersionId` of a DeleteObjects
1345
+ member, and a `?versionId=` after the copy source of CopyObject and
1346
+ UploadPartCopy: `null` is the current object. A well-formed id (a lowercase
1347
+ UUID) names no version and returns `NoSuchVersion` (404; `NoSuchBucket` when
1348
+ the bucket is missing). Anything else, the empty string included, returns
1349
+ `InvalidArgument` (400); in DeleteObjects both fail only their member. A `?`
1100
1350
  inside a copy-source key must be encoded as `%3F`.
1101
1351
  - These reads are authorized as `s3:GetBucketVersioning`,
1102
1352
  `s3:GetLifecycleConfiguration` and `s3:ListBucketVersions`. A `versionId`,
@@ -1205,6 +1455,12 @@ durable cluster metadata. Descriptor-confined standalone startup reclaims
1205
1455
  abandoned private object materializations from interrupted create-only, copy,
1206
1456
  or multipart publication without removing an already published hard link.
1207
1457
 
1458
+ `CompleteMultipartUpload` refuses, in standalone and cluster mode, a part list
1459
+ that names a part that was never uploaded or an ETag that does not match the
1460
+ uploaded part with `InvalidPart` (`400`), and a part listed twice with
1461
+ `InvalidPartOrder` (`400`). The upload stays open, so a completion with the
1462
+ correct parts still succeeds.
1463
+
1208
1464
  Standalone `DeleteObject` also honors `If-Match: *` and comma-separated strong
1209
1465
  ETags. It compares the current ETag and deletes inside the same bucket mutation
1210
1466
  gate used by PUT, COPY, and multipart completion. A mismatch returns the S3
@@ -1455,16 +1711,79 @@ ahead of the client.
1455
1711
  ### QUIC Transport
1456
1712
 
1457
1713
  Inter-node communication uses [QUIC](https://en.wikipedia.org/wiki/QUIC) via the `quinn` library:
1458
- - **Encrypted, not authenticated** — each node generates a self-signed certificate at startup and accepts any peer; run it only on a trusted network (see [Cluster transport trust](#cluster-transport-trust))
1714
+ - **Mutual TLS or unauthenticated** — with `cluster.tls`, every connection is TLS 1.3 with a CA-signed certificate on both ends; without it, each node generates a self-signed certificate at startup and accepts any peer (see [Cluster transport trust](#cluster-transport-trust))
1459
1715
  - 🔀 **Multiplexed streams** — concurrent shard transfers without head-of-line blocking
1460
1716
  - ⚡ **Connection pooling** — persistent connections to peer nodes
1461
1717
  - 🌊 **Natural backpressure** — QUIC flow control prevents overloading slow peers
1462
1718
 
1463
1719
  ### Cluster transport trust
1464
1720
 
1465
- The cluster transport does not authenticate nodes. Each node generates a self-signed certificate at startup and accepts any peer's certificate without verifying it. QUIC encrypts the traffic against passive eavesdropping, but not against an attacker on the path. Any host that can reach a node's `quicPort` can join the cluster, sync topology, and read, write, tombstone and purge shards. S3 credentials and bucket policies do not apply to this port.
1721
+ Without `cluster.tls` the cluster transport does not authenticate nodes. Each node generates a self-signed certificate at startup and accepts any peer's certificate without verifying it. QUIC encrypts the traffic against passive eavesdropping, but not against an attacker on the path. Any host that can reach a node's `quicPort` can join the cluster, sync topology, and read, write, tombstone and purge shards. S3 credentials and bucket policies do not apply to this port. Run such a cluster only on a network where every host that can reach `quicPort` (UDP, default 4000) is a cluster node: a private network or VPN between the nodes, with `quicPort` firewalled from everything else.
1466
1722
 
1467
- Run cluster mode only on a network where every host that can reach `quicPort` (UDP, default 4000) is a cluster node: a private network or VPN between the nodes, with `quicPort` firewalled from everything else. This has been the case since cluster mode was introduced, and 11.0.0 does not change it. Mutual TLS with a cluster CA and certificate-checked joins are planned.
1723
+ With `cluster.tls` set, the transport is mutual TLS (TLS 1.3, no session resumption, no early data) with a cluster CA you run:
1724
+
1725
+ - **Identity.** A node's identity is the single DNS subject alternative name (SAN) of its certificate, and it equals `cluster.nodeId`. With `cluster.tls`, `nodeId` is required and must be a lowercase DNS name such as `node-1` or `node-1.storage.internal`; it is never generated, and it must still match the node id persisted in the control root. One certificate serves as both the server and the client certificate, so it must be valid for both: an extended key usage extension listing `serverAuth` and `clientAuth`, or no extended key usage extension at all (the recipe below issues both). Use a dedicated CA per cluster: every certificate it signs with one lowercase DNS SAN is a member of the cluster.
1726
+ - **Checks.** A node connects to a known peer only if the peer's certificate names exactly that peer's node id (one DNS SAN, no wildcard), and to a seed (`seedNodes`, known only by its address) only if the seed presents a certificate from the cluster CA. A node accepts a connection only with a client certificate from the cluster CA, and a join or heartbeat only for the node id that certificate names. A peer without such a certificate fails the handshake before it can send any request. Every CA-signed node is a full member: it may sync topology and read, write, tombstone and purge any shard, so the way to remove a compromised node is to stop trusting its certificate (a new CA, see the rotation below).
1727
+ - **Startup.** A node checks its own certificate the way its peers will (chain, validity, server and client authentication, the SAN, and that `keyFile` matches `certFile`) and refuses to start if that check or any file check below fails. The error names the file and the reason. A node never falls back to the unauthenticated transport. At startup it logs one line with its certificate's `notAfter` and the SHA-256 of each CA certificate, and `getClusterHealth().transport` reports them (see [Cluster Health](#cluster-health)).
1728
+ - **Files.** `caFile` holds one CA certificate, or two during a CA rotation; anything else is refused, which keeps a system CA bundle out. `certFile` holds the node certificate, optionally followed by intermediates, and nothing but certificates. `keyFile` holds exactly one unencrypted PKCS#8, SEC1 or PKCS#1 key; ECDSA P-256 is recommended. Each path must be absolute and canonical, with no symbolic link anywhere, and lie outside the control root and every drive root (roots are backed up and replaced, and a key must not travel with them). That check compares paths, so a bind mount that makes a root reachable under another path is not detected. Each file must be a regular file of at most 64 KiB. The files and their directories must be owned by the engine's user or by root and must not be group- or world-writable, and `keyFile` must be owned by the engine's user and grant no group or world permission (mode 0600 or 0400).
1729
+ - **Reload.** The files are read once at startup; a restart is the reload. Certificates are checked at every handshake, so a connection established before a certificate's `notAfter` keeps running until it reconnects, and new handshakes after `notAfter` fail. Renew certificates and restart the nodes before they expire.
1730
+
1731
+ #### Making the CA and the node certificates
1732
+
1733
+ The engine ships no CA tooling. With OpenSSL 3 or LibreSSL 3.3 (the `openssl` of macOS), create the CA once and keep `ca.key` offline. `ec_param_enc:named_curve` is required for LibreSSL, which otherwise writes the curve as explicit parameters that the engine refuses; OpenSSL 3 writes the named curve either way:
1734
+
1735
+ ```bash
1736
+ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
1737
+ -pkeyopt ec_param_enc:named_curve -out ca.key
1738
+ openssl req -x509 -new -key ca.key -sha256 -days 3650 \
1739
+ -subj "/CN=objectstorage cluster CA" \
1740
+ -addext "basicConstraints=critical,CA:TRUE" \
1741
+ -addext "keyUsage=critical,keyCertSign,cRLSign" \
1742
+ -out ca.pem
1743
+ ```
1744
+
1745
+ Then issue one certificate per node, with the node id as its only DNS SAN:
1746
+
1747
+ ```bash
1748
+ NODE_ID=node-1
1749
+ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
1750
+ -pkeyopt ec_param_enc:named_curve -out "$NODE_ID.key"
1751
+ openssl req -new -key "$NODE_ID.key" -subj "/CN=$NODE_ID" -out "$NODE_ID.csr"
1752
+ cat > "$NODE_ID.ext" <<EXT
1753
+ basicConstraints = critical,CA:FALSE
1754
+ keyUsage = critical,digitalSignature
1755
+ extendedKeyUsage = serverAuth,clientAuth
1756
+ subjectAltName = DNS:$NODE_ID
1757
+ EXT
1758
+ openssl x509 -req -in "$NODE_ID.csr" -CA ca.pem -CAkey ca.key -CAcreateserial \
1759
+ -days 365 -sha256 -extfile "$NODE_ID.ext" -out "$NODE_ID.pem"
1760
+ ```
1761
+
1762
+ Install `ca.pem` and the node's certificate and key on the node, as the user that runs the engine and outside its control and drive roots:
1763
+
1764
+ ```bash
1765
+ install -d -m 0755 /etc/objectstorage/tls
1766
+ install -m 0644 ca.pem "$NODE_ID.pem" /etc/objectstorage/tls/
1767
+ install -m 0600 "$NODE_ID.key" /etc/objectstorage/tls/
1768
+ ```
1769
+
1770
+ Then set `cluster.tls` to the three paths (see [Configuration](#configuration)). The test suite runs exactly these commands (`test/test.cluster-tls.node.ts`).
1771
+
1772
+ **Docker secrets** are mounted owned by root with mode 0444 by default, which the key check refuses. Set the key secret's `uid` to the engine's user and its `mode` to `0400` in the service's long `secrets` syntax. Docker Compose ignores `uid`, `gid` and `mode` for secrets read from a file, because it bind-mounts the file; there the host file itself must be owned by the engine's uid with mode 0600 or 0400. Kubernetes secret volumes present their files through symbolic links, which the canonical-path rule refuses.
1773
+
1774
+ #### Enabling mutual TLS
1775
+
1776
+ Nodes with and without `cluster.tls` cannot talk to each other: a TLS node refuses peers without a client certificate and refuses self-signed servers. Enabling TLS is therefore not a rolling change: stop every node, configure `cluster.tls` on every node, then start every node. A node whose `nodeId` was generated keeps that id (the control root persists it, and `getClusterHealth().nodeId` reports it): configure it as `nodeId` and issue the node's certificate for it, or the node refuses to start. Disabling TLS takes the same full stop.
1777
+
1778
+ #### Rotating certificates and the CA
1779
+
1780
+ A node certificate is renewed by issuing a new one from the same CA and restarting the node; its peers need no change. A new CA takes three rolling restarts, with no cluster stop:
1781
+
1782
+ 1. Put both CAs into every node's `caFile` (two certificates) and restart the nodes one at a time.
1783
+ 2. Issue every node a certificate from the new CA and restart the nodes one at a time.
1784
+ 3. Remove the old CA from every `caFile` and restart the nodes one at a time.
1785
+
1786
+ Every pair of nodes stays connected throughout, because trust only grows before the certificates change and only shrinks after. The order matters: a node with a certificate from the new CA cannot connect to a node that trusts only the old one.
1468
1787
 
1469
1788
  ### Cluster Membership
1470
1789
 
@@ -1616,6 +1935,10 @@ Spawn the Rust binary and start the HTTP server. Concurrent starts join the same
1616
1935
  attempt; starting an already running instance succeeds without restarting it.
1617
1936
  A start is rejected while shutdown or unconfirmed startup cleanup remains
1618
1937
  pending. After completed shutdown, an explicit start begins a new lifecycle.
1938
+ A start has no time limit, because startup grows with the stored data (a
1939
+ standalone root with bucket versions checks every version chain); it still
1940
+ fails at once when the engine process exits. A long start shows its progress
1941
+ on the host's stderr, where every engine log line is forwarded.
1619
1942
 
1620
1943
  #### `stop(): Promise<void>`
1621
1944
 
@@ -1847,7 +2170,7 @@ Atomically replace the active runtime credential set without restarting the serv
1847
2170
 
1848
2171
  #### `getClusterHealth(): Promise<IClusterHealth>`
1849
2172
 
1850
- Read the Rust core's current cluster, drive, quorum, and repair health snapshot. Standalone mode returns `{ enabled: false }`.
2173
+ Read the Rust core's current cluster, drive, quorum, repair, and transport health snapshot. Standalone mode returns `{ enabled: false }`.
1851
2174
 
1852
2175
  In cluster mode, `quicAddress` is the local QUIC/UDP socket's actual bound
1853
2176
  address. With `cluster.quicPort: 0`, the operating system assigns its port; use
@@ -1855,6 +2178,20 @@ address. With `cluster.quicPort: 0`, the operating system assigns its port; use
1855
2178
  Membership advertisements and peer `quicAddress` values contain that same bound
1856
2179
  address, including after a restart.
1857
2180
 
2181
+ #### `getClusterSnapshot(): Promise<IObjectStorageClusterSnapshot>`
2182
+
2183
+ The [dashboard](#dashboard)'s snapshot, built by the engine from memory only:
2184
+ `local` is this node's `IObjectStorageNodeSnapshot` (counters, totals, drives
2185
+ from the last heartbeat probe, healing state, recent events) and, in cluster
2186
+ mode, `cluster` adds quorum, the erasure layout and every peer, with the peer's
2187
+ own snapshot when it answered within a second. One snapshot runs at a time; a
2188
+ call while another runs rejects. Standalone mode returns `cluster: null`.
2189
+
2190
+ #### `dashboardPort: number | undefined`
2191
+
2192
+ The dashboard's bound TCP port while it runs; `undefined` while it is off or
2193
+ stopped.
2194
+
1858
2195
  #### `runHealingScan(options?: IRunHealingScanOptions): Promise<IClusterHealingScanResult>`
1859
2196
 
1860
2197
  Run a healing scan on this cluster node (see [Self-Healing](#self-healing)) and
@@ -1913,7 +2250,7 @@ objectstorage uses a **hybrid Rust + TypeScript** architecture:
1913
2250
 
1914
2251
  **Why Rust?** The original TypeScript implementation had critical perf issues: OOM on multipart uploads (parts buffered in memory), double stream copying, file descriptor leaks on HEAD requests, full-file reads for range requests, and no backpressure. The Rust binary solves these with bounded-memory streaming I/O, backpressure, and direct `seek()` for range requests.
1915
2252
 
1916
- **IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `findOrphanObjectLeaves`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getBucketResourceFenceState`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, `createPrefixCredential`, `revokePrefixCredential`, `listPrefixCredentials`, `getPrefixCredentialStatus`, `getClusterHealth`, and `runHealingScan`. `getStoragePoolDiagnostic` reads a weak, cached diagnostic source and remains responsive while another management command owns the server; it does not retain the mounted root after server ownership completes. `runHealingScan` answers when its scan completes, with the request's `id`; the engine serves other commands meanwhile, and a shutdown answers a pending request with an error before the `stop` response.
2253
+ **IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `findOrphanObjectLeaves`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getBucketResourceFenceState`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, `createPrefixCredential`, `revokePrefixCredential`, `listPrefixCredentials`, `getPrefixCredentialStatus`, `getClusterHealth`, `getClusterSnapshot`, and `runHealingScan`. `getStoragePoolDiagnostic` reads a weak, cached diagnostic source and remains responsive while another management command owns the server; it does not retain the mounted root after server ownership completes. `runHealingScan` answers when its scan completes, with the request's `id`; the engine serves other commands meanwhile, and a shutdown answers a pending request with an error before the `stop` response. `getClusterSnapshot` is answered the same deferred way, because a cluster node waits up to a second for each peer.
1917
2254
 
1918
2255
  ### S3-Compatible Operations
1919
2256
 
@@ -1965,6 +2302,8 @@ those versions cannot read the canonical v2 objects.
1965
2302
  | AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId` | |
1966
2303
  | ListMultipartUploads | `GET /{bucket}?uploads` | Optional `prefix` |
1967
2304
  | GetBucketLocation | `GET /{bucket}?location` | Returns `server.region`; an empty constraint for `us-east-1` |
2305
+ | GetBucketVersioning, PutBucketVersioning | `GET`, `PUT /{bucket}?versioning` | Standalone; see [Bucket versioning](#bucket-versioning) |
2306
+ | ListObjectVersions | `GET /{bucket}?versions` | Every version and delete marker of a versioned bucket |
1968
2307
  | GetBucketPolicy | `GET /{bucket}?policy` | |
1969
2308
  | PutBucketPolicy | `PUT /{bucket}?policy` | |
1970
2309
  | DeleteBucketPolicy | `DELETE /{bucket}?policy` | |
@@ -2045,8 +2384,47 @@ prefix credentials must supply one inside their prefix.
2045
2384
  # key leaf appears only with the complete payload
2046
2385
  .smartstorage/credentials.json # Runtime and bucket tenant credentials (0600)
2047
2386
  .smartstorage/prefix-credentials.json # Prefix credentials and revocations (0600)
2387
+ .smartstorage/versioning/v1/{bucket}.json
2388
+ # Versioning state of a versioned bucket
2389
+ .smartstorage/versioning/stamp-clock.json
2390
+ # Persisted floor of the version stamp clock
2391
+ .smartstorage/versions/{bucket}/{whole-key-hex-frames}/
2392
+ {seq:016x}-{generation} # Record of a noncurrent version or delete marker
2393
+ {seq:016x}-{generation}.object # Payload of a noncurrent data version
2048
2394
  ```
2049
2395
 
2396
+ Bucket versioning keeps the layout above for current objects and adds a
2397
+ format that later standalone engines read unchanged:
2398
+ - `.smartstorage/versioning/v1/{bucket}.json` is
2399
+ `{"format":"smartstorage.bucket-versioning.v1","bucket":…,"status":"Enabled"|"Suspended"}`.
2400
+ A bucket without it was never versioned.
2401
+ - The current object of a key stays at its key path. A payload written while
2402
+ its bucket is versioned ends in a trailer record of schema 2 whose `version`
2403
+ holds its generation UUID, whether it is the null version, its sequence
2404
+ number and its commit time in epoch milliseconds; payloads written before
2405
+ keep schema 1 and are the key's null version with sequence 0.
2406
+ - Every other version lives in the key's chain directory below
2407
+ `.smartstorage/versions/{bucket}/`, whose frame directories are the same
2408
+ hexadecimal frames as below `.smartstorage-objects-v2`. A record
2409
+ `{seq:016x}-{generation}` (JSON, `"format":"smartstorage.object-version.v1"`,
2410
+ with the key, sequence, generation, null flag, delete marker flag, commit
2411
+ time, the time it became noncurrent, payload length, ETag and metadata)
2412
+ exists for every noncurrent version and every delete marker; a data version
2413
+ also has `{seq:016x}-{generation}.object`, the payload inode it had while it
2414
+ was current. Sequence order is chain order; the version id is the generation
2415
+ UUID, or `null`.
2416
+ - A payload is linked into the chain before its record exists and unlinked
2417
+ after its record is gone. Startup recovery brings every chain to a
2418
+ consistent state after a crash (it removes payloads without a record,
2419
+ finishes or undoes an interrupted change of the current version, clears the
2420
+ noncurrent time of a delete marker that is current again and keeps one null
2421
+ version per key) and refuses to start on a record that is corrupt or misses
2422
+ its payload, on a chain directory with a foreign entry, and on versions of a
2423
+ bucket without versioning state.
2424
+ - Engines before 11.2.0 cannot read a payload with a schema-2 trailer: a
2425
+ standalone root on which any bucket was ever versioned (`PutBucketVersioning`
2426
+ with `Enabled` or `Suspended`) cannot be rolled back.
2427
+
2050
2428
  Rolling a standalone root back from 10.7.0 or later to 8.0.3–10.6.0: a crash
2051
2429
  between staging and publication of a PUT, CopyObject or CompleteMultipartUpload
2052
2430
  can leave a staged leaf directory
@@ -55,12 +55,28 @@ Developer ID signed or notarized.
55
55
  ## Locked Cargo graph
56
56
 
57
57
  [Cargo dependency notices](./third_party_licenses/cargo-dependencies.html)
58
- contain the selected license texts and original attributions for 260 packages.
59
- The complete locked graph also contains three feature-inactive packages:
58
+ contain the selected license texts and original attributions for 279 packages.
59
+ The complete locked graph also contains 18 feature-inactive packages:
60
60
  [aho-corasick 1.1.4](./third_party_licenses/aho-corasick-1.1.4-license.txt),
61
- [valuable 0.1.1](./third_party_licenses/valuable-license.txt) and
61
+ [borsh 1.8.1](./third_party_licenses/borsh-1.8.1-license.txt),
62
+ [borsh-derive 1.8.1](./third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt),
63
+ [dyn-clone 1.0.20](./third_party_licenses/dyn-clone-1.0.20-license.txt),
64
+ [equivalent 1.0.3](./third_party_licenses/equivalent-1.0.3-license.txt),
65
+ [hashbrown 0.17.1](./third_party_licenses/hashbrown-0.17.1-license.txt),
66
+ [indexmap 2.14.2](./third_party_licenses/indexmap-2.14.2-license.txt),
67
+ [proc-macro-crate 3.5.0](./third_party_licenses/proc-macro-crate-3.5.0-license.txt),
68
+ [ref-cast 1.0.27](./third_party_licenses/ref-cast-1.0.27-license.txt),
69
+ [ref-cast-impl 1.0.27](./third_party_licenses/ref-cast-impl-1.0.27-license.txt),
70
+ [schemars 1.2.3](./third_party_licenses/schemars-1.2.3-license.txt),
71
+ [syn 3.0.9](./third_party_licenses/syn-3.0.9-license.txt),
72
+ [toml_datetime 1.1.2](./third_party_licenses/toml_datetime-1.1.2-license.txt),
73
+ [toml_edit 0.25.17](./third_party_licenses/toml_edit-0.25.17-license.txt),
74
+ [toml_parser 1.1.5](./third_party_licenses/toml_parser-1.1.5-license.txt),
75
+ [valuable 0.1.1](./third_party_licenses/valuable-license.txt),
76
+ [winnow 1.0.4](./third_party_licenses/winnow-1.0.4-license.txt) and
62
77
  [zerocopy-derive 0.8.47](./third_party_licenses/zerocopy-derive-0.8.47-license.txt).
63
- Their exact MIT notices bring the inventory to all 261 locked third-party
78
+ Their exact notices (Apache-2.0 for borsh-derive, which offers no MIT
79
+ alternative, MIT for the others) bring the inventory to all 297 locked third-party
64
80
  packages, conservatively including build-only, development and inactive targets.
65
81
  Listing a package does not claim that it is linked into every distributed executable.
66
82