@wtfalch/foundry 0.17.1 → 0.19.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/bootstrap/declare.d.ts +11 -2
- package/dist/bootstrap/declare.js +20 -11
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/migrations/0045_names_to_edge.sql +107 -0
- package/dist/org.d.ts +17 -17
- package/dist/org.js +48 -58
- package/dist/plan/agora.d.ts +25 -0
- package/dist/plan/agora.js +25 -0
- package/dist/plan/boule.d.ts +8 -0
- package/dist/plan/boule.js +8 -0
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +115 -52
- package/dist/plan/entry-edge-key.d.ts +79 -0
- package/dist/plan/{entry-names-key.js → entry-edge-key.js} +90 -66
- package/dist/plan/executor.d.ts +87 -42
- package/dist/plan/executor.js +277 -120
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/login-host.d.ts +15 -11
- package/dist/plan/login-host.js +16 -16
- package/dist/plan/plan-apply.d.ts +6 -5
- package/dist/plan/plan-apply.js +40 -38
- package/dist/plan/platform-diff.d.ts +22 -22
- package/dist/plan/platform-diff.js +45 -45
- package/dist/plan/platform-org-ports.d.ts +1 -0
- package/dist/plan/platform-org-ports.js +3 -0
- package/dist/plan/platform-org.d.ts +8 -8
- package/dist/plan/platform-org.js +7 -7
- package/dist/plan/platform-providers.d.ts +17 -16
- package/dist/plan/platform-providers.js +136 -118
- package/dist/plan/providers-readers.d.ts +34 -11
- package/dist/plan/providers-readers.js +72 -15
- package/dist/plan/step-system.js +1 -1
- package/dist/plan/{edge-worker.d.ts → worker.d.ts} +53 -33
- package/dist/plan/{edge-worker.js → worker.js} +92 -54
- package/dist/platform.d.ts +3 -3
- package/dist/platform.js +1 -1
- package/dist/store/postgres.js +7 -7
- package/dist/store/schema.d.ts +6 -6
- package/dist/store/schema.js +4 -4
- package/package.json +1 -1
- package/dist/plan/entry-names-key.d.ts +0 -73
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { tunnelIngressHostnames } from './hostnames.js';
|
|
2
|
+
import { lockedHostnames } from './locked-hostnames.js';
|
|
1
3
|
import { appStepKey } from './platform-diff.js';
|
|
2
4
|
/**
|
|
3
|
-
* F7 (#317): one
|
|
4
|
-
* below the org with the
|
|
5
|
+
* F7 (#317): one edge key per entry with `uses.edge`, minted directly
|
|
6
|
+
* below the org with the edge service (`EdgePort.mintKey({
|
|
5
7
|
* organisationId, ... })` -- no org-level root key the way files/ai have
|
|
6
|
-
* one, since the
|
|
7
|
-
* `
|
|
8
|
+
* one, since the edge service takes the org id itself). `EDGE_URL` and
|
|
9
|
+
* `EDGE_API_KEY` go to the entry's env and nowhere else. A edge key has no
|
|
8
10
|
* declared renewal window in the issue, so unlike `planEntryAiKeys`/
|
|
9
11
|
* `planFilesKeys` this mints once and never renews.
|
|
10
12
|
*
|
|
11
|
-
* `
|
|
13
|
+
* `EDGE_*` is written in this step, after the app's own `coolify:app`
|
|
12
14
|
* create (which never knows the key yet), so the app's first boot has
|
|
13
15
|
* neither and its first deploy restarts once this step's env write lands --
|
|
14
16
|
* the same one-extra-restart shape `planEntryAiKeys`'s `AI_*`/
|
|
@@ -17,55 +19,77 @@ import { appStepKey } from './platform-diff.js';
|
|
|
17
19
|
* A domain change after mint is not detected (#317 re-review): the key is
|
|
18
20
|
* bound to the domain passed at mint time, but nothing here records that
|
|
19
21
|
* domain to compare against a later `org.domain`, so an org that changes
|
|
20
|
-
* domains keeps a
|
|
21
|
-
* resolving it needs a manual revoke-and-re-mint (drop `uses.
|
|
22
|
+
* domains keeps a edge key still bound to the old one. Not built --
|
|
23
|
+
* resolving it needs a manual revoke-and-re-mint (drop `uses.edge`, apply,
|
|
22
24
|
* re-add it) until this is.
|
|
23
25
|
*/
|
|
24
|
-
export const
|
|
25
|
-
/** The id of a key that was live when its entry stopped declaring `uses.
|
|
26
|
-
export const
|
|
27
|
-
export const
|
|
28
|
-
/** How long an entry's
|
|
29
|
-
export const
|
|
26
|
+
export const entryEdgeUndeliveredScopeKey = (entryKey) => `edge_key_undelivered_${entryKey}`;
|
|
27
|
+
/** The id of a key that was live when its entry stopped declaring `uses.edge`, once handled. */
|
|
28
|
+
export const entryEdgeRetiredScopeKey = (entryKey) => `edge_key_retired_${entryKey}`;
|
|
29
|
+
export const ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS = 3;
|
|
30
|
+
/** How long an entry's edge key lives. The issue names no renewal window, so this mints once and never renews. */
|
|
31
|
+
export const EDGE_ENTRY_KEY_DAYS = 365;
|
|
30
32
|
/** The ISO time a delivered key's mint succeeded -- the only trace of its age, since there is no renewal to track against. */
|
|
31
|
-
export const
|
|
33
|
+
export const entryEdgeMintedAtScopeKey = (entryKey) => `edge_key_minted_at_${entryKey}`;
|
|
32
34
|
/** A key with fewer days left than this is surfaced as a blocked warning row (#317 review): still live, but nothing here renews it. */
|
|
33
|
-
export const
|
|
35
|
+
export const EDGE_ENTRY_KEY_WARN_WINDOW_DAYS = 30;
|
|
34
36
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
35
|
-
/** The reserved labels last sent to the
|
|
36
|
-
export const
|
|
37
|
-
export const
|
|
37
|
+
/** The reserved labels last sent to the edge service, as that sorted list's own JSON. */
|
|
38
|
+
export const EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY = 'edge_reserved_labels_applied';
|
|
39
|
+
export const entryEdgeKeyName = (org, entry) => `${org.slug}-${entry.key}-edge`;
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* comment in `executor.ts` explains is reserved unconditionally.
|
|
41
|
+
* Mail names foundry's mail setup puts on the org's domain. Stalwart's and
|
|
42
|
+
* Lettermint's own record names (DKIM selectors, return path) come back from
|
|
43
|
+
* the mail port at apply time, so only the fixed ones are listed here.
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
+
const MAIL_LABELS = ['mail', 'autoconfig', 'autodiscover', '_dmarc'];
|
|
46
|
+
/**
|
|
47
|
+
* Every label under `org.domain` that foundry provisions or the platform
|
|
48
|
+
* uses, so edge never lets a company claim one: the hostnames foundry routes
|
|
49
|
+
* (`tunnelIngressHostnames`: entries, site, cms, calls, Agora, Boule, share),
|
|
50
|
+
* the names it locks (`lockedHostnames`: also the sign-in host and the
|
|
51
|
+
* platform's service hostnames), each entry's own labels, the mail labels
|
|
52
|
+
* and `www`. The entry's `*` is the space users claim in, so it reserves
|
|
53
|
+
* nothing, and neither does the bare domain.
|
|
54
|
+
*
|
|
55
|
+
* An org without a domain has no hostnames edge sees (`uses.edge` needs a
|
|
56
|
+
* domain), so it reserves `www` only.
|
|
57
|
+
*/
|
|
58
|
+
export function edgeReservedLabels(org, platformHostnames = []) {
|
|
45
59
|
const labels = new Set(['www']);
|
|
60
|
+
const { domain } = org;
|
|
61
|
+
if (!domain)
|
|
62
|
+
return [...labels];
|
|
63
|
+
for (const label of MAIL_LABELS)
|
|
64
|
+
labels.add(label);
|
|
46
65
|
for (const entry of org.hosting.entries) {
|
|
47
66
|
for (const name of entry.hostnames) {
|
|
48
67
|
if (name !== '@' && name !== '*')
|
|
49
68
|
labels.add(name);
|
|
50
69
|
}
|
|
51
70
|
}
|
|
71
|
+
const suffix = `.${domain}`;
|
|
72
|
+
for (const host of [...tunnelIngressHostnames(org), ...lockedHostnames(org, platformHostnames)]) {
|
|
73
|
+
if (host.endsWith(suffix) && !host.startsWith('*.'))
|
|
74
|
+
labels.add(host.slice(0, -suffix.length));
|
|
75
|
+
}
|
|
52
76
|
return [...labels].sort();
|
|
53
77
|
}
|
|
54
78
|
/**
|
|
55
|
-
* One step per entry whose
|
|
56
|
-
* entry whose `uses.
|
|
57
|
-
* Blocked, not planned (rule 2), while the platform has no
|
|
79
|
+
* One step per entry whose edge key is missing, plus one retire step per
|
|
80
|
+
* entry whose `uses.edge` was removed but still has a recorded key.
|
|
81
|
+
* Blocked, not planned (rule 2), while the platform has no edge service
|
|
58
82
|
* wired, the entry's app is not recorded, or a previous mint's delivery
|
|
59
83
|
* failed.
|
|
60
84
|
*/
|
|
61
|
-
export function
|
|
85
|
+
export function planEntryEdgeKeys(org, recordedIds, plannedSteps, readiness, now = Date.now()) {
|
|
62
86
|
const steps = [];
|
|
63
87
|
const blocked = [];
|
|
64
88
|
const planned = (system, kind, k) => plannedSteps.some((p) => p.action.system === system && p.action.kind === kind && p.action.key === k);
|
|
65
|
-
// A key whose entry lost `uses.
|
|
89
|
+
// A key whose entry lost `uses.edge`, or whose entry is gone entirely --
|
|
66
90
|
// either way it is no longer in `declared`, the same "gone" scan
|
|
67
91
|
// `planEntryAiKeys`/`planFilesKeys` run over their own recorded keys.
|
|
68
|
-
// Gated on `
|
|
92
|
+
// Gated on `edgeReady`: without a port there is nothing that could revoke
|
|
69
93
|
// it, so it is reported blocked, not planned to a step that would throw.
|
|
70
94
|
//
|
|
71
95
|
// Scanned from both the main recorded id and the undelivered marker: a
|
|
@@ -73,9 +97,9 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
|
|
|
73
97
|
// throws before `runSteps` sees a `resourceId`), so scanning the main
|
|
74
98
|
// prefix alone would never find it to revoke once its entry is gone
|
|
75
99
|
// (#317 review).
|
|
76
|
-
const declared = new Set(org.hosting.entries.filter((e) => e.uses?.
|
|
77
|
-
const mainPrefix = `
|
|
78
|
-
const undeliveredPrefix = '
|
|
100
|
+
const declared = new Set(org.hosting.entries.filter((e) => e.uses?.edge).map((e) => e.key));
|
|
101
|
+
const mainPrefix = `edge:key:${org.id}:`;
|
|
102
|
+
const undeliveredPrefix = 'edge_key_undelivered_';
|
|
79
103
|
const goneEntryKeys = new Set();
|
|
80
104
|
for (const [name, id] of Object.entries(recordedIds)) {
|
|
81
105
|
if (!id)
|
|
@@ -95,39 +119,39 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
|
|
|
95
119
|
// (mint, three failed deliveries, undeclare again) must not hide
|
|
96
120
|
// behind id A's retired marker just because id A's main-id field is
|
|
97
121
|
// still truthy (#317 re-review).
|
|
98
|
-
if (name.startsWith(mainPrefix) && recordedIds[
|
|
122
|
+
if (name.startsWith(mainPrefix) && recordedIds[entryEdgeRetiredScopeKey(entryKey)] === id) {
|
|
99
123
|
continue;
|
|
100
124
|
}
|
|
101
125
|
goneEntryKeys.add(entryKey);
|
|
102
126
|
}
|
|
103
127
|
for (const entryKey of goneEntryKeys) {
|
|
104
|
-
if (!readiness.
|
|
128
|
+
if (!readiness.edgeReady) {
|
|
105
129
|
blocked.push({
|
|
106
|
-
description: `${org.slug}'s ${entryKey}
|
|
130
|
+
description: `${org.slug}'s ${entryKey} edge key is blocked, not planned: the host supplies no edge service, or the platform has not enabled it`,
|
|
107
131
|
key: `${mainPrefix}${entryKey}`,
|
|
108
132
|
});
|
|
109
133
|
continue;
|
|
110
134
|
}
|
|
111
135
|
steps.push({
|
|
112
136
|
action: {
|
|
113
|
-
system: '
|
|
137
|
+
system: 'edge',
|
|
114
138
|
kind: 'key',
|
|
115
139
|
key: `${org.id}:${entryKey}`,
|
|
116
140
|
desired: { mode: 'retire' },
|
|
117
141
|
},
|
|
118
142
|
step: {
|
|
119
|
-
description: `revoke ${org.slug}'s ${entryKey}
|
|
143
|
+
description: `revoke ${org.slug}'s ${entryKey} edge key: the entry no longer declares uses.edge`,
|
|
120
144
|
destroys: true,
|
|
121
145
|
migrates: false,
|
|
122
146
|
},
|
|
123
147
|
});
|
|
124
148
|
}
|
|
125
149
|
for (const entry of org.hosting.entries) {
|
|
126
|
-
if (!entry.uses?.
|
|
150
|
+
if (!entry.uses?.edge)
|
|
127
151
|
continue;
|
|
128
152
|
const stepKey = appStepKey(org, entry.key);
|
|
129
|
-
const key = `
|
|
130
|
-
const liveKeyId = recordedIds[key] && recordedIds[
|
|
153
|
+
const key = `edge:key:${stepKey}`;
|
|
154
|
+
const liveKeyId = recordedIds[key] && recordedIds[entryEdgeRetiredScopeKey(entry.key)] !== recordedIds[key]
|
|
131
155
|
? recordedIds[key]
|
|
132
156
|
: undefined;
|
|
133
157
|
if (liveKeyId) {
|
|
@@ -140,20 +164,20 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
|
|
|
140
164
|
// mint could ever clear) can tell the two apart -- a permanently
|
|
141
165
|
// "blocked" row past day 335 that no action removes is the problem
|
|
142
166
|
// this prefix exists to avoid.
|
|
143
|
-
const mintedAt = recordedIds[
|
|
167
|
+
const mintedAt = recordedIds[entryEdgeMintedAtScopeKey(entry.key)];
|
|
144
168
|
if (mintedAt) {
|
|
145
|
-
const expiresAt = Date.parse(mintedAt) +
|
|
169
|
+
const expiresAt = Date.parse(mintedAt) + EDGE_ENTRY_KEY_DAYS * DAY_MS;
|
|
146
170
|
const daysLeft = Math.floor((expiresAt - now) / DAY_MS);
|
|
147
|
-
if (daysLeft <
|
|
171
|
+
if (daysLeft < EDGE_ENTRY_KEY_WARN_WINDOW_DAYS) {
|
|
148
172
|
blocked.push({
|
|
149
|
-
description: `warning: ${org.slug}'s ${entry.key}
|
|
173
|
+
description: `warning: ${org.slug}'s ${entry.key} edge key ${daysLeft < 0 ? 'expired' : 'expires'} ${new Date(expiresAt).toISOString().slice(0, 10)} and is never renewed (#317): mint a new key by hand and update the entry's env`,
|
|
150
174
|
key,
|
|
151
175
|
});
|
|
152
176
|
}
|
|
153
177
|
}
|
|
154
178
|
continue;
|
|
155
179
|
}
|
|
156
|
-
const at = `${org.slug}'s ${entry.key}
|
|
180
|
+
const at = `${org.slug}'s ${entry.key} edge key`;
|
|
157
181
|
const block = (why) => blocked.push({ description: `${at} is blocked: ${why}`, key });
|
|
158
182
|
// An undelivered key from a previous mint is no longer a reason to
|
|
159
183
|
// block forever: nothing here keeps the secret around to retry
|
|
@@ -162,8 +186,8 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
|
|
|
162
186
|
// re-review) -- blocking indefinitely, as a prior round did, left the
|
|
163
187
|
// orphan live for up to 365 days with no recorded trace an operator
|
|
164
188
|
// could act on beyond this description.
|
|
165
|
-
if (!readiness.
|
|
166
|
-
block('the host supplies no
|
|
189
|
+
if (!readiness.edgeReady) {
|
|
190
|
+
block('the host supplies no edge service, or the platform has not enabled it');
|
|
167
191
|
continue;
|
|
168
192
|
}
|
|
169
193
|
if (!recordedIds[`coolify:app:${stepKey}`] && !planned('coolify', 'app', stepKey)) {
|
|
@@ -172,17 +196,17 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
|
|
|
172
196
|
}
|
|
173
197
|
steps.push({
|
|
174
198
|
action: {
|
|
175
|
-
system: '
|
|
199
|
+
system: 'edge',
|
|
176
200
|
kind: 'key',
|
|
177
201
|
key: stepKey,
|
|
178
202
|
desired: {
|
|
179
203
|
mode: 'mint',
|
|
180
|
-
name:
|
|
181
|
-
|
|
204
|
+
name: entryEdgeKeyName(org, entry),
|
|
205
|
+
edgeUrl: readiness.edgeUrl,
|
|
182
206
|
},
|
|
183
207
|
},
|
|
184
208
|
step: {
|
|
185
|
-
description: `mint ${at} below the org, write
|
|
209
|
+
description: `mint ${at} below the org, write EDGE_* env, restart the app`,
|
|
186
210
|
destroys: false,
|
|
187
211
|
migrates: false,
|
|
188
212
|
},
|
|
@@ -192,41 +216,41 @@ export function planEntryNamesKeys(org, recordedIds, plannedSteps, readiness, no
|
|
|
192
216
|
}
|
|
193
217
|
/**
|
|
194
218
|
* The reserved-labels step: resent whenever the org's own label set
|
|
195
|
-
* (`
|
|
196
|
-
* uses
|
|
197
|
-
* `
|
|
219
|
+
* (`edgeReservedLabels`) differs from what was last sent. While the org
|
|
220
|
+
* uses edge at all (at least one entry with `uses.edge`) that set is
|
|
221
|
+
* `edgeReservedLabels(org)`; once the last entry drops `uses.edge`, this
|
|
198
222
|
* clears whatever was last sent (an empty list) rather than leaving it
|
|
199
223
|
* stuck in names forever -- a one-time step, same as any other change.
|
|
200
224
|
*/
|
|
201
|
-
export function
|
|
202
|
-
const
|
|
203
|
-
const applied = recordedIds[
|
|
204
|
-
if (!
|
|
225
|
+
export function planEdgeReservedLabels(org, recordedIds, readiness, platformHostnames = []) {
|
|
226
|
+
const usesEdge = org.hosting.entries.some((e) => e.uses?.edge);
|
|
227
|
+
const applied = recordedIds[EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY];
|
|
228
|
+
if (!usesEdge && (!applied || applied === '[]')) {
|
|
205
229
|
return { step: null, blocked: [] };
|
|
206
230
|
}
|
|
207
|
-
if (!readiness.
|
|
231
|
+
if (!readiness.edgeReady) {
|
|
208
232
|
return {
|
|
209
233
|
step: null,
|
|
210
234
|
blocked: [
|
|
211
235
|
{
|
|
212
|
-
description: `${org.slug}'s reserved labels are blocked: the host supplies no
|
|
213
|
-
key: `
|
|
236
|
+
description: `${org.slug}'s reserved labels are blocked: the host supplies no edge service, or the platform has not enabled it`,
|
|
237
|
+
key: `edge:reserved-labels:${org.id}`,
|
|
214
238
|
},
|
|
215
239
|
],
|
|
216
240
|
};
|
|
217
241
|
}
|
|
218
|
-
const labels =
|
|
242
|
+
const labels = usesEdge ? edgeReservedLabels(org, platformHostnames) : [];
|
|
219
243
|
const wantJson = JSON.stringify(labels);
|
|
220
244
|
if (applied === wantJson) {
|
|
221
245
|
return { step: null, blocked: [] };
|
|
222
246
|
}
|
|
223
247
|
return {
|
|
224
248
|
step: {
|
|
225
|
-
action: { system: '
|
|
249
|
+
action: { system: 'edge', kind: 'reserved-labels', key: org.id, desired: { labels } },
|
|
226
250
|
step: {
|
|
227
|
-
description:
|
|
228
|
-
? `send ${org.slug}'s reserved labels to
|
|
229
|
-
: `clear ${org.slug}'s reserved labels from
|
|
251
|
+
description: usesEdge
|
|
252
|
+
? `send ${org.slug}'s reserved labels to edge: ${labels.join(', ')}`
|
|
253
|
+
: `clear ${org.slug}'s reserved labels from edge: no entry uses edge any more`,
|
|
230
254
|
destroys: false,
|
|
231
255
|
migrates: false,
|
|
232
256
|
},
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -89,6 +89,10 @@ export interface CoolifyPort {
|
|
|
89
89
|
* `settings`, not at the top level -- confirmed against live Coolify
|
|
90
90
|
* 4.3.23 (PR #482 review); `custom_network_aliases` and
|
|
91
91
|
* `health_check_enabled` are both top-level there.
|
|
92
|
+
*
|
|
93
|
+
* #506: also the port Traefik routes to, so `coolifyAgora` can tell an app
|
|
94
|
+
* that predates passing `ports_exposes` at create apart from one already
|
|
95
|
+
* fixed.
|
|
92
96
|
*/
|
|
93
97
|
getApplication(uuid: string): Promise<{
|
|
94
98
|
uuid: string;
|
|
@@ -99,6 +103,7 @@ export interface CoolifyPort {
|
|
|
99
103
|
};
|
|
100
104
|
custom_network_aliases?: string | null;
|
|
101
105
|
health_check_enabled?: boolean;
|
|
106
|
+
ports_exposes?: string;
|
|
102
107
|
}>;
|
|
103
108
|
createGithubAppApplication(input: {
|
|
104
109
|
project_uuid: string;
|
|
@@ -565,11 +570,11 @@ export interface WorkersRateLimitBinding {
|
|
|
565
570
|
}
|
|
566
571
|
/**
|
|
567
572
|
* The Cloudflare Workers calls `cloudflareWorker` makes (#318, F8): deploys
|
|
568
|
-
* a plain Worker from an entry's `
|
|
573
|
+
* a plain Worker from an entry's `worker` declaration and routes it on the
|
|
569
574
|
* org's own zone. Optional on {@link FoundryStepExecutorPorts} -- a host
|
|
570
575
|
* with no Workers rights on its Cloudflare token (the issue's own "Human
|
|
571
576
|
* steps": the token needs them added first) wires none, and the planner
|
|
572
|
-
* reports every `
|
|
577
|
+
* reports every `worker` entry blocked instead of planning a step that would
|
|
573
578
|
* throw (`FoundryPlanApplyOptions.edgeWorkers`).
|
|
574
579
|
*
|
|
575
580
|
* `uploadScript` takes the *declared artifact reference*, not bytes:
|
|
@@ -604,10 +609,13 @@ export interface WorkersPort {
|
|
|
604
609
|
/** The script name currently bound to `pattern` in `zoneId`, or `undefined` for no route there. */
|
|
605
610
|
findRoute(zoneId: string, pattern: string): Promise<{
|
|
606
611
|
id: string;
|
|
607
|
-
script
|
|
612
|
+
script?: string;
|
|
608
613
|
} | undefined>;
|
|
609
|
-
/**
|
|
610
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Creates the route if absent; `cloudflareWorker` only calls this once `findRoute` has confirmed the pattern is foundry's own or unclaimed.
|
|
616
|
+
* With no `script` it creates Cloudflare's "no Worker" route (`POST /zones/{zone_id}/workers/routes` with `pattern` only), which turns Workers off for that pattern; a more specific pattern wins over a wildcard.
|
|
617
|
+
*/
|
|
618
|
+
createRoute(zoneId: string, pattern: string, script?: string): Promise<{
|
|
611
619
|
id: string;
|
|
612
620
|
}>;
|
|
613
621
|
/** A no-op, not a throw, when `routeId` is already gone. */
|
|
@@ -1338,22 +1346,22 @@ export interface AiOrganisationPort {
|
|
|
1338
1346
|
}): Promise<unknown>;
|
|
1339
1347
|
}
|
|
1340
1348
|
/**
|
|
1341
|
-
* #317 (F7): the
|
|
1342
|
-
* (https://github.com/wtfalch/
|
|
1349
|
+
* #317 (F7): the edge service's own management API
|
|
1350
|
+
* (https://github.com/wtfalch/edge, live on main, offering `mintKey`,
|
|
1343
1351
|
* `revokeKey` and `setReservedLabels`) -- this port states the shape this
|
|
1344
1352
|
* executor assumes of it. `organisationId` is the org itself: unlike
|
|
1345
|
-
* `AiOrganisationPort.mintNestedKey`,
|
|
1346
|
-
* id, so `
|
|
1353
|
+
* `AiOrganisationPort.mintNestedKey`, edge takes no separate minter key
|
|
1354
|
+
* id, so `edgeEntryKey` mints directly below the org. A real adapter
|
|
1347
1355
|
* wiring this port into `platform-providers.ts` is tracked separately
|
|
1348
1356
|
* (https://github.com/wtfalch/archon/issues/339).
|
|
1349
1357
|
*/
|
|
1350
|
-
export interface
|
|
1358
|
+
export interface EdgePort {
|
|
1351
1359
|
/**
|
|
1352
|
-
* `domain` is required:
|
|
1360
|
+
* `domain` is required: edge's own `mintKey` refuses a request without a
|
|
1353
1361
|
* lowercase hostname and binds the key to it
|
|
1354
|
-
* (`wtfalch/
|
|
1355
|
-
* passes `org.domain`, which `
|
|
1356
|
-
* `uses.
|
|
1362
|
+
* (`wtfalch/edge`'s `packages/service/src/keys.ts`) -- `edgeEntryKey`
|
|
1363
|
+
* passes `org.domain`, which `validateEntryEdge` already requires before
|
|
1364
|
+
* `uses.edge` is accepted.
|
|
1357
1365
|
*/
|
|
1358
1366
|
mintKey(input: {
|
|
1359
1367
|
organisationId: string;
|
|
@@ -1407,6 +1415,8 @@ export interface PlatformOrgPortSource {
|
|
|
1407
1415
|
files(): Promise<FilesOrganisationPort>;
|
|
1408
1416
|
ai(): Promise<AiOrganisationPort>;
|
|
1409
1417
|
jobs(): Promise<JobsSchedulerPort>;
|
|
1418
|
+
/** The platform's shared sign-in host, or `null` when none is declared. */
|
|
1419
|
+
authHostname?(): Promise<string | null>;
|
|
1410
1420
|
}
|
|
1411
1421
|
export interface FoundryStepExecutorPorts {
|
|
1412
1422
|
coolify: CoolifyPort;
|
|
@@ -1437,14 +1447,14 @@ export interface FoundryStepExecutorPorts {
|
|
|
1437
1447
|
filesOrganisation?: FilesOrganisationPort;
|
|
1438
1448
|
aiOrganisation?: AiOrganisationPort;
|
|
1439
1449
|
/**
|
|
1440
|
-
* #317 (F7): mints an entry's
|
|
1450
|
+
* #317 (F7): mints an entry's edge key -- see {@link EdgePort}'s own
|
|
1441
1451
|
* doc comment. Optional the same way `jobsScheduler` is: a host with no
|
|
1442
|
-
*
|
|
1443
|
-
* planner so (`FoundryPlanApplyOptions.
|
|
1444
|
-
* entry's `uses.
|
|
1452
|
+
* edge service deployed builds the executor without one, and tells the
|
|
1453
|
+
* planner so (`FoundryPlanApplyOptions.edge`), which then reports each
|
|
1454
|
+
* entry's `uses.edge` step as blocked instead of planning one that would
|
|
1445
1455
|
* throw.
|
|
1446
1456
|
*/
|
|
1447
|
-
|
|
1457
|
+
edge?: EdgePort;
|
|
1448
1458
|
/**
|
|
1449
1459
|
* #177: builds the files, AI and jobs ports from the platform's addresses
|
|
1450
1460
|
* and held keys. A port set above overrides it (tests); with neither, the
|
|
@@ -1468,9 +1478,9 @@ export interface FoundryStepExecutorPorts {
|
|
|
1468
1478
|
*/
|
|
1469
1479
|
broker?: BrokerPort;
|
|
1470
1480
|
/**
|
|
1471
|
-
* #318: deploys and routes an entry's `
|
|
1481
|
+
* #318: deploys and routes an entry's `worker` Worker -- see
|
|
1472
1482
|
* {@link WorkersPort}'s own doc comment. Optional the same way `broker` is:
|
|
1473
|
-
* without it the plan reports each `
|
|
1483
|
+
* without it the plan reports each `worker` entry blocked (the host also
|
|
1474
1484
|
* sets `FoundryPlanApplyOptions.edgeWorkers`).
|
|
1475
1485
|
*/
|
|
1476
1486
|
workers?: WorkersPort;
|
|
@@ -1618,10 +1628,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1618
1628
|
*/
|
|
1619
1629
|
private cloudflareRateLimit;
|
|
1620
1630
|
/**
|
|
1621
|
-
* `plan/diff.ts`'s `
|
|
1622
|
-
* `
|
|
1631
|
+
* `plan/diff.ts`'s `planWorkers` (#318, F8): deploys one entry's
|
|
1632
|
+
* `worker` Worker and routes it on the org's own zone. Throws by name
|
|
1623
1633
|
* without a {@link WorkersPort}, the same `jobsRegistration` shape --
|
|
1624
|
-
* `
|
|
1634
|
+
* `planWorkers` reports the step blocked instead while the host has
|
|
1625
1635
|
* none, so this only fires against a stale plan. The route is checked
|
|
1626
1636
|
* before upload: a foreign route already on the pattern is refused, never
|
|
1627
1637
|
* adopted or replaced (`cloudflareAccess`'s own precedent for a foreign
|
|
@@ -1638,6 +1648,14 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1638
1648
|
* live state actually matches it.
|
|
1639
1649
|
*/
|
|
1640
1650
|
private cloudflareWorker;
|
|
1651
|
+
/**
|
|
1652
|
+
* Makes the no-Worker routes recorded for an entry match `patterns`: deletes
|
|
1653
|
+
* the ones no longer wanted (all of them if the zone changed), creates the
|
|
1654
|
+
* missing ones without a script. A route already on a pattern that is bound
|
|
1655
|
+
* to a Worker is foreign and refused; one with no script is adopted. Records
|
|
1656
|
+
* and persists after each change.
|
|
1657
|
+
*/
|
|
1658
|
+
private syncWorkerExclusions;
|
|
1641
1659
|
/**
|
|
1642
1660
|
* The zone id a recorded route lives in, or `undefined` only when that
|
|
1643
1661
|
* zone is *positively* gone -- its apex resolves to no zone, or to a
|
|
@@ -1651,15 +1669,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1651
1669
|
* live on the old one, the exact bug a recorded-per-route zone exists to
|
|
1652
1670
|
* avoid (#318 re-review).
|
|
1653
1671
|
*/
|
|
1654
|
-
private
|
|
1672
|
+
private liveWorkerZone;
|
|
1655
1673
|
/** Deletes the route an interrupted apply was creating, if `findRoute` shows it exists, points at that apply's script, and is not `keepRouteId`. */
|
|
1656
|
-
private
|
|
1674
|
+
private deletePendingWorkerRoute;
|
|
1657
1675
|
/**
|
|
1658
1676
|
* Retire and destroy share this: delete the recorded route on its own
|
|
1659
1677
|
* zone, then any route an interrupted apply created but never recorded,
|
|
1660
1678
|
* then the script. Records nothing -- the caller does.
|
|
1661
1679
|
*/
|
|
1662
|
-
private
|
|
1680
|
+
private teardownWorker;
|
|
1663
1681
|
private zitadelIssuer;
|
|
1664
1682
|
/**
|
|
1665
1683
|
* `plan/diff.ts`'s `planOwner`: invites the org's owner into its ZITADEL
|
|
@@ -2320,11 +2338,25 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2320
2338
|
* actually differs, or whenever the last restart this step confirmed
|
|
2321
2339
|
* ({@link AGORA_ACTIVATED_ENV_SCOPE_KEY}) is not this exact env (an org
|
|
2322
2340
|
* applied before this existed, whose live env already matched desired
|
|
2323
|
-
* with nothing ever confirmed running it).
|
|
2324
|
-
*
|
|
2325
|
-
*
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2341
|
+
* with nothing ever confirmed running it). The activation marker is
|
|
2342
|
+
* recorded last, after the restart (if any) returns without throwing, so a
|
|
2343
|
+
* failed restart leaves it stale and the next plan replans this step even
|
|
2344
|
+
* though Coolify's own env store already matches desired.
|
|
2345
|
+
*
|
|
2346
|
+
* #504: a brand new app is never restarted -- but it is deployed, once
|
|
2347
|
+
* ({@link AGORA_DEPLOYED_SCOPE_KEY}). `createGithubAppApplication` never
|
|
2348
|
+
* sets `instant_deploy`, and Coolify defaults it to false, so without an
|
|
2349
|
+
* explicit deploy the app sat created but never built or started.
|
|
2350
|
+
*
|
|
2351
|
+
* #506: `ports_exposes: AGORA_APP_PORTS_EXPOSES` goes on the create call
|
|
2352
|
+
* itself, so Traefik has a port to route to from the moment the app
|
|
2353
|
+
* exists. An app `findOrCreateCallsApp` instead *found* may predate this
|
|
2354
|
+
* fix and carry no `ports_exposes` at all (or the wrong one), so its live
|
|
2355
|
+
* value is read and, when it differs, patched -- and, since the container
|
|
2356
|
+
* listens on `ports_exposes` only at its next deploy (the same rule
|
|
2357
|
+
* `coolifyApp`'s own `settingsNeedDeploy` follows for a port change
|
|
2358
|
+
* there), redeployed even on a run that already has its one #504 deploy
|
|
2359
|
+
* behind it.
|
|
2328
2360
|
*/
|
|
2329
2361
|
private coolifyAgora;
|
|
2330
2362
|
/**
|
|
@@ -2349,6 +2381,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2349
2381
|
*
|
|
2350
2382
|
* `AUTH_APP_KEY` is minted once here; its broker entry is the next step
|
|
2351
2383
|
* (`bouleBrokerEntry`).
|
|
2384
|
+
*
|
|
2385
|
+
* #504: a brand new app is never restarted -- but it is deployed, once
|
|
2386
|
+
* ({@link BOULE_DEPLOYED_SCOPE_KEY}), the same reason and the same marker
|
|
2387
|
+
* shape {@link coolifyAgora}'s own doc comment explains.
|
|
2352
2388
|
*/
|
|
2353
2389
|
private coolifyBoule;
|
|
2354
2390
|
/**
|
|
@@ -2490,6 +2526,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2490
2526
|
* shared-hostname collision this ticket exists to refuse. An org re-running
|
|
2491
2527
|
* its own already-recorded zone is unaffected: the store then names this
|
|
2492
2528
|
* same org as the owner.
|
|
2529
|
+
*
|
|
2530
|
+
* #510: also records the zone's own id under the flat `cloudflare_zone`
|
|
2531
|
+
* scope key, alongside the `cloudflare:zone:<domain>` resource id `runSteps`
|
|
2532
|
+
* already records from this method's return value. Without it, nothing
|
|
2533
|
+
* ever gives `ProvidersCloudflareReader` a zone id to resolve this org's
|
|
2534
|
+
* scope with (`providers-readers.ts`'s `storeScopeResolver` doc comment),
|
|
2535
|
+
* so that reader's `listResources` always returned `[]` and `diff.ts`'s
|
|
2536
|
+
* `planZone` re-proposed "provision a Cloudflare zone" on every apply, even
|
|
2537
|
+
* once the zone was live.
|
|
2493
2538
|
*/
|
|
2494
2539
|
private cloudflareZone;
|
|
2495
2540
|
/**
|
|
@@ -2716,21 +2761,21 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2716
2761
|
*/
|
|
2717
2762
|
private revokeEntryKey;
|
|
2718
2763
|
/**
|
|
2719
|
-
* F7 (#317): mints one entry's
|
|
2720
|
-
* takes no separate minter key id, unlike ai/files), writes `
|
|
2721
|
-
* and `
|
|
2722
|
-
* the key when `uses.
|
|
2764
|
+
* F7 (#317): mints one entry's edge key directly below the org (edge
|
|
2765
|
+
* takes no separate minter key id, unlike ai/files), writes `EDGE_URL`
|
|
2766
|
+
* and `EDGE_API_KEY` to the entry's app env, then restarts it. Retires
|
|
2767
|
+
* the key when `uses.edge` is removed. Follows the "one mint" rule
|
|
2723
2768
|
* (CLAUDE.md "Platform steps"): the id is recorded as undelivered and
|
|
2724
2769
|
* persisted before delivery is attempted, so a crash or a failed write
|
|
2725
|
-
* never leads to a second mint -- `
|
|
2770
|
+
* never leads to a second mint -- `planEntryEdgeKeys` blocks instead.
|
|
2726
2771
|
*/
|
|
2727
|
-
private
|
|
2772
|
+
private edgeEntryKey;
|
|
2728
2773
|
/**
|
|
2729
|
-
* F7 (#317): `plan/entry-
|
|
2730
|
-
* tells the
|
|
2731
|
-
* hold, so
|
|
2774
|
+
* F7 (#317): `plan/entry-edge-key.ts`'s `planEdgeReservedLabels` --
|
|
2775
|
+
* tells the edge service every label this org's own entries already
|
|
2776
|
+
* hold, so edge refuses a customer claiming one of them.
|
|
2732
2777
|
*/
|
|
2733
|
-
private
|
|
2778
|
+
private edgeReservedLabels;
|
|
2734
2779
|
/**
|
|
2735
2780
|
* F6b: mints or renews the org's own files key, or revokes the ones a
|
|
2736
2781
|
* renewal replaced. The key is minted below the files organisation's owner
|