@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/readme.md ADDED
@@ -0,0 +1,1430 @@
1
+ # @lossless.org/objectstorage
2
+
3
+ A high-performance, S3-compatible storage server powered by a **Rust core** with a clean TypeScript API. Runs standalone for dev/test โ€” or scales out as a **distributed, erasure-coded cluster** with QUIC-based inter-node communication. No cloud, no Docker. Just install the package and go. ๐Ÿš€
4
+
5
+ ## Issue Reporting and Security
6
+
7
+ For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
8
+
9
+ ## Moving from @push.rocks/smartstorage
10
+
11
+ Install `@lossless.org/objectstorage` and update imports and the public API names:
12
+
13
+ | Previous name | New name |
14
+ | --- | --- |
15
+ | `@push.rocks/smartstorage` | `@lossless.org/objectstorage` |
16
+ | `SmartStorage` | `ObjectStorage` |
17
+ | `ISmartStorage*` / `TSmartStorage*` | `IObjectStorage*` / `TObjectStorage*` |
18
+ | `SmartStorageResourceFenceError` | `ObjectStorageResourceFenceError` |
19
+ | `computeSmartStorageResourceFenceEffectivePayloadSha256V1` | `computeObjectStorageResourceFenceEffectivePayloadSha256V1` |
20
+ | `smartStorageResourceFenceErrorCodes` | `objectStorageResourceFenceErrorCodes` |
21
+ | Prometheus `smartstorage_*` metric families | Prometheus `objectstorage_*` metric families |
22
+ | Test-only `SMARTSTORAGE_RESOURCE_FENCE_TEST_*` variables | `OBJECTSTORAGE_RESOURCE_FENCE_TEST_*` variables |
23
+
24
+ Update monitoring queries and dashboards alongside the package. The new module
25
+ exports the new API names without aliases for the old names. Generic types such
26
+ as `IStorageConfig`, `IBucketExport`, and `IClusterHealth` keep their names.
27
+
28
+ The rename preserves the current storage and cluster protocols. Existing bucket
29
+ roots, credentials, retention state, migration receipts, and resource-fencing
30
+ identities keep their byte formats. Versioned `smartstorage.*` format identifiers
31
+ and hash domains, `.smartstorage*` filesystem names, retention metadata keys,
32
+ the internal streaming-verification header, and the QUIC `smartstorage` protocol
33
+ identity remain unchanged. Do not rewrite these values in existing data.
34
+ The native `ruststorage` executable and its Linux amd64/arm64 artifact names also
35
+ remain unchanged. Earlier storage-layout migrations retain their existing rules
36
+ described below.
37
+
38
+ ## Why objectstorage?
39
+
40
+ | Feature | objectstorage | MinIO | s3rver |
41
+ |---------|-------------|-------|--------|
42
+ | Install | `pnpm add` | Docker / binary | `npm install` |
43
+ | Startup time | ~20ms | seconds | ~200ms |
44
+ | Large file uploads | Streaming, bounded memory | Yes | OOM risk |
45
+ | Range requests | Seek-based | Yes | Full read |
46
+ | Language | Rust + TypeScript | Go | JavaScript |
47
+ | Multipart uploads | โœ… Full support | Yes | No |
48
+ | Auth | AWS SigV4 (full verification) | Full IAM | Basic |
49
+ | Bucket policies | IAM-style evaluation | Yes | No |
50
+ | Clustering | โœ… Erasure-coded, QUIC | Yes | No |
51
+ | Multi-drive awareness | โœ… Per-drive health | Yes | No |
52
+
53
+ ### Core Features
54
+
55
+ - ๐Ÿฆ€ **Rust-powered HTTP server** โ€” hyper 1.x with streaming I/O, bounded buffering, backpressure
56
+ - ๐Ÿ“ฆ **S3-compatible API** โ€” supported operations work with AWS SDK v3 and SmartBucket
57
+ - ๐Ÿ’พ **Filesystem-backed storage** โ€” buckets map to directories, objects to files
58
+ - ๐Ÿ“ค **Streaming multipart uploads** โ€” large files with bounded memory use
59
+ - ๐Ÿ“ **Byte-range requests** โ€” `seek()` directly to the requested byte offset
60
+ - ๐Ÿ” **AWS SigV4 authentication** โ€” full signature verification with constant-time comparison
61
+ - ๐Ÿ“‹ **Bucket policies** โ€” IAM-style JSON policies with Allow/Deny evaluation and wildcard matching
62
+ - ๐ŸŒ **CORS middleware** โ€” configurable cross-origin support
63
+ - ๐Ÿงน **Clean slate mode** โ€” wipe storage on startup for test isolation
64
+ - ๐Ÿ“Š **Runtime storage stats** โ€” cheap bucket summaries and global counts without S3 list scans
65
+ - ๐Ÿ”‘ **Runtime credential rotation** โ€” list and replace active auth credentials without mutating internals
66
+ - ๐Ÿงฉ **Bucket tenants** โ€” provision one scoped S3 credential per bucket with restart persistence
67
+ - โšก **Test-first design** โ€” start/stop in milliseconds, no port conflicts
68
+
69
+ ### Clustering Features
70
+
71
+ - ๐Ÿ”— **Erasure coding** โ€” Reed-Solomon (configurable k data + m parity shards) for storage efficiency and fault tolerance
72
+ - ๐Ÿš„ **QUIC transport** โ€” multiplexed, encrypted inter-node communication via `quinn` with zero head-of-line blocking
73
+ - ๐Ÿ’ฝ **Multi-drive awareness** โ€” each node manages multiple independent storage paths with health monitoring
74
+ - ๐Ÿฉบ **Cluster health introspection** โ€” query native node, drive, quorum, and healing status for product dashboards
75
+ - ๐Ÿค **Cluster membership** โ€” static seed config + runtime join, heartbeat-based failure detection
76
+ - โœ๏ธ **Quorum writes** โ€” data is only acknowledged after k+1 shards are persisted
77
+ - ๐Ÿ“– **Quorum reads** โ€” reconstruct from any k available shards, local-first fast path
78
+ - ๐Ÿฉน **Self-healing** โ€” background scanner detects and reconstructs missing/corrupt shards
79
+
80
+ ## Installation
81
+
82
+ ```bash
83
+ pnpm add @lossless.org/objectstorage -D
84
+ ```
85
+
86
+ > **Note:** The package ships with precompiled Rust binaries for `linux_amd64` and `linux_arm64`. No Rust toolchain needed on your machine.
87
+
88
+ ## Quick Start
89
+
90
+ ### Standalone Mode (Dev & Test)
91
+
92
+ ```typescript
93
+ import { ObjectStorage } from '@lossless.org/objectstorage';
94
+
95
+ // Start a local S3-compatible storage server
96
+ const storage = await ObjectStorage.createAndStart({
97
+ server: { port: 3000 },
98
+ storage: { cleanSlate: true },
99
+ });
100
+
101
+ // Create a bucket
102
+ await storage.createBucket('my-bucket');
103
+
104
+ // Get connection details for a supported S3 client
105
+ const descriptor = await storage.getStorageDescriptor();
106
+ // โ†’ { endpoint: 'localhost', port: 3000, accessKey: 'STORAGE', accessSecret: 'STORAGE', useSsl: false }
107
+
108
+ // When done
109
+ await storage.stop();
110
+ ```
111
+
112
+ ### Cluster Mode (Distributed)
113
+
114
+ Cluster mode requires the control root and every drive root to exist before
115
+ startup. The process user must own them, no path component may be a symlink,
116
+ and group/world write permission must be disabled.
117
+
118
+ ```typescript
119
+ import { ObjectStorage } from '@lossless.org/objectstorage';
120
+
121
+ const storage = await ObjectStorage.createAndStart({
122
+ server: { port: 3000 },
123
+ storage: {
124
+ directory: '/var/lib/objectstorage/control',
125
+ cleanSlate: false,
126
+ },
127
+ cluster: {
128
+ enabled: true,
129
+ nodeId: 'node-1',
130
+ quicPort: 4000,
131
+ seedNodes: ['192.168.1.11:4000', '192.168.1.12:4000'],
132
+ erasure: {
133
+ dataShards: 4, // k: minimum shards to reconstruct data
134
+ parityShards: 2, // m: fault tolerance (can lose up to m shards)
135
+ },
136
+ drives: {
137
+ paths: ['/mnt/disk1', '/mnt/disk2', '/mnt/disk3'],
138
+ },
139
+ },
140
+ });
141
+ ```
142
+
143
+ Objects are automatically split into chunks (default 4 MB), erasure-coded into 6 shards (4 data + 2 parity), and distributed across drives/nodes. Any 4 of 6 shards can reconstruct the original data.
144
+
145
+ ## Configuration
146
+
147
+ All config fields are optional โ€” sensible defaults are applied automatically.
148
+
149
+ ```typescript
150
+ import { ObjectStorage, IObjectStorageConfig } from '@lossless.org/objectstorage';
151
+
152
+ const config: IObjectStorageConfig = {
153
+ server: {
154
+ port: 3000, // Default: 3000
155
+ address: '0.0.0.0', // Default: '0.0.0.0'
156
+ silent: false, // Default: false
157
+ region: 'us-east-1', // Default: 'us-east-1' โ€” used for SigV4 signing
158
+ },
159
+ storage: {
160
+ directory: './my-data', // Default: .nogit/bucketsDir
161
+ cleanSlate: false, // Default: false โ€” set true to wipe on start
162
+ pool: { // Optional explicit identity for this process' one pool
163
+ id: 'fast-local',
164
+ directory: './my-data',
165
+ backend: { kind: 'localFs' },
166
+ },
167
+ },
168
+ auth: {
169
+ enabled: false, // Default: false
170
+ credentials: [{
171
+ accessKeyId: 'MY_KEY',
172
+ secretAccessKey: 'MY_SECRET',
173
+ }],
174
+ },
175
+ cors: {
176
+ enabled: false, // Default: false
177
+ allowedOrigins: ['*'],
178
+ allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'],
179
+ allowedHeaders: ['*'],
180
+ exposedHeaders: ['ETag', 'x-amz-request-id', 'x-amz-version-id'],
181
+ maxAge: 86400,
182
+ allowCredentials: false,
183
+ },
184
+ logging: {
185
+ level: 'info', // 'error' | 'warn' | 'info' | 'debug'
186
+ format: 'text', // 'text' | 'json'
187
+ enabled: true,
188
+ },
189
+ limits: {
190
+ maxObjectSize: 5 * 1024 * 1024 * 1024, // 5 GB
191
+ maxMetadataSize: 2048,
192
+ requestTimeout: 300000, // 5 minutes
193
+ },
194
+ multipart: {
195
+ expirationDays: 7, // Must be greater than zero
196
+ cleanupIntervalMinutes: 60, // Must be greater than zero
197
+ },
198
+ cluster: { // Optional โ€” omit for standalone mode
199
+ enabled: true,
200
+ nodeId: 'node-1', // Auto-generated UUID if omitted
201
+ quicPort: 4000, // Default: 4000
202
+ seedNodes: [], // Addresses of existing cluster members
203
+ erasure: {
204
+ dataShards: 4, // Default: 4
205
+ parityShards: 2, // Default: 2
206
+ chunkSizeBytes: 4194304, // Default: 4 MB
207
+ },
208
+ drives: {
209
+ paths: ['/mnt/disk1', '/mnt/disk2'],
210
+ },
211
+ heartbeatIntervalMs: 5000, // Default: 5000
212
+ heartbeatTimeoutMs: 30000, // Default: 30000
213
+ },
214
+ };
215
+
216
+ const storage = await ObjectStorage.createAndStart(config);
217
+ ```
218
+
219
+ ### Storage Pools and Mounted-Filesystem Serving
220
+
221
+ One ObjectStorage process serves one storage pool. Existing `storage.directory`
222
+ configuration remains compatible and normalizes to a `default` `localFs` pool.
223
+ For `localFs`, `storage.directory` and `storage.pool.directory` must resolve to
224
+ the same path when both are supplied. For `mountedFs`, both values must instead
225
+ be lexically exact absolute mountpoints and identical strings. A root is durably
226
+ bound to its pool ID and cannot later be reassigned to a different pool.
227
+
228
+ For standalone `localFs` pools, ObjectStorage creates a missing root with `0700`
229
+ permissions. An existing root must be owned by the process user and must not be
230
+ group- or world-writable; startup fails before opening the S3 listener when this
231
+ contract is not met. ObjectStorage never silently changes an existing root's
232
+ ownership or permissions.
233
+
234
+ On Linux, standalone ObjectStorage can serve an exact NFS or SMB mount. The
235
+ configured directory itself must be the absolute mountpoint (not a symlink or
236
+ canonical alias), `expectedSource` must contain a numeric IPv4 or IPv6 address,
237
+ the client mount must enable the Linux `nosymfollow` VFS option, and
238
+ `cleanSlate` is forbidden. Mounted pools are not supported in cluster mode.
239
+ Onebox and other mount-provisioning clients must include `nosymfollow` in their
240
+ NFS or CIFS mount options before ObjectStorage starts. NFS mounts must keep
241
+ server-coordinated locking enabled: `nolock` and
242
+ `local_lock=all|flock` are rejected, while an absent `local_lock` option,
243
+ `local_lock=none`, or `local_lock=posix` is accepted because ObjectStorage uses
244
+ `flock(2)` for its provider locks. CIFS mounts must not use `nobrl`.
245
+ Accepted source forms are `IP:/absolute/export` for NFS and
246
+ `//IP/share[/path]` for SMB. IPv6 addresses use brackets, for example
247
+ `[2001:db8::10]:/archive` and `//[2001:db8::10]/archive`.
248
+
249
+ Startup opens the mountpoint once and confines every standalone storage,
250
+ credential, policy, retention, fencing, and background operation beneath that
251
+ descriptor through `/proc/self/fd`. It verifies the descriptor's mount ID,
252
+ device, NFS/CIFS filesystem magic, and authoritative `ST_NOSYMFOLLOW` VFS flag,
253
+ plus the effective mount and superblock locking options, then rereads the
254
+ complete mount activation before serving. Every storage
255
+ boundary revalidates the activation and `ST_NOSYMFOLLOW`. Any drift permanently
256
+ poisons the process, fails storage-derived HTTP and management boundaries
257
+ closed, never follows descendant symlinks, and never falls back to a host
258
+ directory at the configured path. Lifecycle, liveness, cluster-status, and
259
+ cached-diagnostic operations remain available for shutdown and diagnosis.
260
+
261
+ Every server mode stops accepting HTTP connections, gracefully drains each
262
+ accepted connection, and joins independently owned request and multipart
263
+ workers before releasing the mount descriptor or root-maintenance lock. The
264
+ drain is bounded to 30 seconds. A timeout is reported as an incomplete stop:
265
+ the Rust process retains the server, descriptors, locks, and unfinished task
266
+ handles so the caller can retry `stop()` after hard-mounted I/O recovers.
267
+ Streamed object reads move their file descriptor into the same owned worker
268
+ lifecycle, so a disconnected client cannot detach a blocked filesystem read.
269
+ Active HTTP request lifecycles are capped at 4,096; excess requests receive a
270
+ retryable `503 Service Unavailable` response with `Retry-After: 1`. A terminal
271
+ shutdown result is reported only after all owned work is complete and remains
272
+ available to retrying stop waiters until the management process terminates.
273
+
274
+ ```typescript
275
+ const storage = await ObjectStorage.createAndStart({
276
+ storage: {
277
+ directory: '/mnt/archive',
278
+ pool: {
279
+ id: 'archive',
280
+ directory: '/mnt/archive',
281
+ backend: {
282
+ kind: 'mountedFs',
283
+ expectedFilesystemType: 'nfs',
284
+ expectedSource: '192.0.2.10:/archive',
285
+ // IPv6 is canonicalized as: [2001:db8::10]:/archive
286
+ },
287
+ },
288
+ },
289
+ });
290
+ ```
291
+
292
+ Serving startup also runs the root-ownership, same-filesystem,
293
+ server-coordinated lock-contention, directory-fsync, and durable
294
+ provider-identity checks through the descriptor anchor. A mounted runtime
295
+ observation is `productionEligible` after these runtime checks pass and while
296
+ the activation is unpoisoned. Atomic directory exchange is reported separately
297
+ as an optional capability: NFS filesystems without it can still serve S3,
298
+ atomic create-only uploads, fenced bucket ensure/delete, and migration cleanup,
299
+ while atomic whole-bucket replacement and isolated restore remain unavailable
300
+ and fail closed. Its `remoteLocking` check and
301
+ `filesystem.remoteLockingMode` observation distinguish server-coordinated
302
+ locking from unsafe local or disabled locking. `getStoragePoolDiagnostic()`
303
+ remains available after poison and returns cached activation identity and
304
+ poison evidence without accessing the storage filesystem.
305
+
306
+ Probe an existing exact mount without starting the S3 listener:
307
+
308
+ ```typescript
309
+ const observation = await ObjectStorage.probeStoragePool({
310
+ id: 'archive',
311
+ directory: '/mnt/archive',
312
+ backend: {
313
+ kind: 'mountedFs',
314
+ expectedFilesystemType: 'nfs',
315
+ expectedSource: '192.0.2.10:/archive',
316
+ },
317
+ });
318
+
319
+ console.log(observation.supported);
320
+ console.log(observation.productionEligible); // false: this read-only probe skips write semantics
321
+ console.log(observation.checks);
322
+ ```
323
+
324
+ The read-only probe verifies the exact active mount, numeric source,
325
+ descriptor-bound mount ID, device, filesystem magic, `ST_NOSYMFOLLOW`, and
326
+ server-coordinated locking options, and activation stability.
327
+ It reports observed block and transfer sizes but does not change mount tuning or
328
+ write to the target. Mutating semantic checks remain `notRun`, so a successful
329
+ diagnostic probe keeps `supported` at `null` and `productionEligible` at
330
+ `false`; production eligibility is established only by serving startup.
331
+
332
+ ### Common Configurations
333
+
334
+ **CI/CD testing** โ€” silent, clean, fast:
335
+ ```typescript
336
+ const storage = await ObjectStorage.createAndStart({
337
+ server: { port: 9999, silent: true },
338
+ storage: { cleanSlate: true },
339
+ });
340
+ ```
341
+
342
+ **Auth enabled:**
343
+ ```typescript
344
+ const storage = await ObjectStorage.createAndStart({
345
+ auth: {
346
+ enabled: true,
347
+ credentials: [{ accessKeyId: 'test', secretAccessKey: 'test123' }],
348
+ },
349
+ });
350
+ ```
351
+
352
+ **CORS for local web dev:**
353
+ ```typescript
354
+ const storage = await ObjectStorage.createAndStart({
355
+ cors: {
356
+ enabled: true,
357
+ allowedOrigins: ['http://localhost:5173'],
358
+ allowCredentials: true,
359
+ },
360
+ });
361
+ ```
362
+
363
+ ## Runtime Credentials
364
+
365
+ ```typescript
366
+ const credentials = await storage.listCredentials();
367
+
368
+ await storage.replaceCredentials([
369
+ {
370
+ accessKeyId: 'ADMINA',
371
+ secretAccessKey: 'super-secret-a',
372
+ },
373
+ {
374
+ accessKeyId: 'ADMINB',
375
+ secretAccessKey: 'super-secret-b',
376
+ },
377
+ ]);
378
+ ```
379
+
380
+ ```typescript
381
+ interface IStorageCredential {
382
+ accessKeyId: string;
383
+ secretAccessKey: string;
384
+ bucketName?: string;
385
+ region?: string;
386
+ }
387
+ ```
388
+
389
+ - `listCredentials()` returns the Rust core's current runtime credential set.
390
+ - `replaceCredentials()` swaps the full set atomically and persists it under the storage root. On success, new requests use the new set immediately and the old credentials stop authenticating immediately.
391
+ - Requests that were already authenticated before the replacement keep running; auth is evaluated when each request starts.
392
+ - No restart is required, and runtime-created credentials survive restart unless `storage.cleanSlate` clears the bounded storage contents; the owned root, maintenance-lock inode, and any durable provider/pool identity are preserved.
393
+ - Replacement input must contain at least one credential, each `accessKeyId` and `secretAccessKey` must be non-empty, and `accessKeyId` values must be unique.
394
+
395
+ ## Bucket Tenants
396
+
397
+ Bucket tenants are designed for platform services that need one bucket and one scoped S3 credential per app. Tenant credentials are enforced by the auth layer before the normal bucket-policy/default-auth pipeline, so a scoped credential cannot list all buckets or access another bucket even when it has a valid SigV4 signature.
398
+
399
+ ```typescript
400
+ const tenant = await storage.createBucketTenant({
401
+ bucketName: 'workapp-123',
402
+ });
403
+
404
+ // Directly usable by AWS SDK v3 or env injection
405
+ const client = new S3Client({
406
+ endpoint: `http://${tenant.endpoint}:${tenant.port}`,
407
+ region: tenant.region,
408
+ credentials: {
409
+ accessKeyId: tenant.accessKeyId,
410
+ secretAccessKey: tenant.secretAccessKey,
411
+ },
412
+ forcePathStyle: true,
413
+ });
414
+
415
+ console.log(tenant.env.S3_BUCKET);
416
+ console.log(tenant.env.AWS_ACCESS_KEY_ID);
417
+ ```
418
+
419
+ ```typescript
420
+ await storage.rotateBucketTenantCredentials({ bucketName: 'workapp-123' });
421
+ await storage.deleteBucketTenant({ bucketName: 'workapp-123', accessKeyId: tenant.accessKeyId });
422
+ const descriptor = await storage.getBucketTenantDescriptor({ bucketName: 'workapp-123' });
423
+ const tenants = await storage.listBucketTenants();
424
+ ```
425
+
426
+ - `createBucketTenant()` creates the bucket if needed and stores a scoped credential for that bucket.
427
+ - `rotateBucketTenantCredentials()` replaces the active scoped credential for the bucket and persists the new credential.
428
+ - `deleteBucketTenant({ bucketName, accessKeyId })` revokes one scoped credential and keeps the bucket.
429
+ - `deleteBucketTenant({ bucketName })` revokes scoped credentials for an existing tenant bucket and deletes that bucket's contents recursively.
430
+ - Tenant credentials can list, read, write, and delete objects in their assigned bucket, but cannot list all buckets, access other buckets, copy from other buckets, delete buckets, or mutate bucket policies.
431
+ - Bucket tenant APIs require `auth.enabled: true`.
432
+
433
+ ## Bucket Backup/Restore
434
+
435
+ ```typescript
436
+ const appBackup = await storage.exportBucket({ bucketName: 'workapp-123' });
437
+ await storage.importBucket({ bucketName: 'workapp-123-restore', source: appBackup });
438
+ ```
439
+
440
+ - `exportBucket()` returns a self-contained `smartstorage.bucket.v1` JSON export with only the selected bucket's objects and object metadata. The legacy whole-bucket management export is intentionally bounded to 10,000 objects, 48 MiB of payload, and 8 MiB of serialized keys plus metadata; use the migration transfer surface for larger buckets.
441
+ - `importBucket()` validates object payload size and MD5 before creating the target bucket if needed, then restores the exported objects into that bucket.
442
+ - Exports do not include credentials, policies, or unrelated tenant data.
443
+
444
+ ## Bucket Migration Control
445
+
446
+ Standalone storage exposes durable, provider-bound migration control records.
447
+ Discover the local provider identity, receipt key, and cleanup support with
448
+ `getBucketMigrationCapability()`. Create each side with
449
+ `createSourceMigration()` and `createDestinationMigration()`, then advance only
450
+ the explicit source (`active` โ†’ `sealed` โ†’ `cutoverCommitted` โ†’ `retired`) and
451
+ destination (`holding` โ†’ `verified` โ†’ `cutoverAuthorized` โ†’ `published`) paths.
452
+ Before cutover, a destination `aborted` terminal receipt can release a sealed
453
+ source. Every call binds a positive fence token, exact migration binding, and
454
+ opaque controller capability; exact same-token retries replay the durable
455
+ receipt while stale or mismatched requests fail.
456
+
457
+ Source creation additionally requires `sourceCleanupAccessKeyId`. ObjectStorage
458
+ verifies that it is the bucket's one exact scoped credential, durably binds its
459
+ hash before creating migration state, and revalidates that ownership before
460
+ every source phase transition and before persisting a cleanup request. A
461
+ provider can have only one nonterminal migration per role and logical bucket;
462
+ terminal records remain as history and do not prevent a later migration ID.
463
+ The standalone registry has a finite capacity of 100,000 migration states.
464
+ Terminal receipts and completed-cleanup tombstones continue to consume that
465
+ capacity because they are retained permanently; controllers must treat
466
+ migration IDs as lifecycle records and capacity-plan before exhaustion.
467
+ ObjectStorage serializes registry creation globally and atomically rejects a new
468
+ migration ID when 100,000 states already exist. Exact replays of existing IDs
469
+ remain available at the bound; the provider never deletes history or permits
470
+ manual registry-file reuse to make capacity.
471
+ Registry enumeration budgets durable locks, canonical crash-left atomic
472
+ temporary files, and unrecognized junk separately. This leaves bounded recovery
473
+ headroom at full state capacity without deleting a temporary file another
474
+ process may still own; every partition remains finite and fails closed when its
475
+ own limit is exceeded.
476
+
477
+ Source sealing drains bucket mutations and blocks later object, multipart,
478
+ policy, bucket, tenant, and fenced mutation paths. Reads, heads, listings,
479
+ policy reads, and multipart listings remain available while sealed, but they
480
+ hold the bucket data-plane read guard so a later retirement cleanup drains every
481
+ accepted source read before deletion. A retained bucket cannot become a
482
+ migration source.
483
+
484
+ After the destination is durably `published` and the source is durably
485
+ `retired`, the controller can permanently retire the source data:
486
+
487
+ ```typescript
488
+ const sourceState = await source.inspectBucketMigration({ migrationId });
489
+ const destinationState = await destination.inspectBucketMigration({ migrationId });
490
+
491
+ const cleanup = await source.cleanupSourceMigration({
492
+ binding,
493
+ cleanupFenceToken: 1,
494
+ opaqueCapability: sourceCapability,
495
+ sourceRetiredReceiptSha256: sourceState!.terminalReceipt!.receiptSha256,
496
+ destinationPublishedReceipt: destinationState!.terminalReceipt!,
497
+ resourceFence: {
498
+ version: 1,
499
+ scopeId: `${binding.authorityId}.${binding.logicalBucket}`,
500
+ token: 42,
501
+ mutationId: `${migrationId}.source-cleanup`,
502
+ payloadSha256: cleanupIntentSha256,
503
+ },
504
+ accessKeyId: sourceTenant.accessKeyId,
505
+ });
506
+ ```
507
+
508
+ `cleanupSourceMigration()` first persists a permanent requested cleanup
509
+ subrecord that binds the exact retired-source receipt, fully verified
510
+ destination-published receipt, cleanup token, resource fence, and hashed access
511
+ key. It then reuses exact fenced deletion to remove bucket data, every matching
512
+ multipart upload, the exact credential, and the bucket policy before persisting
513
+ the canonical cleanup receipt and generic delete-result digest. Multipart
514
+ deletion immediately reconciles the runtime admission counter and cleanup queue.
515
+ The lock order drains reads and mutations before credential revocation.
516
+
517
+ The controller owns recovery: ObjectStorage never starts source deletion
518
+ automatically. Retry the exact `cleanupSourceMigration()` request after any
519
+ timeout, process exit, or ambiguous response. A requested-only retry repeats
520
+ the exact resource deletion; if deletion was already durably complete, its
521
+ generic receipt is replayed and the migration cleanup record is finalized.
522
+ After completion the request cannot be rebound, the bucket can never be
523
+ recreated or credentialed, and unrelated global credential replacement resumes.
524
+ Generic bucket deletion and multipart abort remain blocked throughout the
525
+ migration seal.
526
+
527
+ `inspectBucketMigration()` returns the durable phase, terminal receipts, and
528
+ the optional `sourceCleanup` requested/completed subrecord. Terminal receipts
529
+ and completed cleanup receipts remain available across restart.
530
+ `storage.cleanSlate: true` cannot erase or bypass that history: standalone
531
+ startup is refused whenever any durable migration state exists, including a
532
+ terminal receipt or completed-cleanup tombstone.
533
+
534
+ ## Durable Resource Fencing
535
+
536
+ Standalone storage with `cleanSlate: false` supports monotonic, crash-safe bucket
537
+ replacement and deletion. The legacy immediate mode (`holdPublication` omitted
538
+ or `false`) still requires callers to drain normal writes before issuing a
539
+ fenced mutation.
540
+
541
+ ```typescript
542
+ const fence = {
543
+ version: 1 as const,
544
+ scopeId: 'corestore-node-1.workapp-123',
545
+ token: 42,
546
+ mutationId: 'delete-workapp-123',
547
+ payloadSha256: 'a'.repeat(64),
548
+ };
549
+
550
+ const result = await storage.deleteBucket({
551
+ bucketName: 'workapp-123',
552
+ accessKeyId: tenant.accessKeyId,
553
+ fence,
554
+ });
555
+ ```
556
+
557
+ For control planes that cannot atomically publish the provider result, request a
558
+ durable write-publication hold:
559
+
560
+ ```typescript
561
+ const held = await storage.importBucket({
562
+ bucketName: 'workapp-123',
563
+ source: appBackup,
564
+ accessKeyId: tenant.accessKeyId,
565
+ fence,
566
+ holdPublication: true,
567
+ });
568
+
569
+ // Persist the provider result and update the control-plane resource first.
570
+ const release = await storage.commitResourcePublication({
571
+ barrier: held.resourcePublicationBarrier!,
572
+ resourceFenceReceipt: held.resourceFence!,
573
+ });
574
+ ```
575
+
576
+ `holdPublication: true` is part of the effective mutation identity. ObjectStorage
577
+ drains active target-bucket mutations before the destructive provider operation,
578
+ then keeps all new target mutations behind a durable barrier after the provider
579
+ effects and receipt are fully persisted. Mutations include object, multipart,
580
+ bucket, policy, tenant, and credential changes. They receive retryable S3
581
+ `WritePublicationHeld` (`503`, `Retry-After: 1`) or
582
+ `EFENCE_PUBLICATION_HELD` through the management API. Unrelated buckets remain
583
+ available. Reads are not blocked, so this protocol controls write publication;
584
+ it does not hide newly restored data from readers.
585
+
586
+ The exact mutation replay returns the same barrier. Different and higher-token
587
+ mutations cannot clear it. `commitResourcePublication()` validates the complete
588
+ barrier, the canonical fence receipt, and its 256-bit opaque commit capability;
589
+ it is safe to retry and returns the same release receipt. Treat
590
+ `commitCapability` as a secret: do not log it or persist it outside protected
591
+ control-plane state. A successful commit removes the raw capability from provider
592
+ state and retains only its hash.
593
+
594
+ There is no timeout, shutdown release, or administrative bypass. A held barrier
595
+ survives restart and is restored before the listener accepts requests. Invalid,
596
+ unknown, oversized, or insecure publication state fails startup closed.
597
+ `replaceCredentials()` is also rejected while any publication remains held.
598
+ Capability discovery keeps `requiresDrain: true` for legacy immediate mode and
599
+ reports `publicationHoldSupported: true`, `publicationHoldVersion: 1`, and
600
+ `publicationHoldRequiresExternalDrain: false` when the provider can enforce the
601
+ hold internally.
602
+
603
+ When supplied, the exact `accessKeyId` is bound into the durable mutation
604
+ identity. Its delete receipt is committed only after bucket data is durably
605
+ absent, that exact bucket-scoped credential is durably revoked, and the bucket
606
+ policy is durably absent. Retrying the same fence returns the stored receipt.
607
+ Recovery after a process crash repeats the credential and policy cleanup
608
+ idempotently, so `deletedCredentials` can be `0`; a higher fence against an
609
+ already absent bucket can likewise return `bucketDeleted: false`. Completed
610
+ exact deletes return `credentialsPreserved: false` and
611
+ `policyPreserved: false`.
612
+
613
+ For backward compatibility, omitting `accessKeyId` retains the original fenced
614
+ data-only delete identity and receipt: tenant credentials and bucket policy are
615
+ preserved, and both preservation fields are `true`.
616
+
617
+ After an exact delete, a new higher-token authority can recreate the same target
618
+ without removing its durable fence history. Call `ensureBucketTenant()` with the
619
+ new fence first, then issue an exact fenced `importBucket()` with the same
620
+ resource token. Ensure receipts use the
621
+ `smartstorage.bucket.ensure.v1` profile and attest the applied access key,
622
+ provider-computed secret hash, and effective region; the secret itself is never
623
+ written to the receipt or fencing registry.
624
+
625
+ `computeObjectStorageResourceFenceEffectivePayloadSha256V1()` computes the same
626
+ canonical effective payload digest as the Rust provider for replacement,
627
+ exact-delete, and ensure profiles. A completed fenced ensure returns its receipt
628
+ on `descriptor.resourceFence`.
629
+
630
+ Absent-bucket fenced ensure removes any residual policy before creating the new
631
+ resource; existing-bucket ensure preserves policy and object data. Once a bucket
632
+ has durable fence history, legacy `createBucket()`, `createBucketTenant()`,
633
+ `rotateBucketTenantCredentials()`, `deleteBucketTenant()`, and unfenced
634
+ `importBucket()` mutations fail with `EFENCE_REQUIRED`. Use fenced ensure,
635
+ exact import, and exact delete for its lifecycle.
636
+
637
+ `replaceCredentials()` is a trusted process-local administrative override, not
638
+ a fenced lifecycle API. If it changes exact-owned credential material, completed
639
+ receipt replay detects the drift and fails closed with an ownership or durable
640
+ state error.
641
+
642
+ ## Health and Metrics APIs
643
+
644
+ ```typescript
645
+ const health = await storage.getHealth();
646
+ const metrics = await storage.getMetrics();
647
+ ```
648
+
649
+ - `getHealth()` reports running state, storage directory and pool observation, auth enabled state, credential counts, bucket count, object count, total bytes, and cluster health.
650
+ - `getMetrics()` returns numeric counters and a Prometheus text snippet for bucket, object, byte, tenant credential, and cluster-enabled metrics.
651
+
652
+ ## Runtime Stats
653
+
654
+ ```typescript
655
+ const stats = await storage.getStorageStats();
656
+ const bucketSummaries = await storage.listBucketSummaries();
657
+
658
+ console.log(stats.bucketCount);
659
+ console.log(stats.totalObjectCount);
660
+ console.log(stats.totalStorageBytes);
661
+ console.log(bucketSummaries[0]?.name, bucketSummaries[0]?.objectCount);
662
+ ```
663
+
664
+ ```typescript
665
+ interface IBucketSummary {
666
+ name: string;
667
+ objectCount: number;
668
+ totalSizeBytes: number;
669
+ creationDate?: number;
670
+ }
671
+
672
+ interface IStorageLocationSummary {
673
+ path: string;
674
+ totalBytes?: number;
675
+ availableBytes?: number;
676
+ usedBytes?: number;
677
+ pool?: IStoragePoolObservation;
678
+ }
679
+
680
+ interface IStorageStats {
681
+ bucketCount: number;
682
+ totalObjectCount: number;
683
+ totalStorageBytes: number;
684
+ buckets: IBucketSummary[];
685
+ storageDirectory: string;
686
+ storageLocations?: IStorageLocationSummary[];
687
+ }
688
+ ```
689
+
690
+ - `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.
691
+ - 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.
692
+ - Values are exact for mutations performed through objectstorage after startup. Direct filesystem edits outside objectstorage are not watched; restart the server to resync.
693
+ - `storageLocations` is a cheap filesystem-capacity snapshot. Standalone mode reports the storage directory plus its pool observation. Cluster mode reports the configured drive paths.
694
+
695
+ ## Cluster Health
696
+
697
+ ```typescript
698
+ const clusterHealth = await storage.getClusterHealth();
699
+
700
+ if (!clusterHealth.enabled) {
701
+ console.log('Cluster mode is disabled');
702
+ } else {
703
+ console.log(clusterHealth.nodeId, clusterHealth.quorumHealthy);
704
+ console.log(clusterHealth.peers);
705
+ console.log(clusterHealth.drives);
706
+ }
707
+ ```
708
+
709
+ ```typescript
710
+ interface IClusterHealth {
711
+ enabled: boolean;
712
+ nodeId?: string;
713
+ quorumHealthy?: boolean;
714
+ majorityHealthy?: boolean;
715
+ peers?: IClusterPeerHealth[];
716
+ drives?: IClusterDriveHealth[];
717
+ erasure?: IClusterErasureHealth;
718
+ repairs?: IClusterRepairHealth;
719
+ }
720
+ ```
721
+
722
+ - `getClusterHealth()` is served by the Rust core. The TypeScript wrapper does not infer values from static config.
723
+ - Standalone mode returns `{ enabled: false }`.
724
+ - 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.
725
+ - Drive health is based on live native probe checks on the configured local drive paths. Capacity values are cheap filesystem snapshots.
726
+ - `quorumHealthy` means the local node currently sees majority quorum and enough available placements in every erasure set to satisfy the configured write quorum.
727
+ - 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.
728
+
729
+ ## Usage with AWS SDK v3
730
+
731
+ ```typescript
732
+ import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
733
+
734
+ const descriptor = await storage.getStorageDescriptor();
735
+
736
+ const client = new S3Client({
737
+ endpoint: `http://${descriptor.endpoint}:${descriptor.port}`,
738
+ region: 'us-east-1',
739
+ credentials: {
740
+ accessKeyId: descriptor.accessKey,
741
+ secretAccessKey: descriptor.accessSecret,
742
+ },
743
+ forcePathStyle: true, // Required for path-style access
744
+ });
745
+
746
+ // Upload
747
+ await client.send(new PutObjectCommand({
748
+ Bucket: 'my-bucket',
749
+ Key: 'hello.txt',
750
+ Body: 'Hello, Storage!',
751
+ ContentType: 'text/plain',
752
+ }));
753
+
754
+ // Download
755
+ const { Body } = await client.send(new GetObjectCommand({
756
+ Bucket: 'my-bucket',
757
+ Key: 'hello.txt',
758
+ }));
759
+ const content = await Body.transformToString(); // "Hello, Storage!"
760
+
761
+ // Delete
762
+ await client.send(new DeleteObjectCommand({
763
+ Bucket: 'my-bucket',
764
+ Key: 'hello.txt',
765
+ }));
766
+ ```
767
+
768
+ ## Usage with SmartBucket
769
+
770
+ ```typescript
771
+ import { SmartBucket } from '@push.rocks/smartbucket';
772
+
773
+ const smartbucket = new SmartBucket(await storage.getStorageDescriptor());
774
+ const bucket = await smartbucket.createBucket('my-bucket');
775
+ const dir = await bucket.getBaseDirectory();
776
+
777
+ // Upload
778
+ await dir.fastPut({ path: 'docs/readme.txt', contents: 'Hello!' });
779
+
780
+ // Download
781
+ const content = await dir.fastGet('docs/readme.txt');
782
+
783
+ // List
784
+ const files = await dir.listFiles();
785
+ ```
786
+
787
+ Standalone ObjectStorage honors `If-None-Match: *` on both `PutObject` and
788
+ `CompleteMultipartUpload`, publishes through an atomic no-replace operation,
789
+ and implements bounded `ListParts` responses so exact-streaming clients can
790
+ verify multipart abort cleanup. Conditional create-only writes and active
791
+ `ListParts` fail closed in cluster mode until those semantics have complete
792
+ durable cluster metadata. Descriptor-confined standalone startup reclaims
793
+ abandoned private object materializations from interrupted create-only, copy,
794
+ or multipart publication without removing an already published hard link.
795
+
796
+ Standalone `DeleteObject` also honors `If-Match: *` and comma-separated strong
797
+ ETags. It compares the current ETag and deletes inside the same bucket mutation
798
+ gate used by PUT, COPY, and multipart completion. A mismatch returns the S3
799
+ `PreconditionFailed` response without changing object or runtime-stat state; an
800
+ absent conditional target returns `NoSuchKey`. This supplies the conditional
801
+ delete semantics used by SmartBucket's verified exact-path purge capability.
802
+ Conditional deletion returns `NotImplemented` in cluster mode until the
803
+ precondition can be enforced across cluster nodes.
804
+
805
+ AWS SDK streaming `PutObject` and `UploadPart` requests are decoded before
806
+ storage. ObjectStorage verifies the declared decoded length and CRC32 trailer
807
+ for `STREAMING-UNSIGNED-PAYLOAD-TRAILER`, and verifies the complete chained
808
+ signature sequence for `STREAMING-AWS4-HMAC-SHA256-PAYLOAD`. The
809
+ `aws-chunked` transport encoding is not retained as object metadata. Every
810
+ `aws-chunked` request requires a verified SigV4 envelope, even when
811
+ `auth.enabled` is `false`; ordinary non-streaming SDK requests retain the
812
+ configured auth-disabled behavior. Unsupported, malformed, truncated,
813
+ checksum-invalid, or signature-invalid bodies fail closed. Standalone
814
+ overwrites use private same-filesystem staging and are atomically published
815
+ only after terminal verification succeeds. Body verification failures and
816
+ cancellation before publication preserve the previous object.
817
+ Decoder memory is globally bounded, and `limits.requestTimeout` (milliseconds)
818
+ bounds inactivity while waiting for each incoming body frame; timeout or
819
+ memory-pressure failures publish no partial object.
820
+
821
+ ## Multipart Uploads
822
+
823
+ For files larger than 5 MB, use multipart uploads. objectstorage handles them
824
+ with **bounded-memory streaming I/O**. AWS streaming transport chunks are
825
+ decoded through per-chunk buffers of at most 16 MiB under a shared 64 MiB
826
+ decoder budget before the part data is written. In cluster mode, each part is
827
+ independently erasure-coded and distributed.
828
+
829
+ ```typescript
830
+ import {
831
+ CreateMultipartUploadCommand,
832
+ UploadPartCommand,
833
+ CompleteMultipartUploadCommand,
834
+ } from '@aws-sdk/client-s3';
835
+
836
+ // 1. Initiate
837
+ const { UploadId } = await client.send(new CreateMultipartUploadCommand({
838
+ Bucket: 'my-bucket',
839
+ Key: 'large-file.bin',
840
+ }));
841
+
842
+ // 2. Upload parts
843
+ const parts = [];
844
+ for (let i = 0; i < chunks.length; i++) {
845
+ const { ETag } = await client.send(new UploadPartCommand({
846
+ Bucket: 'my-bucket',
847
+ Key: 'large-file.bin',
848
+ UploadId,
849
+ PartNumber: i + 1,
850
+ Body: chunks[i],
851
+ }));
852
+ parts.push({ PartNumber: i + 1, ETag });
853
+ }
854
+
855
+ // 3. Complete
856
+ await client.send(new CompleteMultipartUploadCommand({
857
+ Bucket: 'my-bucket',
858
+ Key: 'large-file.bin',
859
+ UploadId,
860
+ MultipartUpload: { Parts: parts },
861
+ }));
862
+ ```
863
+
864
+ ## Bucket Policies
865
+
866
+ objectstorage supports AWS-style bucket policies for fine-grained access control. Policies use the same IAM JSON format as real S3 โ€” so you can develop and test your policy logic locally before deploying.
867
+
868
+ When `auth.enabled` is `true`, the auth pipeline works as follows:
869
+ 1. **Authenticate** โ€” verify the AWS SigV4 signature (anonymous requests skip this step)
870
+ 2. **Authorize** โ€” evaluate bucket policies against the request action, resource, and caller identity
871
+ 3. **Default** โ€” authenticated users get full access; anonymous requests are denied unless a policy explicitly allows them
872
+
873
+ ### Setting a Bucket Policy
874
+
875
+ ```typescript
876
+ import { PutBucketPolicyCommand } from '@aws-sdk/client-s3';
877
+
878
+ // Allow anonymous read access to all objects in a bucket
879
+ await client.send(new PutBucketPolicyCommand({
880
+ Bucket: 'public-assets',
881
+ Policy: JSON.stringify({
882
+ Version: '2012-10-17',
883
+ Statement: [{
884
+ Sid: 'PublicRead',
885
+ Effect: 'Allow',
886
+ Principal: '*',
887
+ Action: ['s3:GetObject'],
888
+ Resource: ['arn:aws:s3:::public-assets/*'],
889
+ }],
890
+ }),
891
+ }));
892
+ ```
893
+
894
+ ### Policy Features
895
+
896
+ - **Effect**: `Allow` and `Deny` (explicit Deny always wins)
897
+ - **Principal**: `"*"` (everyone) or `{ "AWS": ["arn:..."] }` for specific identities
898
+ - **Action**: IAM-style actions like `s3:GetObject`, `s3:PutObject`, `s3:*`, or prefix wildcards like `s3:Get*`
899
+ - **Resource**: ARN patterns with `*` and `?` wildcards (e.g. `arn:aws:s3:::my-bucket/*`)
900
+ - **Persistence**: Policies survive server restarts โ€” stored as JSON on disk alongside your data
901
+
902
+ ### Policy CRUD Operations
903
+
904
+ | Operation | AWS SDK Command | HTTP |
905
+ |-----------|----------------|------|
906
+ | Get policy | `GetBucketPolicyCommand` | `GET /{bucket}?policy` |
907
+ | Set policy | `PutBucketPolicyCommand` | `PUT /{bucket}?policy` |
908
+ | Delete policy | `DeleteBucketPolicyCommand` | `DELETE /{bucket}?policy` |
909
+
910
+ Deleting a bucket automatically removes its associated policy.
911
+
912
+ ## Clustering Deep Dive ๐Ÿ”—
913
+
914
+ objectstorage can run as a distributed storage cluster where multiple nodes cooperate to store and retrieve data with built-in redundancy.
915
+
916
+ ### How It Works
917
+
918
+ ```
919
+ Client โ”€โ”€HTTP PUTโ”€โ”€โ–ถ Node A (coordinator)
920
+ โ”‚
921
+ โ”œโ”€ Split object into 4 MB chunks
922
+ โ”œโ”€ Erasure-code each chunk (4 data + 2 parity = 6 shards)
923
+ โ”‚
924
+ โ”œโ”€โ”€QUICโ”€โ”€โ–ถ Node B (shard writes)
925
+ โ”œโ”€โ”€QUICโ”€โ”€โ–ถ Node C (shard writes)
926
+ โ””โ”€ Local disk (shard writes)
927
+ ```
928
+
929
+ 1. **Any node can coordinate** โ€” the client connects to any cluster member
930
+ 2. **Objects are chunked** โ€” large objects split into fixed-size pieces (default 4 MB)
931
+ 3. **Each chunk is erasure-coded** โ€” Reed-Solomon produces k data + m parity shards
932
+ 4. **Shards are distributed** โ€” placed across different nodes and drives for fault isolation
933
+ 5. **Quorum guarantees consistency** โ€” writes need k+1 acks, reads need k shards
934
+
935
+ ### Cluster Root and Upgrade Requirements
936
+
937
+ Cluster mode never creates configured control or drive roots. Before startup,
938
+ create `storage.directory` and every `cluster.drives.paths` entry, make them
939
+ owned by the ObjectStorage process user, and remove group/world write permission.
940
+ Every configured root must be a real directory with no symlink path component,
941
+ and every drive root must resolve to a distinct device/inode identity. The
942
+ control root may be distinct or may be the same identity as one drive root; it
943
+ cannot make two drive entries aliases of one another. Every root must support
944
+ the filesystem durability and locking semantics validated at startup.
945
+ ObjectStorage fails closed before accepting traffic when a root does not meet
946
+ these requirements.
947
+
948
+ This release migrates clustered control records, manifests, and shards to
949
+ descriptor-anchored, digest-addressed formats with durable tombstones. Legacy
950
+ records are migrated in bounded steps and the new records become authoritative.
951
+ Before a manifest listing can issue a continuation token, ObjectStorage drains
952
+ all remaining legacy manifests through batches retaining at most 32 MiB, so a
953
+ later migration cannot introduce a key behind an issued token. Background
954
+ healing can stop that drain between committed record migrations during
955
+ shutdown.
956
+ Upgrade every member during one coordinated maintenance window. Once any member
957
+ has written or migrated the new state, do not restart an older binary and do not
958
+ run a mixed-version cluster. Back up the cluster roots before upgrading; rollback
959
+ requires restoring those roots from the pre-upgrade backup.
960
+
961
+ ### Erasure Coding
962
+
963
+ With the default `4+2` configuration:
964
+ - Storage overhead: **33%** (vs. 200% for 3x replication)
965
+ - Fault tolerance: **any 2 drives/nodes can fail** simultaneously
966
+ - Read efficiency: only **4 of 6 shards** needed to reconstruct data
967
+
968
+ | Config | Total Shards | Overhead | Tolerance | Min Nodes |
969
+ |--------|-------------|----------|-----------|-----------|
970
+ | 4+2 | 6 | 33% | 2 failures | 3 |
971
+ | 6+3 | 9 | 50% | 3 failures | 5 |
972
+ | 2+1 | 3 | 50% | 1 failure | 2 |
973
+
974
+ ### QUIC Transport
975
+
976
+ Inter-node communication uses [QUIC](https://en.wikipedia.org/wiki/QUIC) via the `quinn` library:
977
+ - ๐Ÿ”’ **Built-in TLS** โ€” self-signed certs auto-generated at cluster init
978
+ - ๐Ÿ”€ **Multiplexed streams** โ€” concurrent shard transfers without head-of-line blocking
979
+ - โšก **Connection pooling** โ€” persistent connections to peer nodes
980
+ - ๐ŸŒŠ **Natural backpressure** โ€” QUIC flow control prevents overloading slow peers
981
+
982
+ ### Cluster Membership
983
+
984
+ - **Static seed nodes** โ€” initial cluster defined in config
985
+ - **Runtime join** โ€” new nodes can join a running cluster
986
+ - **Heartbeat monitoring** โ€” every 5s (configurable), with suspect/offline detection
987
+ - **Split-brain prevention** โ€” nodes only mark peers offline when they have majority
988
+
989
+ ### Self-Healing
990
+
991
+ A background scanner periodically (default: every 24h):
992
+ 1. Checks shard checksums (CRC32C) for bit-rot detection
993
+ 2. Identifies shards on offline nodes
994
+ 3. Reconstructs missing shards from remaining data using Reed-Solomon
995
+ 4. Places healed shards on healthy drives
996
+
997
+ Healing runs at low priority to avoid impacting foreground I/O.
998
+
999
+ ### Erasure Set Formation
1000
+
1001
+ Drives are organized into fixed **erasure sets** at cluster initialization:
1002
+
1003
+ ```
1004
+ 3 nodes ร— 4 drives each = 12 drives total
1005
+ With 6-shard erasure sets โ†’ 2 erasure sets
1006
+
1007
+ Set 0: Node1-Disk0, Node2-Disk0, Node3-Disk0, Node1-Disk1, Node2-Disk1, Node3-Disk1
1008
+ Set 1: Node1-Disk2, Node2-Disk2, Node3-Disk2, Node1-Disk3, Node2-Disk3, Node3-Disk3
1009
+ ```
1010
+
1011
+ Drives are interleaved across nodes for maximum fault isolation. New nodes form new erasure sets โ€” existing data is never rebalanced.
1012
+
1013
+ ## Testing Integration
1014
+
1015
+ ```typescript
1016
+ import { ObjectStorage } from '@lossless.org/objectstorage';
1017
+ import { tap, expect } from '@git.zone/tstest/tapbundle';
1018
+
1019
+ let storage: ObjectStorage;
1020
+
1021
+ tap.test('setup', async () => {
1022
+ storage = await ObjectStorage.createAndStart({
1023
+ server: { port: 4567, silent: true },
1024
+ storage: { cleanSlate: true },
1025
+ });
1026
+ });
1027
+
1028
+ tap.test('should store and retrieve objects', async () => {
1029
+ await storage.createBucket('test');
1030
+ // ... your test logic using AWS SDK or SmartBucket
1031
+ });
1032
+
1033
+ tap.test('teardown', async () => {
1034
+ await storage.stop();
1035
+ });
1036
+
1037
+ export default tap.start();
1038
+ ```
1039
+
1040
+ ## API Reference
1041
+
1042
+ ### `ObjectStorage` Class
1043
+
1044
+ #### `static createAndStart(config?: IObjectStorageConfig): Promise<ObjectStorage>`
1045
+
1046
+ Create and start a server in one call.
1047
+
1048
+ #### `static probeStoragePool(pool: IMountedStoragePoolConfig): Promise<IStoragePoolObservation>`
1049
+
1050
+ Inspect an exact host-mounted NFS or SMB pool without writing to it or starting
1051
+ the S3 listener. The observation reports the active mount source, filesystem
1052
+ type, capacity/tuning metadata, stability checks, and current production
1053
+ eligibility.
1054
+
1055
+ #### `start(): Promise<void>`
1056
+
1057
+ Spawn the Rust binary and start the HTTP server. Concurrent starts join the same
1058
+ attempt; starting an already running instance succeeds without restarting it.
1059
+ A start is rejected while shutdown or unconfirmed startup cleanup remains
1060
+ pending. After completed shutdown, an explicit start begins a new lifecycle.
1061
+
1062
+ #### `stop(): Promise<void>`
1063
+
1064
+ Gracefully stop the server and then terminate the Rust process. In every server
1065
+ mode, an incomplete 30-second drain rejects this call without terminating the
1066
+ process; call `stop()` again after the blocked operation recovers. A terminal
1067
+ shutdown error is reported only after the completed Rust server has released
1068
+ its task and resource ownership; the wrapper terminates the management process
1069
+ before rejecting with that error.
1070
+
1071
+ Concurrent stops join the same attempt, including any admitted startup. A stop
1072
+ after failed startup joins any remaining bridge cleanup without sending a stop
1073
+ command to an already terminated process. Repeated completed stops replay the
1074
+ same outcome, including terminal shutdown errors. Incomplete drain or process
1075
+ termination remains retryable; completed stop commands are retained so a
1076
+ termination retry cannot issue a duplicate command. A successful stop therefore
1077
+ confirms that the instance no longer owns its listener, work, or process; no
1078
+ health polling is required before removing a disposable storage root.
1079
+
1080
+ The Rust management process reads stdin on a dedicated task. Ordinary commands
1081
+ enter a bounded nonblocking queue and continue to execute one at a time, while
1082
+ `stop` uses an independent retained lifecycle signal plus bounded waiters. The
1083
+ first stop intent immediately closes management and HTTP admission and signals
1084
+ HTTP, multipart, and clustered background producers even when an active mounted
1085
+ filesystem operation is blocked. Already queued ordinary commands are rejected
1086
+ as stopping. A stop waiter that reaches its deadline receives one incomplete
1087
+ error and is never sent a later completion; a retry uses a new request ID and
1088
+ receives the retained terminal result when ownership completes.
1089
+
1090
+ IPC EOF requests the same graceful shutdown. If an owned blocked operation
1091
+ cannot finish by the EOF ownership deadline, the Rust process exits nonzero
1092
+ instead of returning through runtime teardown that could wait forever on a
1093
+ blocked native filesystem worker. Output is serialized through a dedicated
1094
+ writer with a 144 MiB per-line ceiling, a 160 MiB owned queue budget, and a
1095
+ five-second Unix stdout write deadline; writer failure or sustained stdout
1096
+ backpressure fails closed rather than growing memory without bound.
1097
+
1098
+ #### `getStoragePoolDiagnostic(): Promise<IStoragePoolObservation | null>`
1099
+
1100
+ Return the active pool observation. For a poisoned mounted pool, this uses
1101
+ cached activation evidence and remains available without touching the storage
1102
+ filesystem. It returns `null` before the Rust server has started.
1103
+
1104
+ #### `createBucket(name: string): Promise<{ name: string }>`
1105
+
1106
+ Create a storage bucket.
1107
+
1108
+ #### `createBucketTenant(options): Promise<IBucketTenantDescriptor>`
1109
+
1110
+ Create a bucket tenant with a generated or supplied scoped credential. Options: `{ bucketName, accessKeyId?, secretAccessKey?, region? }`.
1111
+
1112
+ #### `ensureBucketTenant(options): Promise<IBucketTenantDescriptor>`
1113
+
1114
+ Atomically create an absent bucket and exact scoped credential, resume an exact
1115
+ orphan credential, or rotate the credential for an existing exact-owned bucket.
1116
+ Foreign or ambiguous ownership is rejected before mutation. Options:
1117
+ `{ bucketName, accessKeyId, secretAccessKey, region?, fence? }`. Once a bucket
1118
+ has durable fence history, supply `fence`; the returned descriptor then includes
1119
+ the durable `resourceFence` receipt.
1120
+
1121
+ #### `deleteBucketTenant(options): Promise<void>`
1122
+
1123
+ Revoke a tenant credential or delete a bucket that still has tenant credentials. Options: `{ bucketName, accessKeyId? }`.
1124
+
1125
+ #### `rotateBucketTenantCredentials(options): Promise<IBucketTenantDescriptor>`
1126
+
1127
+ Replace the scoped credential for a bucket tenant. Options: `{ bucketName, accessKeyId?, secretAccessKey?, region? }`.
1128
+
1129
+ #### `listBucketTenants(): Promise<IBucketTenantMetadata[]>`
1130
+
1131
+ List scoped tenant credential metadata without returning secrets.
1132
+
1133
+ #### `getBucketTenantDescriptor(options): Promise<IBucketTenantDescriptor>`
1134
+
1135
+ Return endpoint, port, region, bucket, access key, secret key, SSL flag, legacy descriptor fields, and S3/AWS env values for the bucket tenant.
1136
+
1137
+ #### `getBucketMigrationCapability(): Promise<IObjectStorageBucketMigrationCapability>`
1138
+
1139
+ Return standalone provider identity and receipt-key evidence plus source seal,
1140
+ destination publication, and controller-retried source-cleanup capabilities.
1141
+ Cluster mode reports this migration metadata surface as unsupported.
1142
+
1143
+ #### `createSourceMigration(options): Promise<IObjectStorageMigrationTransitionReceipt>`
1144
+
1145
+ Create durable source migration ownership for an existing non-retained bucket.
1146
+ `sourceCleanupAccessKeyId` is required and must name the bucket's one exact
1147
+ scoped credential; its hash is permanently bound to the migration.
1148
+
1149
+ #### `createDestinationMigration(options): Promise<IObjectStorageMigrationTransitionReceipt>`
1150
+
1151
+ Create durable destination migration ownership for an existing bucket.
1152
+
1153
+ #### `transitionSourceMigration(options): Promise<IObjectStorageMigrationTransitionReceipt>`
1154
+
1155
+ Advance the source across an allowed phase edge. Sealing introduces the
1156
+ manifest digest; releasing requires the exact signed destination-abort receipt.
1157
+
1158
+ #### `transitionDestinationMigration(options): Promise<IObjectStorageMigrationTransitionReceipt>`
1159
+
1160
+ Advance the destination across an allowed phase edge. Verification introduces
1161
+ the manifest digest and terminal publication produces a signed receipt.
1162
+
1163
+ #### `cleanupSourceMigration(options): Promise<IObjectStorageSourceMigrationCleanupReceipt>`
1164
+
1165
+ Permanently delete a retired source after verifying the exact signed
1166
+ destination-published receipt. The request includes the migration binding,
1167
+ cleanup fence token, source capability, retired receipt hash, published receipt,
1168
+ exact resource fence, and exact source `accessKeyId`. Exact retries return the
1169
+ same durable cleanup receipt.
1170
+
1171
+ #### `inspectBucketMigration(options): Promise<IObjectStorageMigrationInspection | null>`
1172
+
1173
+ Return durable migration, terminal-receipt, and source-cleanup state for one
1174
+ `migrationId`.
1175
+
1176
+ #### `exportBucket(options): Promise<IBucketExport>`
1177
+
1178
+ Export one bucket's objects and metadata into a `smartstorage.bucket.v1` JSON
1179
+ object, subject to the bounded legacy management-export limits documented
1180
+ above.
1181
+
1182
+ #### `importBucket(options): Promise<IObjectStorageBucketMutationResult>`
1183
+
1184
+ Import a `smartstorage.bucket.v1` JSON object into the target bucket after
1185
+ validating object size and MD5. An exact fenced import binds the replacement to
1186
+ the bucket's exclusive credential. Options:
1187
+ `{ bucketName, source, accessKeyId?, fence?, holdPublication? }`. Setting
1188
+ `holdPublication: true` requires a fence and returns
1189
+ `resourcePublicationBarrier`; omitted or `false` preserves immediate behavior.
1190
+
1191
+ #### `deleteBucket(options): Promise<IObjectStorageBucketMutationResult>`
1192
+
1193
+ Delete a fenced bucket. Supplying `accessKeyId` also durably revokes that
1194
+ exact tenant credential and deletes the policy; omitting it preserves the
1195
+ legacy credential/policy behavior. Options:
1196
+ `{ bucketName, accessKeyId?, fence, holdPublication? }`. Setting
1197
+ `holdPublication: true` returns a durable `resourcePublicationBarrier`; omitted
1198
+ or `false` preserves immediate behavior.
1199
+
1200
+ #### `commitResourcePublication(options): Promise<IObjectStorageResourcePublicationReleaseReceipt>`
1201
+
1202
+ Release a durable write-publication hold after the caller has persisted and
1203
+ published the exact provider receipt. Options:
1204
+ `{ barrier, resourceFenceReceipt }`. The operation is exact-match and idempotent.
1205
+
1206
+ #### `getResourceFencingCapability(): Promise<IObjectStorageResourceFencingCapability>`
1207
+
1208
+ Report resource-fencing support, drain requirements, publication-hold version,
1209
+ whether publication holds are supported, and whether the provider must perform
1210
+ an additional external drain.
1211
+
1212
+ #### `getStorageDescriptor(options?): Promise<IS3Descriptor>`
1213
+
1214
+ Get connection details for S3-compatible clients. Returns:
1215
+
1216
+ | Field | Type | Description |
1217
+ |-------|------|-------------|
1218
+ | `endpoint` | `string` | Server hostname (`localhost` by default) |
1219
+ | `port` | `number` | Server port |
1220
+ | `accessKey` | `string` | Access key from first configured credential |
1221
+ | `accessSecret` | `string` | Secret key from first configured credential |
1222
+ | `useSsl` | `boolean` | Always `false` (plain HTTP) |
1223
+
1224
+ #### `getStorageStats(): Promise<IStorageStats>`
1225
+
1226
+ Read cached logical bucket and object totals from the Rust runtime without issuing S3 list calls.
1227
+
1228
+ #### `listBucketSummaries(): Promise<IBucketSummary[]>`
1229
+
1230
+ Get per-bucket logical object counts and total payload sizes.
1231
+
1232
+ #### `listCredentials(): Promise<IStorageCredentialMetadata[]>`
1233
+
1234
+ Return metadata for the currently active runtime credential set without `secretAccessKey` values.
1235
+
1236
+ #### `replaceCredentials(credentials: IStorageCredential[]): Promise<void>`
1237
+
1238
+ Atomically replace the active runtime credential set without restarting the server.
1239
+
1240
+ #### `getClusterHealth(): Promise<IClusterHealth>`
1241
+
1242
+ Read the Rust core's current cluster, drive, quorum, and repair health snapshot. Standalone mode returns `{ enabled: false }`.
1243
+
1244
+ #### `getHealth(): Promise<IObjectStorageHealth>`
1245
+
1246
+ Return running state, storage directory, per-location pool observations, auth
1247
+ state, credential counts, bucket count, object count, total bytes, cluster
1248
+ health, resource-fencing capability, and publication-hold capability fields.
1249
+
1250
+ #### `getMetrics(): Promise<IObjectStorageMetrics>`
1251
+
1252
+ Return numeric metrics plus a Prometheus text snippet for operational scraping.
1253
+
1254
+ ## Architecture
1255
+
1256
+ objectstorage uses a **hybrid Rust + TypeScript** architecture:
1257
+
1258
+ ```
1259
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
1260
+ โ”‚ Your Code (AWS SDK, SmartBucket, etc.) โ”‚
1261
+ โ”‚ โ†• HTTP (localhost:3000) โ”‚
1262
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
1263
+ โ”‚ ruststorage binary (Rust) โ”‚
1264
+ โ”‚ โ”œโ”€ hyper 1.x HTTP server โ”‚
1265
+ โ”‚ โ”œโ”€ S3 path-style routing โ”‚
1266
+ โ”‚ โ”œโ”€ StorageBackend (Standalone or Clustered) โ”‚
1267
+ โ”‚ โ”‚ โ”œโ”€ FileStore (single-node mode) โ”‚
1268
+ โ”‚ โ”‚ โ””โ”€ DistributedStore (cluster mode) โ”‚
1269
+ โ”‚ โ”‚ โ”œโ”€ ErasureCoder (Reed-Solomon) โ”‚
1270
+ โ”‚ โ”‚ โ”œโ”€ ShardStore (per-drive storage) โ”‚
1271
+ โ”‚ โ”‚ โ”œโ”€ QuicTransport (quinn) โ”‚
1272
+ โ”‚ โ”‚ โ”œโ”€ ClusterState & Membership โ”‚
1273
+ โ”‚ โ”‚ โ””โ”€ HealingService โ”‚
1274
+ โ”‚ โ”œโ”€ SigV4 auth + policy engine โ”‚
1275
+ โ”‚ โ”œโ”€ CORS middleware โ”‚
1276
+ โ”‚ โ””โ”€ S3 XML response builder โ”‚
1277
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
1278
+ โ”‚ TypeScript (thin IPC wrapper) โ”‚
1279
+ โ”‚ โ”œโ”€ ObjectStorage class โ”‚
1280
+ โ”‚ โ”œโ”€ RustBridge (stdin/stdout JSON IPC) โ”‚
1281
+ โ”‚ โ””โ”€ Config & S3 descriptor โ”‚
1282
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
1283
+ ```
1284
+
1285
+ **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.
1286
+
1287
+ **IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, and `getClusterHealth`. `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.
1288
+
1289
+ ### S3-Compatible Operations
1290
+
1291
+ Bucket names are validated consistently at HTTP, management, policy, and
1292
+ storage boundaries: they must be 3-63 lowercase letters, digits, dots, or
1293
+ hyphens, begin and end with a letter or digit, contain no consecutive dots,
1294
+ and not be formatted as an IPv4 address.
1295
+
1296
+ Object keys remain S3-compatible UTF-8 strings of up to 1,024 bytes, including
1297
+ leading, repeated, embedded, and trailing `/` characters. On disk, every new
1298
+ object key is encoded below the reserved `.smartstorage-objects-v2` bucket
1299
+ directory as canonical, prefix-free lowercase-hex frames. Frames and fixed
1300
+ payload/sidecar names stay within the common 255-byte `NAME_MAX`, remain safe on
1301
+ CIFS, and listings return the exact original key.
1302
+
1303
+ Existing v7 identity-layout objects remain readable on local and NFS storage
1304
+ when their raw path can be reconstructed without traversal. Their first
1305
+ mutation stages and publishes the canonical v2 object set before removing the
1306
+ legacy payload and sidecars. The migration uses descriptor-confined,
1307
+ digest-bound durable state and reconciles every preparation, publication, and
1308
+ partial-cleanup crash on startup or next access. Canonical and legacy payloads
1309
+ for the same key without that exact owned state, noncanonical entries below the
1310
+ reserved v2 root, and unsafe legacy aliases on case-folding CIFS fail closed
1311
+ instead of choosing an ambiguous object.
1312
+
1313
+ The standalone v2 object-layout upgrade is one-way. After the first canonical
1314
+ v2 object write or legacy-object migration in a standalone storage root,
1315
+ downgrading that root to `@push.rocks/smartstorage` 7.0.0 or earlier is unsupported because
1316
+ those versions cannot read the canonical v2 objects.
1317
+
1318
+ | Operation | Method | Path |
1319
+ |-----------|--------|------|
1320
+ | ListBuckets | `GET /` | |
1321
+ | CreateBucket | `PUT /{bucket}` | |
1322
+ | DeleteBucket | `DELETE /{bucket}` | |
1323
+ | HeadBucket | `HEAD /{bucket}` | |
1324
+ | ListObjects (v1/v2) | `GET /{bucket}` | `?list-type=2` for v2 |
1325
+ | PutObject | `PUT /{bucket}/{key}` | |
1326
+ | GetObject | `GET /{bucket}/{key}` | Supports `Range` header |
1327
+ | HeadObject | `HEAD /{bucket}/{key}` | |
1328
+ | DeleteObject | `DELETE /{bucket}/{key}` | Standalone `If-Match: *` or strong ETag list |
1329
+ | DeleteObjects | `POST /{bucket}?delete` | Up to 1,000 keys with CRC32 or Content-MD5 integrity |
1330
+ | CopyObject | `PUT /{bucket}/{key}` | `x-amz-copy-source` header |
1331
+ | InitiateMultipartUpload | `POST /{bucket}/{key}?uploads` | |
1332
+ | UploadPart | `PUT /{bucket}/{key}?partNumber&uploadId` | |
1333
+ | ListParts | `GET /{bucket}/{key}?uploadId` | Optional `max-parts` and `part-number-marker` |
1334
+ | CompleteMultipartUpload | `POST /{bucket}/{key}?uploadId` | |
1335
+ | AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId` | |
1336
+ | ListMultipartUploads | `GET /{bucket}?uploads` | |
1337
+ | GetBucketPolicy | `GET /{bucket}?policy` | |
1338
+ | PutBucketPolicy | `PUT /{bucket}?policy` | |
1339
+ | DeleteBucketPolicy | `DELETE /{bucket}?policy` | |
1340
+
1341
+ ### On-Disk Format
1342
+
1343
+ **Standalone mode:**
1344
+ ```
1345
+ {storage.directory}/
1346
+ {bucket}/
1347
+ .smartstorage-objects-v2/
1348
+ {whole-key-hex-frames}/
1349
+ object._storage_object_v2 # Object data; completed multipart objects include
1350
+ # an atomic payload/ETag/metadata trailer
1351
+ object._storage_object_v2.metadata.json # Legacy/direct-write metadata sidecar
1352
+ object._storage_object_v2.md5 # Legacy/direct-write cached MD5 hash
1353
+ .multipart/
1354
+ {upload-id}/
1355
+ metadata.json # Upload metadata
1356
+ part-1, part-2, ... # Part data files
1357
+ .policies/
1358
+ {bucket}.policy.json # Bucket policy (IAM JSON format)
1359
+ ```
1360
+
1361
+ **Cluster mode:**
1362
+ ```
1363
+ {storage.directory}/
1364
+ .buckets/
1365
+ v2/{bucket-address}.bucket.json # Active/tombstoned bucket state
1366
+ .policies/{bucket}.policy.json # Bucket policies
1367
+ .manifests/
1368
+ v2/{bucket-prefix}/{bucket-address}/{object-prefix}/
1369
+ {object-address}.state.json
1370
+ # Active/tombstoned object manifest
1371
+ .multipart/{upload-id}/ # Descriptor-anchored multipart state
1372
+ .get-tmp/ # Unlinked GET reconstruction files
1373
+ .smartstorage/cluster/
1374
+ identity.json # Node and cluster identity
1375
+ topology.json # Last durable topology
1376
+ generation-inventory/{address}.json # Deferred physical-generation cleanup
1377
+
1378
+ {drive_path}/.smartstorage/data/
1379
+ v2/{bucket-prefix}/{bucket-address}/{key-prefix}/{generation-address}/
1380
+ {shard-address}.shard # Immutable erasure-coded shard record
1381
+ {shard-address}.tombstone # Crash-safe deletion authority
1382
+ ```
1383
+
1384
+ Superseded physical generations are queued with their exact shard placements.
1385
+ Cluster reads are bounded, cleanup waits a 24-hour reader grace period, and the
1386
+ hourly cleanup worker purges both shard records and tombstones. Multipart
1387
+ uploads older than `multipart.expirationDays`, plus committed uploads whose
1388
+ first cleanup attempt failed, are retried during bounded cleanup passes
1389
+ scheduled every `multipart.cleanupIntervalMinutes`; large registries are
1390
+ visited page by page across successive passes. Generation cleanup retains at
1391
+ most 32 MiB of serialized records per page, multipart session listings process
1392
+ documents one at a time, policy startup loading is bounded and streaming, and
1393
+ both workers observe shutdown within an active pass. Both multipart settings
1394
+ must be greater than zero; ObjectStorage rejects invalid values during startup.
1395
+ Shutdown never aborts or detaches an active multipart descriptor worker; it
1396
+ waits for the worker under the same retryable bounded drain used by mounted
1397
+ HTTP requests.
1398
+ Multipart admission is bounded at 100,000 registry entries per storage
1399
+ process, including cleanup-pending and unreadable entries. Cleanup only removes
1400
+ descriptor-verified expired uploads and cleanup-marked committed entries;
1401
+ unreadable or incomplete descriptors fail closed and require operator repair
1402
+ or removal after their bucket and clustered shard ownership has been
1403
+ established.
1404
+
1405
+ ## Related Packages
1406
+
1407
+ - [`@push.rocks/smartbucket`](https://code.foss.global/push.rocks/smartbucket) โ€” High-level S3-compatible abstraction layer
1408
+ - [`@push.rocks/smartrust`](https://code.foss.global/push.rocks/smartrust) โ€” TypeScript โ†” Rust IPC bridge
1409
+ - [`@git.zone/tsrust`](https://code.foss.global/git.zone/tsrust) โ€” Rust cross-compilation for npm packages
1410
+
1411
+ ## License and Legal Information
1412
+
1413
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license.md) file.
1414
+
1415
+ **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
1416
+
1417
+ ### Trademarks
1418
+
1419
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
1420
+
1421
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
1422
+
1423
+ ### Company Information
1424
+
1425
+ Task Venture Capital GmbH
1426
+ Registered at District Court Bremen HRB 35230 HB, Germany
1427
+
1428
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
1429
+
1430
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.