@j0nathan-ll0yd/aws
Type-safe AWS SDK v3 client factory and service wrappers with built-in retry, observability, and permission decorators.
Client Factory
Create AWS SDK clients with Lambda-optimized defaults. Clients are cached per Lambda container.
import {
createS3Client,
createSQSClient,
createSNSClient,
createEventBridgeClient,
createLambdaClient,
createDynamoDBClient,
createDynamoDBDocumentClient,
createApiGatewayClient,
createCloudWatchClient,
createCloudWatchLogsClient,
createSTSClient,
} from "@j0nathan-ll0yd/aws";S3
S3 Operations
import {
getObject,
putObject,
headObject,
deleteObject,
createUpload,
abortMultipartUpload,
exportToS3,
listObjectsV2,
} from "@j0nathan-ll0yd/aws";
const data = await getObject("my-bucket", "path/to/file.json");
await putObject("my-bucket", "path/to/output.json", JSON.stringify(data));
const metadata = await headObject("my-bucket", "path/to/file.json");
await deleteObject("my-bucket", "path/to/file.json");createUpload(options)
Create a managed multipart upload. Returns an Upload instance to await .done() on.
import { createUpload } from "@j0nathan-ll0yd/aws";
import type { CreateUploadOptions } from "@j0nathan-ll0yd/aws";Options: contentType, partSize (default 5 MB), queueSize (default 4), leavePartsOnError (default false), and metadata — S3 user metadata written with the object (every key lands as x-amz-meta-<key>).
metadata is purely pass-through: unlike exportToS3, nothing is stamped automatically, because a streamed upload has no single body buffer to hash at the call site. Callers opting in to provenance supply their own keys, e.g. the deployment block via deploymentMetadata. See Deployment Provenance.
abortMultipartUpload(bucket, key, uploadId)
Abort an in-progress multipart upload.
exportToS3(options)
Write a JSON payload to S3 with a default CacheControl header. Designed for EventBridge export handlers.
Every write is stamped with two body-derived S3 user metadata keys: content-sha256 (a digest of the exact bytes written) and composed-at. Pass provenance to add the deployment block, and metadata for app-owned keys the framework does not derive. See Deployment Provenance.
| Option | Type | Purpose |
|---|---|---|
bucket | S3BucketName | Destination bucket. |
key | string | Object key. |
data | unknown | Payload, serialized with JSON.stringify. |
cacheControl | string | Defaults to max-age=300, s-maxage=300. |
provenance | DeploymentProvenance | Deployment facts to stamp. Omit to stamp only the body-derived keys. |
metadata | Record<string,string> | Extra user metadata. Merged last, so it can override a computed key. |
import { exportToS3 } from "@j0nathan-ll0yd/aws";
import type { ExportToS3Options } from "@j0nathan-ll0yd/aws";
import { loadDeploymentProvenance } from "@j0nathan-ll0yd/core";
await exportToS3({
bucket: "my-bucket",
key: "exports/items.json",
data: { generatedAt: new Date().toISOString(), items: [] },
provenance: loadDeploymentProvenance(),
});publishArtifact(options)
Publish a value under its artifact contract: encode it, judge the encoded bytes, hash them, stamp, and write those same bytes. This is the one S3 operation that carries a contract guarantee.
Destination, media type, contract, and value arrive bound together, because the key and the codec come from one ArtifactDefinition. A payload validated against one contract and written to another key cannot be spelled.
The framework supplies no contract of its own — encoding and acceptance are the application's, through the codec on its definition. A JSON export and a text document therefore publish through this same function with no branch between them.
Fails before PutObject when the value has no wire representation, when the encoded body is not representable as UTF-8, or when it violates its contract. The codec's own error propagates unchanged.
A codec supplies exactly one contract slot, and which one decides how a violation is reported. Supplying both or neither fails before the encode step, naming the artifact key — the type spells that as a compile error, and this is the runtime half for a JavaScript caller.
With judge, an ok: false verdict fails before PutObject, naming the artifact key and every issue id. An ok: true verdict publishes and its warnings come back on the PublishedArtifact. A result that is not a plain object with a boolean ok and the matching readonly array is refused — a thenable, a generator object, a boolean, undefined, or a verdict whose property access throws. A thenable is settled before the refusal, so declining cannot bring the process down. A verdict built in another realm is accepted, because the test is the shape rather than the constructor identity.
With validate, it also fails when the return shape proves the validator judged nothing. validate is typed as returning void, and TypeScript's void return position accepts anything, so all three of these type-check and all three are refused:
| Returned | Written as | Why it judged nothing |
|---|---|---|
| a thenable | async (wire) => { ... } | Deferred; the promise is pending at the decision. |
| a suspended iterator | function* (wire) { ... } | A generator body does not run when it is called. |
| a boolean | (wire) => schema.safeParse(wire).success | A predicate returns false and never throws. |
A deferred rejection is settled first, so declining cannot bring the process down. The boolean refusal carves out exactly one case: a validator that returns its parsed value can legitimately return a boolean, but only when the artifact IS a boolean, and jsonCodec serializes those two artifacts to exactly true and false. A boolean over any other body is a predicate.
One shape stays undecidable here on each slot, because each returns exactly what a conformant one returns:
// validate: returns `undefined`, as a passing validator does.
textCodec((body) => {
void checkAsync(body).catch(logError); // publishes an invalid body — refused by lint, not here
});
// judge: returns a well-formed verdict, as a clean judgment does.
const codec = {
encode: (value: string) => value,
judge: (body: string) => {
void checkAsync(body).catch(logError); // same defect, same residual
return { ok: true, warnings: [] } as const;
},
};The local-rules/artifact-validator-sync ESLint rule refuses both at authoring time. See the codec contract.
| Option | Type | Purpose |
|---|---|---|
bucket | S3BucketName | Destination bucket. |
artifact | ArtifactDefinition<T> | Key, media type, cache policy, and contract, as one value. |
value | T | The value to publish, typed by the artifact's codec. |
cacheControl | string | Overrides the artifact's own cacheControl. |
composedAt | string | Composition instant to stamp. Defaults to the receipt's own instant. |
provenance | DeploymentProvenance | Deployment facts to stamp. Omit to stamp only the body-derived keys. |
metadata | Record<string,string> | Extra user metadata. Merged last; may not replace content-sha256. |
import { publishArtifact } from "@j0nathan-ll0yd/aws";
import type { PublishArtifactOptions, PublishedArtifact } from "@j0nathan-ll0yd/aws";
import { loadDeploymentProvenance } from "@j0nathan-ll0yd/core";
import { ARTIFACTS } from "../artifacts.js";
const published = await publishArtifact({
bucket,
artifact: ARTIFACTS.catalog,
value: { entries },
provenance: loadDeploymentProvenance(),
});Returns a PublishedArtifact: key, contentType, contentLength (bytes, not characters), contentSha256, the composedAt that was actually stamped, and warnings — the advisory ArtifactFinding[] a judging codec returned beside ok: true, or [] for a validate codec. The findings are not stamped on the object: they describe the judgment rather than the bytes, and S3 user metadata is a fixed header budget rather than a report.
Unlike exportToS3, content-sha256 is not overridable. That writer stamps a digest beside bytes it makes no promise about; this path's entire claim is that the stamp describes the bytes it wrote, so a supplied digest would let the stamp, the receipt, and the object disagree while all three still looked authoritative.
prepareArtifact(artifact, value)
Encode a value under its contract, check it round-trips through UTF-8, judge it, and measure the exact bytes — writing nothing. Returns a frozen PreparedArtifact receipt whose body is retained privately, so the bytes that were judged are the bytes that get written.
Use it when several artifacts must all be valid before any of them is published. S3 is atomic per key and offers nothing across keys, so preparing every value first is what converts "one payload was invalid" from a partial publication into no publication at all — without a cross-object transaction.
import { prepareArtifact, publishPreparedArtifact } from "@j0nathan-ll0yd/aws";
// Every value is encoded and judged here; nothing has been written yet.
const preparedCatalog = prepareArtifact(ARTIFACTS.catalog, { entries });
const preparedLlms = prepareArtifact(ARTIFACTS.llmsTxt, document);
const prepared = [preparedCatalog, preparedLlms]; // for failure reporting only
const results = await Promise.allSettled([
publishPreparedArtifact({ bucket, prepared: preparedCatalog, composedAt, provenance }),
publishPreparedArtifact({ bucket, prepared: preparedLlms, composedAt, provenance }),
]);
const failedKeys = results.flatMap((r, i) => (r.status === "rejected" ? [prepared[i].key] : []));publishArtifacts(options)
Publish a set of artifacts prepare-all-then-publish: encode and judge every value before writing any of them. This is the packaged form of the pattern above, and the one to reach for when a composer publishes a set that must be coherent.
import { publishArtifacts, PartialPublicationError } from "@j0nathan-ll0yd/aws";
try {
const published = await publishArtifacts({
bucket,
composedAt,
provenance,
publications: [
{ artifact: ARTIFACTS.catalog, value: { entries } },
{ artifact: ARTIFACTS.llmsTxt, value: document },
],
});
} catch (error) {
if (error instanceof PartialPublicationError) {
// error.published — the keys that landed; error.failures — {key, cause} for those that did not
}
}What it closes: every contract failure now happens before any byte is written, so one invalid payload publishes nothing at all rather than everything ahead of it in the list. That is the failure composers actually hit, because a contract violation is a property of the value and is knowable without touching the network.
What it does not close, stated rather than implied: a write that fails for an S3 reason — throttling, an expiring credential, a bucket policy change — after an earlier write succeeded. S3 is atomic per key and offers nothing across keys, so no library makes several writes one transaction. That residue surfaces as PartialPublicationError, which names every key that landed and every key that did not. The written objects are not rolled back; the caller reconciles deliberately from those two lists.
Writes run sequentially. Concurrency would widen the partial window rather than narrow it: a parallel batch that fails on its third write may already have issued the fourth and fifth.
PartialPublicationError
Thrown by publishArtifacts when a write fails after an earlier write succeeded. Carries published: readonly PublishedArtifact[] and failures: readonly PublicationFailure[], both frozen and both in attempt order. It is thrown rather than returned so the default path fails loudly; a caller that must reconcile reads the two lists off the error instead of re-deriving them from a log.
Name each receipt and write it at its own call site, as above. mantle generate dataflow reads the key by stepping from the write back through prepareArtifact to the declaration, and that walk needs a binding it can follow. A receipt reached through an anonymous callback parameter -- prepared.map((p) => publishPreparedArtifact({ prepared: p })) -- is outside what the resolver reads, so the key is unknowable and the write lands in the manifest's unresolved array rather than as a declared target. It is reported, never dropped, but it is also not attributed. Preflight ordering is unaffected either way: every prepareArtifact call still runs before any write.
The receipt exposes key, contentType, cacheControl, contentLength, contentSha256, composedAt, and warnings. It exposes no body: there is no public buffer to mutate after validation and no digest field to overwrite, and a hand-built object is rejected because it carries no retained body. warnings is a frozen copy of what the judging codec returned, so a judge that kept a reference to its findings array cannot edit the judgment after the fact.
The composed-at a write stamps must denote a real instant. A value that does not parse, or one that names an impossible calendar date (2026-02-31T00:00:00.000Z, which Date.parse silently rolls into March), is refused before any byte is written. Every freshness reader in the estate computes an age from this string, and an unparseable stamp yields NaN at all of them at once — which compares false everywhere and therefore reads as "not stale" everywhere.
composedAt is recorded when the body is encoded and judged, not when it is written, and it is the default composed-at stamp. A receipt is not a single-use token — publishing one twice is legitimate — and reading the clock at each write gave one set of bytes several composition instants, every one of them later than the composition. That error understates the artifact's age, which is the direction a freshness reader must not be misled in. A composer that wants one instant across a whole run still passes composedAt explicitly.
The UTF-8 round-trip check is not ceremony. An unpaired surrogate is a legal JavaScript string and an illegal UTF-8 sequence, so Buffer.from would replace it with U+FFFD and the consumer would receive text the grammar check never saw.
publishPreparedArtifact(options)
Write a body prepareArtifact already judged, stamping the digest of those same bytes. Same options as publishArtifact, with prepared: PreparedArtifact in place of artifact and value.
Both checked calls apply the same metadata rules. content-sha256 is refused under any casing, because S3 user metadata becomes x-amz-meta-<key> HTTP headers and header names are case-insensitive — Content-SHA256 would land on exactly the header the digest occupies. composed-at is accepted in any casing and normalized to a single entry, and the returned receipt reports the value that landed. Two metadata keys differing only in case are refused rather than left for the wire to arbitrate, and a non-string value is refused because the SDK drops it while the receipt would still claim it.
See Artifact assurance pilot for the end-to-end shape and the guarantees this path does and does not make.
deploymentMetadata(provenance)
Convert deployment facts to the S3 user metadata keys exportToS3 writes. Every returned key becomes x-amz-meta-<key>. Absent optional facts emit no key, so a reader can tell "no claim" from "blank".
import { deploymentMetadata } from "@j0nathan-ll0yd/aws";
const metadata = deploymentMetadata({ stage: "staging", sourceRevision: revision });
// { 'deployment-stage': 'staging', 'source-revision': revision }S3Vendor
Static class with @RequiresS3 decorators for IAM permission extraction.
import { S3Vendor } from "@j0nathan-ll0yd/aws";SQS
SQS Operations
import { sendMessage, stringAttribute, numberAttribute } from "@j0nathan-ll0yd/aws";
import type { MessageAttributeValue } from "@j0nathan-ll0yd/aws";
await sendMessage(
"https://sqs.../my-queue",
{
action: "processItem",
itemId: "123",
},
{
messageAttributes: {
priority: stringAttribute("high"),
retryCount: numberAttribute(0),
},
},
);SQSVendor
Static class with @RequiresSQS decorators.
SNS
SNS Operations
import {
publish,
subscribe,
unsubscribe,
createPlatformEndpoint,
deleteEndpoint,
getEndpointAttributes,
listSubscriptionsByTopic,
} from "@j0nathan-ll0yd/aws";
await publish("arn:aws:sns:...:my-topic", {
event: "user.created",
userId: "123",
});SNSVendor
Static class with @RequiresSNS decorators.
EventBridge
EventBridge Operations
import { putEvents } from "@j0nathan-ll0yd/aws";
import type { PutEventsRequestEntry } from "@j0nathan-ll0yd/aws";
await putEvents([
{
Source: "my-api",
DetailType: "UserCreated",
Detail: JSON.stringify({ userId: "123" }),
},
]);TIP
Prefer emitEvent() / emitEvents() from @j0nathan-ll0yd/core for most use cases -- they provide retry, fire-and-forget error handling, and consistent event structure. Use putEvents() directly only when you need full control over entries.
Rule inspection, used by mantle check observability and instance diagnostics:
import { listRules, listTargetsByRule } from "@j0nathan-ll0yd/aws";
const { Rules } = await listRules({ EventBusName: "my-app-events" });
const { Targets } = await listTargetsByRule({ Rule: "my-rule", EventBusName: "my-app-events" });EventBridgeVendor
Static class with @RequiresEventBridge decorators.
Lambda
Lambda Operations
import { invoke, invokeAsync } from "@j0nathan-ll0yd/aws";
// Synchronous invocation
const result = await invoke("my-function", { key: "value" });
// Async (fire-and-forget)
await invokeAsync("my-function", { key: "value" });LambdaVendor
Static class with @RequiresLambda decorators.
DynamoDB
DynamoDB Operations
import {
dynamoGet,
dynamoPut,
dynamoQuery,
dynamoScan,
dynamoUpdate,
dynamoDelete,
dynamoBatchGet,
dynamoBatchWrite,
dynamoDescribeTable,
} from "@j0nathan-ll0yd/aws";
import type {
GetCommandInput,
GetCommandOutput,
PutCommandInput,
PutCommandOutput,
QueryCommandInput,
QueryCommandOutput,
ScanCommandInput,
ScanCommandOutput,
UpdateCommandInput,
UpdateCommandOutput,
DeleteCommandInput,
DeleteCommandOutput,
BatchGetCommandInput,
BatchGetCommandOutput,
BatchWriteCommandInput,
BatchWriteCommandOutput,
} from "@j0nathan-ll0yd/aws";DynamoDBVendor
Static class with @RequiresDynamoDB decorators.
API Gateway Management (WebSocket)
Post messages to and manage WebSocket API Gateway connections.
import { postToConnection, getConnection, deleteConnection } from "@j0nathan-ll0yd/aws";
import type {
PostToConnectionCommandOutput,
GetConnectionCommandOutput,
DeleteConnectionCommandOutput,
} from "@j0nathan-ll0yd/aws";
await postToConnection(connectionId, data);
const conn = await getConnection(connectionId);
await deleteConnection(connectionId);resetApiGatewayManagementClient()
Reset the cached API Gateway Management client (useful when the endpoint changes between invocations).
ApiGatewayManagementVendor
Static class with @RequiresApiGatewayManagement decorators.
WebSocket Connections
Higher-level DynamoDB-backed connection store for WebSocket APIs.
import {
storeConnection,
removeConnection,
getStoredConnection,
getActiveConnections,
broadcastToConnections,
} from "@j0nathan-ll0yd/aws";
import type { ConnectionRecord, ConnectionMetadata, BroadcastOptions } from "@j0nathan-ll0yd/aws";
await storeConnection(connectionId, metadata);
await removeConnection(connectionId);
const conn = await getStoredConnection(connectionId);
const connections = await getActiveConnections();
await broadcastToConnections(data, options);WebSocketConnectionsVendor
Static class backing the connection store operations.
API Gateway (Admin)
Query API Gateway admin endpoints for key and usage plan management.
import { getApiKeys, getUsage, getUsagePlans } from "@j0nathan-ll0yd/aws";
const keys = await getApiKeys();
const plans = await getUsagePlans();
const usage = await getUsage(usagePlanId, startDate, endDate);ApiGatewayVendor
Static class with @RequiresApiGateway decorators.
CloudWatch
putMetricData(params)
Publish custom metric data to CloudWatch.
import { putMetricData } from "@j0nathan-ll0yd/aws";
import type { MetricData } from "@j0nathan-ll0yd/aws";
await putMetricData({
Namespace: "MyApp",
MetricData: [{ MetricName: "ItemsProcessed", Value: 42, Unit: "Count" }],
});CloudWatchVendor
Static class with @RequiresCloudWatch decorators.
CloudWatch Logs
filterLogEvents(params, retry?)
Search a log group for matching events. Backs the MCP fetch_lambda_logs tool and instance log triage.
import { filterLogEvents } from "@j0nathan-ll0yd/aws";
import type { FilterLogEventsParams, FilterLogEventsResult, LogEvent } from "@j0nathan-ll0yd/aws";
const { events } = await filterLogEvents({
logGroupName: "/aws/lambda/my-function",
filterPattern: "ERROR",
startTime: Date.now() - 3_600_000,
});CloudWatchLogsVendor
Static class with @RequiresCloudWatchLogs decorators.
STS
getCallerIdentity()
Return the account, ARN, and user ID of the caller's credentials. Used to verify which role a Lambda or CLI session is running as.
import { getCallerIdentity } from "@j0nathan-ll0yd/aws";
import type { CallerIdentity } from "@j0nathan-ll0yd/aws";
const identity = await getCallerIdentity();
identity.account; // '123456789012'STSVendor
Static class with @RequiresSTS decorators.
Retry
withRetry(fn, options?)
Generic retry wrapper with exponential backoff and jitter.
import { withRetry } from "@j0nathan-ll0yd/aws";
import type { RetryOptions } from "@j0nathan-ll0yd/aws";
const result = await withRetry(() => callExternalService(), { maxRetries: 3, baseDelayMs: 100 });sleep(ms)
Promise-based sleep utility.
calculateDelayWithJitter(baseMs, attempt)
Calculate exponential backoff delay with random jitter.
Permission Decorators
Stage 3 method decorators that declare IAM permissions. The CLI extracts these at build time for automatic inline policy generation.
| Decorator | Service | Operations Enum |
|---|---|---|
@RequiresS3(resource, ops) | S3 | S3Operation |
@RequiresSQS(resource, ops) | SQS | SQSOperation |
@RequiresSNS(resource, ops) | SNS | SNSOperation |
@RequiresEventBridge(resource, ops) | EventBridge | EventBridgeOperation |
@RequiresLambda(resource, ops) | Lambda | LambdaOperation |
@RequiresDynamoDB(resource, ops) | DynamoDB | DynamoDBOperation |
@RequiresApiGatewayManagement(resource, ops) | API GW Mgmt | ApiGatewayManagementOperation |
@RequiresApiGateway(resource, ops) | API Gateway | ApiGatewayOperation |
@RequiresCloudWatch(resource, ops) | CloudWatch | CloudWatchOperation |
@RequiresCloudWatchLogs(resource, ops) | CW Logs | CloudWatchLogsOperation |
@RequiresSTS(resource, ops) | STS | STSOperation |
@RequiresCloudFrontKeyValueStore(resource, ops) | CF KVS | CloudFrontKeyValueStoreOperation |
Resource is '*' on vendor wrappers (generic). Actual resource ARNs are resolved at build time by the CLI based on which env vars the Lambda uses.
Permission Retrieval
import {
getS3Permissions,
getSQSPermissions,
getSNSPermissions,
getEventBridgePermissions,
getLambdaPermissions,
getDynamoDBPermissions,
getApiGatewayManagementPermissions,
getApiGatewayPermissions,
getCloudWatchPermissions,
getCloudWatchLogsPermissions,
getSTSPermissions,
getCloudFrontKeyValueStorePermissions,
getServicePermissions,
} from "@j0nathan-ll0yd/aws";
// Get all service permissions from a class
const permissions = getServicePermissions(MyVendor);Service Types
import {
AWSService,
S3Operation,
SQSOperation,
SNSOperation,
EventBridgeOperation,
LambdaOperation,
DynamoDBOperation,
ApiGatewayManagementOperation,
ApiGatewayOperation,
CloudWatchOperation,
CloudWatchLogsOperation,
STSOperation,
} from "@j0nathan-ll0yd/aws";
import type { ServiceOperation, ServicePermission } from "@j0nathan-ll0yd/aws";Testing
Inject mock clients for unit testing:
import {
setTestS3Client,
setTestSQSClient,
setTestSNSClient,
setTestEventBridgeClient,
setTestLambdaClient,
setTestDynamoDBClient,
setTestDynamoDBDocumentClient,
setTestApiGatewayClient,
setTestCloudWatchClient,
setTestCloudWatchLogsClient,
setTestSTSClient,
setTestApiGatewayManagementClient,
resetAllClients,
} from "@j0nathan-ll0yd/aws";
import { mockClient } from "aws-sdk-client-mock";
import { S3Client } from "@aws-sdk/client-s3";
const s3Mock = mockClient(S3Client);
setTestS3Client(s3Mock as any);
// After tests
resetAllClients();