@aws-blocks/bb-file-bucket 0.1.1 → 0.1.2

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/DESIGN.md ADDED
@@ -0,0 +1,94 @@
1
+ # FileBucket — Design
2
+
3
+ Design document for FileBucket. For usage, see [README.md](./README.md).
4
+
5
+ **Package:** `@aws-blocks/bb-file-bucket`
6
+ **Type:** Primitive (new infrastructure)
7
+ **AWS Service:** Amazon S3
8
+
9
+ ## Design Decisions
10
+
11
+ **D-FB-1: Buffer body type instead of ReadableStream**
12
+ **Decision:** `put()` accepts `Buffer | string`, `get()` returns `Buffer` in `FileContent.body`.
13
+ **Rationale:** ReadableStream adds complexity for the common case (small-to-medium files). Buffer is simpler to work with in Lambda (where the entire response must be buffered anyway). For large files, presigned URLs (`getUrl`/`putUrl`) are the recommended pattern. This favors client-safe return types.
14
+
15
+ **D-FB-2: Segregated internal storage in mock**
16
+ **Decision:** The mock stores user content, sidecar metadata, and version history under separate sibling roots inside `.bb-data/{fullId}/`:
17
+ ```
18
+ content/{key} file body (byte-identical to what was written)
19
+ meta/{key}.json sidecar metadata
20
+ versions/{key}/{versionId} version body
21
+ versions/{key}/{versionId}.json version metadata
22
+ versions/{key}/__deleted__ delete marker (sentinel)
23
+ ```
24
+ **Rationale:** Keeps file content byte-identical (no wrapping format) while guaranteeing internal bookkeeping can never collide with user keys. An earlier design co-located metadata as `{path}.__meta__.json` next to each file and relied on marker substrings, which meant a user key like `data.__meta__.json` or a directory named `x.__versions__/` could be silently hidden or shadowed by `scan()`. Because user content now lives only under `content/`, `scan()` walks that one root and yields everything with no marker-based filtering — arbitrary keys are supported, matching S3 semantics.
25
+
26
+ **D-FB-3: deleteBatch chunks at 1,000**
27
+ **Decision:** `deleteBatch()` internally chunks into groups of 1,000 and issues separate `DeleteObjects` calls.
28
+ **Rationale:** S3 `DeleteObjects` API supports max 1,000 keys per request. Chunking is transparent to the caller. Batch methods handle pagination internally.
29
+
30
+ **D-FB-4: Presigned URL default expiry of 3600 seconds**
31
+ **Decision:** Both `getUrl` and `putUrl` default to 1 hour expiry.
32
+ **Rationale:** Matches the S3 SDK default. Long enough for typical browser upload/download flows, short enough to limit exposure.
33
+
34
+ **D-FB-5: scan returns AsyncIterable**
35
+ **Decision:** `scan()` returns `AsyncIterable<FileInfo>` with internal pagination.
36
+ **Rationale:** AsyncIterable is used for unbounded result sets. S3 `ListObjectsV2` paginates at 1,000 keys; the iterable handles continuation tokens transparently.
37
+
38
+ **D-FB-6: Versioning is opt-in with runtime API support**
39
+ **Decision:** `versioned: true` enables S3 object versioning and unlocks version-aware methods (`listVersions`, `restoreVersion`, optional `versionId` on `get`/`delete`/`getUrl`/`getFileHandle`). Without the flag, the API surface is unchanged.
40
+ **Rationale:** Versioning adds storage cost and complexity. Making it opt-in keeps the default simple. When enabled, the runtime API exposes the full version lifecycle — listing, retrieving specific versions, permanent deletion of individual versions, and restoring old versions. `restoreVersion` is implemented as a CopyObject from the old version (S3 has no native restore), which creates a new version that becomes current.
41
+
42
+ **D-FB-7: Mock versioning uses filesystem directories**
43
+ **Decision:** Versioned mock stores each version in `versions/{key}/v{n}` with monotonic IDs. Delete markers are `versions/{key}/__deleted__` sentinel files.
44
+ **Rationale:** Simple, inspectable, and matches the S3 semantics closely enough for local development. Monotonic IDs (`v1`, `v2`, ...) are deterministic and easy to reason about in tests, unlike S3's opaque version IDs. Version history lives under the segregated `versions/` root (see D-FB-2), so it never appears in `scan()` or collides with a user key.
45
+
46
+ **D-FB-8: Bucket name validated at synth — error, never truncate/hash**
47
+ **Decision:** The derived bucket name (`scope.fullId`) is validated against S3's naming rules (`bucket-name.ts`) before the bucket is constructed. An invalid name throws a `ValidationFailed` error with an actionable message. The same validator runs in the mock constructor so local dev (`bb dev`) fails identically — parity. `FileBucket.fromExisting(...)` skips validation since the name is externally owned.
48
+ **Rationale:** S3 bucket names are globally unique and immutable. Truncating to fit 63 chars risks collisions, and a name that shifts between deploys (e.g. after a hash input changes) would orphan or replace the customer's data — a far worse outcome than a fast, fixable synth error. Erroring puts the fix in the developer's hands (shorten a scope id once; the name is then stable forever) and matches the manual-shortening pattern already used in `bb-agent`. This deliberately differs from DynamoDB-backed BBs (KVStore/DistributedTable) which `substring(0, 255)` — DynamoDB's 255 limit is generous and table names are internal, disposable, and not globally unique, so silent truncation is acceptable there.
49
+
50
+ ## Infrastructure (CDK)
51
+
52
+ Creates a single S3 bucket:
53
+
54
+ - **Bucket name:** Derived from `scope.fullId` (the bucket id joined to its parent scope ids with `-`). Validated at synth against S3's naming rules — see D-FB-6.
55
+ - **Block public access:** All four settings enabled (BLOCK_ALL)
56
+ - **Encryption:** S3-managed keys (SSE-S3)
57
+ - **Versioning:** Disabled by default, enabled via `options.versioned`
58
+ - **CORS:** Configured from `options.corsRules` if provided
59
+ - **Lifecycle rules:** Configured from `options.lifecycleRules` if provided
60
+ - **Removal policy:** DESTROY (sandbox), configurable for production
61
+ - **Auto-delete objects:** Enabled when removal policy is DESTROY
62
+ - **Permissions:** `grantReadWrite` to the parent scope's handler automatically
63
+
64
+ ## Mock Implementation
65
+
66
+ - Files stored on the local filesystem at `.bb-data/{scope.fullId}/` via `getMockDataDir()` from core.
67
+ - Internal data is segregated into sibling roots so it can never collide with user keys (see D-FB-2):
68
+ - `content/{key}` — file body, byte-identical to what was written.
69
+ - `meta/{key}.json` — sidecar metadata.
70
+ - `versions/{key}/{versionId}` (+ `.json` sidecars, `__deleted__` marker) — version history.
71
+ - Path mapping for both the mock and the dev file-server is centralized in `paths.ts` so they stay in lockstep.
72
+ - Data persists across dev server restarts. Customers can wipe with `rm -rf .bb-data`.
73
+ - Presigned URLs are served by the dev file-server at `/.bb-file-bucket/{scope.fullId}/{path}?token=...`. The path segments are URL-encoded; the server decodes them and validates an HMAC token scoped to method, path, and expiry.
74
+ - `scan()` recursively walks only the `content/` root and yields every file it finds — no marker-based filtering — so user keys are unrestricted.
75
+ - The dev file-server's PUT handler delegates to the registered `FileBucket` instance (via a process-global registry) so uploads get versioning, key validation, and metadata. There is no direct-write fallback; an unregistered bucket fails loud with a 500.
76
+ - Key length validated against S3's 1,024-byte limit (warns, does not reject).
77
+ - Versioning fully supported: each `put` writes to `versions/{key}/v{n}`, delete without `versionId` places a `__deleted__` sentinel, `listVersions` reads the versions directory, `restoreVersion` copies an old version via `put`.
78
+
79
+ ### Mock vs AWS Behavior Differences
80
+
81
+ | Behavior Difference | Impact | Mitigation |
82
+ |------------|--------|------------|
83
+ | No lifecycle rules | Objects never expire or transition locally | No mitigation — lifecycle rules are a background S3 process |
84
+ | No CORS enforcement | Browser requests succeed regardless of origin locally | No mitigation — CORS is enforced by the browser + S3, not the mock |
85
+ | No storage classes | Transition rules have no effect locally | No mitigation — storage classes are a cost optimization |
86
+ | No multipart upload | Large files use simple write locally | No mitigation — mock uses `fs.writeFile` regardless of size |
87
+ | Presigned URLs are localhost-only | URLs only work against the local dev server | No mitigation — expected behavior for local development |
88
+ | No IAM enforcement | Permission errors only surface in AWS | No mitigation — IAM is handled by CDK grants automatically |
89
+ | Filesystem path limits | Some OS path length limits differ from S3 key limits (1,024 bytes) | Mock validates key length and warns when it exceeds 1,024 bytes |
90
+ | Path-traversal keys rejected locally | The mock maps keys onto the real filesystem, so it rejects keys that escape the bucket's content root (e.g. `../escape.txt`). S3 has no filesystem and treats `..` as a literal key segment, so it accepts such keys. A pathological key containing `..` that "works" on S3 will throw `ValidationFailed` locally. | Intentional — the guard prevents a local key from clobbering files outside `.bb-data`. Avoid `..` segments in keys (also S3 best practice). Covered by `src/path-containment.test.ts`. |
91
+ | Non-atomic `put()` | On a versioned bucket, `put()` performs several separate `writeFileSync` calls (version body, version metadata, current body, current metadata, delete-marker cleanup). A crash or process kill mid-`put()` can leave torn state — a body with no metadata sidecar, or a version body with no `.json`. Real S3 `PutObject` is atomic per object. | No mitigation today — the filesystem layout has no transaction boundary. Acceptable for a dev mock (re-running `put()` heals it). See Open Question 4 (storage engine). |
92
+ | Monotonic version IDs | Mock uses `v1`, `v2`, ... vs S3's opaque IDs | No impact — customer code should treat version IDs as opaque strings |
93
+ | Content-Type signed into presigned PUT URLs | When `putUrl`/`createUploadHandle` are given a `contentType`, the AWS SDK signs `content-type` as a required header, so real S3 returns `403 SignatureDoesNotMatch` if the uploaded request's `Content-Type` differs from (or omits) the signed value. The dev file-server enforces the same check (`src/file-server.ts`) so an upload that would fail in prod also fails locally with 403, rather than silently succeeding. Uploads via `createUploadHandle().upload()` always send the signed header, so the typed-handle path round-trips in both environments. Covered by `src/file-server.test.ts`. |
94
+ | Adjacent slashes in keys collapsed | The mock maps keys onto the filesystem via `path.join`, which collapses `//` to `/` (e.g. a key built from a URL-shaped value like `uploads/https://issuer:sub/f.txt`). A later `scan({ prefix })` whose prefix still contains `//` won't match the stored single-slash path, so the file appears "missing" locally. S3 treats keys as opaque byte strings and preserves `//`, so the same prefix matches in production. | Avoid embedding raw URL-shaped values (e.g. an OIDC `userId` of `${iss}:${sub}`) directly in keys — `encodeURIComponent()` the segment first. See the FileBucket README best-practices note. |
package/README.md CHANGED
@@ -6,6 +6,8 @@ File storage backed by Amazon S3.
6
6
 
