Skip to content

@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.

typescript
import {
  createS3Client,
  createSQSClient,
  createSNSClient,
  createEventBridgeClient,
  createLambdaClient,
  createDynamoDBClient,
  createDynamoDBDocumentClient,
  createApiGatewayClient,
  createCloudWatchClient,
  createCloudWatchLogsClient,
  createSTSClient,
} from "@j0nathan-ll0yd/aws";

S3 ​

S3 Operations ​

typescript
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.

typescript
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.

OptionTypePurpose
bucketS3BucketNameDestination bucket.
keystringObject key.
dataunknownPayload, serialized with JSON.stringify.
cacheControlstringDefaults to max-age=300, s-maxage=300.
provenanceDeploymentProvenanceDeployment facts to stamp. Omit to stamp only the body-derived keys.
metadataRecord<string,string>Extra user metadata. Merged last, so it can override a computed key.
typescript
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:

ReturnedWritten asWhy it judged nothing
a thenableasync (wire) => { ... }Deferred; the promise is pending at the decision.
a suspended iteratorfunction* (wire) { ... }A generator body does not run when it is called.
a boolean(wire) => schema.safeParse(wire).successA 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:

typescript
// 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.

OptionTypePurpose
bucketS3BucketNameDestination bucket.
artifactArtifactDefinition<T>Key, media type, cache policy, and contract, as one value.
valueTThe value to publish, typed by the artifact's codec.
cacheControlstringOverrides the artifact's own cacheControl.
composedAtstringComposition instant to stamp. Defaults to the receipt's own instant.
provenanceDeploymentProvenanceDeployment facts to stamp. Omit to stamp only the body-derived keys.
metadataRecord<string,string>Extra user metadata. Merged last; may not replace content-sha256.
typescript
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.

typescript
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.

typescript
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".

typescript
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.

typescript
import { S3Vendor } from "@j0nathan-ll0yd/aws";

SQS ​

SQS Operations ​

typescript
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 ​

typescript
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 ​

typescript
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:

typescript
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 ​

typescript
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 ​

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

DecoratorServiceOperations Enum
@RequiresS3(resource, ops)S3S3Operation
@RequiresSQS(resource, ops)SQSSQSOperation
@RequiresSNS(resource, ops)SNSSNSOperation
@RequiresEventBridge(resource, ops)EventBridgeEventBridgeOperation
@RequiresLambda(resource, ops)LambdaLambdaOperation
@RequiresDynamoDB(resource, ops)DynamoDBDynamoDBOperation
@RequiresApiGatewayManagement(resource, ops)API GW MgmtApiGatewayManagementOperation
@RequiresApiGateway(resource, ops)API GatewayApiGatewayOperation
@RequiresCloudWatch(resource, ops)CloudWatchCloudWatchOperation
@RequiresCloudWatchLogs(resource, ops)CW LogsCloudWatchLogsOperation
@RequiresSTS(resource, ops)STSSTSOperation
@RequiresCloudFrontKeyValueStore(resource, ops)CF KVSCloudFrontKeyValueStoreOperation

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 ​

typescript
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 ​

typescript
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:

typescript
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();