@stacksjs/ts-cloud 0.8.2 → 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.
Files changed (75) hide show
  1. package/dist/aws/index.js +3 -1
  2. package/dist/bin/cli.js +575 -565
  3. package/dist/bin/dashboard-server.js +334 -324
  4. package/dist/cdn/cloudflare-plan.d.ts +61 -0
  5. package/dist/cdn/cloudflare-rules.d.ts +77 -0
  6. package/dist/cdn/cloudflare-settings.d.ts +100 -0
  7. package/dist/cdn/cloudflare.d.ts +108 -0
  8. package/dist/cdn/cloudflare.test.d.ts +1 -0
  9. package/dist/cdn/index.d.ts +17 -0
  10. package/dist/cdn/origin-tls.d.ts +36 -0
  11. package/dist/{chunk-v0vpamnf.js → chunk-5nr95wpj.js} +3 -3
  12. package/dist/{chunk-gttvakpv.js → chunk-atbgd8wz.js} +163 -37
  13. package/dist/{chunk-x6137d59.js → chunk-hg7rhrgd.js} +27 -4
  14. package/dist/{chunk-wxhrr03r.js → chunk-hygwq9y2.js} +2 -2
  15. package/dist/{chunk-kpeb5rcs.js → chunk-ws47smxr.js} +2 -2
  16. package/dist/deploy/index.js +4 -4
  17. package/dist/dns/cloudflare.d.ts +119 -1
  18. package/dist/dns/index.d.ts +4 -1
  19. package/dist/dns/index.js +3 -1
  20. package/dist/dns/types.d.ts +43 -0
  21. package/dist/drivers/index.js +1 -1
  22. package/dist/drivers/shared/rpx-gateway.d.ts +11 -0
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.js +369 -4
  25. package/dist/ui/account/automation.html +3 -3
  26. package/dist/ui/account/security.html +2 -2
  27. package/dist/ui/applications/compose.html +4 -4
  28. package/dist/ui/applications/new.html +2 -2
  29. package/dist/ui/data/backups.html +4 -4
  30. package/dist/ui/data/services.html +4 -4
  31. package/dist/ui/data/volumes.html +4 -4
  32. package/dist/ui/index.html +4 -4
  33. package/dist/ui/infrastructure/topology.html +4 -4
  34. package/dist/ui/integrations.html +2 -2
  35. package/dist/ui/operations/alerts.html +4 -4
  36. package/dist/ui/operations/configuration.html +4 -4
  37. package/dist/ui/operations/jobs.html +4 -4
  38. package/dist/ui/operations/maintenance.html +4 -4
  39. package/dist/ui/operations/observability.html +4 -4
  40. package/dist/ui/operations/previews.html +4 -4
  41. package/dist/ui/operations/queue.html +4 -4
  42. package/dist/ui/operations/regions.html +4 -4
  43. package/dist/ui/operations/releases.html +4 -4
  44. package/dist/ui/operations/spend.html +4 -4
  45. package/dist/ui/operations/workloads.html +4 -4
  46. package/dist/ui/security.html +2 -2
  47. package/dist/ui/server/actions.html +4 -4
  48. package/dist/ui/server/activity.html +2 -2
  49. package/dist/ui/server/capacity.html +4 -4
  50. package/dist/ui/server/database.html +4 -4
  51. package/dist/ui/server/deployments.html +4 -4
  52. package/dist/ui/server/diagnostics.html +2 -2
  53. package/dist/ui/server/firewall.html +4 -4
  54. package/dist/ui/server/fleet.html +4 -4
  55. package/dist/ui/server/logs.html +3 -3
  56. package/dist/ui/server/metrics.html +4 -4
  57. package/dist/ui/server/security.html +2 -2
  58. package/dist/ui/server/services.html +2 -2
  59. package/dist/ui/server/sites.html +4 -4
  60. package/dist/ui/server/ssh-keys.html +4 -4
  61. package/dist/ui/server/team.html +4 -4
  62. package/dist/ui/server/terminal.html +2 -2
  63. package/dist/ui/serverless/alarms.html +4 -4
  64. package/dist/ui/serverless/assets.html +2 -2
  65. package/dist/ui/serverless/data.html +4 -4
  66. package/dist/ui/serverless/deployments.html +2 -2
  67. package/dist/ui/serverless/firewall.html +2 -2
  68. package/dist/ui/serverless/functions.html +4 -4
  69. package/dist/ui/serverless/logs.html +4 -4
  70. package/dist/ui/serverless/metrics.html +2 -2
  71. package/dist/ui/serverless/queues.html +4 -4
  72. package/dist/ui/serverless/secrets.html +4 -4
  73. package/dist/ui/serverless/traces.html +3 -3
  74. package/dist/ui/serverless.html +4 -4
  75. package/package.json +3 -3
