@stacksjs/ts-cloud 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws/index.js +3 -1
- package/dist/bin/cli.js +575 -565
- package/dist/bin/dashboard-server.js +334 -324
- package/dist/cdn/cloudflare-plan.d.ts +61 -0
- package/dist/cdn/cloudflare-rules.d.ts +77 -0
- package/dist/cdn/cloudflare-settings.d.ts +100 -0
- package/dist/cdn/cloudflare.d.ts +108 -0
- package/dist/cdn/cloudflare.test.d.ts +1 -0
- package/dist/cdn/index.d.ts +17 -0
- package/dist/cdn/origin-tls.d.ts +36 -0
- package/dist/{chunk-v0vpamnf.js → chunk-5nr95wpj.js} +3 -3
- package/dist/{chunk-gttvakpv.js → chunk-atbgd8wz.js} +163 -37
- package/dist/{chunk-x6137d59.js → chunk-hg7rhrgd.js} +27 -4
- package/dist/{chunk-wxhrr03r.js → chunk-hygwq9y2.js} +2 -2
- package/dist/{chunk-kpeb5rcs.js → chunk-ws47smxr.js} +2 -2
- package/dist/deploy/index.js +4 -4
- package/dist/dns/cloudflare.d.ts +119 -1
- package/dist/dns/index.d.ts +4 -1
- package/dist/dns/index.js +3 -1
- package/dist/dns/types.d.ts +43 -0
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/shared/rpx-gateway.d.ts +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +365 -4
- package/dist/ui/account/automation.html +4 -4
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +4 -4
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +4 -4
- package/dist/ui/index.html +3 -3
- package/dist/ui/infrastructure/topology.html +3 -3
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +4 -4
- package/dist/ui/operations/maintenance.html +4 -4
- package/dist/ui/operations/observability.html +4 -4
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +4 -4
- package/dist/ui/operations/releases.html +4 -4
- package/dist/ui/operations/spend.html +4 -4
- package/dist/ui/operations/workloads.html +4 -4
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +4 -4
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +4 -4
- package/dist/ui/server/fleet.html +4 -4
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/metrics.html +4 -4
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/sites.html +4 -4
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +4 -4
- package/package.json +3 -3
package/dist/dns/types.d.ts
CHANGED
|
@@ -20,7 +20,32 @@ export interface DnsRecord {
|
|
|
20
20
|
priority?: number;
|
|
21
21
|
weight?: number;
|
|
22
22
|
port?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Cloudflare only: serve this record through Cloudflare's reverse proxy (the
|
|
25
|
+
* "orange cloud") instead of publishing the origin address to the world.
|
|
26
|
+
*
|
|
27
|
+
* This is what turns a plain A/AAAA/CNAME into a CDN-fronted host — the
|
|
28
|
+
* public answer becomes a Cloudflare anycast address and requests reach the
|
|
29
|
+
* origin over Cloudflare's network, picking up its edge cache, TLS
|
|
30
|
+
* termination and DDoS protection on the way.
|
|
31
|
+
*
|
|
32
|
+
* Leave it `undefined` on an upsert to mean "don't care": the provider then
|
|
33
|
+
* PRESERVES whatever proxy state the record already has. That distinction
|
|
34
|
+
* matters because every deploy rewrites these records, and a `false` default
|
|
35
|
+
* would silently grey-cloud a proxied site on the next deploy.
|
|
36
|
+
*
|
|
37
|
+
* Ignored by every non-Cloudflare provider, and by Cloudflare itself for
|
|
38
|
+
* record types it cannot proxy (see {@link PROXIABLE_RECORD_TYPES}).
|
|
39
|
+
*/
|
|
40
|
+
proxied?: boolean;
|
|
23
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Record types Cloudflare is able to put behind its proxy. Anything else (TXT,
|
|
44
|
+
* MX, NS, CAA, SRV) is always served as plain DNS, and sending `proxied` for
|
|
45
|
+
* one is an API error rather than a no-op — so the provider drops the field for
|
|
46
|
+
* those types instead of forwarding it.
|
|
47
|
+
*/
|
|
48
|
+
export declare const PROXIABLE_RECORD_TYPES: ReadonlySet<DnsRecordType>;
|
|
24
49
|
export interface DnsRecordResult extends DnsRecord {
|
|
25
50
|
id?: string;
|
|
26
51
|
}
|
|
@@ -95,6 +120,24 @@ export interface GoDaddyProviderConfig {
|
|
|
95
120
|
export interface CloudflareProviderConfig {
|
|
96
121
|
provider: 'cloudflare';
|
|
97
122
|
apiToken: string;
|
|
123
|
+
/**
|
|
124
|
+
* Zone id for the domain being managed.
|
|
125
|
+
*
|
|
126
|
+
* Supplying it is what lets the API token be scoped to a SINGLE zone. Without
|
|
127
|
+
* it the provider has to find the zone by name through `GET /zones?name=…`,
|
|
128
|
+
* and that endpoint is an account-level listing: a token restricted to one
|
|
129
|
+
* zone gets an empty result back and every operation fails with "Zone not
|
|
130
|
+
* found", which reads like a missing domain rather than a missing permission.
|
|
131
|
+
*
|
|
132
|
+
* Only applies to the zone it belongs to; requests for any other domain fall
|
|
133
|
+
* back to the by-name lookup.
|
|
134
|
+
*/
|
|
135
|
+
zoneId?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Account id. Only needed for account-scoped resources (account-level
|
|
138
|
+
* rulesets); zone-scoped work never uses it.
|
|
139
|
+
*/
|
|
140
|
+
accountId?: string;
|
|
98
141
|
}
|
|
99
142
|
export type DnsProviderConfig = Route53ProviderConfig | PorkbunProviderConfig | GoDaddyProviderConfig | CloudflareProviderConfig;
|
|
100
143
|
/**
|
package/dist/drivers/index.js
CHANGED
|
@@ -48,6 +48,17 @@ export declare function hasAutoWwwVariant(domain: string): boolean;
|
|
|
48
48
|
export declare function gatewayHostnames(sites: Record<string, SiteConfig | undefined>, options?: {
|
|
49
49
|
autoWww?: boolean;
|
|
50
50
|
}): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Public recursive resolvers used to decide whether a hostname is ready for an
|
|
53
|
+
* ACME http-01 challenge.
|
|
54
|
+
*
|
|
55
|
+
* The question the check has to answer is "can Let's Encrypt see this record
|
|
56
|
+
* yet", and LE resolves from the public internet — so the box's own resolver is
|
|
57
|
+
* the wrong vantage point, and a stale negative entry there blocks issuance for
|
|
58
|
+
* a record the rest of the world can already resolve. Two independent operators
|
|
59
|
+
* so one being unreachable does not stall a deploy.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PUBLIC_DNS_RESOLVERS: readonly string[];
|
|
51
62
|
/** Default webroot the gateway serves ACME http-01 challenges from on `:80`. */
|
|
52
63
|
export declare const DEFAULT_ACME_WEBROOT = "/var/www/acme-challenge";
|
|
53
64
|
/** A normalized redirect target on an {@link RpxRoute} (see rpx's `redirect`). */
|
package/dist/index.d.ts
CHANGED
|
@@ -35,16 +35,20 @@ export * from './ssl';
|
|
|
35
35
|
export { deployStaticSite, deployStaticSiteFull, uploadStaticFiles, invalidateCache, deleteStaticSite, generateStaticSiteTemplate, deployStaticSiteWithExternalDns, deployStaticSiteWithExternalDnsFull, generateExternalDnsStaticSiteTemplate, deploySite, hasProxyUpstream, resolveProxyUpstreams, resolveSiteDeployTarget, resolveSiteKind, shipsARelease, validateDeploymentConfig, buildAndPushServerlessImage, buildFunctionEnv, deployServerlessApp, infraEnvFromOutputs, redeployServerlessApp, rollbackServerlessApp, runRemoteCommand, setMaintenance, buildManagementDashboardArtifact, dashboardCredentialsFile, ensureManagementDashboard, MANAGEMENT_DASHBOARD_SITE, resolveDashboardAuth, resolveUiSource, } from './deploy';
|
|
36
36
|
export { collectServerDnsDomains, hostAcceptsIpv6, IPV6_EXCLUDED_HOST_LABELS, normalizePublicIpv6, reconcileAddressRecords, removeStaleServerAddressRecords, verifyAddressRecord, } from './deploy/server-dns';
|
|
37
37
|
export type { AddressRecordReport } from './deploy/server-dns';
|
|
38
|
+
export * from './cdn';
|
|
38
39
|
export type { EnsureDashboardLogger, ResolvedDashboardAuth, StaticSiteConfig, DeployResult, UploadOptions, ExternalDnsStaticSiteConfig, ExternalDnsDeployResult, DeploySiteConfig, DeploySiteResult, StaticSiteDnsProvider, SiteDeployKind, DeploymentValidationResult, BuildImageOptions, BuiltImage, CodeSource, DeployServerlessOptions, ResolvedContext, } from './deploy';
|
|
39
40
|
export { createCloudDriver, CloudDriverFactory, cloudDrivers, AwsDriver, HetznerDriver, HetznerClient, resolveHetznerApiToken, normalizeSshPublicKey, ensureFirewall, ensureServer, ensureSshKey, serverPublicIpv4, sshExec, sshExecOrThrow, scpUpload, waitForSsh, waitForCloudInit, buildSshArgs, generateUbuntuAppCloudInit, wrapCloudInitUserData, buildHostCleanupScript, buildSiteDeployScript, buildStaticSiteDeployScript, resolveExecStart, deployAllComputeSites, deploySiteRelease, reloadRpxGateway, renewRpxCertificates, gatewayHostnames, hasAutoWwwVariant, } from './drivers';
|
|
40
41
|
export type { CreateCloudDriverOptions } from './drivers/factory';
|
|
41
42
|
export { dashboardActions, resolveDashboardAction, sanitizeCloudConfig, startLocalDashboardServer, } from './deploy/local-dashboard-server';
|
|
42
43
|
export type { DashboardAction, LocalDashboardServer, LocalDashboardServerOptions, } from './deploy/local-dashboard-server';
|
|
43
44
|
export { createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, PorkbunProvider, GoDaddyProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, } from './dns';
|
|
45
|
+
export type { CloudflareProviderOptions, CloudflareRule, CloudflareRulesetPhase, CloudflareZoneSetting, } from './dns/cloudflare';
|
|
46
|
+
export { CLOUDFLARE_MANAGED_RULE_PREFIX } from './dns/cloudflare';
|
|
44
47
|
export * from './source';
|
|
45
48
|
export * from './queue';
|
|
46
49
|
export * from './backups';
|
|
47
50
|
export * from './configuration';
|
|
48
51
|
export type { DnsProvider, DnsProviderConfig, DnsRecord, DnsRecordType, DnsRecordResult, CreateRecordResult, DeleteRecordResult, ListRecordsResult, } from './dns';
|
|
52
|
+
export { PROXIABLE_RECORD_TYPES } from './dns';
|
|
49
53
|
export * from '@ts-cloud/core';
|
|
50
54
|
export type { S3Bucket, S3BucketPolicy, CloudFrontDistribution, CloudFrontOriginAccessControl, CloudFrontCacheBehavior, CloudFrontOrigin, } from '@ts-cloud/aws-types';
|
package/dist/index.js
CHANGED
|
@@ -208,7 +208,7 @@ import {
|
|
|
208
208
|
verifyUpdateSignature,
|
|
209
209
|
webhookEndpoint,
|
|
210
210
|
zeroCapacity
|
|
211
|
-
} from "./chunk-
|
|
211
|
+
} from "./chunk-hygwq9y2.js";
|
|
212
212
|
import {
|
|
213
213
|
deleteStaticSite,
|
|
214
214
|
deployStaticSite,
|
|
@@ -219,7 +219,7 @@ import {
|
|
|
219
219
|
generateStaticSiteTemplate,
|
|
220
220
|
invalidateCache,
|
|
221
221
|
uploadStaticFiles
|
|
222
|
-
} from "./chunk-
|
|
222
|
+
} from "./chunk-5nr95wpj.js";
|
|
223
223
|
import {
|
|
224
224
|
buildAndPushServerlessImage
|
|
225
225
|
} from "./chunk-fgk8cqew.js";
|
|
@@ -278,8 +278,10 @@ import {
|
|
|
278
278
|
import {
|
|
279
279
|
ACMClient,
|
|
280
280
|
ACMDnsValidator,
|
|
281
|
+
CLOUDFLARE_MANAGED_RULE_PREFIX,
|
|
281
282
|
DnsProviderFactory,
|
|
282
283
|
GoDaddyProvider,
|
|
284
|
+
PROXIABLE_RECORD_TYPES,
|
|
283
285
|
PorkbunProvider,
|
|
284
286
|
Route53Provider,
|
|
285
287
|
UnifiedDnsValidator,
|
|
@@ -289,7 +291,7 @@ import {
|
|
|
289
291
|
createRoute53Validator,
|
|
290
292
|
detectDnsProvider,
|
|
291
293
|
dnsProviders
|
|
292
|
-
} from "./chunk-
|
|
294
|
+
} from "./chunk-atbgd8wz.js";
|
|
293
295
|
import {
|
|
294
296
|
Route53Client
|
|
295
297
|
} from "./chunk-32e7ya18.js";
|
|
@@ -343,7 +345,7 @@ import {
|
|
|
343
345
|
waitForCloudInit,
|
|
344
346
|
waitForSsh,
|
|
345
347
|
wrapCloudInitUserData
|
|
346
|
-
} from "./chunk-
|
|
348
|
+
} from "./chunk-hg7rhrgd.js";
|
|
347
349
|
import {
|
|
348
350
|
EC2Client,
|
|
349
351
|
SSMClient
|
|
@@ -5284,6 +5286,353 @@ class AcmeClient {
|
|
|
5284
5286
|
return this.accountKey;
|
|
5285
5287
|
}
|
|
5286
5288
|
}
|
|
5289
|
+
// src/cdn/cloudflare-rules.ts
|
|
5290
|
+
var DEFAULT_STATIC_ASSET_EXTENSIONS = [
|
|
5291
|
+
"js",
|
|
5292
|
+
"mjs",
|
|
5293
|
+
"cjs",
|
|
5294
|
+
"css",
|
|
5295
|
+
"map",
|
|
5296
|
+
"woff",
|
|
5297
|
+
"woff2",
|
|
5298
|
+
"ttf",
|
|
5299
|
+
"otf",
|
|
5300
|
+
"eot",
|
|
5301
|
+
"png",
|
|
5302
|
+
"jpg",
|
|
5303
|
+
"jpeg",
|
|
5304
|
+
"gif",
|
|
5305
|
+
"webp",
|
|
5306
|
+
"avif",
|
|
5307
|
+
"svg",
|
|
5308
|
+
"ico",
|
|
5309
|
+
"mp4",
|
|
5310
|
+
"webm",
|
|
5311
|
+
"ogg",
|
|
5312
|
+
"mp3",
|
|
5313
|
+
"wav",
|
|
5314
|
+
"pdf",
|
|
5315
|
+
"wasm"
|
|
5316
|
+
];
|
|
5317
|
+
function quote(value) {
|
|
5318
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
5319
|
+
}
|
|
5320
|
+
function set(values) {
|
|
5321
|
+
return `{${values.map(quote).join(" ")}}`;
|
|
5322
|
+
}
|
|
5323
|
+
function hostCondition(hosts) {
|
|
5324
|
+
if (hosts.length === 1)
|
|
5325
|
+
return `http.host eq ${quote(hosts[0])}`;
|
|
5326
|
+
return `http.host in ${set(hosts)}`;
|
|
5327
|
+
}
|
|
5328
|
+
function buildStaticSiteCacheRules(hosts, settings = {}) {
|
|
5329
|
+
if (settings.enabled === false || hosts.length === 0)
|
|
5330
|
+
return [];
|
|
5331
|
+
const host = hostCondition(hosts);
|
|
5332
|
+
const rules = [];
|
|
5333
|
+
const bypassPaths = settings.bypassPaths ?? [];
|
|
5334
|
+
const bypassClause = bypassPaths.length > 0 ? `(${bypassPaths.map((path) => `starts_with(http.request.uri.path, ${quote(path)})`).join(" or ")})` : undefined;
|
|
5335
|
+
if (bypassClause) {
|
|
5336
|
+
rules.push({
|
|
5337
|
+
action: "set_cache_settings",
|
|
5338
|
+
description: "bypass cache",
|
|
5339
|
+
expression: `(${host} and ${bypassClause})`,
|
|
5340
|
+
enabled: true,
|
|
5341
|
+
action_parameters: { cache: false }
|
|
5342
|
+
});
|
|
5343
|
+
}
|
|
5344
|
+
const cacheable = bypassClause ? `${host} and not ${bypassClause}` : host;
|
|
5345
|
+
const extensions = settings.assetExtensions ?? DEFAULT_STATIC_ASSET_EXTENSIONS;
|
|
5346
|
+
const assetClause = extensions.length > 0 ? `lower(http.request.uri.path.extension) in ${set([...extensions])}` : undefined;
|
|
5347
|
+
if (assetClause) {
|
|
5348
|
+
rules.push({
|
|
5349
|
+
action: "set_cache_settings",
|
|
5350
|
+
description: "cache fingerprinted assets",
|
|
5351
|
+
expression: `(${cacheable} and ${assetClause})`,
|
|
5352
|
+
enabled: true,
|
|
5353
|
+
action_parameters: {
|
|
5354
|
+
cache: true,
|
|
5355
|
+
edge_ttl: { mode: "override_origin", default: settings.assetEdgeTtl ?? 2592000 },
|
|
5356
|
+
browser_ttl: { mode: "override_origin", default: settings.assetBrowserTtl ?? 31536000 }
|
|
5357
|
+
}
|
|
5358
|
+
});
|
|
5359
|
+
}
|
|
5360
|
+
rules.push({
|
|
5361
|
+
action: "set_cache_settings",
|
|
5362
|
+
description: "cache documents",
|
|
5363
|
+
expression: assetClause ? `(${cacheable} and not ${assetClause})` : `(${cacheable})`,
|
|
5364
|
+
enabled: true,
|
|
5365
|
+
action_parameters: {
|
|
5366
|
+
cache: true,
|
|
5367
|
+
edge_ttl: { mode: "override_origin", default: settings.documentEdgeTtl ?? 3600 },
|
|
5368
|
+
browser_ttl: { mode: "override_origin", default: settings.documentBrowserTtl ?? 0 }
|
|
5369
|
+
}
|
|
5370
|
+
});
|
|
5371
|
+
return rules;
|
|
5372
|
+
}
|
|
5373
|
+
function buildOriginGuardRule(hosts, header, value) {
|
|
5374
|
+
if (hosts.length === 0 || !header || !value)
|
|
5375
|
+
return [];
|
|
5376
|
+
return [
|
|
5377
|
+
{
|
|
5378
|
+
action: "rewrite",
|
|
5379
|
+
description: "origin guard header",
|
|
5380
|
+
expression: `(${hostCondition(hosts)})`,
|
|
5381
|
+
enabled: true,
|
|
5382
|
+
action_parameters: {
|
|
5383
|
+
headers: {
|
|
5384
|
+
[header]: { operation: "set", value }
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
];
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
// src/cdn/cloudflare-settings.ts
|
|
5392
|
+
var onOff = (value) => value === undefined ? undefined : value ? "on" : "off";
|
|
5393
|
+
function toCloudflareZoneSettings(settings) {
|
|
5394
|
+
const mapped = {
|
|
5395
|
+
ssl: settings.ssl,
|
|
5396
|
+
always_use_https: onOff(settings.alwaysUseHttps),
|
|
5397
|
+
automatic_https_rewrites: onOff(settings.automaticHttpsRewrites),
|
|
5398
|
+
min_tls_version: settings.minTlsVersion,
|
|
5399
|
+
tls_1_3: onOff(settings.tls13),
|
|
5400
|
+
brotli: onOff(settings.brotli),
|
|
5401
|
+
http3: onOff(settings.http3),
|
|
5402
|
+
"0rtt": onOff(settings.zeroRtt),
|
|
5403
|
+
early_hints: onOff(settings.earlyHints),
|
|
5404
|
+
browser_cache_ttl: settings.browserCacheTtl,
|
|
5405
|
+
always_online: onOff(settings.alwaysOnline),
|
|
5406
|
+
websockets: onOff(settings.websockets)
|
|
5407
|
+
};
|
|
5408
|
+
if (settings.hsts) {
|
|
5409
|
+
mapped.security_header = {
|
|
5410
|
+
strict_transport_security: {
|
|
5411
|
+
enabled: settings.hsts.enabled,
|
|
5412
|
+
max_age: settings.hsts.maxAge ?? 31536000,
|
|
5413
|
+
include_subdomains: settings.hsts.includeSubdomains ?? false,
|
|
5414
|
+
preload: settings.hsts.preload ?? false,
|
|
5415
|
+
nosniff: settings.hsts.noSniff ?? true
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
for (const key of Object.keys(mapped)) {
|
|
5420
|
+
if (mapped[key] === undefined)
|
|
5421
|
+
delete mapped[key];
|
|
5422
|
+
}
|
|
5423
|
+
return { ...mapped, ...settings.raw ?? {} };
|
|
5424
|
+
}
|
|
5425
|
+
var STATIC_SITE_ZONE_SETTINGS = {
|
|
5426
|
+
ssl: "strict",
|
|
5427
|
+
alwaysUseHttps: true,
|
|
5428
|
+
automaticHttpsRewrites: true,
|
|
5429
|
+
minTlsVersion: "1.2",
|
|
5430
|
+
tls13: true,
|
|
5431
|
+
brotli: true,
|
|
5432
|
+
http3: true,
|
|
5433
|
+
earlyHints: true,
|
|
5434
|
+
alwaysOnline: true,
|
|
5435
|
+
browserCacheTtl: 0
|
|
5436
|
+
};
|
|
5437
|
+
|
|
5438
|
+
// src/cdn/origin-tls.ts
|
|
5439
|
+
import { connect } from "node:tls";
|
|
5440
|
+
async function probeOriginTls(options) {
|
|
5441
|
+
const { address, serverName, port = 443, timeoutMs = 5000 } = options;
|
|
5442
|
+
return new Promise((resolve) => {
|
|
5443
|
+
let settled = false;
|
|
5444
|
+
const finish = (result) => {
|
|
5445
|
+
if (settled)
|
|
5446
|
+
return;
|
|
5447
|
+
settled = true;
|
|
5448
|
+
try {
|
|
5449
|
+
socket.destroy();
|
|
5450
|
+
} catch {}
|
|
5451
|
+
resolve(result);
|
|
5452
|
+
};
|
|
5453
|
+
const socket = connect({
|
|
5454
|
+
host: address,
|
|
5455
|
+
port,
|
|
5456
|
+
servername: serverName,
|
|
5457
|
+
rejectUnauthorized: false,
|
|
5458
|
+
timeout: timeoutMs
|
|
5459
|
+
});
|
|
5460
|
+
socket.once("secureConnect", () => {
|
|
5461
|
+
if (socket.authorized) {
|
|
5462
|
+
finish({ reachable: true, trusted: true });
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
finish({
|
|
5466
|
+
reachable: true,
|
|
5467
|
+
trusted: false,
|
|
5468
|
+
reason: socket.authorizationError ? String(socket.authorizationError) : "origin certificate is not trusted for this hostname"
|
|
5469
|
+
});
|
|
5470
|
+
});
|
|
5471
|
+
socket.once("timeout", () => {
|
|
5472
|
+
finish({ reachable: false, trusted: false, reason: `no TLS handshake within ${timeoutMs}ms` });
|
|
5473
|
+
});
|
|
5474
|
+
socket.once("error", (error) => {
|
|
5475
|
+
finish({ reachable: false, trusted: false, reason: error.message });
|
|
5476
|
+
});
|
|
5477
|
+
});
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5480
|
+
// src/cdn/cloudflare.ts
|
|
5481
|
+
async function reconcileCloudflareCdn(options) {
|
|
5482
|
+
const {
|
|
5483
|
+
provider,
|
|
5484
|
+
zone,
|
|
5485
|
+
hosts,
|
|
5486
|
+
ipv4,
|
|
5487
|
+
ipv6,
|
|
5488
|
+
proxied = true,
|
|
5489
|
+
settings = STATIC_SITE_ZONE_SETTINGS,
|
|
5490
|
+
cache,
|
|
5491
|
+
originGuard,
|
|
5492
|
+
purge = true,
|
|
5493
|
+
ttl = 300,
|
|
5494
|
+
skipOriginProbe = false
|
|
5495
|
+
} = options;
|
|
5496
|
+
const report = {
|
|
5497
|
+
records: [],
|
|
5498
|
+
settingsChanged: [],
|
|
5499
|
+
cacheRules: 0,
|
|
5500
|
+
originGuard: false,
|
|
5501
|
+
purged: false,
|
|
5502
|
+
deferredProxy: [],
|
|
5503
|
+
warnings: []
|
|
5504
|
+
};
|
|
5505
|
+
if (hosts.length === 0)
|
|
5506
|
+
return report;
|
|
5507
|
+
const proxiedHosts = [];
|
|
5508
|
+
for (const host of hosts) {
|
|
5509
|
+
let hostProxied = proxied;
|
|
5510
|
+
if (proxied && !skipOriginProbe) {
|
|
5511
|
+
const probe = await probeOriginTls({ address: ipv4, serverName: host });
|
|
5512
|
+
if (!probe.trusted) {
|
|
5513
|
+
hostProxied = false;
|
|
5514
|
+
const reason = probe.reason || "origin is not serving a trusted certificate";
|
|
5515
|
+
report.deferredProxy.push({ host, reason });
|
|
5516
|
+
report.warnings.push(`${host}: publishing DNS-only for now — the origin does not yet serve a certificate this proxy can verify (${reason}). ` + `The origin issues certificates over HTTP-01, which needs the name to reach it directly; re-run the deploy once it has one and the record will be proxied.`);
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
if (hostProxied)
|
|
5520
|
+
proxiedHosts.push(host);
|
|
5521
|
+
const families = [{ type: "A", content: ipv4 }];
|
|
5522
|
+
if (ipv6 && hostAcceptsIpv6(host))
|
|
5523
|
+
families.push({ type: "AAAA", content: ipv6 });
|
|
5524
|
+
for (const { type, content } of families) {
|
|
5525
|
+
const result = await provider.upsertRecord(zone, {
|
|
5526
|
+
name: host,
|
|
5527
|
+
type,
|
|
5528
|
+
content,
|
|
5529
|
+
ttl,
|
|
5530
|
+
proxied: hostProxied
|
|
5531
|
+
});
|
|
5532
|
+
if (result.success)
|
|
5533
|
+
report.records.push({ host, type, content, proxied: hostProxied });
|
|
5534
|
+
else
|
|
5535
|
+
report.warnings.push(`${host} ${type} → ${content} failed: ${result.message || "unknown provider error"}`);
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
if (proxiedHosts.length === 0) {
|
|
5539
|
+
if (proxied) {
|
|
5540
|
+
report.warnings.push("No host is proxied yet, so zone settings, cache rules and purge were skipped — they only take effect through the proxy.");
|
|
5541
|
+
}
|
|
5542
|
+
return report;
|
|
5543
|
+
}
|
|
5544
|
+
try {
|
|
5545
|
+
const desired = toCloudflareZoneSettings(settings);
|
|
5546
|
+
const applied = await provider.applyZoneSettings(zone, desired);
|
|
5547
|
+
report.settingsChanged = applied.changed;
|
|
5548
|
+
for (const failure of applied.failed) {
|
|
5549
|
+
report.warnings.push(`zone setting '${failure.id}' could not be applied: ${failure.error}`);
|
|
5550
|
+
}
|
|
5551
|
+
} catch (error) {
|
|
5552
|
+
report.warnings.push(`zone settings could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
5553
|
+
}
|
|
5554
|
+
const cacheRules = buildStaticSiteCacheRules(proxiedHosts, cache);
|
|
5555
|
+
if (cacheRules.length > 0) {
|
|
5556
|
+
const written = await provider.putManagedPhaseRules(zone, "http_request_cache_settings", cacheRules);
|
|
5557
|
+
if (written.success)
|
|
5558
|
+
report.cacheRules = cacheRules.length;
|
|
5559
|
+
else
|
|
5560
|
+
report.warnings.push(`cache rules could not be written: ${written.message}`);
|
|
5561
|
+
}
|
|
5562
|
+
if (originGuard) {
|
|
5563
|
+
const rules = buildOriginGuardRule(proxiedHosts, originGuard.header, originGuard.value);
|
|
5564
|
+
if (rules.length > 0) {
|
|
5565
|
+
const written = await provider.putManagedPhaseRules(zone, "http_request_late_transform", rules);
|
|
5566
|
+
if (written.success)
|
|
5567
|
+
report.originGuard = true;
|
|
5568
|
+
else {
|
|
5569
|
+
report.warnings.push(`origin guard header could not be written: ${written.message}. ` + `The gateway will reject requests that lack it — disable the guard or grant the token Transform Rules: Edit.`);
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
if (purge) {
|
|
5574
|
+
report.purged = await provider.purgeCache(zone, { hosts: proxiedHosts });
|
|
5575
|
+
if (!report.purged)
|
|
5576
|
+
report.warnings.push("edge cache purge failed — new content may take until TTL expiry to appear.");
|
|
5577
|
+
}
|
|
5578
|
+
return report;
|
|
5579
|
+
}
|
|
5580
|
+
// src/cdn/cloudflare-plan.ts
|
|
5581
|
+
var defaultEnv = (key) => process.env[key];
|
|
5582
|
+
function resolveZoneApex(host, configuredDomain) {
|
|
5583
|
+
const hostname = host.replace(/\.$/, "").toLowerCase();
|
|
5584
|
+
if (configuredDomain) {
|
|
5585
|
+
const domain = configuredDomain.replace(/\.$/, "").toLowerCase();
|
|
5586
|
+
if (hostname === domain || hostname.endsWith(`.${domain}`))
|
|
5587
|
+
return domain;
|
|
5588
|
+
}
|
|
5589
|
+
return hostname.split(".").slice(-2).join(".");
|
|
5590
|
+
}
|
|
5591
|
+
function resolveCloudflareCdnPlan(config2, options = {}) {
|
|
5592
|
+
const env = options.env ?? defaultEnv;
|
|
5593
|
+
const cdn = config2.infrastructure?.compute?.proxy?.cdn;
|
|
5594
|
+
const errors = [];
|
|
5595
|
+
if (!cdn || cdn.provider !== "cloudflare")
|
|
5596
|
+
return { plan: null, errors };
|
|
5597
|
+
if (cdn.originDomain) {
|
|
5598
|
+
errors.push(`compute.proxy.cdn.originDomain (${cdn.originDomain}) is ignored for Cloudflare: the proxy forwards to the address in the record it fronts, ` + `so no separate origin hostname is needed — and publishing one gives clients a documented way around the edge.`);
|
|
5599
|
+
}
|
|
5600
|
+
const apiToken = env("CLOUDFLARE_API_TOKEN");
|
|
5601
|
+
if (!apiToken) {
|
|
5602
|
+
errors.push("CLOUDFLARE_API_TOKEN is not set — the Cloudflare CDN cannot be reconciled.");
|
|
5603
|
+
return { plan: null, errors };
|
|
5604
|
+
}
|
|
5605
|
+
const sites = config2.sites ?? {};
|
|
5606
|
+
const hosts = cdn.frontedHosts?.length ? [...new Set(cdn.frontedHosts.map((host) => host.toLowerCase()))] : gatewayHostnames(sites, { autoWww: config2.infrastructure?.compute?.proxy?.autoWww !== false });
|
|
5607
|
+
if (hosts.length === 0) {
|
|
5608
|
+
errors.push("No hostnames to front — set compute.proxy.cdn.frontedHosts, or give a site a domain.");
|
|
5609
|
+
return { plan: null, errors };
|
|
5610
|
+
}
|
|
5611
|
+
const configuredDomain = config2.infrastructure?.dns?.domain;
|
|
5612
|
+
const zones = new Set(hosts.map((host) => resolveZoneApex(host, configuredDomain)));
|
|
5613
|
+
if (zones.size > 1) {
|
|
5614
|
+
errors.push(`Fronted hosts span more than one zone (${[...zones].join(", ")}). ` + `A Cloudflare zone is configured as a unit, so run one CDN config per zone.`);
|
|
5615
|
+
return { plan: null, errors };
|
|
5616
|
+
}
|
|
5617
|
+
const cf = cdn.cloudflare ?? {};
|
|
5618
|
+
const guard = cdn.secret ? { header: cdn.secretHeader ?? "X-Origin-Verify", value: cdn.secret } : undefined;
|
|
5619
|
+
return {
|
|
5620
|
+
plan: {
|
|
5621
|
+
zone: [...zones][0],
|
|
5622
|
+
hosts,
|
|
5623
|
+
apiToken,
|
|
5624
|
+
zoneId: cf.zoneId ?? env("CLOUDFLARE_ZONE_ID"),
|
|
5625
|
+
accountId: cf.accountId ?? env("CLOUDFLARE_ACCOUNT_ID"),
|
|
5626
|
+
proxied: cf.proxied !== false,
|
|
5627
|
+
settings: cf.settings ?? STATIC_SITE_ZONE_SETTINGS,
|
|
5628
|
+
cache: cf.cache,
|
|
5629
|
+
originGuard: guard,
|
|
5630
|
+
purge: cf.purgeOnDeploy !== false,
|
|
5631
|
+
skipOriginProbe: cf.skipOriginProbe === true
|
|
5632
|
+
},
|
|
5633
|
+
errors
|
|
5634
|
+
};
|
|
5635
|
+
}
|
|
5287
5636
|
export {
|
|
5288
5637
|
zonedParts,
|
|
5289
5638
|
zoneOffsetMs,
|
|
@@ -5331,6 +5680,7 @@ export {
|
|
|
5331
5680
|
totp,
|
|
5332
5681
|
toFocusRecords,
|
|
5333
5682
|
toFocusJsonl,
|
|
5683
|
+
toCloudflareZoneSettings,
|
|
5334
5684
|
tieredMicroCents,
|
|
5335
5685
|
testSourceConnection,
|
|
5336
5686
|
templateCache,
|
|
@@ -5416,6 +5766,7 @@ export {
|
|
|
5416
5766
|
robustZScore,
|
|
5417
5767
|
responseToResult,
|
|
5418
5768
|
resourceManagementManager,
|
|
5769
|
+
resolveZoneApex,
|
|
5419
5770
|
resolveUiSource,
|
|
5420
5771
|
resolveStorageBucketName,
|
|
5421
5772
|
resolveStatePath,
|
|
@@ -5448,6 +5799,7 @@ export {
|
|
|
5448
5799
|
resolveDashboardAuth,
|
|
5449
5800
|
resolveDashboardAction,
|
|
5450
5801
|
resolveCredentials,
|
|
5802
|
+
resolveCloudflareCdnPlan,
|
|
5451
5803
|
resolveCloudProvider,
|
|
5452
5804
|
resolveAuthEncryptionKey,
|
|
5453
5805
|
resolveAppDatabase,
|
|
@@ -5481,6 +5833,7 @@ export {
|
|
|
5481
5833
|
recordDashboardHostPosture,
|
|
5482
5834
|
reconcileSourceWebhook,
|
|
5483
5835
|
reconcileJobObservation,
|
|
5836
|
+
reconcileCloudflareCdn,
|
|
5484
5837
|
reconcileAddressRecords,
|
|
5485
5838
|
rateLimitKey,
|
|
5486
5839
|
rateLimitHeaders,
|
|
@@ -5494,6 +5847,7 @@ export {
|
|
|
5494
5847
|
productionChangeReview,
|
|
5495
5848
|
processSourceWebhook,
|
|
5496
5849
|
processInChunks,
|
|
5850
|
+
probeOriginTls,
|
|
5497
5851
|
priceUsageLines,
|
|
5498
5852
|
priceUsage,
|
|
5499
5853
|
previousBudgetWindow,
|
|
@@ -5590,6 +5944,7 @@ export {
|
|
|
5590
5944
|
hourBucketsBetween,
|
|
5591
5945
|
hourBucket,
|
|
5592
5946
|
hotp,
|
|
5947
|
+
hostCondition,
|
|
5593
5948
|
hostAcceptsIpv6,
|
|
5594
5949
|
healthCheckManager,
|
|
5595
5950
|
hashString,
|
|
@@ -5840,9 +6195,11 @@ export {
|
|
|
5840
6195
|
canaryManager,
|
|
5841
6196
|
bytes,
|
|
5842
6197
|
buildStaticSiteDeployScript,
|
|
6198
|
+
buildStaticSiteCacheRules,
|
|
5843
6199
|
buildSshArgs,
|
|
5844
6200
|
buildSiteDeployScript,
|
|
5845
6201
|
buildPhpRuntimeLayerZip,
|
|
6202
|
+
buildOriginGuardRule,
|
|
5846
6203
|
buildOptimizationManager,
|
|
5847
6204
|
buildNodeRuntimeLayerZip,
|
|
5848
6205
|
buildMediaCdnPlan,
|
|
@@ -5952,6 +6309,7 @@ export {
|
|
|
5952
6309
|
Search,
|
|
5953
6310
|
SchedulerClient,
|
|
5954
6311
|
STSClient,
|
|
6312
|
+
STATIC_SITE_ZONE_SETTINGS,
|
|
5955
6313
|
STATE_DIR_ENV_VAR,
|
|
5956
6314
|
SSMClient,
|
|
5957
6315
|
SQSClient,
|
|
@@ -6022,6 +6380,7 @@ export {
|
|
|
6022
6380
|
Permissions,
|
|
6023
6381
|
PerformanceManager,
|
|
6024
6382
|
ParameterStore,
|
|
6383
|
+
PROXIABLE_RECORD_TYPES,
|
|
6025
6384
|
PHP_LAYER_EXTENSIONS,
|
|
6026
6385
|
PHP_DEFAULT_EXCLUDES,
|
|
6027
6386
|
OrganizationManager,
|
|
@@ -6141,6 +6500,7 @@ export {
|
|
|
6141
6500
|
DEPLOYMENT_QUEUE_KINDS,
|
|
6142
6501
|
DEFAULT_THRESHOLDS,
|
|
6143
6502
|
DEFAULT_TELEMETRY_POLICY,
|
|
6503
|
+
DEFAULT_STATIC_ASSET_EXTENSIONS,
|
|
6144
6504
|
DEFAULT_STATE_DIR,
|
|
6145
6505
|
DEFAULT_SERVICE_LIMITS,
|
|
6146
6506
|
DEFAULT_RECURSION_LIMITS,
|
|
@@ -6184,6 +6544,7 @@ export {
|
|
|
6184
6544
|
Cache,
|
|
6185
6545
|
CONTROL_PLANE_SCHEMA_VERSION,
|
|
6186
6546
|
COMMON_CROSS_ACCOUNT_ROLES,
|
|
6547
|
+
CLOUDFLARE_MANAGED_RULE_PREFIX,
|
|
6187
6548
|
CHAIN_HEADER,
|
|
6188
6549
|
CDN,
|
|
6189
6550
|
BunFilesystemArchiveRuntime,
|