@ayepi/aws 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philip Diffenderfer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @ayepi/aws
2
+
3
+ AWS backends for ayepi: an SQS-backed [`@ayepi/work`](https://www.npmjs.com/package/@ayepi/work)
4
+ `Queue` (with transparent S3 offload of large payloads) and an S3-backed
5
+ [`@ayepi/files`](https://www.npmjs.com/package/@ayepi/files) `FileStore` + `Presigner`, all
6
+ wrapped in [`@ayepi/core`](https://www.npmjs.com/package/@ayepi/core) `retry` so SQS/S3
7
+ throttling is absorbed under load. The AWS SDK v3 clients are **optional peer dependencies**
8
+ you install and own — the package talks to them via `client.send(command)`.
9
+
10
+ ```sh
11
+ pnpm add @ayepi/aws @aws-sdk/client-s3 @aws-sdk/client-sqs @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
12
+ ```
13
+
14
+ ```ts
15
+ import { S3Client } from '@aws-sdk/client-s3'
16
+ import { SQSClient } from '@aws-sdk/client-sqs'
17
+ import { createWork } from '@ayepi/work'
18
+ import { s3Files } from '@ayepi/aws/s3'
19
+ import { sqsQueue } from '@ayepi/aws/sqs'
20
+
21
+ const s3 = new S3Client({ region: 'us-east-1' })
22
+ const sqs = new SQSClient({ region: 'us-east-1' })
23
+
24
+ const files = s3Files({ client: s3, bucket: 'my-bucket' })
25
+ const queue = sqsQueue({
26
+ client: sqs,
27
+ queueUrl: process.env.SQS_URL!,
28
+ waitTimeSeconds: 10,
29
+ largePayload: { store: files }, // bodies near SQS's 256 KB cap go to S3
30
+ })
31
+
32
+ const w = createWork({ queue, pubsub, store, work: [/* ... */] as const })
33
+ ```
34
+
35
+ ## How it works
36
+
37
+ - **S3 store.** `s3Files` implements the `@ayepi/files` `FileStore` (and `Presigner`):
38
+ streaming `put` (multipart via `@aws-sdk/lib-storage`), `get`/`head`/`delete`,
39
+ prefix-paginated `list`, and native presigned download/upload URLs — no server route needed.
40
+ - **SQS queue.** `sqsQueue` maps SQS's native visibility-timeout model onto the `@ayepi/work`
41
+ `Queue`: `push` is `SendMessage`, `pop` is `ReceiveMessage` (a lease), `heartbeat` is
42
+ `ChangeMessageVisibility`, `ack` is `DeleteMessage`, `fail` returns the message early.
43
+ Dead-lettering is the queue's own SQS redrive policy.
44
+ - **Large payloads.** SQS caps a message at 256 KB. With `largePayload`, a body over the
45
+ threshold (default ~240 KB) is written to a `FileStore` and the message carries a small
46
+ pointer; `pop` inlines it back and `ack` deletes the S3 object.
47
+ - **Throttle resilience.** Every `client.send(...)` is wrapped in `@ayepi/core` `retry`, so a
48
+ throttled (rate-limited) reply is retried; on exhaustion the error is reported to `onError`
49
+ and rethrown.
50
+ - **You own the SDK clients.** The clients are optional peer deps — you construct and configure
51
+ them (region, credentials, endpoint) and manage their lifecycle.
52
+
53
+ ## Options
54
+
55
+ ```ts
56
+ // S3 file store
57
+ s3Files({
58
+ client: s3, // a configured @aws-sdk/client-s3 S3Client
59
+ bucket: 'my-bucket',
60
+ prefix: 'docs/', // key namespace prepended to every key (default '')
61
+ retry: { attempts: 8 }, // core retry policy (default absorbs throttling)
62
+ onError: (err) => log(err),
63
+ })
64
+
65
+ // SQS work queue
66
+ sqsQueue({
67
+ client: sqs, // a configured @aws-sdk/client-sqs SQSClient
68
+ queueUrl: process.env.SQS_URL!,
69
+ waitTimeSeconds: 10, // long-poll seconds for pop (0–20, default 0)
70
+ largePayload: { // optional S3 offload of oversized bodies
71
+ store: files, // any @ayepi/files FileStore
72
+ threshold: 240 * 1024, // offload bodies larger than this (default ~240 KB)
73
+ prefix: 'sqs-payloads/', // key prefix for offloaded bodies (default)
74
+ },
75
+ retry: { attempts: 8 },
76
+ onError: (err) => log(err),
77
+ })
78
+ ```
79
+
80
+ ## For AI coding agents
81
+
82
+ This package ships dense, machine-oriented reference docs written for **AI coding agents**
83
+ (Claude Code, Cursor, and the like) to understand and drive the package — point your agent at them:
84
+
85
+ - [`ayepi-aws.md`](./ayepi-aws.md)
86
+
87
+ They live next to the source in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/aws) and are **not** shipped in the npm tarball.
88
+
89
+ ## License
90
+
91
+ MIT © Philip Diffenderfer
package/dist/index.cjs ADDED
@@ -0,0 +1,32 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _ayepi_core = require("@ayepi/core");
3
+ //#region src/index.ts
4
+ /**
5
+ * # @ayepi/aws
6
+ *
7
+ * AWS backends for ayepi:
8
+ * - **`@ayepi/aws/s3`** — an {@link FileStore} over S3 (stream get/put, prefix list,
9
+ * presigned URLs).
10
+ * - **`@ayepi/aws/sqs`** — an `@ayepi/work` `Queue` over SQS, with large payloads
11
+ * transparently offloaded to S3 (SQS caps a message at 256 KB).
12
+ *
13
+ * Both wrap every AWS call in core {@link retry} (configurable) because SQS/S3 throttle
14
+ * under load, and expose an `onError` hook fired when a call finally gives up. The AWS SDK
15
+ * v3 clients are **optional peer dependencies** — install the ones you use.
16
+ *
17
+ * @module
18
+ */
19
+ /** Build a retry-wrapping runner that reports a final failure through `onError`. */
20
+ function makeRun(opts) {
21
+ const report = (err) => {
22
+ try {
23
+ opts.onError?.(err);
24
+ } catch {}
25
+ };
26
+ return (fn) => (0, _ayepi_core.retry)(fn, {
27
+ ...opts.retry,
28
+ onError: (err) => report(err)
29
+ });
30
+ }
31
+ //#endregion
32
+ exports.makeRun = makeRun;
@@ -0,0 +1,23 @@
1
+ import { RetryOptions } from "@ayepi/core";
2
+
3
+ //#region src/index.d.ts
4
+
5
+ /**
6
+ * The minimal AWS SDK v3 client surface used internally — `send(command)`. The real
7
+ * `S3Client` / `SQSClient` satisfy it; a test can pass `{ send: vi.fn() }`. (Presigning and
8
+ * multipart upload need the *concrete* `S3Client`, so `@ayepi/aws/s3` takes that directly.)
9
+ */
10
+ interface AwsClient {
11
+ send(command: unknown): Promise<unknown>;
12
+ }
13
+ /** Resilience options shared by the S3 store and the SQS queue. */
14
+ interface ResilientOptions {
15
+ /** Retry policy for each AWS call (core `retry` — `attempts`/`base`/`factor`/`max`/`jitter`/…). Defaults absorb throttling. */
16
+ readonly retry?: Omit<RetryOptions, 'errorResult'>;
17
+ /** Notified when a call fails after exhausting retries (the error then propagates). Off by default; must not throw. */
18
+ readonly onError?: (err: unknown) => void;
19
+ }
20
+ /** Build a retry-wrapping runner that reports a final failure through `onError`. */
21
+ declare function makeRun(opts: ResilientOptions): <T>(fn: () => Promise<T>) => Promise<T>;
22
+ //#endregion
23
+ export { AwsClient, ResilientOptions, makeRun };
@@ -0,0 +1,23 @@
1
+ import { RetryOptions } from "@ayepi/core";
2
+
3
+ //#region src/index.d.ts
4
+
5
+ /**
6
+ * The minimal AWS SDK v3 client surface used internally — `send(command)`. The real
7
+ * `S3Client` / `SQSClient` satisfy it; a test can pass `{ send: vi.fn() }`. (Presigning and
8
+ * multipart upload need the *concrete* `S3Client`, so `@ayepi/aws/s3` takes that directly.)
9
+ */
10
+ interface AwsClient {
11
+ send(command: unknown): Promise<unknown>;
12
+ }
13
+ /** Resilience options shared by the S3 store and the SQS queue. */
14
+ interface ResilientOptions {
15
+ /** Retry policy for each AWS call (core `retry` — `attempts`/`base`/`factor`/`max`/`jitter`/…). Defaults absorb throttling. */
16
+ readonly retry?: Omit<RetryOptions, 'errorResult'>;
17
+ /** Notified when a call fails after exhausting retries (the error then propagates). Off by default; must not throw. */
18
+ readonly onError?: (err: unknown) => void;
19
+ }
20
+ /** Build a retry-wrapping runner that reports a final failure through `onError`. */
21
+ declare function makeRun(opts: ResilientOptions): <T>(fn: () => Promise<T>) => Promise<T>;
22
+ //#endregion
23
+ export { AwsClient, ResilientOptions, makeRun };
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ import { retry } from "@ayepi/core";
2
+ //#region src/index.ts
3
+ /**
4
+ * # @ayepi/aws
5
+ *
6
+ * AWS backends for ayepi:
7
+ * - **`@ayepi/aws/s3`** — an {@link FileStore} over S3 (stream get/put, prefix list,
8
+ * presigned URLs).
9
+ * - **`@ayepi/aws/sqs`** — an `@ayepi/work` `Queue` over SQS, with large payloads
10
+ * transparently offloaded to S3 (SQS caps a message at 256 KB).
11
+ *
12
+ * Both wrap every AWS call in core {@link retry} (configurable) because SQS/S3 throttle
13
+ * under load, and expose an `onError` hook fired when a call finally gives up. The AWS SDK
14
+ * v3 clients are **optional peer dependencies** — install the ones you use.
15
+ *
16
+ * @module
17
+ */
18
+ /** Build a retry-wrapping runner that reports a final failure through `onError`. */
19
+ function makeRun(opts) {
20
+ const report = (err) => {
21
+ try {
22
+ opts.onError?.(err);
23
+ } catch {}
24
+ };
25
+ return (fn) => retry(fn, {
26
+ ...opts.retry,
27
+ onError: (err) => report(err)
28
+ });
29
+ }
30
+ //#endregion
31
+ export { makeRun };
package/dist/s3.cjs ADDED
@@ -0,0 +1,138 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_index = require("./index.cjs");
3
+ let node_stream = require("node:stream");
4
+ let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
5
+ let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
6
+ let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
7
+ let _ayepi_files = require("@ayepi/files");
8
+ //#region src/s3.ts
9
+ /**
10
+ * # @ayepi/aws/s3 — an S3-backed {@link FileStore}
11
+ *
12
+ * Stream objects to/from S3 under a key, list by prefix, and mint presigned upload/download
13
+ * URLs (native to S3 — no server route needed). Every call is wrapped in core {@link retry}
14
+ * so a throttled reply is absorbed.
15
+ *
16
+ * ```ts
17
+ * import { S3Client } from '@aws-sdk/client-s3';
18
+ * import { s3Files } from '@ayepi/aws/s3';
19
+ * const files = s3Files({ client: new S3Client({ region: 'us-east-1' }), bucket: 'my-bucket' });
20
+ * await files.put('reports/2026.csv', readableStream, { contentType: 'text/csv' });
21
+ * const url = await files.presignDownload('reports/2026.csv', { expiresIn: 300 });
22
+ * ```
23
+ *
24
+ * Note: an S3 `FileObject`'s body is read **once** — call one of `stream()`/`bytes()`/`text()`.
25
+ *
26
+ * @module
27
+ */
28
+ /** Default presigned-URL lifetime (seconds). */
29
+ const DEFAULT_EXPIRES_IN = 900;
30
+ /** S3 surfaces a missing key differently per op (`NoSuchKey` on GET, `NotFound` on HEAD). */
31
+ function isNotFound(err) {
32
+ const name = err?.name;
33
+ const status = err?.$metadata?.httpStatusCode;
34
+ return name === "NoSuchKey" || name === "NotFound" || status === 404;
35
+ }
36
+ /**
37
+ * Create an S3-backed {@link FileStore} (and {@link Presigner}). Pass a configured `S3Client`
38
+ * and a bucket; `prefix` namespaces all keys.
39
+ */
40
+ function s3Files(opts) {
41
+ const { client, bucket } = opts;
42
+ const ns = opts.prefix ?? "";
43
+ const run = require_index.makeRun(opts);
44
+ const k = (key) => ns + key;
45
+ /* v8 ignore start -- SDK glue: exercised only against real S3 (integration test) */
46
+ const sdkUpload = (key, body, contentType, metadata) => new _aws_sdk_lib_storage.Upload({
47
+ client,
48
+ params: {
49
+ Bucket: bucket,
50
+ Key: k(key),
51
+ Body: node_stream.Readable.fromWeb((0, _ayepi_files.toStream)(body)),
52
+ ContentType: contentType,
53
+ Metadata: metadata
54
+ }
55
+ }).done().then(() => void 0);
56
+ const sdkPresign = (kind, key, expiresIn, contentType) => (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, kind === "get" ? new _aws_sdk_client_s3.GetObjectCommand({
57
+ Bucket: bucket,
58
+ Key: k(key)
59
+ }) : new _aws_sdk_client_s3.PutObjectCommand({
60
+ Bucket: bucket,
61
+ Key: k(key),
62
+ ContentType: contentType
63
+ }), { expiresIn });
64
+ /* v8 ignore stop */
65
+ const upload = opts.upload ?? sdkUpload;
66
+ const presign = opts.presign ?? sdkPresign;
67
+ const infoFrom = (key, size, contentType, etag, modified, metadata) => ({
68
+ key,
69
+ size: size ?? 0,
70
+ contentType,
71
+ etag,
72
+ modifiedAt: modified ? modified.getTime() : 0,
73
+ metadata
74
+ });
75
+ const headRaw = async (key) => {
76
+ try {
77
+ const out = await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
78
+ Bucket: bucket,
79
+ Key: k(key)
80
+ }));
81
+ return infoFrom(key, out.ContentLength, out.ContentType, out.ETag, out.LastModified, out.Metadata);
82
+ } catch (err) {
83
+ if (isNotFound(err)) return;
84
+ throw err;
85
+ }
86
+ };
87
+ return {
88
+ put: (key, body, options) => run(async () => {
89
+ await upload(key, body, options?.contentType, options?.metadata);
90
+ return await headRaw(key);
91
+ }),
92
+ head: (key) => run(() => headRaw(key)),
93
+ get: (key) => run(async () => {
94
+ let out;
95
+ try {
96
+ out = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
97
+ Bucket: bucket,
98
+ Key: k(key)
99
+ }));
100
+ } catch (err) {
101
+ if (isNotFound(err)) return;
102
+ throw err;
103
+ }
104
+ const info = infoFrom(key, out.ContentLength, out.ContentType, out.ETag, out.LastModified, out.Metadata);
105
+ const body = out.Body;
106
+ return {
107
+ info,
108
+ stream: () => body.transformToWebStream(),
109
+ bytes: () => body.transformToByteArray(),
110
+ text: () => body.transformToString()
111
+ };
112
+ }),
113
+ delete: (key) => run(async () => {
114
+ const existed = await headRaw(key) !== void 0;
115
+ await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
116
+ Bucket: bucket,
117
+ Key: k(key)
118
+ }));
119
+ return existed;
120
+ }),
121
+ list: (prefix, options) => run(async () => {
122
+ const out = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
123
+ Bucket: bucket,
124
+ Prefix: k(prefix),
125
+ MaxKeys: options?.limit,
126
+ ContinuationToken: options?.cursor
127
+ }));
128
+ return {
129
+ files: (out.Contents ?? []).map((o) => infoFrom((o.Key ?? "").slice(ns.length), o.Size, void 0, o.ETag, o.LastModified)),
130
+ cursor: out.NextContinuationToken
131
+ };
132
+ }),
133
+ presignDownload: (key, o) => presign("get", key, o?.expiresIn ?? DEFAULT_EXPIRES_IN),
134
+ presignUpload: (key, o) => presign("put", key, o?.expiresIn ?? DEFAULT_EXPIRES_IN, o?.contentType)
135
+ };
136
+ }
137
+ //#endregion
138
+ exports.s3Files = s3Files;
package/dist/s3.d.cts ADDED
@@ -0,0 +1,26 @@
1
+ import { ResilientOptions } from "./index.cjs";
2
+ import { S3Client } from "@aws-sdk/client-s3";
3
+ import { FileBody, FileStore, Presigner } from "@ayepi/files";
4
+
5
+ //#region src/s3.d.ts
6
+
7
+ /** Options for {@link s3Files}. */
8
+ interface S3FilesOptions extends ResilientOptions {
9
+ /** A configured `@aws-sdk/client-s3` `S3Client`. */
10
+ readonly client: S3Client;
11
+ /** Target bucket. */
12
+ readonly bucket: string;
13
+ /** Key prefix prepended to every key (default `''`). */
14
+ readonly prefix?: string;
15
+ /** @internal Upload seam (default: `@aws-sdk/lib-storage` multipart `Upload`) — injectable for tests. */
16
+ readonly upload?: (key: string, body: FileBody, contentType?: string, metadata?: Record<string, string>) => Promise<void>;
17
+ /** @internal Presign seam (default: `@aws-sdk/s3-request-presigner`) — injectable for tests. */
18
+ readonly presign?: (kind: 'get' | 'put', key: string, expiresIn: number, contentType?: string) => Promise<string>;
19
+ }
20
+ /**
21
+ * Create an S3-backed {@link FileStore} (and {@link Presigner}). Pass a configured `S3Client`
22
+ * and a bucket; `prefix` namespaces all keys.
23
+ */
24
+ declare function s3Files(opts: S3FilesOptions): FileStore & Presigner;
25
+ //#endregion
26
+ export { S3FilesOptions, s3Files };
package/dist/s3.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { ResilientOptions } from "./index.js";
2
+ import { S3Client } from "@aws-sdk/client-s3";
3
+ import { FileBody, FileStore, Presigner } from "@ayepi/files";
4
+
5
+ //#region src/s3.d.ts
6
+
7
+ /** Options for {@link s3Files}. */
8
+ interface S3FilesOptions extends ResilientOptions {
9
+ /** A configured `@aws-sdk/client-s3` `S3Client`. */
10
+ readonly client: S3Client;
11
+ /** Target bucket. */
12
+ readonly bucket: string;
13
+ /** Key prefix prepended to every key (default `''`). */
14
+ readonly prefix?: string;
15
+ /** @internal Upload seam (default: `@aws-sdk/lib-storage` multipart `Upload`) — injectable for tests. */
16
+ readonly upload?: (key: string, body: FileBody, contentType?: string, metadata?: Record<string, string>) => Promise<void>;
17
+ /** @internal Presign seam (default: `@aws-sdk/s3-request-presigner`) — injectable for tests. */
18
+ readonly presign?: (kind: 'get' | 'put', key: string, expiresIn: number, contentType?: string) => Promise<string>;
19
+ }
20
+ /**
21
+ * Create an S3-backed {@link FileStore} (and {@link Presigner}). Pass a configured `S3Client`
22
+ * and a bucket; `prefix` namespaces all keys.
23
+ */
24
+ declare function s3Files(opts: S3FilesOptions): FileStore & Presigner;
25
+ //#endregion
26
+ export { S3FilesOptions, s3Files };
package/dist/s3.js ADDED
@@ -0,0 +1,137 @@
1
+ import { makeRun } from "./index.js";
2
+ import { Readable } from "node:stream";
3
+ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
4
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
5
+ import { Upload } from "@aws-sdk/lib-storage";
6
+ import { toStream } from "@ayepi/files";
7
+ //#region src/s3.ts
8
+ /**
9
+ * # @ayepi/aws/s3 — an S3-backed {@link FileStore}
10
+ *
11
+ * Stream objects to/from S3 under a key, list by prefix, and mint presigned upload/download
12
+ * URLs (native to S3 — no server route needed). Every call is wrapped in core {@link retry}
13
+ * so a throttled reply is absorbed.
14
+ *
15
+ * ```ts
16
+ * import { S3Client } from '@aws-sdk/client-s3';
17
+ * import { s3Files } from '@ayepi/aws/s3';
18
+ * const files = s3Files({ client: new S3Client({ region: 'us-east-1' }), bucket: 'my-bucket' });
19
+ * await files.put('reports/2026.csv', readableStream, { contentType: 'text/csv' });
20
+ * const url = await files.presignDownload('reports/2026.csv', { expiresIn: 300 });
21
+ * ```
22
+ *
23
+ * Note: an S3 `FileObject`'s body is read **once** — call one of `stream()`/`bytes()`/`text()`.
24
+ *
25
+ * @module
26
+ */
27
+ /** Default presigned-URL lifetime (seconds). */
28
+ const DEFAULT_EXPIRES_IN = 900;
29
+ /** S3 surfaces a missing key differently per op (`NoSuchKey` on GET, `NotFound` on HEAD). */
30
+ function isNotFound(err) {
31
+ const name = err?.name;
32
+ const status = err?.$metadata?.httpStatusCode;
33
+ return name === "NoSuchKey" || name === "NotFound" || status === 404;
34
+ }
35
+ /**
36
+ * Create an S3-backed {@link FileStore} (and {@link Presigner}). Pass a configured `S3Client`
37
+ * and a bucket; `prefix` namespaces all keys.
38
+ */
39
+ function s3Files(opts) {
40
+ const { client, bucket } = opts;
41
+ const ns = opts.prefix ?? "";
42
+ const run = makeRun(opts);
43
+ const k = (key) => ns + key;
44
+ /* v8 ignore start -- SDK glue: exercised only against real S3 (integration test) */
45
+ const sdkUpload = (key, body, contentType, metadata) => new Upload({
46
+ client,
47
+ params: {
48
+ Bucket: bucket,
49
+ Key: k(key),
50
+ Body: Readable.fromWeb(toStream(body)),
51
+ ContentType: contentType,
52
+ Metadata: metadata
53
+ }
54
+ }).done().then(() => void 0);
55
+ const sdkPresign = (kind, key, expiresIn, contentType) => getSignedUrl(client, kind === "get" ? new GetObjectCommand({
56
+ Bucket: bucket,
57
+ Key: k(key)
58
+ }) : new PutObjectCommand({
59
+ Bucket: bucket,
60
+ Key: k(key),
61
+ ContentType: contentType
62
+ }), { expiresIn });
63
+ /* v8 ignore stop */
64
+ const upload = opts.upload ?? sdkUpload;
65
+ const presign = opts.presign ?? sdkPresign;
66
+ const infoFrom = (key, size, contentType, etag, modified, metadata) => ({
67
+ key,
68
+ size: size ?? 0,
69
+ contentType,
70
+ etag,
71
+ modifiedAt: modified ? modified.getTime() : 0,
72
+ metadata
73
+ });
74
+ const headRaw = async (key) => {
75
+ try {
76
+ const out = await client.send(new HeadObjectCommand({
77
+ Bucket: bucket,
78
+ Key: k(key)
79
+ }));
80
+ return infoFrom(key, out.ContentLength, out.ContentType, out.ETag, out.LastModified, out.Metadata);
81
+ } catch (err) {
82
+ if (isNotFound(err)) return;
83
+ throw err;
84
+ }
85
+ };
86
+ return {
87
+ put: (key, body, options) => run(async () => {
88
+ await upload(key, body, options?.contentType, options?.metadata);
89
+ return await headRaw(key);
90
+ }),
91
+ head: (key) => run(() => headRaw(key)),
92
+ get: (key) => run(async () => {
93
+ let out;
94
+ try {
95
+ out = await client.send(new GetObjectCommand({
96
+ Bucket: bucket,
97
+ Key: k(key)
98
+ }));
99
+ } catch (err) {
100
+ if (isNotFound(err)) return;
101
+ throw err;
102
+ }
103
+ const info = infoFrom(key, out.ContentLength, out.ContentType, out.ETag, out.LastModified, out.Metadata);
104
+ const body = out.Body;
105
+ return {
106
+ info,
107
+ stream: () => body.transformToWebStream(),
108
+ bytes: () => body.transformToByteArray(),
109
+ text: () => body.transformToString()
110
+ };
111
+ }),
112
+ delete: (key) => run(async () => {
113
+ const existed = await headRaw(key) !== void 0;
114
+ await client.send(new DeleteObjectCommand({
115
+ Bucket: bucket,
116
+ Key: k(key)
117
+ }));
118
+ return existed;
119
+ }),
120
+ list: (prefix, options) => run(async () => {
121
+ const out = await client.send(new ListObjectsV2Command({
122
+ Bucket: bucket,
123
+ Prefix: k(prefix),
124
+ MaxKeys: options?.limit,
125
+ ContinuationToken: options?.cursor
126
+ }));
127
+ return {
128
+ files: (out.Contents ?? []).map((o) => infoFrom((o.Key ?? "").slice(ns.length), o.Size, void 0, o.ETag, o.LastModified)),
129
+ cursor: out.NextContinuationToken
130
+ };
131
+ }),
132
+ presignDownload: (key, o) => presign("get", key, o?.expiresIn ?? DEFAULT_EXPIRES_IN),
133
+ presignUpload: (key, o) => presign("put", key, o?.expiresIn ?? DEFAULT_EXPIRES_IN, o?.contentType)
134
+ };
135
+ }
136
+ //#endregion
137
+ export { s3Files };
package/dist/sqs.cjs ADDED
@@ -0,0 +1,135 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_index = require("./index.cjs");
3
+ let node_crypto = require("node:crypto");
4
+ let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
5
+ //#region src/sqs.ts
6
+ /**
7
+ * # @ayepi/aws/sqs — an SQS-backed `@ayepi/work` {@link Queue}
8
+ *
9
+ * SQS's native visibility-timeout model maps directly onto the work {@link Queue} contract:
10
+ * `pop` is `ReceiveMessage` (a lease), `heartbeat` is `ChangeMessageVisibility`, `ack` is
11
+ * `DeleteMessage`, `fail` returns the message early. A worker that dies lets the visibility
12
+ * lapse and SQS redelivers (with `ApproximateReceiveCount` → `attempt`); exhausted retries go
13
+ * to the queue's configured **dead-letter queue** (SQS redrive).
14
+ *
15
+ * **Large payloads** (SQS caps a message at 256 KB) are transparently offloaded: a body over
16
+ * the threshold is written to S3 (any {@link FileStore}) and the message carries a small
17
+ * pointer; `pop` inlines it back and `ack` deletes the S3 object. Every AWS call is wrapped in
18
+ * core {@link retry}.
19
+ *
20
+ * @module
21
+ */
22
+ /** SQS allows at most 10 messages per `ReceiveMessage`. */
23
+ const MAX_RECEIVE = 10;
24
+ /** SQS rejects `DelaySeconds` outside 0–900 (15 min). */
25
+ const MAX_DELAY_SECONDS = 900;
26
+ /** SQS rejects a visibility timeout outside 0–43200 (12 h). */
27
+ const MAX_VISIBILITY_SECONDS = 43200;
28
+ /** Default large-payload threshold (bytes) — comfortably under SQS's 256 KB message limit. */
29
+ const DEFAULT_THRESHOLD = 240 * 1024;
30
+ /** The marker key identifying an S3-offloaded message body. */
31
+ const S3_MARKER = "__ayepiS3__";
32
+ const MS_PER_SECOND = 1e3;
33
+ const secs = (ms) => Math.ceil(ms / MS_PER_SECOND);
34
+ /** Clamp a delay (ms) to SQS's `DelaySeconds` range so a far-future schedule doesn't error (the engine re-defers). */
35
+ const delaySecs = (ms) => Math.min(MAX_DELAY_SECONDS, Math.max(0, secs(ms)));
36
+ /** Clamp a visibility/backoff (ms) to SQS's max so a long `fail`/`heartbeat` doesn't error (the engine re-defers). */
37
+ const visSecs = (ms) => Math.min(MAX_VISIBILITY_SECONDS, Math.max(0, secs(ms)));
38
+ /** If `body` is an S3 pointer, return the key; else `undefined`. */
39
+ function pointerKey(body) {
40
+ try {
41
+ const key = JSON.parse(body)[S3_MARKER];
42
+ return typeof key === "string" ? key : void 0;
43
+ } catch {
44
+ return;
45
+ }
46
+ }
47
+ /**
48
+ * Create an SQS-backed `@ayepi/work` {@link Queue}. Configure the SQS DLQ (redrive policy) on
49
+ * the queue itself for dead-lettering; pass `largePayload` to offload oversized bodies to S3.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { SQSClient } from '@aws-sdk/client-sqs';
54
+ * import { sqsQueue } from '@ayepi/aws/sqs';
55
+ * import { s3Files } from '@ayepi/aws/s3';
56
+ * const queue = sqsQueue({
57
+ * client: new SQSClient({ region: 'us-east-1' }),
58
+ * queueUrl: process.env.SQS_URL!,
59
+ * waitTimeSeconds: 10,
60
+ * largePayload: { store: s3Files({ client: s3, bucket: 'work-payloads' }) },
61
+ * });
62
+ * createWork({ work, queue, store: redisStore(redis), pubsub: redisPubSub(redis) });
63
+ * ```
64
+ */
65
+ function sqsQueue(opts) {
66
+ const { client, queueUrl } = opts;
67
+ const run = require_index.makeRun(opts);
68
+ const large = opts.largePayload;
69
+ const threshold = large?.threshold ?? DEFAULT_THRESHOLD;
70
+ const largePrefix = large?.prefix ?? "sqs-payloads/";
71
+ const waitTimeSeconds = opts.waitTimeSeconds;
72
+ const handleOf = (pulled) => pulled.handle;
73
+ return {
74
+ push: (body, o) => run(async () => {
75
+ let messageBody = body;
76
+ if (large && body.length > threshold) {
77
+ const key = `${largePrefix}${(0, node_crypto.randomUUID)()}`;
78
+ await large.store.put(key, body, { contentType: "application/json" });
79
+ messageBody = JSON.stringify({ [S3_MARKER]: key });
80
+ }
81
+ await client.send(new _aws_sdk_client_sqs.SendMessageCommand({
82
+ QueueUrl: queueUrl,
83
+ MessageBody: messageBody,
84
+ DelaySeconds: o?.delay !== void 0 ? delaySecs(o.delay) : void 0
85
+ }));
86
+ }),
87
+ pop: (max, visibility) => run(async () => {
88
+ const out = await client.send(new _aws_sdk_client_sqs.ReceiveMessageCommand({
89
+ QueueUrl: queueUrl,
90
+ MaxNumberOfMessages: Math.min(max, MAX_RECEIVE),
91
+ VisibilityTimeout: visSecs(visibility),
92
+ WaitTimeSeconds: waitTimeSeconds,
93
+ MessageSystemAttributeNames: ["ApproximateReceiveCount"]
94
+ }));
95
+ return Promise.all((out.Messages ?? []).map(async (m) => {
96
+ const attempt = Number(m.Attributes?.ApproximateReceiveCount ?? "1");
97
+ let body = m.Body ?? "";
98
+ const s3Key = large ? pointerKey(body) : void 0;
99
+ if (s3Key) body = await (await large.store.get(s3Key))?.text() ?? "";
100
+ return {
101
+ body,
102
+ handle: {
103
+ receiptHandle: m.ReceiptHandle ?? "",
104
+ s3Key
105
+ },
106
+ attempt
107
+ };
108
+ }));
109
+ }),
110
+ heartbeat: (pulled, visibility) => run(async () => {
111
+ await client.send(new _aws_sdk_client_sqs.ChangeMessageVisibilityCommand({
112
+ QueueUrl: queueUrl,
113
+ ReceiptHandle: handleOf(pulled).receiptHandle,
114
+ VisibilityTimeout: visSecs(visibility)
115
+ }));
116
+ }),
117
+ ack: (pulled) => run(async () => {
118
+ const h = handleOf(pulled);
119
+ await client.send(new _aws_sdk_client_sqs.DeleteMessageCommand({
120
+ QueueUrl: queueUrl,
121
+ ReceiptHandle: h.receiptHandle
122
+ }));
123
+ if (h.s3Key && large) await large.store.delete(h.s3Key);
124
+ }),
125
+ fail: (pulled, delay) => run(async () => {
126
+ await client.send(new _aws_sdk_client_sqs.ChangeMessageVisibilityCommand({
127
+ QueueUrl: queueUrl,
128
+ ReceiptHandle: handleOf(pulled).receiptHandle,
129
+ VisibilityTimeout: visSecs(delay ?? 0)
130
+ }));
131
+ })
132
+ };
133
+ }
134
+ //#endregion
135
+ exports.sqsQueue = sqsQueue;
package/dist/sqs.d.cts ADDED
@@ -0,0 +1,48 @@
1
+ import { ResilientOptions } from "./index.cjs";
2
+ import { FileStore } from "@ayepi/files";
3
+ import { SQSClient } from "@aws-sdk/client-sqs";
4
+ import { Queue } from "@ayepi/work";
5
+
6
+ //#region src/sqs.d.ts
7
+
8
+ /** Offload config — where oversized message bodies live. */
9
+ interface LargePayloadOptions {
10
+ /** The store oversized bodies are written to (e.g. `s3Files({...})`). */
11
+ readonly store: FileStore;
12
+ /** Offload bodies larger than this many bytes (default ~240 KB). */
13
+ readonly threshold?: number;
14
+ /** Key prefix for offloaded bodies (default `'sqs-payloads/'`). */
15
+ readonly prefix?: string;
16
+ }
17
+ /** Options for {@link sqsQueue}. */
18
+ interface SqsQueueOptions extends ResilientOptions {
19
+ /** A configured `@aws-sdk/client-sqs` `SQSClient`. */
20
+ readonly client: SQSClient;
21
+ /** The target queue URL. */
22
+ readonly queueUrl: string;
23
+ /** Long-poll seconds for `pop` (0–20, default 0). */
24
+ readonly waitTimeSeconds?: number;
25
+ /** Transparently offload large bodies to S3. */
26
+ readonly largePayload?: LargePayloadOptions;
27
+ }
28
+ /**
29
+ * Create an SQS-backed `@ayepi/work` {@link Queue}. Configure the SQS DLQ (redrive policy) on
30
+ * the queue itself for dead-lettering; pass `largePayload` to offload oversized bodies to S3.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { SQSClient } from '@aws-sdk/client-sqs';
35
+ * import { sqsQueue } from '@ayepi/aws/sqs';
36
+ * import { s3Files } from '@ayepi/aws/s3';
37
+ * const queue = sqsQueue({
38
+ * client: new SQSClient({ region: 'us-east-1' }),
39
+ * queueUrl: process.env.SQS_URL!,
40
+ * waitTimeSeconds: 10,
41
+ * largePayload: { store: s3Files({ client: s3, bucket: 'work-payloads' }) },
42
+ * });
43
+ * createWork({ work, queue, store: redisStore(redis), pubsub: redisPubSub(redis) });
44
+ * ```
45
+ */
46
+ declare function sqsQueue(opts: SqsQueueOptions): Queue;
47
+ //#endregion
48
+ export { LargePayloadOptions, SqsQueueOptions, sqsQueue };
package/dist/sqs.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { ResilientOptions } from "./index.js";
2
+ import { FileStore } from "@ayepi/files";
3
+ import { SQSClient } from "@aws-sdk/client-sqs";
4
+ import { Queue } from "@ayepi/work";
5
+
6
+ //#region src/sqs.d.ts
7
+
8
+ /** Offload config — where oversized message bodies live. */
9
+ interface LargePayloadOptions {
10
+ /** The store oversized bodies are written to (e.g. `s3Files({...})`). */
11
+ readonly store: FileStore;
12
+ /** Offload bodies larger than this many bytes (default ~240 KB). */
13
+ readonly threshold?: number;
14
+ /** Key prefix for offloaded bodies (default `'sqs-payloads/'`). */
15
+ readonly prefix?: string;
16
+ }
17
+ /** Options for {@link sqsQueue}. */
18
+ interface SqsQueueOptions extends ResilientOptions {
19
+ /** A configured `@aws-sdk/client-sqs` `SQSClient`. */
20
+ readonly client: SQSClient;
21
+ /** The target queue URL. */
22
+ readonly queueUrl: string;
23
+ /** Long-poll seconds for `pop` (0–20, default 0). */
24
+ readonly waitTimeSeconds?: number;
25
+ /** Transparently offload large bodies to S3. */
26
+ readonly largePayload?: LargePayloadOptions;
27
+ }
28
+ /**
29
+ * Create an SQS-backed `@ayepi/work` {@link Queue}. Configure the SQS DLQ (redrive policy) on
30
+ * the queue itself for dead-lettering; pass `largePayload` to offload oversized bodies to S3.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { SQSClient } from '@aws-sdk/client-sqs';
35
+ * import { sqsQueue } from '@ayepi/aws/sqs';
36
+ * import { s3Files } from '@ayepi/aws/s3';
37
+ * const queue = sqsQueue({
38
+ * client: new SQSClient({ region: 'us-east-1' }),
39
+ * queueUrl: process.env.SQS_URL!,
40
+ * waitTimeSeconds: 10,
41
+ * largePayload: { store: s3Files({ client: s3, bucket: 'work-payloads' }) },
42
+ * });
43
+ * createWork({ work, queue, store: redisStore(redis), pubsub: redisPubSub(redis) });
44
+ * ```
45
+ */
46
+ declare function sqsQueue(opts: SqsQueueOptions): Queue;
47
+ //#endregion
48
+ export { LargePayloadOptions, SqsQueueOptions, sqsQueue };
package/dist/sqs.js ADDED
@@ -0,0 +1,134 @@
1
+ import { makeRun } from "./index.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { ChangeMessageVisibilityCommand, DeleteMessageCommand, ReceiveMessageCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
4
+ //#region src/sqs.ts
5
+ /**
6
+ * # @ayepi/aws/sqs — an SQS-backed `@ayepi/work` {@link Queue}
7
+ *
8
+ * SQS's native visibility-timeout model maps directly onto the work {@link Queue} contract:
9
+ * `pop` is `ReceiveMessage` (a lease), `heartbeat` is `ChangeMessageVisibility`, `ack` is
10
+ * `DeleteMessage`, `fail` returns the message early. A worker that dies lets the visibility
11
+ * lapse and SQS redelivers (with `ApproximateReceiveCount` → `attempt`); exhausted retries go
12
+ * to the queue's configured **dead-letter queue** (SQS redrive).
13
+ *
14
+ * **Large payloads** (SQS caps a message at 256 KB) are transparently offloaded: a body over
15
+ * the threshold is written to S3 (any {@link FileStore}) and the message carries a small
16
+ * pointer; `pop` inlines it back and `ack` deletes the S3 object. Every AWS call is wrapped in
17
+ * core {@link retry}.
18
+ *
19
+ * @module
20
+ */
21
+ /** SQS allows at most 10 messages per `ReceiveMessage`. */
22
+ const MAX_RECEIVE = 10;
23
+ /** SQS rejects `DelaySeconds` outside 0–900 (15 min). */
24
+ const MAX_DELAY_SECONDS = 900;
25
+ /** SQS rejects a visibility timeout outside 0–43200 (12 h). */
26
+ const MAX_VISIBILITY_SECONDS = 43200;
27
+ /** Default large-payload threshold (bytes) — comfortably under SQS's 256 KB message limit. */
28
+ const DEFAULT_THRESHOLD = 240 * 1024;
29
+ /** The marker key identifying an S3-offloaded message body. */
30
+ const S3_MARKER = "__ayepiS3__";
31
+ const MS_PER_SECOND = 1e3;
32
+ const secs = (ms) => Math.ceil(ms / MS_PER_SECOND);
33
+ /** Clamp a delay (ms) to SQS's `DelaySeconds` range so a far-future schedule doesn't error (the engine re-defers). */
34
+ const delaySecs = (ms) => Math.min(MAX_DELAY_SECONDS, Math.max(0, secs(ms)));
35
+ /** Clamp a visibility/backoff (ms) to SQS's max so a long `fail`/`heartbeat` doesn't error (the engine re-defers). */
36
+ const visSecs = (ms) => Math.min(MAX_VISIBILITY_SECONDS, Math.max(0, secs(ms)));
37
+ /** If `body` is an S3 pointer, return the key; else `undefined`. */
38
+ function pointerKey(body) {
39
+ try {
40
+ const key = JSON.parse(body)[S3_MARKER];
41
+ return typeof key === "string" ? key : void 0;
42
+ } catch {
43
+ return;
44
+ }
45
+ }
46
+ /**
47
+ * Create an SQS-backed `@ayepi/work` {@link Queue}. Configure the SQS DLQ (redrive policy) on
48
+ * the queue itself for dead-lettering; pass `largePayload` to offload oversized bodies to S3.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { SQSClient } from '@aws-sdk/client-sqs';
53
+ * import { sqsQueue } from '@ayepi/aws/sqs';
54
+ * import { s3Files } from '@ayepi/aws/s3';
55
+ * const queue = sqsQueue({
56
+ * client: new SQSClient({ region: 'us-east-1' }),
57
+ * queueUrl: process.env.SQS_URL!,
58
+ * waitTimeSeconds: 10,
59
+ * largePayload: { store: s3Files({ client: s3, bucket: 'work-payloads' }) },
60
+ * });
61
+ * createWork({ work, queue, store: redisStore(redis), pubsub: redisPubSub(redis) });
62
+ * ```
63
+ */
64
+ function sqsQueue(opts) {
65
+ const { client, queueUrl } = opts;
66
+ const run = makeRun(opts);
67
+ const large = opts.largePayload;
68
+ const threshold = large?.threshold ?? DEFAULT_THRESHOLD;
69
+ const largePrefix = large?.prefix ?? "sqs-payloads/";
70
+ const waitTimeSeconds = opts.waitTimeSeconds;
71
+ const handleOf = (pulled) => pulled.handle;
72
+ return {
73
+ push: (body, o) => run(async () => {
74
+ let messageBody = body;
75
+ if (large && body.length > threshold) {
76
+ const key = `${largePrefix}${randomUUID()}`;
77
+ await large.store.put(key, body, { contentType: "application/json" });
78
+ messageBody = JSON.stringify({ [S3_MARKER]: key });
79
+ }
80
+ await client.send(new SendMessageCommand({
81
+ QueueUrl: queueUrl,
82
+ MessageBody: messageBody,
83
+ DelaySeconds: o?.delay !== void 0 ? delaySecs(o.delay) : void 0
84
+ }));
85
+ }),
86
+ pop: (max, visibility) => run(async () => {
87
+ const out = await client.send(new ReceiveMessageCommand({
88
+ QueueUrl: queueUrl,
89
+ MaxNumberOfMessages: Math.min(max, MAX_RECEIVE),
90
+ VisibilityTimeout: visSecs(visibility),
91
+ WaitTimeSeconds: waitTimeSeconds,
92
+ MessageSystemAttributeNames: ["ApproximateReceiveCount"]
93
+ }));
94
+ return Promise.all((out.Messages ?? []).map(async (m) => {
95
+ const attempt = Number(m.Attributes?.ApproximateReceiveCount ?? "1");
96
+ let body = m.Body ?? "";
97
+ const s3Key = large ? pointerKey(body) : void 0;
98
+ if (s3Key) body = await (await large.store.get(s3Key))?.text() ?? "";
99
+ return {
100
+ body,
101
+ handle: {
102
+ receiptHandle: m.ReceiptHandle ?? "",
103
+ s3Key
104
+ },
105
+ attempt
106
+ };
107
+ }));
108
+ }),
109
+ heartbeat: (pulled, visibility) => run(async () => {
110
+ await client.send(new ChangeMessageVisibilityCommand({
111
+ QueueUrl: queueUrl,
112
+ ReceiptHandle: handleOf(pulled).receiptHandle,
113
+ VisibilityTimeout: visSecs(visibility)
114
+ }));
115
+ }),
116
+ ack: (pulled) => run(async () => {
117
+ const h = handleOf(pulled);
118
+ await client.send(new DeleteMessageCommand({
119
+ QueueUrl: queueUrl,
120
+ ReceiptHandle: h.receiptHandle
121
+ }));
122
+ if (h.s3Key && large) await large.store.delete(h.s3Key);
123
+ }),
124
+ fail: (pulled, delay) => run(async () => {
125
+ await client.send(new ChangeMessageVisibilityCommand({
126
+ QueueUrl: queueUrl,
127
+ ReceiptHandle: handleOf(pulled).receiptHandle,
128
+ VisibilityTimeout: visSecs(delay ?? 0)
129
+ }));
130
+ })
131
+ };
132
+ }
133
+ //#endregion
134
+ export { sqsQueue };
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@ayepi/aws",
3
+ "version": "0.1.0",
4
+ "description": "AWS backends for ayepi — an SQS @ayepi/work queue (large payloads offloaded to S3) and an S3 @ayepi/files store, all retry-wrapped for throttle resilience",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ClickerMonkey/ayepi.git",
12
+ "directory": "packages/aws"
13
+ },
14
+ "homepage": "https://github.com/ClickerMonkey/ayepi/tree/main/packages/aws#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/ClickerMonkey/ayepi/issues"
17
+ },
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "require": {
30
+ "types": "./dist/index.d.cts",
31
+ "default": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "./s3": {
35
+ "import": {
36
+ "types": "./dist/s3.d.ts",
37
+ "default": "./dist/s3.js"
38
+ },
39
+ "require": {
40
+ "types": "./dist/s3.d.cts",
41
+ "default": "./dist/s3.cjs"
42
+ }
43
+ },
44
+ "./sqs": {
45
+ "import": {
46
+ "types": "./dist/sqs.d.ts",
47
+ "default": "./dist/sqs.js"
48
+ },
49
+ "require": {
50
+ "types": "./dist/sqs.d.cts",
51
+ "default": "./dist/sqs.cjs"
52
+ }
53
+ },
54
+ "./package.json": "./package.json"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ },
59
+ "peerDependencies": {
60
+ "@aws-sdk/client-s3": "^3",
61
+ "@aws-sdk/client-sqs": "^3",
62
+ "@aws-sdk/lib-storage": "^3",
63
+ "@aws-sdk/s3-request-presigner": "^3",
64
+ "@ayepi/core": "^0.1.0",
65
+ "@ayepi/files": "^0.1.0",
66
+ "@ayepi/work": "^0.1.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@ayepi/work": {
70
+ "optional": true
71
+ },
72
+ "@ayepi/files": {
73
+ "optional": true
74
+ },
75
+ "@aws-sdk/client-s3": {
76
+ "optional": true
77
+ },
78
+ "@aws-sdk/client-sqs": {
79
+ "optional": true
80
+ },
81
+ "@aws-sdk/lib-storage": {
82
+ "optional": true
83
+ },
84
+ "@aws-sdk/s3-request-presigner": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "devDependencies": {
89
+ "@aws-sdk/client-s3": "^3.700.0",
90
+ "@aws-sdk/client-sqs": "^3.700.0",
91
+ "@aws-sdk/lib-storage": "^3.700.0",
92
+ "@aws-sdk/s3-request-presigner": "^3.700.0",
93
+ "@vitest/coverage-v8": "^2.1.8",
94
+ "publint": "^0.3.0",
95
+ "testcontainers": "^10.13.0",
96
+ "tsdown": "^0.12.0",
97
+ "vitest": "^2.1.8",
98
+ "zod": "^4.4.3",
99
+ "@ayepi/files": "0.1.0",
100
+ "@ayepi/core": "0.1.0",
101
+ "@ayepi/work": "0.1.0"
102
+ },
103
+ "keywords": [
104
+ "ayepi",
105
+ "aws",
106
+ "sqs",
107
+ "s3",
108
+ "work-queue",
109
+ "file-storage",
110
+ "@ayepi/work",
111
+ "@ayepi/files"
112
+ ],
113
+ "scripts": {
114
+ "build": "tsdown",
115
+ "typecheck": "tsc --noEmit",
116
+ "test": "vitest run --coverage",
117
+ "test:integration": "vitest run --config vitest.integration.config.ts",
118
+ "publint": "publint"
119
+ }
120
+ }