@wtfalch/foundry 0.18.0 → 0.19.1
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/bootstrap/declare.d.ts +11 -2
- package/dist/bootstrap/declare.js +17 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/plan/agora.d.ts +36 -0
- package/dist/plan/agora.js +38 -0
- package/dist/plan/boule.d.ts +8 -0
- package/dist/plan/boule.js +8 -0
- package/dist/plan/chat-matrix.d.ts +3 -0
- package/dist/plan/chat-matrix.js +2 -0
- package/dist/plan/diff.js +120 -36
- package/dist/plan/executor.d.ts +51 -5
- package/dist/plan/executor.js +209 -23
- package/dist/plan/login-host.d.ts +15 -11
- package/dist/plan/login-host.js +16 -16
- package/dist/plan/platform-org-ports.d.ts +1 -0
- package/dist/plan/platform-org-ports.js +3 -0
- package/dist/plan/platform-providers.d.ts +3 -2
- package/dist/plan/platform-providers.js +29 -11
- package/dist/plan/providers-readers.d.ts +34 -11
- package/dist/plan/providers-readers.js +72 -15
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
|
|
2
2
|
import type { CoolifyClient } from '@wtfalch/providers/coolify';
|
|
3
|
-
import type
|
|
3
|
+
import { type ZitadelClient } from '@wtfalch/providers/zitadel';
|
|
4
4
|
import type { OrgId } from '../org.js';
|
|
5
5
|
import type { DeclaredOrgStore } from '../store.js';
|
|
6
6
|
import type { CloudflareReader, CmsReader, CoolifyReader, LiveResource, ZitadelReader } from './live.js';
|
|
@@ -53,15 +53,17 @@ export type ScopeResolver = (orgId: OrgId) => Promise<string | null>;
|
|
|
53
53
|
* resource it creates, keyed `system:kind:key` ({@link storeRecordedIds}
|
|
54
54
|
* below reads those back), and #20's `FoundryStepExecutor` (`executor.ts`)
|
|
55
55
|
* now also calls `StepExecutionContext.recordScopeId` for `coolify_project`,
|
|
56
|
-
* `zitadel_org` and `
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
56
|
+
* `zitadel_org`, `zitadel_project` and (#510) `cloudflare_zone`, so a
|
|
57
|
+
* resolver built with one of those `serviceKey`s returns the real id once
|
|
58
|
+
* that step has run. Before #510, `cloudflareZone` recorded its result only
|
|
59
|
+
* under `cloudflare:zone:<domain>` (a resource id, read back by
|
|
60
|
+
* {@link storeRecordedIds}), never under the plain `cloudflare_zone` scope
|
|
61
|
+
* key — so a resolver built with that `serviceKey` always returned `null`,
|
|
62
|
+
* `ProvidersCloudflareReader.listResources` always returned `[]`, and
|
|
63
|
+
* `diff.ts`'s `planZone` re-proposed provisioning the zone on every apply
|
|
64
|
+
* even once it was live. A scope key that genuinely has nothing recorded yet
|
|
65
|
+
* still correctly resolves to `null`: a reader passed `null` returns `[]`,
|
|
66
|
+
* which `computePlanDetail` (`diff.ts`) already reads as "no live resource".
|
|
65
67
|
*/
|
|
66
68
|
export declare function storeScopeResolver(store: DeclaredOrgStore, serviceKey: string): ScopeResolver;
|
|
67
69
|
/**
|
|
@@ -112,7 +114,8 @@ export declare class ProvidersCloudflareReader implements CloudflareReader {
|
|
|
112
114
|
/**
|
|
113
115
|
* Backed by `@wtfalch/providers/zitadel`'s `ZitadelClient.listResources`: one
|
|
114
116
|
* resource per project and per project grant in a ZITADEL org (v1 scope —
|
|
115
|
-
* see that method's own doc comment)
|
|
117
|
+
* see that method's own doc comment), plus (#510) a synthesized `issuer`
|
|
118
|
+
* resource once the org's own ZITADEL org is confirmed live.
|
|
116
119
|
*/
|
|
117
120
|
export declare class ProvidersZitadelReader implements ZitadelReader {
|
|
118
121
|
private readonly client;
|
|
@@ -120,6 +123,26 @@ export declare class ProvidersZitadelReader implements ZitadelReader {
|
|
|
120
123
|
private readonly resolveRecordedIds;
|
|
121
124
|
constructor(client: ZitadelClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds);
|
|
122
125
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
126
|
+
/**
|
|
127
|
+
* #510: `client.listResources` reports only what is *inside* a ZITADEL org
|
|
128
|
+
* (its projects and grants), never the org record itself, so nothing ever
|
|
129
|
+
* matched `diff.ts`'s `planIssuer`'s own `zitadel:issuer:<orgId>` lookup —
|
|
130
|
+
* every plan re-proposed "provision the org's own ZITADEL org" after a
|
|
131
|
+
* clean apply, even once `zitadel_org` was recorded and live. `orgId` here
|
|
132
|
+
* is foundry's own id ({@link resourceKey}'s `key` for an issuer, `diff.ts`'s
|
|
133
|
+
* `planIssuer`), not `zitadelOrgId` — the resource foundry tracks is "this
|
|
134
|
+
* org's issuer", and ZITADEL's own org id is an implementation detail of
|
|
135
|
+
* where it lives, the same distinction {@link applyRecordedIds} draws for
|
|
136
|
+
* every other recorded resource.
|
|
137
|
+
*
|
|
138
|
+
* `resolveScope` only reads what was last recorded; it never confirms the
|
|
139
|
+
* org still exists in ZITADEL. A 404 here means the recorded id no longer
|
|
140
|
+
* resolves (the org was removed out of band) and reads as "not live" — the
|
|
141
|
+
* same `[]` answer a never-recorded scope already gives. Any other failure
|
|
142
|
+
* propagates, same as every other call in this reader. An org ZITADEL still
|
|
143
|
+
* returns but marks `ORG_STATE_REMOVED` is not live either.
|
|
144
|
+
*/
|
|
145
|
+
private liveIssuer;
|
|
123
146
|
}
|
|
124
147
|
/**
|
|
125
148
|
* Backed by the same `@wtfalch/providers/coolify`'s `CoolifyClient.listResources`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ZitadelError } from '@wtfalch/providers/zitadel';
|
|
1
2
|
import { BOULE_MANAGED_ENV_KEYS } from './boule.js';
|
|
2
3
|
/**
|
|
3
4
|
* A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
|
|
@@ -8,15 +9,17 @@ import { BOULE_MANAGED_ENV_KEYS } from './boule.js';
|
|
|
8
9
|
* resource it creates, keyed `system:kind:key` ({@link storeRecordedIds}
|
|
9
10
|
* below reads those back), and #20's `FoundryStepExecutor` (`executor.ts`)
|
|
10
11
|
* now also calls `StepExecutionContext.recordScopeId` for `coolify_project`,
|
|
11
|
-
* `zitadel_org` and `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* `zitadel_org`, `zitadel_project` and (#510) `cloudflare_zone`, so a
|
|
13
|
+
* resolver built with one of those `serviceKey`s returns the real id once
|
|
14
|
+
* that step has run. Before #510, `cloudflareZone` recorded its result only
|
|
15
|
+
* under `cloudflare:zone:<domain>` (a resource id, read back by
|
|
16
|
+
* {@link storeRecordedIds}), never under the plain `cloudflare_zone` scope
|
|
17
|
+
* key — so a resolver built with that `serviceKey` always returned `null`,
|
|
18
|
+
* `ProvidersCloudflareReader.listResources` always returned `[]`, and
|
|
19
|
+
* `diff.ts`'s `planZone` re-proposed provisioning the zone on every apply
|
|
20
|
+
* even once it was live. A scope key that genuinely has nothing recorded yet
|
|
21
|
+
* still correctly resolves to `null`: a reader passed `null` returns `[]`,
|
|
22
|
+
* which `computePlanDetail` (`diff.ts`) already reads as "no live resource".
|
|
20
23
|
*/
|
|
21
24
|
export function storeScopeResolver(store, serviceKey) {
|
|
22
25
|
return async (orgId) => {
|
|
@@ -179,18 +182,34 @@ async function withEnvNamesConfig(client, resource, recorded) {
|
|
|
179
182
|
* the `agora` kind, like {@link withLogVolumeConfig}. Unlike that read, a
|
|
180
183
|
* failure here is not swallowed: a plan that cannot see the live config must
|
|
181
184
|
* fail, not report it as missing.
|
|
185
|
+
*
|
|
186
|
+
* #506: also the live app's own `ports_exposes`, as `config.ports_exposes`,
|
|
187
|
+
* so `planAgora` can plan the fix for an app Coolify never got that setting
|
|
188
|
+
* on even when its env already matches -- the exact shape of the Company
|
|
189
|
+
* Included incident this issue names, where re-planning without this read
|
|
190
|
+
* would see no drift at all and never run `coolifyAgora` again.
|
|
182
191
|
*/
|
|
183
192
|
async function withAgoraEnvConfig(client, resource, recorded) {
|
|
184
193
|
if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('agora:')) {
|
|
185
194
|
return resource;
|
|
186
195
|
}
|
|
187
|
-
const envs = await
|
|
196
|
+
const [envs, app] = await Promise.all([
|
|
197
|
+
client.listEnvs(resource.key),
|
|
198
|
+
client.getApplication(resource.key),
|
|
199
|
+
]);
|
|
188
200
|
const env = {};
|
|
189
201
|
for (const e of envs) {
|
|
190
202
|
if (!e.is_preview && e.key.startsWith('AGORA_'))
|
|
191
203
|
env[e.key] = e.value;
|
|
192
204
|
}
|
|
193
|
-
|
|
205
|
+
// `@wtfalch/providers`' `CoolifyApplication` type does not declare
|
|
206
|
+
// `ports_exposes` (as of 0.7.3), but `getApplication` returns the raw
|
|
207
|
+
// Coolify API response (`coolify.ts`'s own `getApplication`, a plain
|
|
208
|
+
// `request` passthrough), which does carry it -- the same gap that
|
|
209
|
+
// repo's own CLAUDE.md names for other fields ("verified against a live
|
|
210
|
+
// instance, not the bundled openapi.yaml").
|
|
211
|
+
const portsExposes = app.ports_exposes;
|
|
212
|
+
return { ...resource, config: { ...resource.config, env, ports_exposes: portsExposes } };
|
|
194
213
|
}
|
|
195
214
|
/**
|
|
196
215
|
* #161: the org's own recorded Boule app's live sign-in env vars, as
|
|
@@ -273,13 +292,17 @@ export class ProvidersCloudflareReader {
|
|
|
273
292
|
// A zone needs no mapping: its key is already the zone name, which is
|
|
274
293
|
// foundry's own zone key (`diff.ts`'s `planZone`), so it never matches a
|
|
275
294
|
// recorded provider id and always passes through unchanged.
|
|
276
|
-
return applyRecordedIds(toFoundryResources('cloudflare', resources)
|
|
295
|
+
return applyRecordedIds(toFoundryResources('cloudflare', resources)
|
|
296
|
+
// Keyed by the zone id, so a recorded zone id would map it onto the
|
|
297
|
+
// zone's own key and replace the zone's status; nothing plans from it.
|
|
298
|
+
.filter((resource) => resource.kind !== 'email_routing'), recorded, CLOUDFLARE_DROP_UNRECORDED);
|
|
277
299
|
}
|
|
278
300
|
}
|
|
279
301
|
/**
|
|
280
302
|
* Backed by `@wtfalch/providers/zitadel`'s `ZitadelClient.listResources`: one
|
|
281
303
|
* resource per project and per project grant in a ZITADEL org (v1 scope —
|
|
282
|
-
* see that method's own doc comment)
|
|
304
|
+
* see that method's own doc comment), plus (#510) a synthesized `issuer`
|
|
305
|
+
* resource once the org's own ZITADEL org is confirmed live.
|
|
283
306
|
*/
|
|
284
307
|
export class ProvidersZitadelReader {
|
|
285
308
|
client;
|
|
@@ -294,11 +317,45 @@ export class ProvidersZitadelReader {
|
|
|
294
317
|
const zitadelOrgId = await this.resolveScope(orgId);
|
|
295
318
|
if (!zitadelOrgId)
|
|
296
319
|
return [];
|
|
297
|
-
const [resources, recorded] = await Promise.all([
|
|
320
|
+
const [resources, recorded, issuer] = await Promise.all([
|
|
298
321
|
this.client.listResources(zitadelOrgId),
|
|
299
322
|
this.resolveRecordedIds(orgId),
|
|
323
|
+
this.liveIssuer(orgId, zitadelOrgId),
|
|
300
324
|
]);
|
|
301
|
-
|
|
325
|
+
const mapped = applyRecordedIds(toFoundryResources('zitadel', resources), recorded);
|
|
326
|
+
return issuer ? [...mapped, issuer] : mapped;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* #510: `client.listResources` reports only what is *inside* a ZITADEL org
|
|
330
|
+
* (its projects and grants), never the org record itself, so nothing ever
|
|
331
|
+
* matched `diff.ts`'s `planIssuer`'s own `zitadel:issuer:<orgId>` lookup —
|
|
332
|
+
* every plan re-proposed "provision the org's own ZITADEL org" after a
|
|
333
|
+
* clean apply, even once `zitadel_org` was recorded and live. `orgId` here
|
|
334
|
+
* is foundry's own id ({@link resourceKey}'s `key` for an issuer, `diff.ts`'s
|
|
335
|
+
* `planIssuer`), not `zitadelOrgId` — the resource foundry tracks is "this
|
|
336
|
+
* org's issuer", and ZITADEL's own org id is an implementation detail of
|
|
337
|
+
* where it lives, the same distinction {@link applyRecordedIds} draws for
|
|
338
|
+
* every other recorded resource.
|
|
339
|
+
*
|
|
340
|
+
* `resolveScope` only reads what was last recorded; it never confirms the
|
|
341
|
+
* org still exists in ZITADEL. A 404 here means the recorded id no longer
|
|
342
|
+
* resolves (the org was removed out of band) and reads as "not live" — the
|
|
343
|
+
* same `[]` answer a never-recorded scope already gives. Any other failure
|
|
344
|
+
* propagates, same as every other call in this reader. An org ZITADEL still
|
|
345
|
+
* returns but marks `ORG_STATE_REMOVED` is not live either.
|
|
346
|
+
*/
|
|
347
|
+
async liveIssuer(orgId, zitadelOrgId) {
|
|
348
|
+
try {
|
|
349
|
+
const found = await this.client.getOrg(zitadelOrgId);
|
|
350
|
+
if (found.state === 'ORG_STATE_REMOVED')
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
if (err instanceof ZitadelError && err.status === 404)
|
|
355
|
+
return null;
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
return { system: 'zitadel', kind: 'issuer', key: orgId, config: {} };
|
|
302
359
|
}
|
|
303
360
|
}
|
|
304
361
|
/**
|