@chidchanun/bcp 0.1.26 → 0.1.28
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/README.md +151 -523
- package/docs/README.md +69 -82
- package/docs/production-hardening.md +152 -0
- package/docs/releases/0.1.27.md +350 -0
- package/docs/releases/0.1.28.md +134 -0
- package/docs/s3-storage.md +212 -55
- package/docs/storage-ecosystem.md +434 -0
- package/package.json +2 -1
- package/packages/client/src/server.ts +42 -2
- package/packages/server/src/hardening-proxy.ts +393 -0
- package/packages/server/src/production-hardening.ts +374 -0
- package/packages/server/src/standalone-production-runtime-v7.ts +299 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# BCP Framework 0.1.27
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.1.27` is the **Storage Ecosystem** milestone.
|
|
4
|
+
|
|
5
|
+
> Release state: unreleased development target until local validation, RC checks, tagging and npm publication complete.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- Added object listing with prefix filtering, limits and opaque cursors.
|
|
10
|
+
- Added generic and native copy/move operations.
|
|
11
|
+
- Added application user metadata for local and S3-compatible storage.
|
|
12
|
+
- Added bulk deletion helpers.
|
|
13
|
+
- Added S3 presigned read and write URLs.
|
|
14
|
+
- Expanded storage capability discovery without breaking the `0.1.26` `StorageAdapter` contract.
|
|
15
|
+
- Preserved fallback copy/move/delete-many behavior for older custom adapters.
|
|
16
|
+
- Added `create-bcp-app` storage provider selection for Local Server, Amazon S3 and Cloudflare R2.
|
|
17
|
+
- Added provider-specific `lib/storage.ts` scaffolding and `.env.example` configuration.
|
|
18
|
+
- Added local storage ecosystem unit coverage.
|
|
19
|
+
- Added S3-compatible ecosystem integration coverage.
|
|
20
|
+
- Extended packed-package smoke checks for the new runtime and presigner dependency.
|
|
21
|
+
|
|
22
|
+
## Public API additions
|
|
23
|
+
|
|
24
|
+
All new application APIs are available through `bcp/server`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
copyStorageObject,
|
|
29
|
+
createStorageSignedReadUrl,
|
|
30
|
+
createStorageSignedWriteUrl,
|
|
31
|
+
deleteStorageObjects,
|
|
32
|
+
getStorageEcosystemCapabilities,
|
|
33
|
+
getStorageMetadata,
|
|
34
|
+
listStorageObjects,
|
|
35
|
+
moveStorageObject,
|
|
36
|
+
setStorageMetadata,
|
|
37
|
+
} from "bcp/server";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
New public types include:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
StorageEcosystemAdapter
|
|
44
|
+
StorageEcosystemCapabilities
|
|
45
|
+
StorageEcosystemPutOptions
|
|
46
|
+
StorageEcosystemPutStreamOptions
|
|
47
|
+
StorageListOptions
|
|
48
|
+
StorageListResult
|
|
49
|
+
StorageCopyOptions
|
|
50
|
+
StorageDeleteManyResult
|
|
51
|
+
StorageSignedUrlOptions
|
|
52
|
+
StorageSignedWriteUrlOptions
|
|
53
|
+
StorageUserMetadata
|
|
54
|
+
S3StorageEcosystemAdapter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Backward compatibility
|
|
58
|
+
|
|
59
|
+
The original `StorageAdapter` remains valid. Ecosystem methods are additive and optional.
|
|
60
|
+
|
|
61
|
+
Generic helpers select a native adapter method when available. Operations that can be implemented portably, such as copy/move/delete-many, also have compatibility fallbacks where practical.
|
|
62
|
+
|
|
63
|
+
Custom `0.1.25` / `0.1.26` adapters therefore do not need to implement all `0.1.27` methods immediately.
|
|
64
|
+
|
|
65
|
+
## create-bcp-app storage provider selection
|
|
66
|
+
|
|
67
|
+
Interactive project creation now includes:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Select storage provider:
|
|
71
|
+
None
|
|
72
|
+
Local Server
|
|
73
|
+
Amazon S3
|
|
74
|
+
Cloudflare R2
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The same choices can be automated:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx create-bcp-app my-app --storage local
|
|
81
|
+
npx create-bcp-app my-app --storage amazon-s3
|
|
82
|
+
npx create-bcp-app my-app --storage cloudflare-r2
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Supported flag values are:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
none
|
|
89
|
+
local
|
|
90
|
+
amazon-s3
|
|
91
|
+
cloudflare-r2
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Selecting a provider creates `lib/storage.ts` and appends provider-specific settings to `.env.example`.
|
|
95
|
+
|
|
96
|
+
### Local Server preset
|
|
97
|
+
|
|
98
|
+
```dotenv
|
|
99
|
+
STORAGE_LOCAL_DIRECTORY=./storage
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The generated helper uses `createLocalStorage()` and adds `storage/` to `.gitignore`.
|
|
103
|
+
|
|
104
|
+
### Amazon S3 preset
|
|
105
|
+
|
|
106
|
+
```dotenv
|
|
107
|
+
AWS_S3_BUCKET=
|
|
108
|
+
AWS_REGION=ap-southeast-1
|
|
109
|
+
AWS_ACCESS_KEY_ID=
|
|
110
|
+
AWS_SECRET_ACCESS_KEY=
|
|
111
|
+
AWS_SESSION_TOKEN=
|
|
112
|
+
AWS_S3_PREFIX=
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The generated helper uses `createS3Storage()`. Explicit credentials are optional when the application deployment uses the AWS SDK server-side credential provider chain.
|
|
116
|
+
|
|
117
|
+
### Cloudflare R2 preset
|
|
118
|
+
|
|
119
|
+
```dotenv
|
|
120
|
+
R2_ACCOUNT_ID=
|
|
121
|
+
R2_BUCKET=
|
|
122
|
+
R2_ACCESS_KEY_ID=
|
|
123
|
+
R2_SECRET_ACCESS_KEY=
|
|
124
|
+
R2_PREFIX=
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The generated helper uses the S3-compatible adapter with `region: "auto"` and the Cloudflare account endpoint.
|
|
128
|
+
|
|
129
|
+
All generated storage credentials remain server-only and must not be moved into `BCP_PUBLIC_*` environment variables.
|
|
130
|
+
|
|
131
|
+
## Listing
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const page =
|
|
135
|
+
await listStorageObjects(
|
|
136
|
+
storage,
|
|
137
|
+
{
|
|
138
|
+
prefix: "documents/",
|
|
139
|
+
limit: 50,
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The result contains:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
{
|
|
148
|
+
objects: StorageObjectMetadata[];
|
|
149
|
+
cursor?: string;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Cursors are opaque. Applications should pass them back unchanged rather than decoding or constructing them.
|
|
154
|
+
|
|
155
|
+
The built-in local adapter uses a stable key-order cursor. The S3 adapter maps the generic cursor to the provider continuation token.
|
|
156
|
+
|
|
157
|
+
## Copy and move
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
await copyStorageObject(
|
|
161
|
+
storage,
|
|
162
|
+
"incoming/report.pdf",
|
|
163
|
+
"archive/report.pdf"
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await moveStorageObject(
|
|
167
|
+
storage,
|
|
168
|
+
"incoming/avatar.webp",
|
|
169
|
+
"users/42/avatar.webp"
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
S3 uses native `CopyObject`. Other adapters can fall back to streamed read/write copy behavior.
|
|
174
|
+
|
|
175
|
+
Overwrite remains opt-in.
|
|
176
|
+
|
|
177
|
+
## User metadata
|
|
178
|
+
|
|
179
|
+
`0.1.27` adds portable string metadata separate from BCP's internal checksum metadata:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
await storage.put(
|
|
183
|
+
"documents/report.pdf",
|
|
184
|
+
bytes,
|
|
185
|
+
{
|
|
186
|
+
contentType: "application/pdf",
|
|
187
|
+
metadata: {
|
|
188
|
+
owner: "user-42",
|
|
189
|
+
category: "report",
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Read or replace metadata independently:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const metadata =
|
|
199
|
+
await getStorageMetadata(
|
|
200
|
+
storage,
|
|
201
|
+
"documents/report.pdf"
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await setStorageMetadata(
|
|
205
|
+
storage,
|
|
206
|
+
"documents/report.pdf",
|
|
207
|
+
{
|
|
208
|
+
owner: "user-42",
|
|
209
|
+
state: "approved",
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Metadata keys are normalized to lowercase and must use portable storage-safe characters. The reserved `bcp_sha256` key cannot be set by applications.
|
|
215
|
+
|
|
216
|
+
## Bulk deletion
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const result =
|
|
220
|
+
await deleteStorageObjects(
|
|
221
|
+
storage,
|
|
222
|
+
[
|
|
223
|
+
"tmp/a.bin",
|
|
224
|
+
"tmp/b.bin",
|
|
225
|
+
]
|
|
226
|
+
);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The result contains:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
{
|
|
233
|
+
deleted: string[];
|
|
234
|
+
missing: string[];
|
|
235
|
+
failed: Array<{
|
|
236
|
+
key: string;
|
|
237
|
+
message: string;
|
|
238
|
+
}>;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
S3 deletion semantics treat deletion of a missing object as successful, so S3 normally reports those keys under `deleted` rather than `missing`.
|
|
243
|
+
|
|
244
|
+
## Presigned URLs
|
|
245
|
+
|
|
246
|
+
S3-compatible adapters expose direct read/write signing:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const downloadUrl =
|
|
250
|
+
await createStorageSignedReadUrl(
|
|
251
|
+
storage,
|
|
252
|
+
"videos/demo.mp4",
|
|
253
|
+
{
|
|
254
|
+
expiresIn: 300,
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const uploadUrl =
|
|
259
|
+
await createStorageSignedWriteUrl(
|
|
260
|
+
storage,
|
|
261
|
+
"uploads/demo.mp4",
|
|
262
|
+
{
|
|
263
|
+
expiresIn: 300,
|
|
264
|
+
contentType: "video/mp4",
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Signed URLs are useful when browsers or other clients should transfer large files directly with object storage rather than proxying bytes through the application server.
|
|
270
|
+
|
|
271
|
+
The default expiry is 15 minutes. `expiresIn` is limited to the S3 SigV4 maximum of seven days.
|
|
272
|
+
|
|
273
|
+
Local filesystem storage intentionally does not emulate signed URLs.
|
|
274
|
+
|
|
275
|
+
## Capabilities
|
|
276
|
+
|
|
277
|
+
Use:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
getStorageEcosystemCapabilities(storage)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The extended capability result includes:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
{
|
|
287
|
+
streamingRead,
|
|
288
|
+
streamingWrite,
|
|
289
|
+
ranges,
|
|
290
|
+
signedUrls,
|
|
291
|
+
listing,
|
|
292
|
+
signedReadUrls,
|
|
293
|
+
signedWriteUrls,
|
|
294
|
+
copy,
|
|
295
|
+
move,
|
|
296
|
+
metadata,
|
|
297
|
+
bulkDelete,
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
For the built-in S3 ecosystem adapter all of these capabilities are enabled.
|
|
302
|
+
|
|
303
|
+
For local storage, listing/copy/move/metadata/bulk-delete are enabled while signed URL capabilities remain disabled.
|
|
304
|
+
|
|
305
|
+
## Dependency change
|
|
306
|
+
|
|
307
|
+
The framework package adds:
|
|
308
|
+
|
|
309
|
+
```text
|
|
310
|
+
@aws-sdk/s3-request-presigner
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
alongside the S3 client and multipart upload dependencies introduced in `0.1.26`.
|
|
314
|
+
|
|
315
|
+
## Testing
|
|
316
|
+
|
|
317
|
+
`0.1.27` adds regression coverage for:
|
|
318
|
+
|
|
319
|
+
- local listing and pagination,
|
|
320
|
+
- portable metadata,
|
|
321
|
+
- metadata preservation during copy/move,
|
|
322
|
+
- bulk deletion,
|
|
323
|
+
- unsupported local signed URLs,
|
|
324
|
+
- S3 listing,
|
|
325
|
+
- S3 native copy/move,
|
|
326
|
+
- S3 metadata replacement,
|
|
327
|
+
- S3 bulk deletion,
|
|
328
|
+
- S3 signed read/write URL generation,
|
|
329
|
+
- create-app Local Server storage scaffolding,
|
|
330
|
+
- create-app Amazon S3 storage scaffolding,
|
|
331
|
+
- create-app Cloudflare R2 storage scaffolding,
|
|
332
|
+
- non-interactive `--storage` CLI selection,
|
|
333
|
+
- public package exports,
|
|
334
|
+
- packed dependency/runtime presence.
|
|
335
|
+
|
|
336
|
+
## Remaining release work
|
|
337
|
+
|
|
338
|
+
Before publication:
|
|
339
|
+
|
|
340
|
+
1. sync `package-lock.json` with `0.1.27` and the presigner dependency,
|
|
341
|
+
2. run `npm run typecheck`,
|
|
342
|
+
3. run unit/integration/E2E/package tests,
|
|
343
|
+
4. run `npm run rc:check`,
|
|
344
|
+
5. validate a packed `0.1.27` package in a representative application,
|
|
345
|
+
6. create the `v0.1.27` tag only after the final release commit is known,
|
|
346
|
+
7. publish and verify npm visibility.
|
|
347
|
+
|
|
348
|
+
## Next milestone
|
|
349
|
+
|
|
350
|
+
The planned next milestone is `0.1.28 — Production Hardening`, focused on graceful shutdown, proxy awareness, timeouts, security and Docker/standalone runtime reliability.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# BCP Framework 0.1.28
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.1.28` is the **Production Hardening** milestone.
|
|
4
|
+
|
|
5
|
+
> Release state: unreleased development target until local validation, RC checks, tagging and npm publication complete.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- Added a public production hardening gateway around the existing standalone runtime.
|
|
10
|
+
- Added configurable request, header, keep-alive and shutdown timeouts.
|
|
11
|
+
- Added graceful `SIGTERM` / `SIGINT` handling for Docker and process managers.
|
|
12
|
+
- Added application shutdown hooks through `registerShutdownHook()`.
|
|
13
|
+
- Added trusted-proxy forwarding-header sanitization with secure default `BCP_TRUST_PROXY=false`.
|
|
14
|
+
- Added force-close fallback after the configured graceful shutdown timeout.
|
|
15
|
+
- Fixed `create-bcp-app --storage local` so generated projects no longer contain an unexplained empty storage directory.
|
|
16
|
+
- Added `storage/README.md` and `storage/.gitkeep` while continuing to ignore runtime storage objects.
|
|
17
|
+
- Added regression coverage for production hardening primitives and Local Server scaffolding.
|
|
18
|
+
|
|
19
|
+
## Production environment controls
|
|
20
|
+
|
|
21
|
+
```dotenv
|
|
22
|
+
BCP_REQUEST_TIMEOUT_MS=120000
|
|
23
|
+
BCP_HEADERS_TIMEOUT_MS=66000
|
|
24
|
+
BCP_KEEP_ALIVE_TIMEOUT_MS=65000
|
|
25
|
+
BCP_SHUTDOWN_TIMEOUT_MS=10000
|
|
26
|
+
BCP_TRUST_PROXY=false
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Defaults are production-safe and require no configuration for ordinary direct HTTP deployments.
|
|
30
|
+
|
|
31
|
+
`BCP_HEADERS_TIMEOUT_MS` must be greater than `BCP_KEEP_ALIVE_TIMEOUT_MS`.
|
|
32
|
+
|
|
33
|
+
## Graceful shutdown
|
|
34
|
+
|
|
35
|
+
The standalone production runtime now handles:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
SIGTERM
|
|
39
|
+
SIGINT
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The runtime first stops/drains the public hardening gateway, then runs application shutdown hooks, then stops internal BCP runtime layers.
|
|
43
|
+
|
|
44
|
+
After `BCP_SHUTDOWN_TIMEOUT_MS`, remaining public connections are force-closed.
|
|
45
|
+
|
|
46
|
+
## Public API addition
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import {
|
|
50
|
+
getProductionHardeningConfig,
|
|
51
|
+
registerShutdownHook,
|
|
52
|
+
} from "bcp/server";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
registerShutdownHook(
|
|
59
|
+
() => {
|
|
60
|
+
storage.destroy();
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "storage",
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Hooks run in reverse registration order and can be unregistered with the cleanup function returned by `registerShutdownHook()`.
|
|
69
|
+
|
|
70
|
+
## Trusted proxy behavior
|
|
71
|
+
|
|
72
|
+
Trusted proxy mode is disabled by default.
|
|
73
|
+
|
|
74
|
+
When disabled, BCP strips spoofable incoming forwarding headers at the public hardening gateway and writes forwarding information from the actual connection.
|
|
75
|
+
|
|
76
|
+
When the application is intentionally deployed behind a trusted reverse proxy/load balancer, enable:
|
|
77
|
+
|
|
78
|
+
```dotenv
|
|
79
|
+
BCP_TRUST_PROXY=true
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Only enable it when direct untrusted traffic cannot bypass the trusted proxy.
|
|
83
|
+
|
|
84
|
+
## Local Server storage fix
|
|
85
|
+
|
|
86
|
+
Before `0.1.28`, selecting Local Server generated `lib/storage.ts` and configured `./storage`, but the runtime directory had no explanatory scaffold before the first upload.
|
|
87
|
+
|
|
88
|
+
`0.1.28` generates:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
storage/
|
|
92
|
+
├─ .gitkeep
|
|
93
|
+
└─ README.md
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
and uses Git ignore rules:
|
|
97
|
+
|
|
98
|
+
```gitignore
|
|
99
|
+
storage/*
|
|
100
|
+
!storage/.gitkeep
|
|
101
|
+
!storage/README.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Runtime uploads and `.bcp-storage-meta` remain untracked while the directory structure is visible in a fresh project.
|
|
105
|
+
|
|
106
|
+
## Testing
|
|
107
|
+
|
|
108
|
+
`0.1.28` adds coverage for:
|
|
109
|
+
|
|
110
|
+
- production timeout parsing,
|
|
111
|
+
- invalid timeout relationships,
|
|
112
|
+
- applying Node HTTP server timeout settings,
|
|
113
|
+
- graceful HTTP server close,
|
|
114
|
+
- shutdown hook ordering/unregistration,
|
|
115
|
+
- generated Local Server storage directory scaffold,
|
|
116
|
+
- generated Local Server Git ignore behavior.
|
|
117
|
+
|
|
118
|
+
## Remaining release work
|
|
119
|
+
|
|
120
|
+
Before publication:
|
|
121
|
+
|
|
122
|
+
1. sync `package-lock.json` to `0.1.28`,
|
|
123
|
+
2. run `npm run typecheck`,
|
|
124
|
+
3. run unit/integration/E2E/package tests,
|
|
125
|
+
4. run `npm run rc:check`,
|
|
126
|
+
5. build a representative BCP application,
|
|
127
|
+
6. run the standalone artifact in Docker,
|
|
128
|
+
7. verify `docker stop` produces graceful shutdown logs,
|
|
129
|
+
8. create the `v0.1.28` tag only after the final release commit is known,
|
|
130
|
+
9. publish and verify npm visibility.
|
|
131
|
+
|
|
132
|
+
## Next milestone
|
|
133
|
+
|
|
134
|
+
The planned next milestone is `0.1.29 — Developer Experience`, focused on generators, richer `doctor` / `inspect`, improved diagnostics and create-app workflow improvements.
|