@wtfalch/foundry 0.22.3 → 0.22.4
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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/plan/diff.js +77 -1
- package/dist/plan/edge-dns.d.ts +5 -0
- package/dist/plan/edge-dns.js +12 -0
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/providers-readers.d.ts +19 -6
- package/dist/plan/providers-readers.js +18 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { FoundryStoreDb } from './store/postgres.js';
|
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
21
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
|
-
export { createEdgeAccess, createEdgeDns, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
23
|
+
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
24
24
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
25
25
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
26
26
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
@@ -32,7 +32,7 @@ export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from
|
|
|
32
32
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
33
33
|
export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './plan/index.js';
|
|
34
34
|
export { healthTargets, platformHealthTargets } from './plan/index.js';
|
|
35
|
-
export type { ScopeResolver, RecordedIds } from './plan/index.js';
|
|
35
|
+
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './plan/index.js';
|
|
36
36
|
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
37
37
|
export type { ZitadelListAppsRawApp } from './plan/index.js';
|
|
38
38
|
export { adaptZitadelListApps } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
|
|
|
12
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
14
|
export { AdoptRefusedError } from './plan/index.js';
|
|
15
|
-
export { createEdgeAccess, createEdgeDns, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
15
|
+
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
16
16
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
17
17
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
18
18
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
package/dist/plan/diff.js
CHANGED
|
@@ -3,6 +3,7 @@ import { APP_ENTRY_KEY, ownerMailboxRecorded, ownerMailboxes } from '../org.js';
|
|
|
3
3
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
4
|
import { BOULE_BROKER_ENTRY_KEY, bouleEnvDrift, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
|
+
import { chatInternalNames } from './chat-matrix.js';
|
|
6
7
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
7
8
|
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
8
9
|
import { planFilesKeys } from './entry-files-key.js';
|
|
@@ -27,6 +28,74 @@ export function toPlan(detail) {
|
|
|
27
28
|
function resourceKey(system, kind, key) {
|
|
28
29
|
return `${system}:${kind}:${key}`;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether an unclaimed live resource is one foundry made for the org, so the
|
|
33
|
+
* plan does not list it as a stranger. Nothing here claims a resource: no step
|
|
34
|
+
* is planned from it and none is removed.
|
|
35
|
+
*
|
|
36
|
+
* - An id foundry recorded, under any key. One step often makes several
|
|
37
|
+
* resources and records the others under a flat key (`coolify_chat_synapse`,
|
|
38
|
+
* `zitadel_project`), which the readers do not map to a name. Only a
|
|
39
|
+
* resource still under its provider's own kind and id is matched: a mapped
|
|
40
|
+
* one carries foundry's key (the org id, a name), which other recorded
|
|
41
|
+
* values equal, and a planner that leaves it unclaimed wants it listed.
|
|
42
|
+
* - The environment of the org's own Coolify project.
|
|
43
|
+
* - The chat login app. Its id is recorded under `coolify:chat:<org>` until
|
|
44
|
+
* the introspection step records the chat service there instead.
|
|
45
|
+
* - A DNS record edge holds a row for (`config.owner`): foundry or the company
|
|
46
|
+
* wrote it through edge.
|
|
47
|
+
* - A DNS record written before edge held rows: a tunnel hostname foundry
|
|
48
|
+
* published that points at a tunnel, or the mail records of the recorded
|
|
49
|
+
* mail domain.
|
|
50
|
+
*/
|
|
51
|
+
function madeByFoundry(org, r, recordedIds, recordedValues) {
|
|
52
|
+
if (PROVIDER_KINDS[r.system]?.includes(r.kind) && recordedValues.has(r.key))
|
|
53
|
+
return true;
|
|
54
|
+
if (r.system === 'coolify') {
|
|
55
|
+
if (r.kind === 'environment')
|
|
56
|
+
return true;
|
|
57
|
+
return r.kind === 'application' && r.config.name === chatInternalNames(org.slug).mas;
|
|
58
|
+
}
|
|
59
|
+
if (r.system !== 'cloudflare' || r.kind !== 'dns_record')
|
|
60
|
+
return false;
|
|
61
|
+
if (typeof r.config.owner === 'string')
|
|
62
|
+
return true;
|
|
63
|
+
const name = typeof r.config.name === 'string' ? r.config.name.toLowerCase() : '';
|
|
64
|
+
if (name === '')
|
|
65
|
+
return false;
|
|
66
|
+
const type = r.config.type;
|
|
67
|
+
const content = typeof r.config.content === 'string' ? r.config.content.toLowerCase() : '';
|
|
68
|
+
const tunnelHostnames = (recordedIds[CLOUDFLARE_TUNNEL_DNS_HOSTNAMES_SCOPE_KEY] ?? '')
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.split(',');
|
|
71
|
+
if (type === 'CNAME' && content.endsWith('.cfargotunnel.com') && tunnelHostnames.includes(name)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const mailDomain = recordedIds[MAIL_DOMAIN_SCOPE_KEY]?.toLowerCase();
|
|
75
|
+
if (!mailDomain)
|
|
76
|
+
return false;
|
|
77
|
+
if (name === mailDomain) {
|
|
78
|
+
return type === 'MX' || (type === 'TXT' && /^"?v=spf1\b/.test(content));
|
|
79
|
+
}
|
|
80
|
+
return name === `_dmarc.${mailDomain}` || name.endsWith(`._domainkey.${mailDomain}`);
|
|
81
|
+
}
|
|
82
|
+
/** The kinds a provider lists a resource under before a recorded `system:kind:key` maps it to foundry's own. */
|
|
83
|
+
const PROVIDER_KINDS = {
|
|
84
|
+
coolify: ['application', 'database'],
|
|
85
|
+
zitadel: ['project', 'app'],
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* What a listed resource is called: the provider's own name for it, then its
|
|
89
|
+
* id, which tells two resources of one name apart. The id alone when it has
|
|
90
|
+
* no name.
|
|
91
|
+
*/
|
|
92
|
+
function liveResourceName(r) {
|
|
93
|
+
const name = typeof r.config.name === 'string' && r.config.name !== '' ? r.config.name : null;
|
|
94
|
+
if (name === null || name === r.key)
|
|
95
|
+
return r.key;
|
|
96
|
+
const type = r.kind === 'dns_record' && typeof r.config.type === 'string' ? `${r.config.type} ` : '';
|
|
97
|
+
return `${type}${name}, ${r.key}`;
|
|
98
|
+
}
|
|
30
99
|
/**
|
|
31
100
|
* The diff engine: #19's "a plan reads live state from Zitadel, Coolify and
|
|
32
101
|
* Cloudflare and diffs it against the declared org", built on #10's rule
|
|
@@ -356,10 +425,17 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
356
425
|
}
|
|
357
426
|
// #10 (resolved): "foundry manages only what the declared org names";
|
|
358
427
|
// anything left unclaimed is listed, never proposed for deletion.
|
|
428
|
+
// A resource foundry made for the org is not a finding: only a resource
|
|
429
|
+
// nobody recorded is listed, under the name its provider shows.
|
|
430
|
+
// The org's own id and names are recorded as values too, and are no provider id.
|
|
431
|
+
const recordedValues = new Set(Object.values(recordedIds));
|
|
432
|
+
for (const own of ['', org.id, org.slug, org.domain ?? ''])
|
|
433
|
+
recordedValues.delete(own);
|
|
359
434
|
const unmanaged = live
|
|
360
435
|
.filter((r) => !claimed.has(resourceKey(r.system, r.kind, r.key)))
|
|
436
|
+
.filter((r) => !madeByFoundry(org, r, recordedIds, recordedValues))
|
|
361
437
|
.map((r) => ({
|
|
362
|
-
description: `${r.system} ${r.kind} "${r
|
|
438
|
+
description: `${r.system} ${r.kind} "${liveResourceName(r)}" is live but not named by the declared org`,
|
|
363
439
|
}));
|
|
364
440
|
// A blocked Agora is reported here, not planned: a step the executor must
|
|
365
441
|
// refuse would halt every step after it. This is the plan's one non-step
|
package/dist/plan/edge-dns.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface DnsPort {
|
|
|
47
47
|
}>;
|
|
48
48
|
deleteDnsRecord(zone: string, recordId: string): Promise<void>;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The owner edge reports for each record of a zone that has an edge row, by
|
|
52
|
+
* record id. For `ProvidersCloudflareReader`'s `resolveDnsOwners`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function edgeDnsRecordOwners(client: Pick<EdgePlatformDns, 'list'>, zone: string): Promise<Record<string, string>>;
|
|
50
55
|
/** Edge answers its platform writes from one shared slot of 4 (company-less writes). */
|
|
51
56
|
export declare const EDGE_DNS_WRITE_CONCURRENCY = 4;
|
|
52
57
|
/**
|
package/dist/plan/edge-dns.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { EdgeApiError } from '@wtfalch/edge';
|
|
2
|
+
/**
|
|
3
|
+
* The owner edge reports for each record of a zone that has an edge row, by
|
|
4
|
+
* record id. For `ProvidersCloudflareReader`'s `resolveDnsOwners`.
|
|
5
|
+
*/
|
|
6
|
+
export async function edgeDnsRecordOwners(client, zone) {
|
|
7
|
+
const owners = {};
|
|
8
|
+
for (const record of (await client.list({ zone })).records) {
|
|
9
|
+
if (record.owner !== null)
|
|
10
|
+
owners[record.id] = record.owner;
|
|
11
|
+
}
|
|
12
|
+
return owners;
|
|
13
|
+
}
|
|
2
14
|
/** Edge answers its platform writes from one shared slot of 4 (company-less writes). */
|
|
3
15
|
export const EDGE_DNS_WRITE_CONCURRENCY = 4;
|
|
4
16
|
/**
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, Call
|
|
|
26
26
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
27
27
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
|
|
28
28
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
29
|
-
export type { ScopeResolver, RecordedIds } from './providers-readers.js';
|
|
29
|
+
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
|
|
30
30
|
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
31
31
|
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
32
32
|
export type { BrokerPort } from './broker-entries.js';
|
|
@@ -41,7 +41,7 @@ export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedu
|
|
|
41
41
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
42
42
|
export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './health-targets.js';
|
|
43
43
|
export { healthTargets, platformHealthTargets } from './health-targets.js';
|
|
44
|
-
export { createEdgeDns, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
44
|
+
export { createEdgeDns, edgeDnsRecordOwners, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
45
45
|
export type { DnsPort, DnsRecordInput, DnsRecordSummary, EdgePlatformDns } from './edge-dns.js';
|
|
46
46
|
export { createEdgeAccess } from './edge-access.js';
|
|
47
47
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, EdgePlatformAccess, } from './edge-access.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export { checkMailIdentityAudience } from './mail-identity.js';
|
|
|
21
21
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
22
22
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
23
23
|
export { healthTargets, platformHealthTargets } from './health-targets.js';
|
|
24
|
-
export { createEdgeDns, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
24
|
+
export { createEdgeDns, edgeDnsRecordOwners, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
25
25
|
export { createEdgeAccess } from './edge-access.js';
|
|
26
26
|
export { createEdgeWorkers } from './edge-workers.js';
|
|
27
27
|
export { createEdgeRateLimits } from './edge-ratelimits.js';
|
|
@@ -99,16 +99,29 @@ export declare class ProvidersCoolifyReader implements CoolifyReader {
|
|
|
99
99
|
/** The project's resources as Coolify reports them, unmapped: what adopt checks against a declaration. */
|
|
100
100
|
listProject(projectUuid: string): Promise<LiveResource[]>;
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
* a zone's DNS records plus its email-routing status (v1 scope — see that
|
|
105
|
-
* method's own doc comment for what it leaves out).
|
|
106
|
-
*/
|
|
102
|
+
/** Record id to the owner edge reports for it (`platform` or `organisation`); a record with no edge row is absent. */
|
|
103
|
+
export type DnsRecordOwners = (orgId: OrgId) => Promise<Readonly<Record<string, string>>>;
|
|
107
104
|
export declare class ProvidersCloudflareReader implements CloudflareReader {
|
|
108
105
|
private readonly client;
|
|
109
106
|
private readonly resolveScope;
|
|
110
107
|
private readonly resolveRecordedIds;
|
|
111
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Who edge holds each DNS record of the org's zone for. A `dns_record`
|
|
110
|
+
* with a row gets `config.owner`, and the plan does not list it as a
|
|
111
|
+
* stranger. Left out, every record reads as having no row. A failing
|
|
112
|
+
* lookup fails the read: a plan that listed those records as strangers
|
|
113
|
+
* would not match the next one.
|
|
114
|
+
*/
|
|
115
|
+
private readonly resolveDnsOwners?;
|
|
116
|
+
constructor(client: CloudflareClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds,
|
|
117
|
+
/**
|
|
118
|
+
* Who edge holds each DNS record of the org's zone for. A `dns_record`
|
|
119
|
+
* with a row gets `config.owner`, and the plan does not list it as a
|
|
120
|
+
* stranger. Left out, every record reads as having no row. A failing
|
|
121
|
+
* lookup fails the read: a plan that listed those records as strangers
|
|
122
|
+
* would not match the next one.
|
|
123
|
+
*/
|
|
124
|
+
resolveDnsOwners?: DnsRecordOwners | undefined);
|
|
112
125
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
113
126
|
}
|
|
114
127
|
/**
|
|
@@ -303,23 +303,39 @@ export class ProvidersCoolifyReader {
|
|
|
303
303
|
* a zone's DNS records plus its email-routing status (v1 scope — see that
|
|
304
304
|
* method's own doc comment for what it leaves out).
|
|
305
305
|
*/
|
|
306
|
+
const NO_DNS_OWNERS = {};
|
|
306
307
|
export class ProvidersCloudflareReader {
|
|
307
308
|
client;
|
|
308
309
|
resolveScope;
|
|
309
310
|
resolveRecordedIds;
|
|
310
|
-
|
|
311
|
+
resolveDnsOwners;
|
|
312
|
+
constructor(client, resolveScope, resolveRecordedIds,
|
|
313
|
+
/**
|
|
314
|
+
* Who edge holds each DNS record of the org's zone for. A `dns_record`
|
|
315
|
+
* with a row gets `config.owner`, and the plan does not list it as a
|
|
316
|
+
* stranger. Left out, every record reads as having no row. A failing
|
|
317
|
+
* lookup fails the read: a plan that listed those records as strangers
|
|
318
|
+
* would not match the next one.
|
|
319
|
+
*/
|
|
320
|
+
resolveDnsOwners) {
|
|
311
321
|
this.client = client;
|
|
312
322
|
this.resolveScope = resolveScope;
|
|
313
323
|
this.resolveRecordedIds = resolveRecordedIds;
|
|
324
|
+
this.resolveDnsOwners = resolveDnsOwners;
|
|
314
325
|
}
|
|
315
326
|
async listResources(orgId) {
|
|
316
327
|
const zoneId = await this.resolveScope(orgId);
|
|
317
328
|
if (!zoneId)
|
|
318
329
|
return [];
|
|
319
|
-
const [
|
|
330
|
+
const [listed, recorded, owners] = await Promise.all([
|
|
320
331
|
this.client.listResources(zoneId),
|
|
321
332
|
this.resolveRecordedIds(orgId),
|
|
333
|
+
this.resolveDnsOwners?.(orgId) ?? NO_DNS_OWNERS,
|
|
322
334
|
]);
|
|
335
|
+
const resources = listed.map((resource) => {
|
|
336
|
+
const owner = resource.kind === 'dns_record' ? owners[resource.key] : undefined;
|
|
337
|
+
return owner ? { ...resource, config: { ...resource.config, owner } } : resource;
|
|
338
|
+
});
|
|
323
339
|
// A zone needs no mapping: its key is already the zone name, which is
|
|
324
340
|
// foundry's own zone key (`diff.ts`'s `planZone`), so it never matches a
|
|
325
341
|
// recorded provider id and always passes through unchanged.
|