Node SDK
:::caution LNW/1 migration The package-root NativeClient now uses the server-only binary LNW/1 transport on Node 22+. Existing HTTP/JSON callers must migrate their constructor and method names, or temporarily import LegacyNativeClient from @kelphect/sdk/legacy-native. The S3 Client and JSON AdminClient remain separate compatibility and control-plane surfaces. Browser and Edge bundles cannot import the primary LNW/1 entry. :::
The first-party Lockwell SDK for Node.js is a native, encrypted alternative to the AWS S3 SDK for Node and Next.js apps. It is ESM and CommonJS and Promise-based. The package-root data plane bundles no second transport implementation: it uses the peer @kelphect/sdk-native core. The S3 entry uses node:crypto plus global fetch; the dedicated /edge entry uses only web globals such as crypto.subtle and ReadableStream.
Historical package line The private 0.2.2 package line is historical and predates the selected PolyForm
distribution payload. It is not an approved TangibleShift or commercial release; wait for B-010/B-013 clearance and the required written grant before commercial deployment. :::
It shares the language-neutral SigV4 signing fixtures with the Go and Java SDKs, so all three sign byte-for-byte identically.
npm install @kelphect/sdk @kelphect/sdk-nativeRequires Node.js >= 22 for LNW/1. Both import { Client } from '@kelphect/sdk' (ESM) and const { Client } = require('@kelphect/sdk') (CJS) work.
The package is published privately as
@kelphect/sdkon GitHub Packages. The npm scope matches the GitHub owner namespace and is the supported package name for private app development.
Exports
| Export | What it is |
|---|---|
Client | the S3 (SigV4 + XML) data-plane client. Node-only. |
NativeClient | the canonical LNW/1 binary data-plane client. Node-only. |
LegacyNativeClient | explicit HTTP/JSON migration client (/legacy-native). |
AdminClient | the JSON admin client (admin listener). |
LockwellKit | the high-level app kit. |
verifyWebhook | standalone, edge-safe webhook verification. |
ErpScopes / StorageProfiles / ErpErrors | pure ERP scoping, storage-recipe, and safe error-classification helpers. |
Error helpers ship alongside the clients:
- S3:
APIErrorandisNotFound. - LNW/1:
LockwellError,ProtocolError,TransportError,ServiceError,CancelledError, andDeadlineExceededError. - Legacy HTTP/JSON:
NativeErrorandisNative*helpers from@kelphect/sdk/legacy-native. - Admin:
AdminErrorandisAdminNotFound.
Plus RetryPolicy, TimeoutError, sha256ChecksumBase64, computeChecksumBase64, checksumHeaderName, CHECKSUM_ALGORITHMS, buildPresignedGetUrl, buildPresignedObjectUrl, urlForKey, createNodeFetch, and WEBHOOK_SIGNATURE_HEADER_NAME.
On edge runtimes, import from @kelphect/sdk/edge instead.
Client (the S3 client)
import { Client } from "@kelphect/sdk";
import { createReadStream } from "node:fs";
const client = new Client({
endpoint: "https://objects.example.com",
accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID!,
secretKey: process.env.LOCKWELL_SECRET_KEY!,
});
// SSE-S3 at rest, a server-verified CRC64NVME checksum, and an idempotency key
// for safe retries, in one call.
const put = await client.putObject("reports", "q1.txt", Buffer.from("hello"), {
contentType: "text/plain",
serverSideEncryption: true,
checksumAlgorithm: "CRC64NVME",
idempotencyKey: "q1-2026",
});
const got = await client.getObject("reports", "q1.txt");
console.log(got.body.toString());
// Stream a large file without buffering (checksum sent in an aws-chunked trailer).
await client.putObjectStream("reports", "big.bin", createReadStream("big.bin"), "CRC64NVME");
// Presigned GET; PUT, HEAD, and DELETE helpers are also available.
const url = client.presignGetObject("reports", "q1.txt", 900);Path-style addressing is the default. A clean reverse-proxy prefix such as https://objects.example.com/lockwell is preserved in normal and presigned request paths and participates in SigV4 signing. Already-mounted /api/v1 and /admin/api/v1 suffixes are normalized once, and traversal, encoded separators, malformed escapes, and unsafe interior path segments are rejected. Set virtualHostedStyle: true only when the endpoint has wildcard bucket DNS; normal requests and presigned URLs then put the bucket, including dotted bucket names, in the signed host.
Hover any identifier above: the tooltips are the SDK's real type signatures, checked at build time against the published type definitions.
Construction
Constructor options include endpoint, accessKeyId, secretKey, optional fetch, userAgent, now, retry, timeoutMs, signal, and virtualHostedStyle.
Retry is opt-in: pass retry: RetryPolicy.default() or retry: { maxAttempts: 3 }; the default is a single attempt. fetch lets you inject a custom fetch implementation, and now overrides the signing clock (useful in tests).
Buckets
| Method | Signature |
|---|---|
createBucket | createBucket(bucket, opts?) (opts { objectLock: { mode, days } }) |
headBucket | headBucket(bucket) |
deleteBucket | deleteBucket(bucket) |
putBucketVersioning | putBucketVersioning(bucket, status) ('Enabled' / 'Suspended') |
getBucketVersioning | getBucketVersioning(bucket) (returns '' when never enabled) |
Objects
| Method | Signature |
|---|---|
putObject | putObject(bucket, key, body, opts?) |
putObjectStream | putObjectStream(bucket, key, source, checksumAlgorithm, opts?) |
getObject | getObject(bucket, key, opts?) (fully buffered body) |
getObjectStream | getObjectStream(bucket, key, opts?) (body is the ReadableStream; readAll() buffers it) |
headObject | headObject(bucket, key, opts?) |
deleteObject | deleteObject(bucket, key, opts?) (opts.versionId) |
deleteObjects | deleteObjects(bucket, objects, opts?) (batch <= 1000) |
copyObject | copyObject(srcBucket, srcKey, srcVersionId, dstBucket, dstKey, opts?) |
putObject options:
| Option | Effect |
|---|---|
contentType | Sets Content-Type. |
metadata | Object of user metadata, stored as x-amz-meta-*. |
idempotencyKey | The signed x-lockwell-idempotency-key; makes the write replay-safe. |
serverSideEncryption: true | Requests SSE-S3 at rest. |
checksumAlgorithm | One of CHECKSUM_ALGORITHMS; the SDK computes and sends the verified digest. |
objectLockMode | 'GOVERNANCE' or 'COMPLIANCE', set on PUT. |
objectLockRetainUntil | RFC3339 retain-until date. |
objectLockLegalHold | true/false (mapped to ON/OFF). |
getObject / getObjectStream / headObject options: range, partNumber, versionId, responseContentType. getObjectStream is the right call for large objects: iterate body yourself or call readAll() to buffer into a Buffer.
deleteObjects takes entries of { key, versionId? } (or bare key strings) and returns { deleted, errors }. The batch may partially succeed; opts.quiet suppresses the per-key deleted entries (errors are always returned).
Listing and pagination
| Method | Signature |
|---|---|
listObjectsV2 | listObjectsV2(bucket, opts?) (prefix, delimiter, startAfter, continuationToken, maxKeys) |
listObjects | listObjects(bucket, opts?) (v1: marker pagination) |
listObjectVersions | listObjectVersions(bucket, opts?) (keyMarker, versionIdMarker, ...) |
listMultipartUploads | listMultipartUploads(bucket, opts?) |
listParts | listParts(bucket, key, uploadId, opts?) |
Each list* has a paginate* async iterator that threads continuation/marker tokens for you:
for await (const page of client.paginateObjectsV2("reports", { prefix: "logs/" })) {
for (const obj of page.objects) console.log(obj.key, obj.size);
}The five iterators are paginateObjectsV2, paginateObjects, paginateObjectVersions, paginateMultipartUploads, and paginateParts.
Multipart
const mpu = await client.createMultipartUpload("reports", "big.bin", { checksumAlgorithm: "CRC32C" });
const p1 = await client.uploadPart("reports", "big.bin", mpu.uploadId, 1, part1, {
checksumAlgorithm: mpu.checksumAlgorithm,
});
const p2 = await client.uploadPart("reports", "big.bin", mpu.uploadId, 2, part2, {
checksumAlgorithm: mpu.checksumAlgorithm,
});
const done = await client.completeMultipartUpload("reports", "big.bin", mpu.uploadId, [
{ partNumber: 1, etag: p1.etag },
{ partNumber: 2, etag: p2.etag },
]);Also uploadPartStream(bucket, key, uploadId, partNumber, source, checksumAlgorithm, opts?), uploadPartCopy(srcBucket, srcKey, srcVersionId, dstBucket, dstKey, uploadId, partNumber, byteRange, opts?), and abortMultipartUpload(bucket, key, uploadId). When you declare a checksumAlgorithm on create, pass it back on every part, and the composite checksum returns on complete. SSE-C uses the same raw 32-byte sseCustomerKey at create, part, and completion; copy parts separately accept copySourceSseCustomerKey.
Tagging and Object Lock reads
putObjectTagging(bucket, key, tags), getObjectTagging(bucket, key), deleteObjectTagging(bucket, key), getObjectRetention(bucket, key, opts?), and getObjectLegalHold(bucket, key, opts?). Retention and legal hold are set on the write through the putObject object-lock options above, then read back here.
Presigned GET
presignGetObject(bucket, key, expiresSeconds, opts?) returns a time-limited GET URL (opts.versionId, opts.responseContentType are folded into the signature). presignPutObject, presignHeadObject, and presignDeleteObject cover the other supported object methods; for a native signed write URL use the native signUrl.
Streaming uploads and checksums
putObjectStream(bucket, key, source, checksumAlgorithm, opts?) streams source (a ReadableStream or async-iterable) and sends the checksum in an aws-chunked trailer, so checksumAlgorithm is required.
Streaming PUT/GET and multipart part options accept an awaited onProgress callback. TransferProgress reports direction, cumulative bytes, chunkBytes, totalBytes (null when unknown), and partNumber for a part. The callback is awaited before the next chunk is read/enqueued, preserving backpressure; a rejection aborts the request and cancels its source. onResponseMetadata is an additive client option for successful request-id/trace headers. One-shot streams are not transparently resumed; use range/ETag or multipart list/abort and application-owned atomic file handling for recovery.
The exported computeChecksumBase64(alg, data) and checksumHeaderName(alg) let you precompute a digest yourself. CHECKSUM_ALGORITHMS is the supported list (CRC32, CRC32C, CRC64NVME, SHA1, SHA256).
Errors
Client throws APIError with code, message, statusCode, requestId. Use isNotFound(err) for a missing bucket/key/upload.
NativeError and AdminError preserve RFC 9457 type, title, detail, instance, and unknown extension members alongside code, statusCode, and requestId. Treat the HTTP status and exact machine code as authoritative; never branch on the human message.
import { isNotFound } from "@kelphect/sdk";
try {
await client.getObject("reports", "missing.txt");
} catch (err) {
if (isNotFound(err)) {
/* 404 */
} else throw err;
}NativeClient (canonical LNW/1 data plane)
The package-root NativeClient is the server-only binary LNW/1 client backed by the shared @kelphect/sdk-native TypeScript core. It requires Node 22+ (or Bun 1.4+ when using the shared runtime package) and preserves bounded frames, TLS/mTLS verification, multiplexing, flow-control backpressure, deadlines, cancellation, replay-safe retries, redacted diagnostics, and typed response metadata.
import { NativeClient } from "@kelphect/sdk";
const native = new NativeClient({
host: "objects.example.com",
port: 9444,
tls: { ca: process.env.LOCKWELL_CA_PEM },
credentials: { accessKeyId, secretKey },
});
await native.putObject({
bucket: "reports",
key: "daily.json",
body: new TextEncoder().encode("{}"),
contentLength: 2,
options: { idempotencyKey: "daily-2026-08-31" },
});
const response = await native.getObject({ bucket: "reports", key: "daily.json" });
for await (const chunk of response.body) {
// consume the bounded stream; cancellation propagates to LNW/1
void chunk;
}
await native.close();The typed client includes buckets, objects, ranges, metadata, checksums, pagination, copy, multipart, versioning/delete markers, Object Lock retention, legal hold, CORS, notifications, and signed capabilities. Use createLockwellClient(config) when selecting the Node or Bun connector explicitly. The JSON AdminClient remains a separate control plane; no admin wire opcodes are added to LNW/1.
LegacyNativeClient (HTTP/JSON migration client)
The legacy JSON data plane at /api/v1/ remains available only for bounded migrations. New server code must use the package-root LNW/1 NativeClient.
Configured with an access-key id plus secret, it auto-manages the bearer token: mints a lwtk_… token on first use, caches it until shortly before expiry, refreshes transparently, and re-mints once on a 401. Token acquisition is concurrency-safe, with a single in-flight mint shared across a burst of requests (single-flight), never one mint per request.
import { LegacyNativeClient, sha256ChecksumBase64 } from "@kelphect/sdk/legacy-native";
const native = new LegacyNativeClient({
endpoint: "https://objects.example.com", // public listener (same as S3)
accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID,
secretKey: process.env.LOCKWELL_SECRET_KEY,
});
await native.createBucket("reports", { versioning: true });
// Body may be a Buffer/Uint8Array/string OR a ReadableStream/async-iterable.
// An idempotent PUT needs a body-integrity signal: pass an expected checksum.
const put = await native.putObject("reports", "q1.txt", Buffer.from("hello"), {
contentType: "text/plain",
idempotencyKey: "q1-2026",
checksums: { sha256: await sha256ChecksumBase64("hello") },
});
console.log(put.versionId, put.etag);
// Streaming GET (no whole-object buffering) with a Range.
const got = await native.getObjectStream("reports", "big.bin", { range: "bytes=0-1023" });
for await (const chunk of got.body) {
/* Uint8Array */
}Buckets and objects
| Method | Signature |
|---|---|
listBuckets | listBuckets() |
createBucket | createBucket(bucket, opts?) ({ versioning?, objectLock? }) |
getBucket | getBucket(bucket) |
deleteBucket | deleteBucket(bucket) |
getBucketVersioning / setBucketVersioning | versioning state ('Enabled'/'Suspended') |
setBucketCors / getBucketCors / deleteBucketCors | browser CORS rules |
putObject | putObject(bucket, key, body, opts?) (streaming) |
getObject / getObjectStream | buffered / streaming download ({ range?, versionId? }) |
headObject | headObject(bucket, key, opts?) |
deleteObject | deleteObject(bucket, key, opts?) |
listObjects | listObjects(bucket, opts?) |
batchDeleteObjects | batchDeleteObjects(bucket, objects, opts?) (<= 1000) |
copyObject | copyObject(dstBucket, dstKey, source) |
putObject options: { contentType?, metadata?, idempotencyKey?, ifMatch?, ifNoneMatch?, checksums?: { <alg>: <base64> }, contentLength? }. ifNoneMatch: '*' creates only when absent; ifMatch: '<etag>' overwrites only on a matching ETag. A bad checksums digest is rejected before any bytes are committed.
copyObject(dstBucket, dstKey, source) takes the source plus directives in source:
{ sourceBucket, sourceKey, sourceVersionId?, metadataDirective?, contentType?, metadata?,
ifMatch?, ifNoneMatch?, ifModifiedSince?, ifUnmodifiedSince?, requireAbsent?, requireMatchEtag? }Tags, retention, legal hold, versions, multipart
getObjectTags / setObjectTags / deleteObjectTags, getObjectRetention / setObjectRetention, getObjectLegalHold / setObjectLegalHold, listObjectVersions, and multipart (createMultipartUpload, uploadPart, listParts, completeMultipartUpload, abortMultipartUpload, listMultipartUploads).
The async iterators paginateObjects, paginateObjectVersions, and paginateMultipartUploads page automatically.
Bucket CORS
setBucketCors accepts the native shape ({ rules: [...] }), a single rule, or an array of rules:
await native.setBucketCors("reports", {
rules: [
{
allowedOrigins: ["https://app.example.com"],
allowedMethods: ["GET", "HEAD", "PUT"],
allowedHeaders: ["content-type"],
exposeHeaders: ["ETag"],
maxAgeSeconds: 600,
},
],
});
const cors = await native.getBucketCors("reports");
await native.deleteBucketCors("reports");CORS is browser policy, not authorization. Changing it is an admin-scoped bucket operation; app code should usually use kit.configureBucketCors(...) or provisionTenant(..., { bucketCors }) so the admin-capable key is transient.
Signed URLs (GET and PUT)
The native API supports signed write URLs (unlike the S3 presigner):
const upload = await native.signUrl({ method: "PUT", bucket: "reports", key: "incoming.bin", ttlSeconds: 300 });
const download = await native.signUrl({ method: "GET", bucket: "reports", key: "q1.txt" });The minted URL is usable without a bearer token and can never exceed the minting key's scope (a read-only key minting a PUT URL is a 403). See signed URLs.
Bucket notifications
Webhook-only delivery; a new config ID returns its signing secret exactly once (GET/update carry only hasSecret). Events accept the canonical s3:Object* names or the shorthand 'object-created' / 'object-removed'; filters take { prefix?, suffix? }.
const created = await native.setBucketNotification("reports", {
id: "reports-events",
webhookUrl: "https://my-app.example.com/hooks/lockwell",
events: ["object-created", "object-removed"],
filters: { prefix: "uploads/", suffix: ".pdf" },
});
console.log(created.configs[0].signingSecret); // shown once; store securely
const cfg = await native.getBucketNotification("reports");
console.log(cfg.configs[0].hasSecret); // signingSecret is omitted on GET
await native.deleteBucketNotification("reports"); // clearErrors
LegacyNativeClient throws NativeError mapped from the API's problem+json: isNativeNotFound (404), isNativeConflict (409), isNativePreconditionFailed (412), isNativeForbidden (403), isNativeUnauthorized (401), isNativeQuotaExceeded (507).
AdminClient (the admin client)
Targets /admin/api/v1/ on the admin listener (never the public S3 port). It authenticates by an admin API bearer token (lockwell admin-token create).
A created or rotated key returns its secret exactly once. Store it on the spot; listKeys never returns
secrets. :::
import { AdminClient } from "@kelphect/sdk";
const admin = new AdminClient({
endpoint: "https://admin.example.com", // admin listener, not the S3 port
token: process.env.LOCKWELL_ADMIN_TOKEN, // Authorization: Bearer <token>
});
const tenant = await admin.createTenant({ id: "acme", name: "Acme Inc" });
// Mutations accept { dryRun } to preview the plan (sends ?dryRun=true).
const plan = await admin.deleteTenant("acme", { reason: "offboarding", confirm: "acme", dryRun: true });
// The secret is returned exactly once on create/rotate. Store it immediately.
const key = await admin.createKey("acme", { scopes: "read,write,delete" });
console.log(key.secretKey);| Method | Signature |
|---|---|
listTenants / getTenant | listTenants() / getTenant(id) |
createTenant | createTenant({ id, name?, dryRun? }) |
disableTenant | disableTenant(id, { reason, dryRun? }) |
deleteTenant | deleteTenant(id, { reason, confirm, dryRun? }) |
getQuota / setQuota / clearQuota | setQuota(id, bytes, { dryRun? }) |
getUsage | getUsage(id) |
listAccounts / createAccount | createAccount(id, { name, dryRun? }) |
listKeys | listKeys(id) (never returns secrets) |
createKey | createKey(id, { accountId?, scopes?, expiresAt?, dryRun? }) |
rotateKey | rotateKey(id, keyId, { scopes?, expiresAt?, dryRun? }) |
revokeKey | revokeKey(id, keyId, { reason, dryRun? }) |
queryAudit | queryAudit({ tenant?, since?, limit? }) |
scopes follows the verb list (read,write,delete,admin) or resource grammar (op=read:bucket=reports:prefix=in/,op=write:bucket=reports:prefix=in/); expiresAt is RFC3339 or YYYY-MM-DD. Errors throw AdminError; use isAdminNotFound(err). See the Admin API reference.
LockwellKit (the app kit)
Composes the Admin and Native clients so an app does not hand-roll tenant-to-key provisioning, per-tenant clients, browser direct upload and download, or webhook verification.
import { LockwellKit } from "@kelphect/sdk";
const kit = new LockwellKit({
admin: { endpoint: "https://admin.example.com", token: process.env.LOCKWELL_ADMIN_TOKEN },
native: { endpoint: "https://objects.example.com" }, // public listener; creds are per-tenant
});
// 1) Provision: ensure the tenant exists (a pre-existing tenant is NOT an error),
// mint a FRESH scoped key, optionally create a default bucket. Secret returned ONCE.
const { key } = await kit.provisionTenant("acme", { defaultBucket: "inbox" });
// persist { key.accessKeyId, key.secretKey } in your tenant store
// 2) A per-tenant native client (cached per tenant+accessKeyId).
const client = kit.clientForTenant("acme", { accessKeyId: key.accessKeyId, secretKey: key.secretKey });
await client.putObject("inbox", "hello.txt", "hi");
// 3) Browser direct upload/download. Hand the URL straight to the browser.
const up = await kit.signedUploadUrl(client, "inbox", "photo.jpg", { ttl: 300, contentType: "image/jpeg" });
const down = await kit.signedDownloadUrl(client, "inbox", "photo.jpg", { ttl: 300 });
// Or update CORS later with another transient admin-scoped key.
await kit.configureBucketCors("acme", "inbox", {
rules: [{ allowedOrigins: ["https://app.example.com"], allowedMethods: ["GET", "HEAD", "PUT"] }],
});
// 4) Verify an incoming Lockwell webhook (constant-time HMAC-SHA256).
const ok = await kit.verifyWebhook(rawRequestBodyBytes, req.headers["x-lockwell-signature"], secret);provisionTenant(tenantId, opts?) options: { name?, scopes?, accountId?, expiresAt?, defaultBucket?, bucketVersioning?, bucketScope?, bucketCors? }. It mints a data key only (default read,write,delete, or narrowed via bucketScope). A defaultBucket is created with a transient admin-on-that-bucket key that is revoked immediately; bucketCors uses that same transient key before revoke.
signedUploadUrl and signedDownloadUrl accept either a LegacyNativeClient (from clientForTenant) or { accessKeyId, secretKey, tenantId? } creds. configureBucketCors mints and revokes its own transient admin-scoped bucket key. clientForTenant is also aliased as forTenant. See the app kit guide.
The retry-safe adoption helpers are ensureTenant, ensureKey, and ensureTenantProvisioning. They reuse only a matching active key anchored by externalRef; a reused key never pretends to have a recoverable secret. The ERP helper families derive opaque tenant/company/purpose paths and least-privilege scopes, produce explicit storage recipes, and classify native/admin failures without copying unsafe server messages. They do not choose legal policy or persist ERP mappings for you.
NativeClient and AdminClient also expose healthz() and readyz() with per-call signal/timeoutMs controls and typed readiness components. Probe calls do not send data-plane or admin credentials.
Edge safety
The default barrel re-exports the S3 Client, whose SigV4 code statically imports node:crypto. On Cloudflare Workers, Vercel Edge, Bun, and Deno, import from the dedicated edge entry instead:
import { LockwellKit, LegacyNativeClient, AdminClient, verifyWebhook } from "@kelphect/sdk/edge";@kelphect/sdk/edge re-exports only the node:*-free compatibility surface (the LegacyNativeClient, AdminClient, LockwellKit, verifyWebhook, sha256ChecksumBase64, RetryPolicy). It deliberately omits the S3 Client, so a bundler produces a bundle with zero node:crypto and no nodejs_compat flag.
The LegacyNativeClient itself uses only web globals (fetch, ReadableStream, TextEncoder, btoa, crypto.subtle). For a SHA-256 checksum without node:crypto, sha256ChecksumBase64 computes one with WebCrypto. See edge runtimes.
The default @kelphect/sdk import pulls in node:crypto through the S3 Client. On an edge runtime,
import from @kelphect/sdk/edge, which omits the S3 Client. :::
Retry policy
RetryPolicy uses the shared replay-safety rules, while language defaults and callback shapes differ where the capability index says so. RetryPolicy.default() is up to 3 attempts, 100ms base backoff doubling to a 2s cap, with full jitter; RetryPolicy.disabled() attempts every request once. The constructor accepts { maxAttempts?, baseBackoffMs?, maxBackoffMs?, jitter?, rand?, sleep? }.
It retries GET/HEAD/DELETE and any PUT/POST carrying an idempotency key, on transport errors and 5xx/429; a 4xx other than 429 is never retried, and streaming bodies are never retried.
TypeScript
The package ships type declarations (@kelphect/sdk resolves to types/index.d.ts), so endpoint, accessKeyId, secretKey, the PutOptions and GetOptions shapes, the ChecksumAlgorithm union ('CRC32' | 'CRC32C' | 'CRC64NVME' | 'SHA1' | 'SHA256'), and the result types are all typed. The edge entry has its own types/edge.d.ts.
Not supported (by design)
The S3 Client presigns GET/PUT/HEAD/DELETE. Everywhere: no public-bucket or anonymous access without a token or signed URL, no SSE-KMS, no IAM/STS/bucket-policy, no website/tiering/Select/Object-Lambda, and webhook is the only notification target (SNS/SQS/Lambda are a 501 non-goal).
The Lockwell server and this first-party client support SSE-C and copy-source SSE-C through sseCustomerKey and copySourceSseCustomerKey raw 32-byte options on object, copy, and multipart operations.