From 12f17be1442e1a8fab7b39073acd0303896427b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:19:20 +0000 Subject: [PATCH] Add blob-adapter-s3: StackBlobAdapter over the S3 API (#160) Closes the gap combineAdapters() names as its own motivating example: S3/R2-backed blob storage previously had no shipped adapter, so any server deployment wanting object storage had to implement StackBlobAdapter itself. blob-adapter-s3 mirrors blob-adapter-disk's content-addressed design (SHA-256 keys, same C3 error contract) and supports S3-compatible endpoints like Cloudflare R2 via `endpoint` + `forcePathStyle`. Uses @aws-sdk/client-s3 for correct SigV4 signing rather than a hand-rolled implementation. Tests mock the S3 client with a small stateful in-memory fake (aws-sdk-client-mock has no server-side state of its own), mirroring blob-adapter-disk's test list. Wires the new package into README, docs/spec/adapters.md, CONTRIBUTING.md, scripts/verify-pack.mjs, and the changeset workflow's package dropdown. --- .github/workflows/changeset.yml | 1 + CONTRIBUTING.md | 3 +- README.md | 8 +- docs/spec/adapters.md | 21 +- packages/blob-adapter-s3/package.json | 49 +++ packages/blob-adapter-s3/src/index.ts | 138 ++++++ packages/blob-adapter-s3/tests/blob.test.ts | 210 +++++++++ packages/blob-adapter-s3/tsconfig.build.json | 8 + packages/blob-adapter-s3/tsconfig.json | 8 + packages/blob-adapter-s3/vitest.config.ts | 14 + pnpm-lock.yaml | 440 +++++++++++++++++++ scripts/verify-pack.mjs | 1 + 12 files changed, 890 insertions(+), 11 deletions(-) create mode 100644 packages/blob-adapter-s3/package.json create mode 100644 packages/blob-adapter-s3/src/index.ts create mode 100644 packages/blob-adapter-s3/tests/blob.test.ts create mode 100644 packages/blob-adapter-s3/tsconfig.build.json create mode 100644 packages/blob-adapter-s3/tsconfig.json create mode 100644 packages/blob-adapter-s3/vitest.config.ts diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index b5f0ffe..68d6eb3 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -17,6 +17,7 @@ on: - '@haverstack/commons' - '@haverstack/conformance-fixtures' - '@haverstack/blob-adapter-disk' + - '@haverstack/blob-adapter-s3' - '@haverstack/record-adapter-sqlite' - '@haverstack/adapter-local' - '@haverstack/adapter-api' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adc2ba8..8a55bc8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ Renaming the workflow file breaks every trusted publisher at once; each matches Under **Settings → Actions → General**, _Allow GitHub Actions to create and approve pull requests_ must stay on, or the version PR is never opened. -**A ninth package** needs one manual publish (trusted publishing configures only on a package that exists), then its own trusted publisher and an entry in the `package` dropdown in `.github/workflows/changeset.yml`. +**A tenth package** needs one manual publish (trusted publishing configures only on a package that exists), then its own trusted publisher and an entry in the `package` dropdown in `.github/workflows/changeset.yml`. **To gate the publish on a human:** create a GitHub environment with required reviewers, add `environment:` to the release job, and name it in every trusted publisher. All three must agree. @@ -268,6 +268,7 @@ packages/ sqlite-shared/ # Internal: shared SQL logic, bundled into consumers, not published record-adapter-sqlite/ # Node native SQLite (node:sqlite), FTS5, WAL blob-adapter-disk/ # Content-addressed blobs on disk + blob-adapter-s3/ # Content-addressed blobs on S3 / S3-compatible stores adapter-local/ # Convenience: SQLite records + disk blobs adapter-api/ # HTTP client for stack servers wire-types/ # Wire serialization shapes and error mapping diff --git a/README.md b/README.md index ae5fd46..a534877 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ This is a monorepo. Packages are published to npm under the `@haverstack` scope. | [`@haverstack/adapter-local`](./packages/adapter-local) | Local adapter (native SQLite + disk) — single-app/embedded or server use | | [`@haverstack/record-adapter-sqlite`](./packages/record-adapter-sqlite) | Node native SQLite (`node:sqlite`) `StackRecordAdapter` — used by `adapter-local` | | [`@haverstack/blob-adapter-disk`](./packages/blob-adapter-disk) | Disk filesystem `StackBlobAdapter` | +| [`@haverstack/blob-adapter-s3`](./packages/blob-adapter-s3) | S3 (and S3-compatible, e.g. Cloudflare R2) `StackBlobAdapter` | | [`@haverstack/adapter-api`](./packages/adapter-api) | HTTP adapter for remote stack servers | | [`@haverstack/commons`](./packages/commons) | Canonical Schema Commons type definitions (`note`, `task`, `contact`, ...) | @@ -248,10 +249,11 @@ The adapter interface is split into `StackRecordAdapter` (structured records) an | `adapter-local` | full | Single-app/embedded or server use — native SQLite records + disk blobs | | `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL — used by `adapter-local` | | `blob-adapter-disk` | blob | Content-addressed blobs on the local filesystem | +| `blob-adapter-s3` | blob | Content-addressed blobs on S3 or an S3-compatible store (e.g. Cloudflare R2) | | `adapter-api` | full | Hosted/shared stacks via HTTP | | `adapter-json` | full | Portable JSON files _(planned)_ | -Use `combineAdapters({ record, blob })` from `@haverstack/core/adapter` to compose a record adapter with a different blob backend — for example, `NativeSQLiteRecordAdapter` with a future `S3BlobAdapter`. `adapter-local` wraps this pattern for the common case. +Use `combineAdapters({ record, blob })` from `@haverstack/core/adapter` to compose a record adapter with a different blob backend — for example, `NativeSQLiteRecordAdapter` with `S3BlobAdapter`. `adapter-local` wraps this pattern for the common case. --- @@ -316,6 +318,10 @@ packages/ src/ index.ts # DiskBlobAdapter (StackBlobAdapter) tests/ + blob-adapter-s3/ # @haverstack/blob-adapter-s3 + src/ + index.ts # S3BlobAdapter (StackBlobAdapter) + tests/ adapter-api/ # @haverstack/adapter-api src/ index.ts # APIAdapter (StackAdapter) diff --git a/docs/spec/adapters.md b/docs/spec/adapters.md index 87fc46e..f075bc1 100644 --- a/docs/spec/adapters.md +++ b/docs/spec/adapters.md @@ -36,13 +36,14 @@ Packages follow a naming convention that makes the adapter type discoverable: ## Adapter backends -| Package | Type | Use case | -| ----------------------- | ------ | ----------------------------------------------------- | -| `adapter-local` | full | Local app storage — native SQLite + disk blobs | -| `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL | -| `blob-adapter-disk` | blob | Content-addressed blobs on disk | -| `adapter-api` | full | Hosted/shared stacks via HTTP | -| `adapter-json` | full | Portable JSON files _(planned)_ | +| Package | Type | Use case | +| ----------------------- | ------ | ---------------------------------------------------------------------------- | +| `adapter-local` | full | Local app storage — native SQLite + disk blobs | +| `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL | +| `blob-adapter-disk` | blob | Content-addressed blobs on disk | +| `blob-adapter-s3` | blob | Content-addressed blobs on S3 or an S3-compatible store (e.g. Cloudflare R2) | +| `adapter-api` | full | Hosted/shared stacks via HTTP | +| `adapter-json` | full | Portable JSON files _(planned)_ | `adapter-local` is the batteries-included package for the common local case. It wraps `NativeSQLiteRecordAdapter` and `DiskBlobAdapter` and stores attachments in an `attachments/` subdirectory next to the database file. Bearer tokens, when used, live in a separate sibling file (`.tokens`, via `NativeTokenStore`) — never inside the portable stack database. @@ -51,14 +52,16 @@ Use `combineAdapters()` from `@haverstack/core/adapter` when you want different ```ts import { combineAdapters } from '@haverstack/core/adapter'; import { NativeSQLiteRecordAdapter } from '@haverstack/record-adapter-sqlite'; -import { S3BlobAdapter } from '@haverstack/blob-adapter-s3'; // hypothetical +import { S3BlobAdapter } from '@haverstack/blob-adapter-s3'; const record = await NativeSQLiteRecordAdapter.initialize({ path, entityId, timezone }); -const blob = new S3BlobAdapter(bucketConfig); +const blob = new S3BlobAdapter({ bucket: 'my-bucket' }); const adapter = combineAdapters({ record, blob }); const stack = await Stack.create(adapter); ``` +`maxAttachmentBytes` lives on `AdapterCapabilities` (below), which `combineAdapters()` always reads from the `record` half — a blob-only package like `blob-adapter-s3` has no ceiling of its own to declare. Whichever `StackRecordAdapter` it's paired with should keep declaring `maxAttachmentBytes: null`, per the local-adapter rule above: a blob adapter isn't the wire boundary that would justify one. Point `S3BlobAdapter` at Cloudflare R2 or another S3-compatible store by passing `endpoint` and `forcePathStyle: true`. + All adapters support the full Record API. Performance guarantees differ; correctness does not. **`@haverstack/sqlite-shared`** is an internal, non-public package holding everything a SQLite-backed record adapter needs that isn't specific to one binding — schema DDL, `WHERE`/`ORDER` building, the cursor codec, row mappers, the FTS5 sanitizer and indexing strategy, the storage-ownership lock, and (via a small `SqlExecutor` interface normalizing a binding's call convention) the actual CRUD/query/version/type/association/token logic itself. An adapter implements only what's genuinely engine-specific: database construction, pragma/WAL setup, and lifecycle. `record-adapter-sqlite` is its only consumer today; the split exists so a second SQLite engine inherits the behavior rather than reimplementing it, and so a cursor minted by one is decodable by another. diff --git a/packages/blob-adapter-s3/package.json b/packages/blob-adapter-s3/package.json new file mode 100644 index 0000000..746bb99 --- /dev/null +++ b/packages/blob-adapter-s3/package.json @@ -0,0 +1,49 @@ +{ + "name": "@haverstack/blob-adapter-s3", + "version": "0.1.0", + "description": "S3 (and S3-compatible) blob adapter for Haverstack", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/haverstack/core.git", + "directory": "packages/blob-adapter-s3" + }, + "license": "CC0-1.0", + "keywords": [ + "haverstack", + "s3", + "r2", + "blob", + "adapter", + "personal data", + "storage" + ], + "scripts": { + "prepublishOnly": "pnpm run build", + "build": "tsc -p tsconfig.build.json", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "lint": "eslint src tests" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "@haverstack/core": "workspace:^" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "aws-sdk-client-mock": "^4.0.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + } +} diff --git a/packages/blob-adapter-s3/src/index.ts b/packages/blob-adapter-s3/src/index.ts new file mode 100644 index 0000000..47610b2 --- /dev/null +++ b/packages/blob-adapter-s3/src/index.ts @@ -0,0 +1,138 @@ +/** + * Haverstack — S3 Blob Adapter + * ------------------------------------------------------- + * Implements StackBlobAdapter over the S3 API, storing + * content-addressed blobs keyed by the SHA-256 hash of their + * bytes. Pass `endpoint` + `forcePathStyle` to target an + * S3-compatible store (e.g. Cloudflare R2) instead of AWS S3 + * itself — the rest of the adapter is identical either way. + * + * Unlike blob-adapter-disk, no temp-file-plus-rename dance is + * needed: S3's PutObject is atomic per key, so two callers + * writing the same content-addressed key concurrently just + * write the same bytes twice — never a torn object. + */ + +import { createHash } from 'node:crypto'; +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + HeadObjectCommand, + DeleteObjectCommand, + ListObjectsV2Command, + NoSuchKey, + NotFound, + type S3ClientConfig, +} from '@aws-sdk/client-s3'; +import { StackNotFoundError, StackQueryError } from '@haverstack/core'; +import type { FileId } from '@haverstack/core'; +import type { StackBlobAdapter, BlobFileInfo } from '@haverstack/core/adapter'; + +const SHA256_HEX_RE = /^[0-9a-f]{64}$/; + +const assertFileId = (fileId: string): void => { + if (!SHA256_HEX_RE.test(fileId)) { + throw new StackQueryError(`Invalid fileId: expected 64-character lowercase hex string`); + } +}; + +export type S3BlobAdapterOptions = { + /** Bucket to store blobs in. */ + bucket: string; + /** + * A pre-configured S3Client, for callers who need control over retry + * policy, credential providers, request middleware, etc. When omitted, + * a client is built from the remaining options. + */ + client?: S3Client; + /** AWS region. Ignored when `client` is provided. */ + region?: string; + /** + * Custom endpoint — set this together with `forcePathStyle` to target an + * S3-compatible store (e.g. Cloudflare R2's S3 endpoint) instead of AWS + * S3. Ignored when `client` is provided. + */ + endpoint?: string; + /** Ignored when `client` is provided. */ + forcePathStyle?: boolean; + /** Ignored when `client` is provided. */ + credentials?: S3ClientConfig['credentials']; +}; + +export class S3BlobAdapter implements StackBlobAdapter { + private readonly client: S3Client; + private readonly bucket: string; + + constructor(options: S3BlobAdapterOptions) { + this.bucket = options.bucket; + this.client = + options.client ?? + new S3Client({ + region: options.region, + endpoint: options.endpoint, + forcePathStyle: options.forcePathStyle, + credentials: options.credentials, + }); + } + + private async objectExists(fileId: FileId): Promise { + try { + await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: fileId })); + return true; + } catch (err) { + if (err instanceof NotFound) return false; + throw err; + } + } + + async putAttachment(data: Uint8Array): Promise { + const fileId = createHash('sha256').update(data).digest('hex'); + if (!(await this.objectExists(fileId))) { + await this.client.send( + new PutObjectCommand({ Bucket: this.bucket, Key: fileId, Body: data }), + ); + } + return fileId; + } + + async getAttachment(fileId: FileId): Promise { + assertFileId(fileId); + try { + const response = await this.client.send( + new GetObjectCommand({ Bucket: this.bucket, Key: fileId }), + ); + return await response.Body!.transformToByteArray(); + } catch (err) { + if (err instanceof NoSuchKey) { + throw new StackNotFoundError(`Attachment not found: "${fileId}"`); + } + throw err; + } + } + + async deleteAttachment(fileId: FileId): Promise { + assertFileId(fileId); + // S3's DeleteObject is idempotent — a missing key is treated as + // already-deleted rather than an error, so no non-fatal catch is + // needed here the way blob-adapter-disk needs one for fs semantics. + await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: fileId })); + } + + async listFiles(): Promise { + const files: BlobFileInfo[] = []; + let continuationToken: string | undefined; + do { + const response = await this.client.send( + new ListObjectsV2Command({ Bucket: this.bucket, ContinuationToken: continuationToken }), + ); + for (const obj of response.Contents ?? []) { + if (obj.Key && SHA256_HEX_RE.test(obj.Key) && obj.Size !== undefined && obj.LastModified) { + files.push({ fileId: obj.Key, size: obj.Size, modifiedAt: obj.LastModified }); + } + } + continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; + } while (continuationToken); + return files; + } +} diff --git a/packages/blob-adapter-s3/tests/blob.test.ts b/packages/blob-adapter-s3/tests/blob.test.ts new file mode 100644 index 0000000..ed6f7f4 --- /dev/null +++ b/packages/blob-adapter-s3/tests/blob.test.ts @@ -0,0 +1,210 @@ +import { describe, test, expect, beforeEach } from 'vitest'; +import { createHash } from 'node:crypto'; +import { mockClient } from 'aws-sdk-client-mock'; +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + HeadObjectCommand, + DeleteObjectCommand, + ListObjectsV2Command, + NotFound, + NoSuchKey, +} from '@aws-sdk/client-s3'; +import { StackNotFoundError, StackQueryError } from '@haverstack/core'; +import { S3BlobAdapter } from '../src/index.js'; + +const BUCKET = 'test-bucket'; + +// aws-sdk-client-mock stubs one command response at a time; it has no +// notion of server-side state. This fake backs the mocked commands with a +// real in-memory Map so the tests exercise the same read-your-writes +// behavior a real bucket gives the adapter. +class FakeBucket { + private readonly objects = new Map(); + + wire(client: S3Client): void { + const s3Mock = mockClient(client); + + s3Mock.on(HeadObjectCommand).callsFake(({ Key }) => { + const obj = this.objects.get(Key!); + if (!obj) throw new NotFound({ message: 'Not Found', $metadata: {} }); + return {}; + }); + + s3Mock.on(PutObjectCommand).callsFake(({ Key, Body }) => { + this.objects.set(Key!, { body: Body as Uint8Array, modifiedAt: new Date() }); + return {}; + }); + + s3Mock.on(GetObjectCommand).callsFake(({ Key }) => { + const obj = this.objects.get(Key!); + if (!obj) + throw new NoSuchKey({ message: 'The specified key does not exist.', $metadata: {} }); + return { Body: { transformToByteArray: async () => obj.body } }; + }); + + s3Mock.on(DeleteObjectCommand).callsFake(({ Key }) => { + this.objects.delete(Key!); + return {}; + }); + + s3Mock.on(ListObjectsV2Command).callsFake(() => ({ + Contents: [...this.objects.entries()].map(([Key, obj]) => ({ + Key, + Size: obj.body.byteLength, + LastModified: obj.modifiedAt, + })), + IsTruncated: false, + })); + } + + has(key: string): boolean { + return this.objects.has(key); + } + + size(): number { + return this.objects.size; + } + + seed(key: string, body: Uint8Array): void { + this.objects.set(key, { body, modifiedAt: new Date() }); + } +} + +let bucket: FakeBucket; +let adapter: S3BlobAdapter; + +beforeEach(() => { + bucket = new FakeBucket(); + const client = new S3Client({ region: 'us-east-1' }); + bucket.wire(client); + adapter = new S3BlobAdapter({ bucket: BUCKET, client }); +}); + +describe('S3BlobAdapter', () => { + test('putAttachment returns a fileId', async () => { + const fileId = await adapter.putAttachment(Buffer.from('hello')); + expect(typeof fileId).toBe('string'); + expect(fileId.length).toBeGreaterThan(0); + }); + + test('putAttachment returns SHA-256 hex string', async () => { + const fileId = await adapter.putAttachment(Buffer.from('hello')); + expect(fileId).toMatch(/^[0-9a-f]{64}$/); + expect(fileId).toBe(createHash('sha256').update('hello').digest('hex')); + }); + + test('getAttachment returns the stored data', async () => { + const data = Buffer.from('hello attachment'); + const fileId = await adapter.putAttachment(data); + const retrieved = await adapter.getAttachment(fileId); + expect(Buffer.from(retrieved).toString()).toBe('hello attachment'); + }); + + test('attachment object exists after put', async () => { + const fileId = await adapter.putAttachment(Buffer.from('test')); + expect(bucket.has(fileId)).toBe(true); + }); + + test('getAttachment throws StackNotFoundError for unknown fileId', async () => { + const validHash = 'a'.repeat(64); + await expect(adapter.getAttachment(validHash)).rejects.toThrow(StackNotFoundError); + }); + + test('getAttachment throws StackQueryError for invalid fileId format', async () => { + await expect(adapter.getAttachment('nonexistent')).rejects.toThrow(StackQueryError); + await expect(adapter.getAttachment('nonexistent')).rejects.toThrow(/Invalid fileId/); + }); + + test('putAttachment stores binary data correctly', async () => { + const binary = Buffer.from([0x89, 0x50, 0x4e, 0x47]); // PNG magic bytes + const fileId = await adapter.putAttachment(binary); + const retrieved = await adapter.getAttachment(fileId); + expect(Buffer.from(retrieved)).toEqual(binary); + }); + + test('putAttachment deduplicates identical content', async () => { + const data = Buffer.from('same content'); + const id1 = await adapter.putAttachment(data); + const id2 = await adapter.putAttachment(data); + expect(id1).toBe(id2); + expect(bucket.size()).toBe(1); + }); + + test('getAttachment throws StackNotFoundError after deleteAttachment', async () => { + const fileId = await adapter.putAttachment(Buffer.from('bye')); + await adapter.deleteAttachment(fileId); + await expect(adapter.getAttachment(fileId)).rejects.toThrow(StackNotFoundError); + }); + + test('deleteAttachment removes the object', async () => { + const fileId = await adapter.putAttachment(Buffer.from('gone')); + await adapter.deleteAttachment(fileId); + expect(bucket.has(fileId)).toBe(false); + }); + + test('deleteAttachment is non-fatal for missing object', async () => { + const validHash = 'b'.repeat(64); + await expect(adapter.deleteAttachment(validHash)).resolves.toBeUndefined(); + }); + + test('deleteAttachment throws StackQueryError for invalid fileId format', async () => { + await expect(adapter.deleteAttachment('nonexistent')).rejects.toThrow(StackQueryError); + }); + + describe('listFiles', () => { + test('returns an empty array for an empty bucket', async () => { + expect(await adapter.listFiles()).toEqual([]); + }); + + test('lists every stored blob with fileId and size', async () => { + const id1 = await adapter.putAttachment(Buffer.from('hello')); + const id2 = await adapter.putAttachment(Buffer.from('a longer blob body')); + + const files = await adapter.listFiles(); + expect(files.map((f) => f.fileId).sort()).toEqual([id1, id2].sort()); + expect(files.find((f) => f.fileId === id1)?.size).toBe(Buffer.from('hello').byteLength); + expect(files.find((f) => f.fileId === id2)?.size).toBe( + Buffer.from('a longer blob body').byteLength, + ); + }); + + test('each entry carries a modifiedAt date', async () => { + const fileId = await adapter.putAttachment(Buffer.from('timestamped')); + const [file] = await adapter.listFiles(); + expect(file.modifiedAt).toBeInstanceOf(Date); + expect(file.fileId).toBe(fileId); + }); + + test('deleted blobs no longer appear', async () => { + const fileId = await adapter.putAttachment(Buffer.from('temporary')); + await adapter.deleteAttachment(fileId); + expect(await adapter.listFiles()).toEqual([]); + }); + + test('ignores non-fileId keys in the bucket', async () => { + const fileId = await adapter.putAttachment(Buffer.from('real blob')); + // Simulate a stray, non-content-addressed key landing in the bucket. + bucket.seed('.DS_Store', Buffer.from('stray')); + + const files = await adapter.listFiles(); + expect(files.map((f) => f.fileId)).toEqual([fileId]); + }); + }); + + describe('constructor', () => { + test('builds its own client from region/endpoint/forcePathStyle when none is provided', () => { + expect( + () => + new S3BlobAdapter({ + bucket: BUCKET, + endpoint: 'https://example.r2.cloudflarestorage.com', + region: 'auto', + forcePathStyle: true, + credentials: { accessKeyId: 'id', secretAccessKey: 'secret' }, + }), + ).not.toThrow(); + }); + }); +}); diff --git a/packages/blob-adapter-s3/tsconfig.build.json b/packages/blob-adapter-s3/tsconfig.build.json new file mode 100644 index 0000000..57d0596 --- /dev/null +++ b/packages/blob-adapter-s3/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "noEmit": false + }, + "exclude": ["tests/**/*.ts"] +} diff --git a/packages/blob-adapter-s3/tsconfig.json b/packages/blob-adapter-s3/tsconfig.json new file mode 100644 index 0000000..dbd41f4 --- /dev/null +++ b/packages/blob-adapter-s3/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "noEmit": true + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/packages/blob-adapter-s3/vitest.config.ts b/packages/blob-adapter-s3/vitest.config.ts new file mode 100644 index 0000000..024ed05 --- /dev/null +++ b/packages/blob-adapter-s3/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + resolve: { + alias: { + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), + '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), + }, + }, + test: { + environment: 'node', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 672cabf..8e73a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,28 @@ importers: specifier: ^2.0.0 version: 2.1.9(@types/node@22.19.17) + packages/blob-adapter-s3: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.700.0 + version: 3.1121.0 + '@haverstack/core': + specifier: workspace:^ + version: link:../core + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + aws-sdk-client-mock: + specifier: ^4.0.0 + version: 4.1.0 + typescript: + specifier: ^5.5.0 + version: 5.9.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@22.19.17) + packages/commons: dependencies: '@haverstack/core': @@ -202,6 +224,78 @@ importers: packages: + '@aws-sdk/checksums@3.1000.29': + resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1121.0': + resolution: {integrity: sha512-hBnoqaVBeWdkgXcJElMXA2yUZWkBCBntu2qmN+tfqmzC+j4LzJC3ox8qIgS2WdMS1cb8UwyBogUVrkRXybNm0A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.81': + resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.75': + resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@changesets/apply-release-plan@8.0.0': resolution: {integrity: sha512-kUd2pbf1w5/AYmBMb0Tt+rkIPCjFJdT0SZMrkOjJT/WV/QbtmvkyB5jkV0oaNPheavprZk+SfUiozUti7TIL2w==} engines: {node: ^22.11 || ^24 || >=26} @@ -799,6 +893,42 @@ packages: cpu: [x64] os: [win32] + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@11.2.2': + resolution: {integrity: sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==} + + '@sinonjs/fake-timers@15.4.0': + resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} + + '@sinonjs/samsam@8.0.3': + resolution: {integrity: sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==} + + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.11.3': + resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -811,6 +941,12 @@ packages: '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@types/sinon@17.0.4': + resolution: {integrity: sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==} + + '@types/sinonjs__fake-timers@15.0.1': + resolution: {integrity: sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==} + '@typescript-eslint/eslint-plugin@8.59.1': resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -919,10 +1055,16 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + aws-sdk-client-mock@4.1.0: + resolution: {integrity: sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -987,6 +1129,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1113,6 +1259,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + human-id@4.2.1: resolution: {integrity: sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==} hasBin: true @@ -1162,6 +1312,9 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + just-extend@6.2.0: + resolution: {integrity: sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1214,6 +1367,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nise@6.1.5: + resolution: {integrity: sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1241,6 +1397,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1338,6 +1497,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sinon@18.0.1: + resolution: {integrity: sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1360,6 +1522,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1406,6 +1572,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -1429,6 +1598,14 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + typescript-eslint@8.59.1: resolution: {integrity: sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1536,6 +1713,171 @@ packages: snapshots: + '@aws-sdk/checksums@3.1000.29': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1121.0': + dependencies: + '@aws-sdk/checksums': 3.1000.29 + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/middleware-sdk-s3': 3.972.75 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.9': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.77': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.81': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.75': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.44': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@changesets/apply-release-plan@8.0.0': dependencies: '@changesets/config': 4.0.0 @@ -1964,6 +2306,56 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@11.2.2': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@15.4.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/samsam@8.0.3': + dependencies: + '@sinonjs/commons': 3.0.1 + type-detect: 4.1.0 + + '@smithy/core@3.33.3': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} @@ -1974,6 +2366,12 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/sinon@17.0.4': + dependencies: + '@types/sinonjs__fake-timers': 15.0.1 + + '@types/sinonjs__fake-timers@15.0.1': {} + '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2122,8 +2520,16 @@ snapshots: assertion-error@2.0.1: {} + aws-sdk-client-mock@4.1.0: + dependencies: + '@types/sinon': 17.0.4 + sinon: 18.0.1 + tslib: 2.8.1 + balanced-match@4.0.4: {} + bowser@2.14.1: {} + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -2173,6 +2579,8 @@ snapshots: deep-is@0.1.4: {} + diff@5.2.2: {} + es-module-lexer@1.7.0: {} esbuild@0.21.5: @@ -2355,6 +2763,8 @@ snapshots: dependencies: is-glob: 4.0.3 + has-flag@4.0.0: {} + human-id@4.2.1: {} ignore@5.3.2: {} @@ -2385,6 +2795,8 @@ snapshots: jsonc-parser@3.3.1: {} + just-extend@6.2.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2438,6 +2850,13 @@ snapshots: natural-compare@1.4.0: {} + nise@6.1.5: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.4.0 + just-extend: 6.2.0 + path-to-regexp: 8.4.2 + object-assign@4.1.1: {} optionator@0.9.4: @@ -2463,6 +2882,8 @@ snapshots: path-key@3.1.1: {} + path-to-regexp@8.4.2: {} + pathe@1.1.2: {} pathe@2.0.3: {} @@ -2549,6 +2970,15 @@ snapshots: siginfo@2.0.0: {} + sinon@18.0.1: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 11.2.2 + '@sinonjs/samsam': 8.0.3 + diff: 5.2.2 + nise: 6.1.5 + supports-color: 7.2.0 + sisteransi@1.0.5: {} source-map-js@1.2.1: {} @@ -2569,6 +2999,10 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -2602,6 +3036,8 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.8.1: {} + tsup@8.5.1(postcss@8.5.13)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) @@ -2634,6 +3070,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-detect@4.0.8: {} + + type-detect@4.1.0: {} + typescript-eslint@8.59.1(eslint@10.2.1)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3) diff --git a/scripts/verify-pack.mjs b/scripts/verify-pack.mjs index 9056630..2647440 100644 --- a/scripts/verify-pack.mjs +++ b/scripts/verify-pack.mjs @@ -39,6 +39,7 @@ const EXPECTATIONS = { ], '@haverstack/wire-types': [['.', ['serializeRecord', 'serializeError', 'WIRE_PROTOCOL_VERSION']]], '@haverstack/blob-adapter-disk': [['.', ['DiskBlobAdapter']]], + '@haverstack/blob-adapter-s3': [['.', ['S3BlobAdapter']]], '@haverstack/record-adapter-sqlite': [['.', ['NativeSQLiteRecordAdapter', 'NativeTokenStore']]], '@haverstack/adapter-local': [['.', ['LocalAdapter']]], '@haverstack/adapter-api': [['.', ['APIAdapter']]],