@ts-cloud/core 0.5.16 → 0.5.18
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/dist/aws/s3.d.ts +1 -1
- package/dist/index.js +39 -30
- package/dist/types.d.ts +75 -4
- package/package.json +2 -2
package/dist/aws/s3.d.ts
CHANGED
|
@@ -235,7 +235,7 @@ export declare class S3Client {
|
|
|
235
235
|
/**
|
|
236
236
|
* Abort a multipart upload
|
|
237
237
|
*/
|
|
238
|
-
abortMultipartUpload(bucket: string, key: string, uploadId: string): Promise<void>;
|
|
238
|
+
abortMultipartUpload(bucket: string, key: string, uploadId: string, baseUrl?: string): Promise<void>;
|
|
239
239
|
/**
|
|
240
240
|
* Empty all objects in a bucket and then delete the bucket
|
|
241
241
|
*/
|
package/dist/index.js
CHANGED
|
@@ -23532,18 +23532,19 @@ class S3Client {
|
|
|
23532
23532
|
stream = blob.stream();
|
|
23533
23533
|
totalSize = blob.size;
|
|
23534
23534
|
}
|
|
23535
|
-
const uploadId = await this.initiateMultipartUpload(bucket, key, options);
|
|
23535
|
+
const { uploadId, baseUrl } = await this.initiateMultipartUpload(bucket, key, options);
|
|
23536
23536
|
try {
|
|
23537
|
-
const parts = await this.uploadParts(bucket, key, uploadId, stream, partSize, credentials, totalSize, options.concurrency || 4, options.onProgress);
|
|
23538
|
-
return await this.completeMultipartUpload(bucket, key, uploadId, parts);
|
|
23537
|
+
const parts = await this.uploadParts(bucket, key, baseUrl, uploadId, stream, partSize, credentials, totalSize, options.concurrency || 4, options.onProgress);
|
|
23538
|
+
return await this.completeMultipartUpload(bucket, key, baseUrl, uploadId, parts);
|
|
23539
23539
|
} catch (error) {
|
|
23540
|
-
await this.abortMultipartUpload(bucket, key, uploadId).catch(() => {});
|
|
23540
|
+
await this.abortMultipartUpload(bucket, key, uploadId, baseUrl).catch(() => {});
|
|
23541
23541
|
throw error;
|
|
23542
23542
|
}
|
|
23543
23543
|
}
|
|
23544
23544
|
async initiateMultipartUpload(bucket, key, options) {
|
|
23545
23545
|
const credentials = await this.getCredentials();
|
|
23546
|
-
const
|
|
23546
|
+
const baseUrl = this.buildUrl(bucket, key);
|
|
23547
|
+
const url = `${baseUrl}?uploads`;
|
|
23547
23548
|
const headers = {
|
|
23548
23549
|
"Content-Type": options.contentType || detectContentType(key)
|
|
23549
23550
|
};
|
|
@@ -23579,18 +23580,26 @@ class S3Client {
|
|
|
23579
23580
|
if (!uploadIdMatch) {
|
|
23580
23581
|
throw new S3Error("Failed to parse upload ID from response", 0, bucket, key);
|
|
23581
23582
|
}
|
|
23582
|
-
|
|
23583
|
+
let resolvedBaseUrl = baseUrl;
|
|
23584
|
+
if (response.url) {
|
|
23585
|
+
try {
|
|
23586
|
+
const resolved = new URL(response.url);
|
|
23587
|
+
resolved.search = "";
|
|
23588
|
+
resolvedBaseUrl = resolved.toString();
|
|
23589
|
+
} catch {}
|
|
23590
|
+
}
|
|
23591
|
+
return { uploadId: uploadIdMatch[1], baseUrl: resolvedBaseUrl };
|
|
23583
23592
|
}
|
|
23584
|
-
async uploadParts(bucket, key, uploadId, stream, partSize, credentials, totalSize, concurrency, onProgress) {
|
|
23593
|
+
async uploadParts(bucket, key, baseUrl, uploadId, stream, partSize, credentials, totalSize, concurrency, onProgress) {
|
|
23585
23594
|
const parts = [];
|
|
23586
23595
|
const reader = stream.getReader();
|
|
23587
23596
|
let partNumber = 1;
|
|
23588
23597
|
let buffer = new Uint8Array(0);
|
|
23589
23598
|
let loaded = 0;
|
|
23590
23599
|
const totalParts = totalSize ? Math.ceil(totalSize / partSize) : undefined;
|
|
23591
|
-
const
|
|
23600
|
+
const inFlight = new Map;
|
|
23592
23601
|
const uploadPart = async (data, num) => {
|
|
23593
|
-
const url = `${
|
|
23602
|
+
const url = `${baseUrl}?partNumber=${num}&uploadId=${encodeURIComponent(uploadId)}`;
|
|
23594
23603
|
const headers = {
|
|
23595
23604
|
"Content-Length": String(data.byteLength),
|
|
23596
23605
|
"x-amz-content-sha256": "UNSIGNED-PAYLOAD"
|
|
@@ -23606,15 +23615,24 @@ class S3Client {
|
|
|
23606
23615
|
const response = await fetch(signed.url, {
|
|
23607
23616
|
method: signed.method,
|
|
23608
23617
|
headers: signed.headers,
|
|
23609
|
-
body: data
|
|
23618
|
+
body: data,
|
|
23619
|
+
redirect: "error"
|
|
23610
23620
|
});
|
|
23611
23621
|
if (!response.ok) {
|
|
23612
23622
|
const error = await response.text();
|
|
23613
23623
|
throw new S3Error(`Failed to upload part ${num}: ${error}`, response.status, bucket, key);
|
|
23614
23624
|
}
|
|
23615
23625
|
const etag = (response.headers.get("ETag") || "").replace(/"/g, "");
|
|
23626
|
+
if (!etag) {
|
|
23627
|
+
throw new S3Error(`Missing ETag for part ${num}`, response.status, bucket, key);
|
|
23628
|
+
}
|
|
23616
23629
|
return { partNumber: num, etag };
|
|
23617
23630
|
};
|
|
23631
|
+
const drainOne = async () => {
|
|
23632
|
+
const completed = await Promise.race(inFlight.values());
|
|
23633
|
+
inFlight.delete(completed.partNumber);
|
|
23634
|
+
parts.push(completed);
|
|
23635
|
+
};
|
|
23618
23636
|
while (true) {
|
|
23619
23637
|
const { done, value } = await reader.read();
|
|
23620
23638
|
if (value) {
|
|
@@ -23627,13 +23645,10 @@ class S3Client {
|
|
|
23627
23645
|
const partData = buffer.slice(0, partSize);
|
|
23628
23646
|
buffer = buffer.slice(partSize);
|
|
23629
23647
|
const currentPartNumber = partNumber++;
|
|
23630
|
-
if (
|
|
23631
|
-
|
|
23632
|
-
parts.push(completed);
|
|
23633
|
-
uploadQueue.splice(uploadQueue.indexOf(Promise.resolve(completed)), 1);
|
|
23648
|
+
if (inFlight.size >= concurrency) {
|
|
23649
|
+
await drainOne();
|
|
23634
23650
|
}
|
|
23635
|
-
|
|
23636
|
-
uploadQueue.push(uploadPromise);
|
|
23651
|
+
inFlight.set(currentPartNumber, uploadPart(partData, currentPartNumber));
|
|
23637
23652
|
loaded += partData.byteLength;
|
|
23638
23653
|
if (onProgress) {
|
|
23639
23654
|
onProgress({
|
|
@@ -23647,14 +23662,15 @@ class S3Client {
|
|
|
23647
23662
|
if (done)
|
|
23648
23663
|
break;
|
|
23649
23664
|
}
|
|
23650
|
-
|
|
23651
|
-
|
|
23665
|
+
while (inFlight.size > 0) {
|
|
23666
|
+
await drainOne();
|
|
23667
|
+
}
|
|
23652
23668
|
parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
23653
23669
|
return parts;
|
|
23654
23670
|
}
|
|
23655
|
-
async completeMultipartUpload(bucket, key, uploadId, parts) {
|
|
23671
|
+
async completeMultipartUpload(bucket, key, baseUrl, uploadId, parts) {
|
|
23656
23672
|
const credentials = await this.getCredentials();
|
|
23657
|
-
const url = `${
|
|
23673
|
+
const url = `${baseUrl}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
23658
23674
|
const partsXml = parts.map((p) => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>"${p.etag}"</ETag></Part>`).join("");
|
|
23659
23675
|
const body = `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
|
|
23660
23676
|
const signed = signRequest({
|
|
@@ -23680,9 +23696,9 @@ class S3Client {
|
|
|
23680
23696
|
const etag = etagMatch ? etagMatch[1] : "";
|
|
23681
23697
|
return { etag };
|
|
23682
23698
|
}
|
|
23683
|
-
async abortMultipartUpload(bucket, key, uploadId) {
|
|
23699
|
+
async abortMultipartUpload(bucket, key, uploadId, baseUrl) {
|
|
23684
23700
|
const credentials = await this.getCredentials();
|
|
23685
|
-
const url = `${this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
23701
|
+
const url = `${baseUrl ?? this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
23686
23702
|
const signed = signRequest({
|
|
23687
23703
|
method: "DELETE",
|
|
23688
23704
|
url,
|
|
@@ -45970,14 +45986,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
45970
45986
|
VisibilityConfig: { SampledRequestsEnabled: true, CloudWatchMetricsEnabled: true, MetricName: `${slug}-${environment}-waf` }
|
|
45971
45987
|
}
|
|
45972
45988
|
};
|
|
45973
|
-
|
|
45974
|
-
Type: "AWS::WAFv2::WebACLAssociation",
|
|
45975
|
-
DependsOn: ["HttpStage"],
|
|
45976
|
-
Properties: {
|
|
45977
|
-
ResourceArn: Fn2.sub("arn:aws:apigateway:${AWS::Region}::/apis/${HttpApi}/stages/$default"),
|
|
45978
|
-
WebACLArn: Fn2.getAtt("WebAcl", "Arn")
|
|
45979
|
-
}
|
|
45980
|
-
};
|
|
45989
|
+
outputs.WafAclArn = { Description: "WAF web ACL ARN. Attach to a CloudFront distribution fronting the API to enforce (HTTP API v2 stages do not support direct WAF association).", Value: Fn2.getAtt("WebAcl", "Arn") };
|
|
45981
45990
|
}
|
|
45982
45991
|
if (hasVpc && needsDataVpc) {
|
|
45983
45992
|
if (!app.vpc?.id)
|
package/dist/types.d.ts
CHANGED
|
@@ -932,9 +932,11 @@ export interface SiteConfig {
|
|
|
932
932
|
redirects?: Record<string, string>;
|
|
933
933
|
/**
|
|
934
934
|
* Give this site a dedicated php-fpm pool (isolated user/process) rather than
|
|
935
|
-
* sharing the default pool.
|
|
935
|
+
* sharing the default pool (Forge's "User Isolation").
|
|
936
936
|
*/
|
|
937
937
|
isolation?: boolean;
|
|
938
|
+
/** Per-site nginx access control (IP allow/deny — Forge "Security Rules"). */
|
|
939
|
+
security?: SiteSecurityConfig;
|
|
938
940
|
/** Post-deploy health check (Forge-style) pinged after `current` is flipped. */
|
|
939
941
|
healthCheck?: {
|
|
940
942
|
path?: string;
|
|
@@ -1059,6 +1061,33 @@ export interface SiteSslConfig {
|
|
|
1059
1061
|
* domain resolves to the box. The plugin + credentials are wired on the box.
|
|
1060
1062
|
*/
|
|
1061
1063
|
dns?: SslDnsConfig;
|
|
1064
|
+
/**
|
|
1065
|
+
* Emit an HSTS (`Strict-Transport-Security`) header so browsers force HTTPS.
|
|
1066
|
+
* `true` uses a 1-year max-age with `includeSubDomains`; an object customizes
|
|
1067
|
+
* it. Only meaningful when the site serves TLS.
|
|
1068
|
+
*/
|
|
1069
|
+
hsts?: boolean | {
|
|
1070
|
+
maxAge?: number;
|
|
1071
|
+
includeSubDomains?: boolean;
|
|
1072
|
+
preload?: boolean;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* `ssl_protocols` for the vhost (e.g. `['TLSv1.2', 'TLSv1.3']`). Applied to
|
|
1076
|
+
* the `custom`-cert :443 block; for Let's Encrypt the protocols are managed by
|
|
1077
|
+
* certbot's options file.
|
|
1078
|
+
*/
|
|
1079
|
+
tlsProtocols?: string[];
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Per-site access control at the nginx layer (Forge's "Security Rules"). IPs/
|
|
1083
|
+
* CIDRs in {@link allow} are permitted and everything else denied; {@link deny}
|
|
1084
|
+
* blocks specific IPs/CIDRs. `allow` takes precedence (allow-list mode).
|
|
1085
|
+
*/
|
|
1086
|
+
export interface SiteSecurityConfig {
|
|
1087
|
+
/** Allow only these IPs/CIDRs (everything else gets 403). */
|
|
1088
|
+
allow?: string[];
|
|
1089
|
+
/** Block these IPs/CIDRs. */
|
|
1090
|
+
deny?: string[];
|
|
1062
1091
|
}
|
|
1063
1092
|
/** certbot DNS-01 plugin configuration for DNS-validated / wildcard certs. */
|
|
1064
1093
|
export interface SslDnsConfig {
|
|
@@ -2169,10 +2198,12 @@ export interface ComputeConfig {
|
|
|
2169
2198
|
*/
|
|
2170
2199
|
sshKey?: string;
|
|
2171
2200
|
/**
|
|
2172
|
-
*
|
|
2173
|
-
* @
|
|
2201
|
+
* On-box server monitoring (Forge-style). `true`/`false` toggles the metrics
|
|
2202
|
+
* collector; pass a {@link ComputeMonitoringConfig} to also set resource
|
|
2203
|
+
* alert thresholds that notify the configured channels on breach.
|
|
2204
|
+
* @default true for PHP boxes
|
|
2174
2205
|
*/
|
|
2175
|
-
monitoring?: boolean;
|
|
2206
|
+
monitoring?: boolean | ComputeMonitoringConfig;
|
|
2176
2207
|
/**
|
|
2177
2208
|
* Spot/preemptible instance settings (when using fleet)
|
|
2178
2209
|
*/
|
|
@@ -2287,6 +2318,25 @@ export interface SshKeyConfig {
|
|
|
2287
2318
|
/** The public key line (e.g. `ssh-ed25519 AAAA… user@host`). */
|
|
2288
2319
|
publicKey: string;
|
|
2289
2320
|
}
|
|
2321
|
+
/**
|
|
2322
|
+
* On-box monitoring + resource alerts. See {@link ComputeConfig.monitoring}.
|
|
2323
|
+
* The collector writes a metrics snapshot every minute; when a threshold is
|
|
2324
|
+
* breached it calls the on-box notifier (Slack/Discord/Telegram/webhook) once
|
|
2325
|
+
* per OK→alert transition (and again on recovery), so channels aren't spammed.
|
|
2326
|
+
*/
|
|
2327
|
+
export interface ComputeMonitoringConfig {
|
|
2328
|
+
/** Enable the metrics collector. @default true */
|
|
2329
|
+
enabled?: boolean;
|
|
2330
|
+
/** Alert thresholds; omit a field to keep its default. */
|
|
2331
|
+
alerts?: {
|
|
2332
|
+
/** Alert when 1-min load average per CPU exceeds this. @default 2 */
|
|
2333
|
+
cpuLoadPerCore?: number;
|
|
2334
|
+
/** Alert when used memory percentage is ≥ this. @default 90 */
|
|
2335
|
+
memPercent?: number;
|
|
2336
|
+
/** Alert when root-filesystem usage percentage is ≥ this. @default 90 */
|
|
2337
|
+
diskPercent?: number;
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2290
2340
|
/** Host firewall (UFW) configuration. See {@link ComputeConfig.firewall}. */
|
|
2291
2341
|
export interface ComputeFirewallConfig {
|
|
2292
2342
|
/** Enable UFW. @default true */
|
|
@@ -2393,6 +2443,27 @@ export interface ComputeProxyConfig {
|
|
|
2393
2443
|
* served per-SNI by rpx. @default '/etc/rpx/certs'
|
|
2394
2444
|
*/
|
|
2395
2445
|
certsDir?: string;
|
|
2446
|
+
/**
|
|
2447
|
+
* Inactivity timeout (seconds) for rpx's pooled upstream connections, surfaced
|
|
2448
|
+
* to the gateway as `RPX_UPSTREAM_TIMEOUT`. rpx leaves this **off by default**
|
|
2449
|
+
* because it commonly fronts dev servers doing SSE/HMR/long-poll, but a
|
|
2450
|
+
* production gateway fronting real apps must bound it: the pool caps
|
|
2451
|
+
* connections per upstream and queues requests for a free slot, so a single
|
|
2452
|
+
* stalled upstream socket with no timeout holds its slot forever — enough of
|
|
2453
|
+
* them leak the pool and the gateway wedges (TLS handshakes succeed but no
|
|
2454
|
+
* request is ever answered). The timer resets on every byte, so a stream that
|
|
2455
|
+
* emits data periodically never trips it; only a fully-stalled upstream does.
|
|
2456
|
+
* Set `0` to disable (match rpx's dev default for streaming-heavy upstreams).
|
|
2457
|
+
* @default 60
|
|
2458
|
+
*/
|
|
2459
|
+
upstreamTimeout?: number;
|
|
2460
|
+
/**
|
|
2461
|
+
* Max open connections rpx keeps per upstream `host:port`, surfaced as
|
|
2462
|
+
* `RPX_MAX_UPSTREAM_CONNS`. Requests beyond this queue for a free slot rather
|
|
2463
|
+
* than churning sockets into TIME_WAIT. Raise for higher peak parallelism per
|
|
2464
|
+
* upstream; omit to use rpx's built-in default (256).
|
|
2465
|
+
*/
|
|
2466
|
+
maxUpstreamConns?: number;
|
|
2396
2467
|
/**
|
|
2397
2468
|
* Enable rpx on-demand TLS: lazily issue a real (Let's Encrypt) cert for an
|
|
2398
2469
|
* approved host the first time it's needed. The site domains are used as the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ts-cloud/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core CloudFormation generation library for ts-cloud",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.com>",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ts-cloud/aws-types": "0.5.
|
|
34
|
+
"@ts-cloud/aws-types": "0.5.18"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.9.3"
|