Client configuration
All three SDKs expose S3, native JSON, and admin clients, but the listeners and credentials are deliberately separate. Use the public object endpoint for S3 and native clients; use the private admin endpoint only for admin clients.
Endpoint and path prefixes
An endpoint may include a clean reverse-proxy prefix, such as https://objects.example.com/lockwell. The SDK preserves that prefix in normal requests, SigV4 canonical paths, and presigned URLs. Do not append /api/v1 or /admin/api/v1 yourself unless the proxy exposes that exact mounted URL; duplicate suffixes are normalized once. User information, query strings, fragments, traversal segments, encoded separators, and malformed escapes are rejected.
Path-style S3 addressing is the default. Enable virtual-hosted style only when wildcard bucket DNS and certificates are configured. Dotted bucket names remain part of the signed host.
The S3 signing region defaults to us-east-1; it must match the server-configured region. Native and admin bearer-token requests do not use a signing region.
TLS and a private CA
Production endpoints should use TLS. A private CA belongs in the injected transport, not in an SDK-wide “skip verify” flag. Never disable certificate verification.
roots, err := x509.SystemCertPool()
if err != nil { log.Fatal(err) }
pem, err := os.ReadFile("/run/secrets/lockwell-ca.pem")
if err != nil || !roots.AppendCertsFromPEM(pem) { log.Fatal("invalid Lockwell CA") }
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: roots,
}}}
s3, err := lockwellsdk.New(endpoint, creds,
lockwellsdk.WithHTTPClient(httpClient),
lockwellsdk.WithRequestTimeout(30*time.Second))import https from "node:https";
import { readFileSync } from "node:fs";
import { Client, createNodeFetch } from "@kelphect/sdk";
const httpsAgent = new https.Agent({
keepAlive: true,
ca: readFileSync("/run/secrets/lockwell-ca.pem"),
});
const transport = createNodeFetch({ httpsAgent, maxSockets: 64 });
const s3 = new Client({ endpoint, accessKeyId, secretKey, fetch: transport, timeoutMs: 30_000 });
// Call transport.close() during process shutdown. Caller-supplied agents remain caller-owned.KeyStore trust = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream in = Files.newInputStream(Path.of("/run/secrets/lockwell-truststore.p12"))) {
trust.load(in, System.getenv("LOCKWELL_TRUSTSTORE_PASSWORD").toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trust);
SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(null, tmf.getTrustManagers(), null);
HttpClient http = HttpClient.newBuilder().sslContext(ssl).build();
LockwellClient s3 = LockwellClient.builder()
.endpoint(endpoint).credentials(credentials).httpClient(http)
.requestTimeout(Duration.ofSeconds(30)).build();The JVM-wide alternative is -Djavax.net.ssl.trustStore=/path/lockwell-truststore.p12 plus -Djavax.net.ssl.trustStorePassword=...; prefer an injected HttpClient when one process talks to services with different trust roots.
Timeouts, retries, and cancellation
Treat the per-attempt timeout and the caller deadline as different budgets. A retryable operation can consume several attempt timeouts plus backoff; a caller deadline or cancellation stops the whole operation.
| Language | Attempt timeout | Whole-operation cancellation | Retry default |
|---|---|---|---|
| Go | WithRequestTimeout | context.Context deadline/cancel | S3/native/admin use their documented policy; disable explicitly when needed |
| Node | timeoutMs (bounded to 30 seconds) | caller-owned AbortSignal | S3 one attempt; native/admin three attempts |
| Java | .requestTimeout(Duration) | synchronous interruption/HTTP state; CompletableFuture.cancel for async wrapper | S3 opt-in; native/admin default retries |
Retries are limited to transient transport failures, 429, and bounded 5xx responses when the request is replay-safe. Buffered writes need an idempotency key; one-shot streams and ambiguous admin mutations are not replayed. Authentication, authorization, checksum, retention, legal-hold, quota, and ordinary 4xx failures are not automatic retry candidates.
Progress and streaming recovery
Progress callbacks run synchronously or are awaited, so a slow callback applies backpressure. Callback failure cancels the request. Go reports cumulative bytes and a known/unknown total. Node and Java also report direction, chunk bytes, and multipart part number where applicable. None of the SDKs claims transparent stream resume: recover with explicit range reads or multipart list/abort APIs and an application-owned checkpoint.
Response metadata and audit correlation
Configure WithResponseMetadata, onResponseMetadata, or responseMetadataListener to observe successful requestId, optional Amazon request id, and optional traceparent. Errors carry their request id on the typed error. Store these identifiers beside an application job id; do not put secrets, bearer tokens, signed URL query strings, or object bytes in logs.
Concurrency and ownership
Clients are safe for concurrent use. Go clients may be shared between goroutines; Node clients share token minting and pooled transports; Java clients are thread-safe. Individual paginator/iterator instances and streaming bodies are operation-owned and should not be advanced or consumed concurrently. Close download bodies and any transport/executor that the application owns.