7
7
  **When NOT to use:** If you need structured key-value data with conditional writes, use `KVStore`. If you need queryable records with indexes, use `DistributedTable`.
8
8
 
9
+ > Design & mock parity details: [DESIGN.md](./DESIGN.md)
10
+
9
11
  ## API
10
12
 
11
13
  ```typescript
@@ -46,6 +48,39 @@ const bucket = new FileBucket(scope, id, options?)
46
48
  | `metadata` | `Record<string, string>` | Custom metadata key-value pairs. |
47
49
  | `cacheControl` | `string` | Cache-Control header value. |
48
50
 
51
+ ### CorsRule
52
+
53
+ CORS configuration for browser-based access. Supplied via the `corsRules` option.
54
+
55
+ | Field | Type | Description |
56
+ |-------|------|-------------|
57
+ | `allowedOrigins` | `string[]` | Origins permitted to make cross-origin requests (e.g., `['https://app.example.com']`). |
58
+ | `allowedMethods` | `('GET' \| 'PUT' \| 'POST' \| 'DELETE' \| 'HEAD')[]` | HTTP methods permitted for cross-origin requests. |
59
+ | `allowedHeaders` | `string[]` | Optional. Request headers permitted in the actual request. |
60
+ | `exposedHeaders` | `string[]` | Optional. Response headers exposed to the browser. |
61
+ | `maxAge` | `number` | Optional. Seconds the browser may cache the preflight response. |
62
+
63
+ ### LifecycleRule
64
+
65
+ Lifecycle configuration for automatic expiration or storage-class transitions. Supplied via the `lifecycleRules` option.
66
+
67
+ | Field | Type | Description |
68
+ |-------|------|-------------|
69
+ | `prefix` | `string` | Optional. Prefix filter — the rule applies only to keys starting with this prefix. Omit to apply to all objects. |
70
+ | `expirationDays` | `number` | Optional. Days after creation to expire (delete) objects. |
71
+ | `transitionToIaDays` | `number` | Optional. Days after creation to transition objects to Infrequent Access storage. |
72
+
73
+ ### FileVersionInfo
74
+
75
+ Returned by `listVersions()` — one entry per object version, newest first.
76
+
77
+ | Field | Type | Description |
78
+ |-------|------|-------------|
79
+ | `versionId` | `string` | The version identifier. |
80
+ | `lastModified` | `Date` | When this version was created. |
81
+ | `size` | `number` | Size in bytes. |
82
+ | `isCurrent` | `boolean` | Whether this is the current (latest) version. |
83
+
49
84
  ### Error Handling
50
85
 
51
86
  `get()` returns `null` for a missing file — it does **not** throw `FileNotFound`. Check for null:
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare const BB_NAME = "FileBucket";
2
- export declare const BB_VERSION = "0.1.1";
2
+ export declare const BB_VERSION = "0.1.2";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
2
  export const BB_NAME = 'FileBucket';
3
- export const BB_VERSION = '0.1.1';
3
+ export const BB_VERSION = '0.1.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-blocks/bb-file-bucket",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "author": "Amazon Web Services",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "test": "node --test dist/index.test.js dist/index.cdk.test.js dist/scan.test.js dist/file-server.test.js dist/url-encoding.test.js dist/path-containment.test.js dist/bucket-name.test.js"
42
42
  },
43
43
  "dependencies": {
44
- "@aws-blocks/bb-logger": "^0.1.1",
44
+ "@aws-blocks/bb-logger": "^0.1.2",
45
45
  "@aws-blocks/core": "^0.1.1",
46
46
  "@aws-sdk/client-s3": "^3.0.0",
47
47
  "@aws-sdk/s3-request-presigner": "^3.0.0"
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
2
  export const BB_NAME = 'FileBucket';
3
- export const BB_VERSION = '0.1.1';
3
+ export const BB_VERSION = '0.1.2';