@wtfalch/foundry 0.36.1 → 0.37.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/plan/executor.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { type MailCopyPort } from './mail-copy.js';
|
|
|
15
15
|
import { type OwnServicesPort } from './own-services.js';
|
|
16
16
|
import type { DestroyTarget, StepDestroyer, StepExecutionContext, StepExecutor } from './plan-apply.js';
|
|
17
17
|
import type { EdgePortName } from './platform-org.js';
|
|
18
|
+
import type { RequestHeaderPort } from './platform-providers.js';
|
|
18
19
|
export declare function coolifyProjectMark(org: Pick<DeclaredOrg, 'id'>): string;
|
|
19
20
|
/**
|
|
20
21
|
* The Coolify calls this executor makes, shaped to match
|
|
@@ -1595,6 +1596,13 @@ export interface FoundryStepExecutorPorts {
|
|
|
1595
1596
|
* `edge.tunnels` (see {@link EdgePort.tunnels}). Kept for a host or a test that supplies its own.
|
|
1596
1597
|
*/
|
|
1597
1598
|
tunnels?: TunnelPort;
|
|
1599
|
+
/**
|
|
1600
|
+
* #745: the Cloudflare request header rule call, edge's `platform.requestHeaders`
|
|
1601
|
+
* (`@wtfalch/edge` 0.14.0). A company's sign-in hostname gets its header strip rule
|
|
1602
|
+
* through it before its DNS record; without it the step logs one fixed line and
|
|
1603
|
+
* writes no rule. Mail and SFU hostnames never use it.
|
|
1604
|
+
*/
|
|
1605
|
+
requestHeaders?: RequestHeaderPort;
|
|
1598
1606
|
/**
|
|
1599
1607
|
* F4.access: overrides the Access calls, which otherwise go through
|
|
1600
1608
|
* `edge.access` (see {@link EdgePort.access}). Kept for a host or a test that supplies its own.
|
package/dist/plan/executor.js
CHANGED
|
@@ -5749,7 +5749,12 @@ export class FoundryStepExecutor {
|
|
|
5749
5749
|
apex: hn.zone,
|
|
5750
5750
|
gitRef,
|
|
5751
5751
|
project: { ensure: () => this.ensureCoolifyProject(org, context) },
|
|
5752
|
-
network: orgNetwork({
|
|
5752
|
+
network: orgNetwork({
|
|
5753
|
+
dns: this.dnsPort(),
|
|
5754
|
+
tunnels: this.tunnelPort(),
|
|
5755
|
+
requestHeaders: this.ports.requestHeaders,
|
|
5756
|
+
zones: this.zonePort(),
|
|
5757
|
+
}, scope, `${org.slug}:auth`),
|
|
5753
5758
|
smtpSender: `noreply@${hn.mailDomain}`,
|
|
5754
5759
|
defaultRedirectUri: `https://${agoraHostnameFor(org)}`,
|
|
5755
5760
|
ownerCompany: null,
|
|
@@ -6,7 +6,7 @@ import type { DnsPort } from './edge-dns.js';
|
|
|
6
6
|
import type { ZonePort } from './edge-zones.js';
|
|
7
7
|
import type { CallsSfuPort, CloudflarePort, CoolifyPort, FilesOrganisationPort, FoundryStepExecutorPorts, JobsSchedulerPort, MailPort, ZitadelPort } from './executor.js';
|
|
8
8
|
import type { PlatformStepExecutionContext, StepExecutionContext } from './plan-apply.js';
|
|
9
|
-
import type { FoundryPlatformStepExecutorOptions, PlatformAuthDeps, PlatformDnsFactory, PlatformFilesDeps, PlatformMailDeps, PlatformTunnelPort, PlatformTunnelsFactory } from './platform-providers.js';
|
|
9
|
+
import type { FoundryPlatformStepExecutorOptions, PlatformAuthDeps, PlatformDnsFactory, PlatformFilesDeps, PlatformMailDeps, PlatformRequestHeadersFactory, PlatformTunnelPort, PlatformTunnelsFactory, RequestHeaderPort } from './platform-providers.js';
|
|
10
10
|
/** Applied-state key under which an own org's provisioned-services marker is recorded. */
|
|
11
11
|
export declare const OWN_SERVICES_APPLIED_SCOPE_KEY = "own_services_applied";
|
|
12
12
|
/**
|
|
@@ -82,10 +82,13 @@ export interface ServiceNetwork {
|
|
|
82
82
|
dns(): Promise<DnsPort>;
|
|
83
83
|
tunnels(): Promise<PlatformTunnelPort>;
|
|
84
84
|
zoneId(apex: string): Promise<string | undefined>;
|
|
85
|
+
/** Undefined when the host supplies no way to write Cloudflare request header rules. */
|
|
86
|
+
requestHeaders(): Promise<RequestHeaderPort | undefined>;
|
|
85
87
|
}
|
|
86
88
|
export declare function platformNetwork(deps: {
|
|
87
89
|
dns: PlatformDnsFactory;
|
|
88
90
|
tunnels: PlatformTunnelsFactory;
|
|
91
|
+
requestHeaders?: PlatformRequestHeadersFactory;
|
|
89
92
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
90
93
|
}, scope: StepScope, step: string): ServiceNetwork;
|
|
91
94
|
/**
|
|
@@ -96,6 +99,7 @@ export declare function platformNetwork(deps: {
|
|
|
96
99
|
export declare function orgNetwork(ports: {
|
|
97
100
|
dns?: DnsPort;
|
|
98
101
|
tunnels?: PlatformTunnelPort;
|
|
102
|
+
requestHeaders?: RequestHeaderPort;
|
|
99
103
|
zones?: Pick<ZonePort, 'findZoneByApex'>;
|
|
100
104
|
}, scope: StepScope, step: string): ServiceNetwork;
|
|
101
105
|
/** Where the project comes from: the platform's 'platform' project, or the org's own. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NO_DOMAIN_ZONE } from './own-hostnames.js';
|
|
2
|
-
import { apexZoneKey, exactZone, platformDns, platformTunnels } from './platform-providers.js';
|
|
2
|
+
import { apexZoneKey, exactZone, platformDns, platformRequestHeaders, platformTunnels, } from './platform-providers.js';
|
|
3
3
|
/** The key the org's zone step records its Cloudflare zone id under. */
|
|
4
4
|
const ORG_ZONE_SCOPE_KEY = 'cloudflare_zone';
|
|
5
5
|
/** Applied-state key under which an own org's provisioned-services marker is recorded. */
|
|
@@ -123,6 +123,7 @@ export function platformNetwork(deps, scope, step) {
|
|
|
123
123
|
return {
|
|
124
124
|
dns: () => platformDns(deps, scope.recordedIds(), step),
|
|
125
125
|
tunnels: () => platformTunnels(deps, scope.recordedIds(), step),
|
|
126
|
+
requestHeaders: () => platformRequestHeaders(deps, scope.recordedIds()),
|
|
126
127
|
zoneId: async (apex) => scope.recordedIds()[apexZoneKey(apex)] ?? (await exactZone(deps.cloudflare, apex))?.id,
|
|
127
128
|
};
|
|
128
129
|
}
|
|
@@ -143,6 +144,7 @@ export function orgNetwork(ports, scope, step) {
|
|
|
143
144
|
throw new Error(`${step}: the host supplies no tunnel port for this org`);
|
|
144
145
|
return ports.tunnels;
|
|
145
146
|
},
|
|
147
|
+
requestHeaders: async () => ports.requestHeaders,
|
|
146
148
|
zoneId: async (apex) => {
|
|
147
149
|
// The recorded zone is the company's own; `oikos.host` is never it.
|
|
148
150
|
const recorded = apex === NO_DOMAIN_ZONE ? undefined : scope.recordedIds()[ORG_ZONE_SCOPE_KEY];
|
|
@@ -59,6 +59,33 @@ export type PlatformTunnelPort = Pick<TunnelPort, 'findTunnelByName' | 'createTu
|
|
|
59
59
|
export declare function platformTunnels(deps: {
|
|
60
60
|
tunnels: PlatformTunnelsFactory;
|
|
61
61
|
}, recorded: Readonly<Record<string, string>>, step: string): Promise<PlatformTunnelPort>;
|
|
62
|
+
/**
|
|
63
|
+
* Request headers a client can set that ZITADEL reads to pick or name an
|
|
64
|
+
* instance (auth#241, auth#252). Cloudflare removes them from every inbound
|
|
65
|
+
* request on a sign-in hostname, because the tunnel routes ZITADEL's paths
|
|
66
|
+
* straight to ZITADEL and no app code sees them first.
|
|
67
|
+
*/
|
|
68
|
+
export declare const SIGNIN_STRIPPED_REQUEST_HEADERS: readonly ["X-Zitadel-Instance-Host", "X-Zitadel-Public-Host", "X-Zitadel-Forward-Host", "X-Forwarded-Host"];
|
|
69
|
+
/** One Cloudflare request-header Transform Rule (phase `http_request_late_transform`) that removes headers. */
|
|
70
|
+
export interface RequestHeaderStripRule {
|
|
71
|
+
/** Names the rule in the zone: the key that makes a repeat safe. */
|
|
72
|
+
description: string;
|
|
73
|
+
expression: string;
|
|
74
|
+
headers: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
/** The Cloudflare request header rule calls a sign-in step makes. */
|
|
77
|
+
export interface RequestHeaderPort {
|
|
78
|
+
/** Makes the rule, or returns the one already made for that zone and description. */
|
|
79
|
+
ensureRequestHeaderStrip(zoneId: string, rule: RequestHeaderStripRule): Promise<{
|
|
80
|
+
id: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
/** Like {@link PlatformTunnelsFactory}: builds the {@link RequestHeaderPort} at edge's recorded address. */
|
|
84
|
+
export type PlatformRequestHeadersFactory = (edgeUrl: string) => RequestHeaderPort | Promise<RequestHeaderPort>;
|
|
85
|
+
/** The request header port for a platform step; undefined when edge is not recorded or the host supplied no factory. */
|
|
86
|
+
export declare function platformRequestHeaders(deps: {
|
|
87
|
+
requestHeaders?: PlatformRequestHeadersFactory;
|
|
88
|
+
}, recorded: Readonly<Record<string, string>>): Promise<RequestHeaderPort | undefined>;
|
|
62
89
|
/**
|
|
63
90
|
* F4.access: builds the {@link AccessPort} a platform step finds, creates and
|
|
64
91
|
* deletes its Access app through, given the same address as
|
|
@@ -110,6 +137,11 @@ export interface PlatformAuthDeps {
|
|
|
110
137
|
dns: PlatformDnsFactory;
|
|
111
138
|
/** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
|
|
112
139
|
tunnels: PlatformTunnelsFactory;
|
|
140
|
+
/**
|
|
141
|
+
* Request header rules through edge. Edge has no such call yet (@wtfalch/edge 0.13.0 offers
|
|
142
|
+
* rate limit rules only), so a host that leaves this out gets no rule and a logged line per run.
|
|
143
|
+
*/
|
|
144
|
+
requestHeaders?: PlatformRequestHeadersFactory;
|
|
113
145
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
114
146
|
/** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
|
|
115
147
|
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'ensureDomainPolicy' | 'addOrgDomain' | 'removeOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp;
|
|
@@ -53,6 +53,23 @@ export async function platformTunnels(deps, recorded, step) {
|
|
|
53
53
|
}
|
|
54
54
|
return deps.tunnels(edgeUrl);
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Request headers a client can set that ZITADEL reads to pick or name an
|
|
58
|
+
* instance (auth#241, auth#252). Cloudflare removes them from every inbound
|
|
59
|
+
* request on a sign-in hostname, because the tunnel routes ZITADEL's paths
|
|
60
|
+
* straight to ZITADEL and no app code sees them first.
|
|
61
|
+
*/
|
|
62
|
+
export const SIGNIN_STRIPPED_REQUEST_HEADERS = [
|
|
63
|
+
'X-Zitadel-Instance-Host',
|
|
64
|
+
'X-Zitadel-Public-Host',
|
|
65
|
+
'X-Zitadel-Forward-Host',
|
|
66
|
+
'X-Forwarded-Host',
|
|
67
|
+
];
|
|
68
|
+
/** The request header port for a platform step; undefined when edge is not recorded or the host supplied no factory. */
|
|
69
|
+
export async function platformRequestHeaders(deps, recorded) {
|
|
70
|
+
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
71
|
+
return edgeUrl && deps.requestHeaders ? deps.requestHeaders(edgeUrl) : undefined;
|
|
72
|
+
}
|
|
56
73
|
/** The Access port for a platform step; same address and same reason as {@link platformDns}. */
|
|
57
74
|
async function platformAccess(deps, recorded, step) {
|
|
58
75
|
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
@@ -1701,6 +1718,18 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1701
1718
|
if (!zoneId) {
|
|
1702
1719
|
throw new Error(`${label}: no Cloudflare zone for ${apex} to put ${hostname} in`);
|
|
1703
1720
|
}
|
|
1721
|
+
// Before the DNS record, so the hostname is never published without the rule.
|
|
1722
|
+
const requestHeaders = await network.requestHeaders();
|
|
1723
|
+
if (requestHeaders) {
|
|
1724
|
+
await requestHeaders.ensureRequestHeaderStrip(zoneId, {
|
|
1725
|
+
description: `strip spoofable sign-in headers: ${hostname}`,
|
|
1726
|
+
expression: `(http.host eq "${hostname}")`,
|
|
1727
|
+
headers: SIGNIN_STRIPPED_REQUEST_HEADERS,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
console.error(`${label}: no request header port, so the sign-in hostname keeps X-Zitadel-* and X-Forwarded-Host from clients`);
|
|
1732
|
+
}
|
|
1704
1733
|
const dnsTarget = `${tunnel.id}.cfargotunnel.com`;
|
|
1705
1734
|
const dns = await network.dns();
|
|
1706
1735
|
const [dnsRecord] = await dns.findDnsRecord(apex, hostname, 'CNAME');
|