@@ -4,6 +4,9 @@ import {
4
4
  import {
5
5
  AWSClient
6
6
  } from "./chunk-zqtpg06c.js";
7
+
8
+ // src/dns/types.ts
9
+ var PROXIABLE_RECORD_TYPES = new Set(["A", "AAAA", "CNAME"]);
7
10
  // src/dns/porkbun.ts
8
11
  var PORKBUN_API_URL = "https://api.porkbun.com/api/json/v3";
9
12
  var PORKBUN_MAX_ATTEMPTS = 8;
@@ -544,13 +547,19 @@ class GoDaddyProvider {
544
547
  }
545
548
  // src/dns/cloudflare.ts
546
549
  var CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4";
550
+ var CLOUDFLARE_MANAGED_RULE_PREFIX = "[ts-cloud]";
547
551
 
548
552
  class CloudflareProvider {
549
553
  name = "cloudflare";
550
554
  apiToken;
551
555
  zoneCache = new Map;
552
- constructor(apiToken) {
556
+ configuredZoneId;
557
+ accountId;
558
+ configuredZoneName;
559
+ constructor(apiToken, options = {}) {
553
560
  this.apiToken = apiToken;
561
+ this.configuredZoneId = options.zoneId;
562
+ this.accountId = options.accountId;
554
563
  }
555
564
  async request(method, endpoint, body) {
556
565
  const url = `${CLOUDFLARE_API_URL}${endpoint}`;
@@ -580,38 +589,70 @@ class CloudflareProvider {
580
589
  }
581
590
  return domain;
582
591
  }
583
- async getZoneId(domain) {
584
- const rootDomain = this.getRootDomain(domain);
592
+ async resolveZone(domain) {
593
+ const hostname = domain.replace(/\.$/, "").toLowerCase();
594
+ if (this.configuredZoneId) {
595
+ const name = await this.resolveConfiguredZoneName();
596
+ if (name && (hostname === name || hostname.endsWith(`.${name}`))) {
597
+ return { id: this.configuredZoneId, name };
598
+ }
599
+ }
600
+ const rootDomain = this.getRootDomain(hostname);
585
601
  const cached = this.zoneCache.get(rootDomain);
586
602
  if (cached) {
587
- return cached;
603
+ return { id: cached, name: rootDomain };
588
604
  }
589
605
  const response = await this.request("GET", `/zones?name=${encodeURIComponent(rootDomain)}`);
590
606
  if (!response.result || response.result.length === 0) {
591
- throw new Error(`Zone not found for domain: ${rootDomain}`);
607
+ throw new Error(`Zone not found for domain: ${rootDomain}. ` + `If the API token is scoped to a single zone, set its zone id (CLOUDFLARE_ZONE_ID) — ` + `zone lookup by name needs account-wide Zone:Read.`);
592
608
  }
593
609
  const zoneId = response.result[0].id;
594
610
  this.zoneCache.set(rootDomain, zoneId);
595
- return zoneId;
611
+ return { id: zoneId, name: response.result[0].name || rootDomain };
596
612
  }
597
- getFullRecordName(name, domain) {
598
- const rootDomain = this.getRootDomain(domain);
613
+ async resolveConfiguredZoneName() {
614
+ if (!this.configuredZoneId)
615
+ return;
616
+ if (this.configuredZoneName)
617
+ return this.configuredZoneName;
618
+ try {
619
+ const response = await this.request("GET", `/zones/${this.configuredZoneId}`);
620
+ const name = response.result?.name?.toLowerCase();
621
+ if (!name)
622
+ return;
623
+ this.configuredZoneName = name;
624
+ this.zoneCache.set(name, this.configuredZoneId);
625
+ return name;
626
+ } catch {
627
+ return;
628
+ }
629
+ }
630
+ async getZoneId(domain) {
631
+ return (await this.resolveZone(domain)).id;
632
+ }
633
+ getFullRecordName(name, zoneName) {
599
634
  const cleanName = name.replace(/\.$/, "");
600
- if (!cleanName || cleanName === rootDomain || cleanName === "@") {
601
- return rootDomain;
635
+ if (!cleanName || cleanName === zoneName || cleanName === "@") {
636
+ return zoneName;
602
637
  }
603
- if (cleanName.endsWith(`.${rootDomain}`)) {
638
+ if (cleanName.endsWith(`.${zoneName}`)) {
604
639
  return cleanName;
605
640
  }
606
- return `${cleanName}.${rootDomain}`;
641
+ return `${cleanName}.${zoneName}`;
607
642
  }
608
- toCloudflareRecord(record, domain) {
643
+ toCloudflareRecord(record, zoneName, proxied) {
609
644
  const cfRecord = {
610
645
  type: record.type,
611
- name: this.getFullRecordName(record.name, domain),
646
+ name: this.getFullRecordName(record.name, zoneName),
612
647
  content: record.content || record.value || "",
613
648
  ttl: record.ttl || 1
614
649
  };
650
+ const desiredProxy = proxied ?? record.proxied;
651
+ if (desiredProxy !== undefined && PROXIABLE_RECORD_TYPES.has(record.type)) {
652
+ cfRecord.proxied = desiredProxy;
653
+ if (desiredProxy)
654
+ cfRecord.ttl = 1;
655
+ }
615
656
  if (record.type === "MX" && record.priority !== undefined) {
616
657
  cfRecord.priority = record.priority;
617
658
  }
@@ -629,14 +670,15 @@ class CloudflareProvider {
629
670
  type: record.type,
630
671
  content: record.content,
631
672
  ttl: record.ttl,
632
- priority: record.priority
673
+ priority: record.priority,
674
+ proxied: record.proxied
633
675
  };
634
676
  }
635
677
  async createRecord(domain, record) {
636
678
  try {
637
- const zoneId = await this.getZoneId(domain);
638
- const cfRecord = this.toCloudflareRecord(record, domain);
639
- const response = await this.request("POST", `/zones/${zoneId}/dns_records`, cfRecord);
679
+ const zone = await this.resolveZone(domain);
680
+ const cfRecord = this.toCloudflareRecord(record, zone.name);
681
+ const response = await this.request("POST", `/zones/${zone.id}/dns_records`, cfRecord);
640
682
  return {
641
683
  success: true,
642
684
  id: response.result.id,
@@ -651,20 +693,21 @@ class CloudflareProvider {
651
693
  }
652
694
  async upsertRecord(domain, record) {
653
695
  try {
654
- const zoneId = await this.getZoneId(domain);
655
- const fullName = this.getFullRecordName(record.name, domain);
656
- const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
657
- const cfRecord = this.toCloudflareRecord(record, domain);
658
- if (existingResponse.result && existingResponse.result.length > 0) {
659
- const existingId = existingResponse.result[0].id;
660
- const response2 = await this.request("PUT", `/zones/${zoneId}/dns_records/${existingId}`, cfRecord);
696
+ const zone = await this.resolveZone(domain);
697
+ const fullName = this.getFullRecordName(record.name, zone.name);
698
+ const existingResponse = await this.request("GET", `/zones/${zone.id}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
699
+ const existing = existingResponse.result?.[0];
700
+ const proxied = record.proxied ?? existing?.proxied;
701
+ const cfRecord = this.toCloudflareRecord(record, zone.name, proxied);
702
+ if (existing) {
703
+ const response2 = await this.request("PUT", `/zones/${zone.id}/dns_records/${existing.id}`, cfRecord);
661
704
  return {
662
705
  success: true,
663
706
  id: response2.result.id,
664
707
  message: "Record updated successfully"
665
708
  };
666
709
  }
667
- const response = await this.request("POST", `/zones/${zoneId}/dns_records`, cfRecord);
710
+ const response = await this.request("POST", `/zones/${zone.id}/dns_records`, cfRecord);
668
711
  return {
669
712
  success: true,
670
713
  id: response.result.id,
@@ -676,8 +719,9 @@ class CloudflareProvider {
676
719
  }
677
720
  async deleteRecord(domain, record) {
678
721
  try {
679
- const zoneId = await this.getZoneId(domain);
680
- const fullName = this.getFullRecordName(record.name, domain);
722
+ const zone = await this.resolveZone(domain);
723
+ const zoneId = zone.id;
724
+ const fullName = this.getFullRecordName(record.name, zone.name);
681
725
  const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
682
726
  if (!existingResponse.result || existingResponse.result.length === 0) {
683
727
  return {
@@ -804,8 +848,9 @@ class CloudflareProvider {
804
848
  }
805
849
  async getRecordProxyStatus(domain, record) {
806
850
  try {
807
- const zoneId = await this.getZoneId(domain);
808
- const fullName = this.getFullRecordName(record.name, domain);
851
+ const zone = await this.resolveZone(domain);
852
+ const zoneId = zone.id;
853
+ const fullName = this.getFullRecordName(record.name, zone.name);
809
854
  const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
810
855
  if (response.result && response.result.length > 0) {
811
856
  const matchingRecord = response.result.find((r) => r.content === record.content);
@@ -818,8 +863,9 @@ class CloudflareProvider {
818
863
  }
819
864
  async setRecordProxyStatus(domain, record, proxied) {
820
865
  try {
821
- const zoneId = await this.getZoneId(domain);
822
- const fullName = this.getFullRecordName(record.name, domain);
866
+ const zone = await this.resolveZone(domain);
867
+ const zoneId = zone.id;
868
+ const fullName = this.getFullRecordName(record.name, zone.name);
823
869
  const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
824
870
  if (!response.result || response.result.length === 0) {
825
871
  return false;
@@ -834,6 +880,81 @@ class CloudflareProvider {
834
880
  return false;
835
881
  }
836
882
  }
883
+ async zoneIdFor(domain) {
884
+ return this.getZoneId(domain);
885
+ }
886
+ get account() {
887
+ return this.accountId;
888
+ }
889
+ async getZoneSettings(domain) {
890
+ const zoneId = await this.getZoneId(domain);
891
+ const response = await this.request("GET", `/zones/${zoneId}/settings`);
892
+ const settings = {};
893
+ for (const setting of response.result || [])
894
+ settings[setting.id] = setting.value;
895
+ return settings;
896
+ }
897
+ async applyZoneSettings(domain, desired) {
898
+ const zoneId = await this.getZoneId(domain);
899
+ const current = await this.getZoneSettings(domain).catch(() => ({}));
900
+ const changed = [];
901
+ const failed = [];
902
+ for (const [id, value] of Object.entries(desired)) {
903
+ if (value === undefined)
904
+ continue;
905
+ const existing = current[id];
906
+ if (existing !== undefined && deepEqual(existing, value))
907
+ continue;
908
+ try {
909
+ await this.request("PATCH", `/zones/${zoneId}/settings/${id}`, { value });
910
+ changed.push({ id, from: existing, to: value });
911
+ } catch (error) {
912
+ failed.push({ id, error: error instanceof Error ? error.message : String(error) });
913
+ }
914
+ }
915
+ return { changed, failed };
916
+ }
917
+ async getPhaseRules(domain, phase) {
918
+ const zoneId = await this.getZoneId(domain);
919
+ try {
920
+ const response = await this.request("GET", `/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`);
921
+ return response.result?.rules || [];
922
+ } catch {
923
+ return [];
924
+ }
925
+ }
926
+ async putManagedPhaseRules(domain, phase, rules) {
927
+ try {
928
+ const zoneId = await this.getZoneId(domain);
929
+ const existing = await this.getPhaseRules(domain, phase);
930
+ const foreign = existing.filter((rule) => !(rule.description || "").startsWith(CLOUDFLARE_MANAGED_RULE_PREFIX));
931
+ const managed = rules.map((rule) => ({
932
+ ...rule,
933
+ description: rule.description?.startsWith(CLOUDFLARE_MANAGED_RULE_PREFIX) ? rule.description : `${CLOUDFLARE_MANAGED_RULE_PREFIX} ${rule.description || phase}`
934
+ }));
935
+ const payload = [...foreign, ...managed].map(({ id: _id, ...rule }) => rule);
936
+ await this.request("PUT", `/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`, { rules: payload });
937
+ return { success: true };
938
+ } catch (error) {
939
+ return { success: false, message: error instanceof Error ? error.message : String(error) };
940
+ }
941
+ }
942
+ }
943
+ function deepEqual(a, b) {
944
+ if (a === b)
945
+ return true;
946
+ if (typeof a !== typeof b || a === null || b === null)
947
+ return false;
948
+ if (typeof a !== "object")
949
+ return false;
950
+ if (Array.isArray(a) || Array.isArray(b)) {
951
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
952
+ return false;
953
+ return a.every((item, index) => deepEqual(item, b[index]));
954
+ }
955
+ const aRecord = a;
956
+ const bRecord = b;
957
+ return Object.keys(bRecord).every((key) => deepEqual(aRecord[key], bRecord[key]));
837
958
  }
838
959
  // src/dns/route53-adapter.ts
839
960
  class Route53Provider {
@@ -1779,7 +1900,7 @@ function createDnsProvider(config) {
1779
1900
  case "godaddy":
1780
1901
  return new GoDaddyProvider(config.apiKey, config.apiSecret, config.environment);
1781
1902
  case "cloudflare":
1782
- return new CloudflareProvider(config.apiToken);
1903
+ return new CloudflareProvider(config.apiToken, { zoneId: config.zoneId, accountId: config.accountId });
1783
1904
  default:
1784
1905
  throw new Error(`Unknown DNS provider: ${config.provider}`);
1785
1906
  }
@@ -1826,10 +1947,12 @@ class DnsProviderFactory {
1826
1947
  });
1827
1948
  return this;
1828
1949
  }
1829
- addCloudflare(apiToken) {
1950
+ addCloudflare(apiToken, options = {}) {
1830
1951
  this.configs.push({
1831
1952
  provider: "cloudflare",
1832
- apiToken
1953
+ apiToken,
1954
+ zoneId: options.zoneId,
1955
+ accountId: options.accountId
1833
1956
  });
1834
1957
  return this;
1835
1958
  }
@@ -1850,7 +1973,10 @@ class DnsProviderFactory {
1850
1973
  }
1851
1974
  const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
1852
1975
  if (cloudflareApiToken) {
1853
- this.addCloudflare(cloudflareApiToken);
1976
+ this.addCloudflare(cloudflareApiToken, {
1977
+ zoneId: process.env.CLOUDFLARE_ZONE_ID,
1978
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID
1979
+ });
1854
1980
  }
1855
1981
  return this;
1856
1982
  }
@@ -1882,4 +2008,4 @@ class DnsProviderFactory {
1882
2008
  }
1883
2009
  var dnsProviders = new DnsProviderFactory;
1884
2010
 
1885
- export { PorkbunProvider, GoDaddyProvider, CloudflareProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, ACMClient, ACMDnsValidator };
2011
+ export { PROXIABLE_RECORD_TYPES, PorkbunProvider, GoDaddyProvider, CLOUDFLARE_MANAGED_RULE_PREFIX, CloudflareProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, ACMClient, ACMDnsValidator };
@@ -1519,6 +1519,7 @@ function gatewayHostnames(sites, options = {}) {
1519
1519
  }
1520
1520
  return [...hosts];
1521
1521
  }
1522
+ var PUBLIC_DNS_RESOLVERS = ["1.1.1.1", "8.8.8.8"];
1522
1523
  var DEFAULT_ACME_WEBROOT = "/var/www/acme-challenge";
1523
1524
  function normalizeSiteRedirect(input) {
1524
1525
  if (typeof input === "string")
@@ -1665,11 +1666,12 @@ function buildRpxConfigInternal(sites, options, appBoxes) {
1665
1666
  config.acmeChallengeWebroot = options.proxy.acmeWebroot ?? DEFAULT_ACME_WEBROOT;
1666
1667
  }
1667
1668
  const cdn = options.proxy.cdn;
1668
- if (cdn?.secret && cdn.frontedHosts.length > 0) {
1669
+ const guardedHosts = cdn?.frontedHosts?.length ? cdn.frontedHosts : [...domains];
1670
+ if (cdn?.secret && guardedHosts.length > 0) {
1669
1671
  config.originGuard = {
1670
1672
  header: cdn.secretHeader ?? "X-Origin-Verify",
1671
1673
  value: cdn.secret,
1672
- hosts: cdn.frontedHosts
1674
+ hosts: guardedHosts
1673
1675
  };
1674
1676
  }
1675
1677
  return config;
@@ -1751,8 +1753,18 @@ for (const f of files) {
1751
1753
  if (frag.productionCerts?.certsDir) certsDir = frag.productionCerts.certsDir
1752
1754
  acmeChallengeWebroot ??= frag.acmeChallengeWebroot
1753
1755
  if (frag.originGuard) {
1756
+ // One gateway means one origin-guard secret (rpx takes a single
1757
+ // header/value). A tenant whose secret disagrees with the adopted one must
1758
+ // NOT have its hosts guarded: its CDN would send a different value, the
1759
+ // gateway would reject every request, and a working host would go dark.
1760
+ // Unguarded is weaker but serving, and the mismatch is logged.
1754
1761
  guard ??= { header: frag.originGuard.header, value: frag.originGuard.value }
1755
- for (const h of frag.originGuard.hosts ?? []) guardHosts.add(h)
1762
+ if (frag.originGuard.header === guard.header && frag.originGuard.value === guard.value) {
1763
+ for (const h of frag.originGuard.hosts ?? []) guardHosts.add(h)
1764
+ }
1765
+ else {
1766
+ console.warn('[rpx-assembler] origin-guard secret in ' + f + ' differs from the one already in force; its hosts stay unguarded rather than rejecting all traffic')
1767
+ }
1756
1768
  }
1757
1769
  }
1758
1770
  const config = {
@@ -1861,13 +1873,24 @@ function buildCertManagementCommands(options) {
1861
1873
  `WEBROOT='${webroot}'`,
1862
1874
  `EMAIL='${email}'`,
1863
1875
  `TLSX="${tlsxCli}"`,
1876
+ `BUN='${bunBin}'`,
1864
1877
  `DOMAINS='${csv}'`,
1865
1878
  "DNS_ATTEMPTS='24'",
1866
1879
  "DNS_DELAY_SECONDS='5'",
1880
+ `DNS_RESOLVERS='${PUBLIC_DNS_RESOLVERS.join(" ")}'`,
1881
+ "dns_resolves() {",
1882
+ ' d="$1"',
1883
+ " for ns in $DNS_RESOLVERS; do",
1884
+ ` if $BUN --eval "const{Resolver}=require('node:dns');const r=new Resolver();r.setServers([process.argv[1]]);r.resolve4(process.argv[2],e=>process.exit(e?1:0))" "$ns" "$d" >/dev/null 2>&1; then`,
1885
+ " return 0",
1886
+ " fi",
1887
+ " done",
1888
+ ' getent ahosts "$d" >/dev/null 2>&1',
1889
+ "}",
1867
1890
  "wait_for_dns() {",
1868
1891
  ' d="$1"',
1869
1892
  " attempt=1",
1870
- ' while ! getent ahosts "$d" >/dev/null 2>&1; do',
1893
+ ' while ! dns_resolves "$d"; do',
1871
1894
  ' if [ "$attempt" -ge "$DNS_ATTEMPTS" ]; then',
1872
1895
  ' echo "DNS for $d did not become resolvable after $DNS_ATTEMPTS attempts" >&2',
1873
1896
  " return 1",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  deployStaticSiteWithExternalDnsFull
3
- } from "./chunk-v0vpamnf.js";
3
+ } from "./chunk-5nr95wpj.js";
4
4
  import {
5
5
  box,
6
6
  error,
@@ -50,7 +50,7 @@ import {
50
50
  resolveSiteKind,
51
51
  resolveUiSource,
52
52
  siteInstallBase
53
- } from "./chunk-x6137d59.js";
53
+ } from "./chunk-hg7rhrgd.js";
54
54
  import {
55
55
  SSMClient
56
56
  } from "./chunk-7m60qnc8.js";
@@ -5,9 +5,9 @@ import {
5
5
  generateStaticSiteTemplate,
6
6
  invalidateCache,
7
7
  uploadStaticFiles
8
- } from "./chunk-v0vpamnf.js";
8
+ } from "./chunk-5nr95wpj.js";
9
9
  import"./chunk-01d86gt1.js";
10
- import"./chunk-gttvakpv.js";
10
+ import"./chunk-atbgd8wz.js";
11
11
  import"./chunk-32e7ya18.js";
12
12
  import"./chunk-4cjrg98a.js";
13
13
  import"./chunk-rds0sy87.js";
@@ -36,7 +36,7 @@ import {
36
36
  synchronizeDashboardUsers,
37
37
  trackDashboardOperation,
38
38
  verifyStaticApiOrigin
39
- } from "../chunk-wxhrr03r.js";
39
+ } from "../chunk-hygwq9y2.js";
40
40
  import {
41
41
  deleteStaticSite,
42
42
  deployStaticSite,
@@ -47,7 +47,7 @@ import {
47
47
  generateStaticSiteTemplate,
48
48
  invalidateCache,
49
49
  uploadStaticFiles
50
- } from "../chunk-v0vpamnf.js";
50
+ } from "../chunk-5nr95wpj.js";
51
51
  import {
52
52
  buildAndPushServerlessImage
53
53
  } from "../chunk-fgk8cqew.js";
@@ -56,7 +56,7 @@ import"../chunk-nsgqsmrh.js";
56
56
  import"../chunk-01d86gt1.js";
57
57
  import"../chunk-50jpda9q.js";
58
58
  import"../chunk-he9a874b.js";
59
- import"../chunk-gttvakpv.js";
59
+ import"../chunk-atbgd8wz.js";
60
60
  import"../chunk-32e7ya18.js";
61
61
  import {
62
62
  IPV6_EXCLUDED_HOST_LABELS,
@@ -83,7 +83,7 @@ import {
83
83
  siteInstallBase,
84
84
  validateDeploymentConfig,
85
85
  verifyAddressRecord
86
- } from "../chunk-x6137d59.js";
86
+ } from "../chunk-hg7rhrgd.js";
87
87
  import"../chunk-7m60qnc8.js";
88
88
  import"../chunk-4cjrg98a.js";
89
89
  import"../chunk-rds0sy87.js";
@@ -3,11 +3,53 @@
3
3
  * API documentation: https://developers.cloudflare.com/api/resources/dns/subresources/records/
4
4
  */
5
5
  import type { CreateRecordResult, DeleteRecordResult, DnsProvider, DnsRecord, DnsRecordType, ListRecordsResult } from './types';
6
+ /** One entry of `GET /zones/{id}/settings` (and the body of a per-setting PATCH). */
7
+ export interface CloudflareZoneSetting {
8
+ id: string;
9
+ value: unknown;
10
+ editable?: boolean;
11
+ modified_on?: string | null;
12
+ }
13
+ /**
14
+ * A rule inside a phase entrypoint ruleset (cache rules, transform rules, …).
15
+ * Only the fields ts-cloud writes or has to preserve are modelled.
16
+ */
17
+ export interface CloudflareRule {
18
+ id?: string;
19
+ action: string;
20
+ expression: string;
21
+ description?: string;
22
+ enabled?: boolean;
23
+ action_parameters?: Record<string, unknown>;
24
+ }
25
+ /**
26
+ * Marker that identifies a rule as ts-cloud's, carried in the rule description.
27
+ *
28
+ * Cloudflare's phase entrypoints are replace-only — `PUT .../entrypoint` sets
29
+ * the WHOLE rule list — so writing our rules naively would delete every rule a
30
+ * human added in the dashboard. Tagging ours lets a reconcile rewrite only what
31
+ * it owns and carry everything else through untouched.
32
+ */
33
+ export declare const CLOUDFLARE_MANAGED_RULE_PREFIX = "[ts-cloud]";
34
+ /** Ruleset phases ts-cloud manages rules in. */
35
+ export type CloudflareRulesetPhase = 'http_request_cache_settings' | 'http_request_late_transform';
36
+ export interface CloudflareProviderOptions {
37
+ /** Zone id, so the API token can be scoped to a single zone. */
38
+ zoneId?: string;
39
+ /** Account id, for account-scoped resources. */
40
+ accountId?: string;
41
+ }
6
42
  export declare class CloudflareProvider implements DnsProvider {
7
43
  readonly name = "cloudflare";
8
44
  private apiToken;
45
+ /** Zone name → zone id. */
9
46
  private zoneCache;
10
- constructor(apiToken: string);
47
+ /** Zone id configured up front (single-zone token); resolved to a name on first use. */
48
+ private readonly configuredZoneId?;
49
+ private readonly accountId?;
50
+ /** Name of {@link configuredZoneId}, learned once from the API. */
51
+ private configuredZoneName?;
52
+ constructor(apiToken: string, options?: CloudflareProviderOptions);
11
53
  /**
12
54
  * Make an authenticated API request to Cloudflare
13
55
  */
@@ -17,6 +59,22 @@ export declare class CloudflareProvider implements DnsProvider {
17
59
  * e.g., "api.example.com" -> "example.com"
18
60
  */
19
61
  private getRootDomain;
62
+ /**
63
+ * Resolve the zone a hostname belongs to, as both id and name.
64
+ *
65
+ * A configured {@link CloudflareProviderOptions.zoneId} is tried first and is
66
+ * the only path that works with a single-zone API token: the by-name fallback
67
+ * calls `GET /zones?name=…`, an ACCOUNT-level listing that a zone-scoped token
68
+ * cannot read — it returns an empty list, which is indistinguishable from "the
69
+ * domain isn't here" unless you already know the token is narrow.
70
+ *
71
+ * Resolving the configured zone also yields its real name, which is what makes
72
+ * record naming correct for multi-label suffixes (`example.co.uk`). The
73
+ * fallback can only guess the apex from the last two labels.
74
+ */
75
+ private resolveZone;
76
+ /** Name of the configured zone, fetched once. Undefined if it can't be read. */
77
+ private resolveConfiguredZoneName;
20
78
  /**
21
79
  * Get Zone ID for a domain (with caching)
22
80
  */
@@ -24,6 +82,9 @@ export declare class CloudflareProvider implements DnsProvider {
24
82
  /**
25
83
  * Get the full record name
26
84
  * Cloudflare stores records with full domain names
85
+ *
86
+ * `zoneName` is the zone's actual name as Cloudflare reports it, not a guess
87
+ * from the requested domain — see {@link resolveZone}.
27
88
  */
28
89
  private getFullRecordName;
29
90
  /**
@@ -71,4 +132,61 @@ export declare class CloudflareProvider implements DnsProvider {
71
132
  * Update proxy status for a record (Cloudflare-specific)
72
133
  */
73
134
  setRecordProxyStatus(domain: string, record: DnsRecord, proxied: boolean): Promise<boolean>;
135
+ /**
136
+ * Public zone id for a hostname — the entry point for the Cloudflare-specific
137
+ * work (settings, rulesets, purge) that sits outside the DnsProvider contract.
138
+ */
139
+ zoneIdFor(domain: string): Promise<string>;
140
+ /** Account id this provider was configured with, if any. */
141
+ get account(): string | undefined;
142
+ /**
143
+ * Read every zone setting, keyed by setting id.
144
+ *
145
+ * Used to make {@link applyZoneSettings} a real reconcile: settings that
146
+ * already hold the desired value are left alone, so a deploy reports what it
147
+ * actually changed instead of rewriting the whole zone every time.
148
+ */
149
+ getZoneSettings(domain: string): Promise<Record<string, unknown>>;
150
+ /**
151
+ * Apply zone settings, skipping any that already match.
152
+ *
153
+ * Returns one entry per setting that was actually written, plus the failures.
154
+ * Failures are collected rather than thrown: zone settings are largely
155
+ * independent, several are plan-gated (a Free zone rejects `min_tls_version`
156
+ * above 1.0 on some plans, `http3` needs the feature enabled), and one
157
+ * unavailable toggle must not abort a deploy that has already shipped code.
158
+ */
159
+ applyZoneSettings(domain: string, desired: Record<string, unknown>): Promise<{
160
+ changed: Array<{
161
+ id: string;
162
+ from: unknown;
163
+ to: unknown;
164
+ }>;
165
+ failed: Array<{
166
+ id: string;
167
+ error: string;
168
+ }>;
169
+ }>;
170
+ /**
171
+ * Read the rules currently in a phase entrypoint ruleset.
172
+ *
173
+ * A zone that has never had a rule in this phase has no entrypoint at all and
174
+ * the API answers 404, which is a normal empty state rather than a failure.
175
+ */
176
+ getPhaseRules(domain: string, phase: CloudflareRulesetPhase): Promise<CloudflareRule[]>;
177
+ /**
178
+ * Replace ts-cloud's rules in a phase while preserving everyone else's.
179
+ *
180
+ * Cloudflare only exposes a whole-list PUT for a phase entrypoint, so a naive
181
+ * write deletes every rule someone added in the dashboard. Rules ts-cloud owns
182
+ * are tagged in their description with {@link CLOUDFLARE_MANAGED_RULE_PREFIX};
183
+ * this drops exactly those, appends the new ones, and carries the rest through
184
+ * in their original order.
185
+ *
186
+ * Passing an empty `rules` removes ts-cloud's rules from the phase.
187
+ */
188
+ putManagedPhaseRules(domain: string, phase: CloudflareRulesetPhase, rules: CloudflareRule[]): Promise<{
189
+ success: boolean;
190
+ message?: string;
191
+ }>;
74
192
  }
@@ -43,7 +43,10 @@ export declare class DnsProviderFactory {
43
43
  /**
44
44
  * Add Cloudflare provider
45
45
  */
46
- addCloudflare(apiToken: string): this;
46
+ addCloudflare(apiToken: string, options?: {
47
+ zoneId?: string;
48
+ accountId?: string;
49
+ }): this;
47
50
  /**
48
51
  * Load providers from environment variables
49
52
  */
package/dist/dns/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  CloudflareProvider,
3
3
  DnsProviderFactory,
4
4
  GoDaddyProvider,
5
+ PROXIABLE_RECORD_TYPES,
5
6
  PorkbunProvider,
6
7
  Route53Provider,
7
8
  UnifiedDnsValidator,
@@ -11,7 +12,7 @@ import {
11
12
  createRoute53Validator,
12
13
  detectDnsProvider,
13
14
  dnsProviders
14
- } from "../chunk-gttvakpv.js";
15
+ } from "../chunk-atbgd8wz.js";
15
16
  import"../chunk-32e7ya18.js";
16
17
  import"../chunk-zqtpg06c.js";
17
18
  import"../chunk-v0bahtg2.js";
@@ -25,6 +26,7 @@ export {
25
26
  UnifiedDnsValidator,
26
27
  Route53Provider,
27
28
  PorkbunProvider,
29
+ PROXIABLE_RECORD_TYPES,
28
30
  GoDaddyProvider,
29
31
  DnsProviderFactory,
30
32
  CloudflareProvider