@wtfalch/foundry 0.70.0 → 0.71.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.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/migrations/0070_platform_usage_tail.sql +12 -0
- package/dist/plan/diff.js +23 -7
- package/dist/plan/edge-access.d.ts +9 -0
- package/dist/plan/edge-access.js +20 -0
- package/dist/plan/executor.d.ts +16 -0
- package/dist/plan/executor.js +61 -10
- package/dist/plan/own-diff.js +4 -1
- package/dist/plan/plan-apply.d.ts +23 -0
- package/dist/plan/plan-apply.js +70 -0
- package/dist/plan/platform-diff.d.ts +62 -0
- package/dist/plan/platform-diff.js +165 -4
- package/dist/plan/platform-providers.d.ts +16 -0
- package/dist/plan/platform-providers.js +86 -4
- package/dist/plan/usage-tail-worker.d.ts +29 -0
- package/dist/plan/usage-tail-worker.js +105 -0
- package/dist/platform.d.ts +23 -0
- package/dist/platform.js +14 -0
- package/dist/release/fanout.d.ts +77 -0
- package/dist/release/fanout.js +177 -0
- package/dist/release/run.js +2 -1
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +3 -0
- package/dist/release/types.d.ts +9 -1
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +55 -0
- package/dist/store/schema.js +4 -0
- package/package.json +1 -1
|
@@ -170,6 +170,8 @@ export function declaredPlatformFor(apex, serverIpv4) {
|
|
|
170
170
|
// only after Archon runs. Declared on with no pin, it would be reported
|
|
171
171
|
// blocked and the bootstrap would never be complete.
|
|
172
172
|
uptime: { enabled: false, tag: '', sha256: '' },
|
|
173
|
+
// foundry#1024: off until a human pins an edge release tag and digest, the same reason as uptime.
|
|
174
|
+
usageTail: { enabled: false, tag: '', sha256: '' },
|
|
173
175
|
heartbeatsProvider: 'self-hosted',
|
|
174
176
|
};
|
|
175
177
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export { createPostgresAuditLog } from './store/audit.js';
|
|
|
25
25
|
export type { Color, ReleaseAction, ReleaseEvent, ReleaseEventKind, ReleaseSeries, ReleaseSeriesColor, ReleaseSignal, ReleaseState, WindowState, } from './release/types.js';
|
|
26
26
|
export type { ReleaseStore } from './release/state.js';
|
|
27
27
|
export { readReleaseHistory, readReleaseState, requestReleaseAction } from './release/state.js';
|
|
28
|
+
export type { BuiltinFanOut, FanOutApp, FanOutOrgState } from './release/fanout.js';
|
|
29
|
+
export { readBuiltinFanOut } from './release/fanout.js';
|
|
28
30
|
export type { ReleaseSeriesHost, ReleaseSeriesQuery } from './release/series.js';
|
|
29
31
|
export { readReleaseSeries } from './release/series.js';
|
|
30
32
|
export type { EntrySeriesDeps } from './plan/release-series-reader.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { StaleLeaseError } from './store.js';
|
|
|
15
15
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
16
16
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
17
17
|
export { readReleaseHistory, readReleaseState, requestReleaseAction } from './release/state.js';
|
|
18
|
+
export { readBuiltinFanOut } from './release/fanout.js';
|
|
18
19
|
export { readReleaseSeries } from './release/series.js';
|
|
19
20
|
export { readEntryReleaseSeries } from './plan/release-series-reader.js';
|
|
20
21
|
export { checkMigrationCompatibility } from './release/migration-compatibility.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the usage tail Worker on the declared platform (foundry#1024).
|
|
2
|
+
-- Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` additions.
|
|
3
|
+
-- `usage_tail_tag` and `usage_tail_sha256` are the pin of the edge release
|
|
4
|
+
-- whose built Worker foundry deploys; both empty means nothing is pinned
|
|
5
|
+
-- yet, and the planner reports that instead of planning the step. Unlike the
|
|
6
|
+
-- uptime Worker, this one is off by default: nothing has pinned a release
|
|
7
|
+
-- yet on any existing platform.
|
|
8
|
+
|
|
9
|
+
alter table foundry_declared_platform
|
|
10
|
+
add column if not exists usage_tail_enabled boolean not null default false,
|
|
11
|
+
add column if not exists usage_tail_tag text not null default '',
|
|
12
|
+
add column if not exists usage_tail_sha256 text not null default '';
|
package/dist/plan/diff.js
CHANGED
|
@@ -176,6 +176,18 @@ function liveResourceName(r) {
|
|
|
176
176
|
const type = r.kind === 'dns_record' && typeof r.config.type === 'string' ? `${r.config.type} ` : '';
|
|
177
177
|
return `${type}${name}, ${r.key}`;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The provider's own name for a live resource, for a park or provision
|
|
181
|
+
* step's own prose (#1062, pain point 3): an operator told to "park the
|
|
182
|
+
* database" acted on the company's Coolify server, named after the same
|
|
183
|
+
* company, instead of the database resource the step meant. `null` when
|
|
184
|
+
* there is no live resource yet, or it carries no name -- unlike
|
|
185
|
+
* {@link liveResourceName} above, this never falls back to the bare key,
|
|
186
|
+
* since an id is not a name a step's own text should ever show.
|
|
187
|
+
*/
|
|
188
|
+
function liveName(live) {
|
|
189
|
+
return typeof live?.config.name === 'string' && live.config.name !== '' ? live.config.name : null;
|
|
190
|
+
}
|
|
179
191
|
/**
|
|
180
192
|
* The diff engine: #19's "a plan reads live state from Zitadel, Coolify and
|
|
181
193
|
* Cloudflare and diffs it against the declared org", built on #10's rule
|
|
@@ -3166,7 +3178,7 @@ function planKosmos(org, byKey, claimed, recordedIds, issuer, otherSlugs) {
|
|
|
3166
3178
|
if (!kosmosOn(org)) {
|
|
3167
3179
|
// Switched off: park a live app (stop it, keep the database), like CMS and calls. A stopped app is
|
|
3168
3180
|
// already parked, so the plan reaches empty.
|
|
3169
|
-
const parked = planHostingToggle('coolify', 'kosmos', false, org, byKey, claimed, () => `provision ${org.slug}'s kosmos`, () => `park ${org.slug}'s kosmos (stops serving; its database is retained, not deleted)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
3181
|
+
const parked = planHostingToggle('coolify', 'kosmos', false, org, byKey, claimed, () => `provision ${org.slug}'s kosmos`, (live) => `park ${org.slug}'s kosmos${liveName(live) ? ` ${liveName(live)}` : ''} (stops serving; its database is retained, not deleted)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
3170
3182
|
return { step: parked, blocked: null };
|
|
3171
3183
|
}
|
|
3172
3184
|
const key = resourceKey('coolify', 'kosmos', org.id);
|
|
@@ -3533,7 +3545,11 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
3533
3545
|
* park-not-delete rule (#7 corrections 1) -- turning it off never deletes
|
|
3534
3546
|
* the org's own app, database or tunnel configuration, it parks it.
|
|
3535
3547
|
*/
|
|
3536
|
-
function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed,
|
|
3548
|
+
function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed,
|
|
3549
|
+
// #1062: takes the live resource so a caller can name it (Coolify's own
|
|
3550
|
+
// name for it, from `config.name`), rather than a generic noun a park or
|
|
3551
|
+
// provision step's own text can be told apart from the server it runs on.
|
|
3552
|
+
describeProvision, describePark, stepKey = org.id, isOn = (live) => live.config.enabled !== false) {
|
|
3537
3553
|
const key = resourceKey(system, kind, stepKey);
|
|
3538
3554
|
const live = byKey.get(key);
|
|
3539
3555
|
// A live resource is on unless it says it is parked. Providers' own
|
|
@@ -3551,7 +3567,7 @@ function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed, de
|
|
|
3551
3567
|
return null;
|
|
3552
3568
|
return {
|
|
3553
3569
|
action: { system, kind, key: stepKey, desired: { enabled: false } },
|
|
3554
|
-
step: { description: describePark(), destroys: true, migrates: false },
|
|
3570
|
+
step: { description: describePark(live), destroys: true, migrates: false },
|
|
3555
3571
|
};
|
|
3556
3572
|
}
|
|
3557
3573
|
if (live)
|
|
@@ -3562,7 +3578,7 @@ function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed, de
|
|
|
3562
3578
|
return null;
|
|
3563
3579
|
return {
|
|
3564
3580
|
action: { system, kind, key: stepKey, desired: { enabled: true } },
|
|
3565
|
-
step: { description: describeProvision(), destroys: false, migrates: false },
|
|
3581
|
+
step: { description: describeProvision(live), destroys: false, migrates: false },
|
|
3566
3582
|
};
|
|
3567
3583
|
}
|
|
3568
3584
|
/**
|
|
@@ -3701,7 +3717,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
3701
3717
|
});
|
|
3702
3718
|
}
|
|
3703
3719
|
const refused = noRepo || composeBuilt;
|
|
3704
|
-
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !refused, org, byKey, claimed, () => `provision ${org.slug}'s ${label}${volumesNote(entry)}`, () => `park ${org.slug}'s ${label} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
3720
|
+
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !refused, org, byKey, claimed, () => `provision ${org.slug}'s ${label}${volumesNote(entry)}`, (live) => `park ${org.slug}'s ${label}${liveName(live) ? ` ${liveName(live)}` : ''} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
3705
3721
|
// #447: only the live app's own `git_repository` decides, never the
|
|
3706
3722
|
// declared entry (a `repo: null` entry can sit over a bespoke app, which is
|
|
3707
3723
|
// parked as before). Only the `app` key ever held the default app, and an
|
|
@@ -4253,7 +4269,7 @@ function planDatabase(org, byKey, claimed, recordedIds) {
|
|
|
4253
4269
|
if (!appHome || !legacy || !recordedIds[legacy] || serviceOnServer(org, appHome, recordedIds)) {
|
|
4254
4270
|
return null;
|
|
4255
4271
|
}
|
|
4256
|
-
return planHostingToggle('coolify', 'database', org.hosting.entries.some(entryHasDatabase), org, byKey, claimed, () => `provision a database for ${org.slug}'s app`, () => `park ${org.slug}'s database (data is retained, not deleted -- #7 corrections 1)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
4272
|
+
return planHostingToggle('coolify', 'database', org.hosting.entries.some(entryHasDatabase), org, byKey, claimed, () => `provision a database for ${org.slug}'s app`, (live) => `park ${org.slug}'s database${liveName(live) ? ` ${liveName(live)}` : ''} (data is retained, not deleted -- #7 corrections 1)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
4257
4273
|
}
|
|
4258
4274
|
function sortedNames(names) {
|
|
4259
4275
|
return names.filter(Boolean).sort().join(',');
|
|
@@ -4298,7 +4314,7 @@ function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
|
4298
4314
|
appsOn(org) ||
|
|
4299
4315
|
// A company without a domain still gets a tunnel of its own for an entry at `<slug>.oikos.host`.
|
|
4300
4316
|
entryRoutes(org, recordedIds).length > 0;
|
|
4301
|
-
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, () => `park ${org.slug}'s Cloudflare tunnel (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
4317
|
+
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, (live) => `park ${org.slug}'s Cloudflare tunnel${liveName(live) ? ` ${liveName(live)}` : ''} (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
4302
4318
|
// Cloudflare's status, read against what is declared: a park cannot change
|
|
4303
4319
|
// it, so judging on/off by status alone would never let the plan empty.
|
|
4304
4320
|
// Wanted on: only `down` needs a repair (`inactive` is a fresh tunnel
|
|
@@ -115,6 +115,15 @@ export interface AccessGateInput {
|
|
|
115
115
|
emails: string[];
|
|
116
116
|
}
|
|
117
117
|
export declare const isNotFound: (error: unknown) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Throws the operator-facing message when `error` is edge's generic 404 for
|
|
120
|
+
* a route this SDK expects but the running release predates (#1061, pain
|
|
121
|
+
* point 12: production edge six days behind the SDK failed foundry's first
|
|
122
|
+
* apps step with the raw "Route not found" and no hint why). Returns
|
|
123
|
+
* otherwise, so the caller's own handling of `error` -- a conflict branch,
|
|
124
|
+
* an `isNotFound`/`isNotFoundError` "already gone" check -- still runs.
|
|
125
|
+
*/
|
|
126
|
+
export declare function edgeTooOld(error: unknown, what: string): void;
|
|
118
127
|
/**
|
|
119
128
|
* Edge answers a policy, delete or create call for a live Access app with no
|
|
120
129
|
* edge row `not_found` or 409 `exists` ("create or adopt it first"). So the
|
package/dist/plan/edge-access.js
CHANGED
|
@@ -3,6 +3,26 @@ import { EDGE_DNS_WRITE_CONCURRENCY, limiter, rateLimitRetrier, } from './edge-d
|
|
|
3
3
|
export const isNotFound = (error) => error instanceof EdgeApiError && error.code === 'not_found';
|
|
4
4
|
/** Edge's 409 for a live app on the domain with no edge row. */
|
|
5
5
|
const isExists = (error) => error instanceof EdgeApiError && error.code === 'conflict' && error.reason === 'exists';
|
|
6
|
+
/**
|
|
7
|
+
* Edge's generic 404 fallback for a route no release of it answers yet
|
|
8
|
+
* (`service.ts`'s `errorResponse('not_found', 'Route not found')`), as
|
|
9
|
+
* opposed to a resource-specific 404 ("Key not found" and the like), which
|
|
10
|
+
* never carries this exact message.
|
|
11
|
+
*/
|
|
12
|
+
const isRouteMissing = (error) => error instanceof EdgeApiError && error.status === 404 && error.message === 'Route not found';
|
|
13
|
+
/**
|
|
14
|
+
* Throws the operator-facing message when `error` is edge's generic 404 for
|
|
15
|
+
* a route this SDK expects but the running release predates (#1061, pain
|
|
16
|
+
* point 12: production edge six days behind the SDK failed foundry's first
|
|
17
|
+
* apps step with the raw "Route not found" and no hint why). Returns
|
|
18
|
+
* otherwise, so the caller's own handling of `error` -- a conflict branch,
|
|
19
|
+
* an `isNotFound`/`isNotFoundError` "already gone" check -- still runs.
|
|
20
|
+
*/
|
|
21
|
+
export function edgeTooOld(error, what) {
|
|
22
|
+
if (isRouteMissing(error)) {
|
|
23
|
+
throw new Error(`${what} is blocked: the running edge has no route for this step; deploy edge to the release foundry expects`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Edge answers a policy, delete or create call for a live Access app with no
|
|
8
28
|
* edge row `not_found` or 409 `exists` ("create or adopt it first"). So the
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2489,6 +2489,22 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2489
2489
|
}>;
|
|
2490
2490
|
down: string[];
|
|
2491
2491
|
}>;
|
|
2492
|
+
/**
|
|
2493
|
+
* The built-in fan-out's in-place deploy (`release/fanout.ts`): pins the org's Boule or Agora to `sha`, deploys
|
|
2494
|
+
* it and waits at most STAGE_DEPLOY_ATTEMPTS polls (10 minutes) for Coolify to finish. The reasons are fixed
|
|
2495
|
+
* texts. Never touches a copy that runs blue/green: that one has colours and is released by its own runner.
|
|
2496
|
+
*/
|
|
2497
|
+
deployBuiltinAt(input: {
|
|
2498
|
+
app: 'boule' | 'agora';
|
|
2499
|
+
org: DeclaredOrg;
|
|
2500
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
2501
|
+
sha: string;
|
|
2502
|
+
}): Promise<{
|
|
2503
|
+
ok: true;
|
|
2504
|
+
} | {
|
|
2505
|
+
ok: false;
|
|
2506
|
+
reason: string;
|
|
2507
|
+
}>;
|
|
2492
2508
|
/**
|
|
2493
2509
|
* The release runner's outside calls for one blue/green entry (`plan/release-host.ts`), or `null` while its
|
|
2494
2510
|
* blue copy is not recorded. The health token is only read here, never minted.
|
package/dist/plan/executor.js
CHANGED
|
@@ -30,12 +30,12 @@ import { BACKUP_POSTGRES_DATABASES_SCOPE_KEY, BACKUP_SCOPE_PREFIX, COMPANY_BACKU
|
|
|
30
30
|
import { LOGIN_COOKIE_DOMAIN_SCOPE_KEY } from './cookie-domain.js';
|
|
31
31
|
import { previewsOn } from './coolify-previews.js';
|
|
32
32
|
import { CORE_FILES_COLLECTION_NAME, FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY, } from './core-files.js';
|
|
33
|
-
import { waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
33
|
+
import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
34
34
|
import { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_DOCKERFILE, DESKTOP_AUTH_HOME, DESKTOP_AUTH_ORIGIN_SCOPE_KEY, DESKTOP_AUTH_READY_PATH, DESKTOP_AUTH_READY_SCOPE_KEY, DESKTOP_AUTH_REDIRECT_SCOPE_KEY, DESKTOP_AUTH_REVISION_SCOPE_KEY, desktopAuthCallbackUri, desktopAuthEnv, desktopAuthInputs, desktopAuthRedirectUnsupported, desktopAuthRevision, } from './desktop-auth.js';
|
|
35
35
|
import { desktopCallbackUri, signInCallbackAppliedValue, signInRedirectUris, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
|
|
36
36
|
import { PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, onPlatformNetwork, placed, placedByScope, placedOnPlatform, } from './destinations.js';
|
|
37
37
|
import { DEFAULT_AI_LIMIT_MICROS } from './diff.js';
|
|
38
|
-
import { createEdgeAccess, isNotFound as isEdgeNotFound, } from './edge-access.js';
|
|
38
|
+
import { createEdgeAccess, edgeTooOld, isNotFound as isEdgeNotFound, } from './edge-access.js';
|
|
39
39
|
import { EDGE_DNS_WRITE_CONCURRENCY, createEdgeDns, limiter, } from './edge-dns.js';
|
|
40
40
|
import { createEdgeRateLimits, } from './edge-ratelimits.js';
|
|
41
41
|
import { createEdgeTunnels } from './edge-tunnels.js';
|
|
@@ -77,7 +77,7 @@ import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_
|
|
|
77
77
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_PROJECT, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
|
|
78
78
|
import { companyAppPostLogoutUris, postLogoutAppliedValue, postLogoutUrisFor, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
79
79
|
import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, ENTRY_CONTAINER_LABELS, appLabelsAppliedScopeKey, builtinRoutes, entryAppsHostnames, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryProxyLabels, entryRoutes, entryRoutesMarker, ownsAppsZoneName, ownsOikosName, routesWithoutToken, sameLabelSets, } from './proxy-labels.js';
|
|
80
|
-
import { createReleaseHost } from './release-host.js';
|
|
80
|
+
import { STAGE_DEPLOY_ATTEMPTS, STAGE_DEPLOY_INTERVAL_MS, createReleaseHost, } from './release-host.js';
|
|
81
81
|
import { TUNNEL_HTTP_INGRESS_SCOPE_KEY, applyRemoteDatabase, mergeTcpIngress, recordedTcpRoutes, } from './remote-database.js';
|
|
82
82
|
import { editSignInBindings } from './sign-in-bindings.js';
|
|
83
83
|
import { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, closeSignInOrgRegistration, ensureSignInOrg, inviteOwnerWithLink, registerSignInDomainOrBlock, signInOrgDomain, } from './sign-in-org.js';
|
|
@@ -3183,6 +3183,40 @@ export class FoundryStepExecutor {
|
|
|
3183
3183
|
down,
|
|
3184
3184
|
};
|
|
3185
3185
|
}
|
|
3186
|
+
/**
|
|
3187
|
+
* The built-in fan-out's in-place deploy (`release/fanout.ts`): pins the org's Boule or Agora to `sha`, deploys
|
|
3188
|
+
* it and waits at most STAGE_DEPLOY_ATTEMPTS polls (10 minutes) for Coolify to finish. The reasons are fixed
|
|
3189
|
+
* texts. Never touches a copy that runs blue/green: that one has colours and is released by its own runner.
|
|
3190
|
+
*/
|
|
3191
|
+
async deployBuiltinAt(input) {
|
|
3192
|
+
const { app, org, sha } = input;
|
|
3193
|
+
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
3194
|
+
return { ok: false, reason: 'not a built commit' };
|
|
3195
|
+
if (org[app]?.blueGreen !== undefined)
|
|
3196
|
+
return { ok: false, reason: 'copy runs blue/green' };
|
|
3197
|
+
const appId = input.recordedIds[`coolify:${app}:${org.id}`];
|
|
3198
|
+
if (!appId)
|
|
3199
|
+
return { ok: false, reason: 'app not recorded' };
|
|
3200
|
+
try {
|
|
3201
|
+
await this.ports.coolify.updateApplication(appId, { git_commit_sha: sha });
|
|
3202
|
+
}
|
|
3203
|
+
catch (error) {
|
|
3204
|
+
console.error(`fanout ${app}: selecting the commit failed (${error instanceof Error ? error.name : 'Error'})`);
|
|
3205
|
+
return { ok: false, reason: 'select commit failed' };
|
|
3206
|
+
}
|
|
3207
|
+
try {
|
|
3208
|
+
await waitForDeploymentFinished(this.ports.coolify, appId, {
|
|
3209
|
+
...(this.ports.runtime ?? {}),
|
|
3210
|
+
deployAttempts: STAGE_DEPLOY_ATTEMPTS,
|
|
3211
|
+
deployIntervalMs: STAGE_DEPLOY_INTERVAL_MS,
|
|
3212
|
+
}, `coolify:fanout:${app}`, 'the copy stays as it was');
|
|
3213
|
+
}
|
|
3214
|
+
catch (error) {
|
|
3215
|
+
console.error(`fanout ${app}: the deployment did not finish (${error instanceof Error ? error.name : 'Error'})`);
|
|
3216
|
+
return { ok: false, reason: 'deployment did not finish' };
|
|
3217
|
+
}
|
|
3218
|
+
return { ok: true };
|
|
3219
|
+
}
|
|
3186
3220
|
/**
|
|
3187
3221
|
* The release runner's outside calls for one blue/green entry (`plan/release-host.ts`), or `null` while its
|
|
3188
3222
|
* blue copy is not recorded. The health token is only read here, never minted.
|
|
@@ -10853,6 +10887,7 @@ export class FoundryStepExecutor {
|
|
|
10853
10887
|
await edge.revokeKey(org.id, id);
|
|
10854
10888
|
}
|
|
10855
10889
|
catch (error) {
|
|
10890
|
+
edgeTooOld(error, at);
|
|
10856
10891
|
// A 404 means it is already gone -- recorded as retired below,
|
|
10857
10892
|
// the same as a successful revoke. Anything else is a real
|
|
10858
10893
|
// failure: thrown, not swallowed, so the key stays live and the
|
|
@@ -10922,18 +10957,26 @@ export class FoundryStepExecutor {
|
|
|
10922
10957
|
await edge.revokeKey(org.id, staleUndelivered);
|
|
10923
10958
|
}
|
|
10924
10959
|
catch (error) {
|
|
10960
|
+
edgeTooOld(error, at);
|
|
10925
10961
|
if (!isNotFoundError(error))
|
|
10926
10962
|
throw error;
|
|
10927
10963
|
}
|
|
10928
10964
|
context.recordScopeId(entryEdgeUndeliveredScopeKey(entryKey), '');
|
|
10929
10965
|
await context.persistScopeIds();
|
|
10930
10966
|
}
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10967
|
+
let minted;
|
|
10968
|
+
try {
|
|
10969
|
+
minted = await edge.mintKey({
|
|
10970
|
+
organisationId: org.id,
|
|
10971
|
+
domain: edgeKeyDomain(org),
|
|
10972
|
+
name: desired.name,
|
|
10973
|
+
expiresDays: EDGE_ENTRY_KEY_DAYS,
|
|
10974
|
+
});
|
|
10975
|
+
}
|
|
10976
|
+
catch (e) {
|
|
10977
|
+
edgeTooOld(e, at);
|
|
10978
|
+
throw e;
|
|
10979
|
+
}
|
|
10937
10980
|
context.recordScopeId(entryEdgeUndeliveredScopeKey(entryKey), minted.id);
|
|
10938
10981
|
await context.persistScopeIds();
|
|
10939
10982
|
const envs = [
|
|
@@ -11094,6 +11137,7 @@ export class FoundryStepExecutor {
|
|
|
11094
11137
|
await edge.setOrganisationSlug(org.id, slug);
|
|
11095
11138
|
}
|
|
11096
11139
|
catch (e) {
|
|
11140
|
+
edgeTooOld(e, `edge:slug for ${org.slug}`);
|
|
11097
11141
|
if (e instanceof EdgeApiError && e.code === 'conflict') {
|
|
11098
11142
|
throw new Error(`edge:slug for ${org.slug} is blocked: edge says another company already holds the slug '${slug}', so its platform hostnames cannot be accepted; change the slug`);
|
|
11099
11143
|
}
|
|
@@ -11113,7 +11157,13 @@ export class FoundryStepExecutor {
|
|
|
11113
11157
|
throw new Error(`edge:reserved-labels for ${org.slug}: this executor was built without an EdgePort (#317)`);
|
|
11114
11158
|
}
|
|
11115
11159
|
const labels = action.desired.labels;
|
|
11116
|
-
|
|
11160
|
+
try {
|
|
11161
|
+
await edge.setReservedLabels(org.id, labels);
|
|
11162
|
+
}
|
|
11163
|
+
catch (e) {
|
|
11164
|
+
edgeTooOld(e, `edge:reserved-labels for ${org.slug}`);
|
|
11165
|
+
throw e;
|
|
11166
|
+
}
|
|
11117
11167
|
context.recordScopeId(EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, JSON.stringify(labels));
|
|
11118
11168
|
return {};
|
|
11119
11169
|
}
|
|
@@ -11658,6 +11708,7 @@ export class FoundryStepExecutor {
|
|
|
11658
11708
|
await edge.revokeKey(org.id, target.id);
|
|
11659
11709
|
}
|
|
11660
11710
|
catch (error) {
|
|
11711
|
+
edgeTooOld(error, `edge:key '${target.key}' for ${org.slug}`);
|
|
11661
11712
|
// Already gone -- the same already-retired key a retire step
|
|
11662
11713
|
// handled before org destroy ever ran can still reach here
|
|
11663
11714
|
// (#317 review); a 404 is success, not a reason to block
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ownHostnames } from './own-hostnames.js';
|
|
|
11
11
|
import { JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY } from './own-jobs-route.js';
|
|
12
12
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
13
13
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
14
|
-
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
14
|
+
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
15
15
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
16
16
|
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
17
17
|
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
|
|
@@ -217,6 +217,9 @@ export function planOwnAuth(options) {
|
|
|
217
217
|
recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
|
|
218
218
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
219
219
|
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
220
|
+
// The login apps' mail env (`LOGIN_MAIL_FROM`, the Lettermint key); a stack from before it is written again.
|
|
221
|
+
recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
|
|
222
|
+
authLoginEnvMarker(`noreply@${names.mailDomain}`) &&
|
|
220
223
|
// The code mail carries the company's name and design; an older apply, or a renamed company, writes it again.
|
|
221
224
|
(options.signinMailWritable === false ||
|
|
222
225
|
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] === signinMailMarker(org.displayName)) &&
|
|
@@ -234,6 +234,21 @@ export interface StepExecutor {
|
|
|
234
234
|
* copy is not recorded. Optional: an executor without it runs no release.
|
|
235
235
|
*/
|
|
236
236
|
releaseHost?(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
237
|
+
/**
|
|
238
|
+
* The built-in fan-out's in-place deploy of one org's Boule or Agora at `sha` (`release/fanout.ts`). Optional:
|
|
239
|
+
* an executor without it runs no fan-out.
|
|
240
|
+
*/
|
|
241
|
+
deployBuiltinAt?(input: {
|
|
242
|
+
app: 'boule' | 'agora';
|
|
243
|
+
org: DeclaredOrg;
|
|
244
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
245
|
+
sha: string;
|
|
246
|
+
}): Promise<{
|
|
247
|
+
ok: true;
|
|
248
|
+
} | {
|
|
249
|
+
ok: false;
|
|
250
|
+
reason: string;
|
|
251
|
+
}>;
|
|
237
252
|
/**
|
|
238
253
|
* One release tick for one subject: the recorded stage gate's list follows the people holding `releases:preview`,
|
|
239
254
|
* owners staying. Never throws. Optional: an executor without it leaves every gate as it is.
|
|
@@ -337,6 +352,8 @@ export interface PlatformStepExecutor {
|
|
|
337
352
|
authExecutorMissing?: boolean;
|
|
338
353
|
/** #216 round 5: see `PlanPlatformOptions.blocked.authOtherInputsMissing`. */
|
|
339
354
|
authOtherInputsMissing?: string[];
|
|
355
|
+
/** The sealed mail key resolved: `planAuth` asks for the login env marker only when it did. */
|
|
356
|
+
authMailKeyHeld?: boolean;
|
|
340
357
|
files?: string;
|
|
341
358
|
integrations?: string;
|
|
342
359
|
ai?: string;
|
|
@@ -933,6 +950,12 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
933
950
|
target: string;
|
|
934
951
|
error?: string;
|
|
935
952
|
}>>;
|
|
953
|
+
/**
|
|
954
|
+
* One fan-out pass for the owner org's built-in `app` (`release/fanout.ts`): the other orgs' copies are put on
|
|
955
|
+
* the commit the owner's copy runs, one after the other, in place, stopping at the first failure. Each state
|
|
956
|
+
* write takes the owner's lease on its own; no lease is held while a deploy waits.
|
|
957
|
+
*/
|
|
958
|
+
private runBuiltinFanOut;
|
|
936
959
|
/**
|
|
937
960
|
* Phase 2 for one org: takes the copy lock of each stage copy request under the org's lease, then runs the
|
|
938
961
|
* copies after the lease is released. While a copy runs, every other tick and apply for the org goes on, a
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -4,6 +4,7 @@ import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
|
4
4
|
import { sfuSlotPorts } from '../calls-sfu.js';
|
|
5
5
|
import { ORG_SERVICE_SCHEMAS, heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
6
6
|
import { PLATFORM_SERVICE_KEYS } from '../platform.js';
|
|
7
|
+
import { runBuiltinFanOut } from '../release/fanout.js';
|
|
7
8
|
import { recordedFrom, releaseKeys } from '../release/state.js';
|
|
8
9
|
import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
9
10
|
import { StaleLeaseError, } from '../store.js';
|
|
@@ -1338,6 +1339,16 @@ export class FoundryPlanApply {
|
|
|
1338
1339
|
settle('platform', e);
|
|
1339
1340
|
}
|
|
1340
1341
|
}
|
|
1342
|
+
// Between the phases: once the owner's Boule or Agora is fully live, copy it to every other org. Its errors
|
|
1343
|
+
// are reported by app, never as an org.
|
|
1344
|
+
for (const app of ['boule', 'agora']) {
|
|
1345
|
+
try {
|
|
1346
|
+
await this.runBuiltinFanOut(app);
|
|
1347
|
+
}
|
|
1348
|
+
catch (e) {
|
|
1349
|
+
settle(`fanout:${app}`, e);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1341
1352
|
// Phase 2: the copies, one subject at a time. A copy takes its lock under the org's lease and runs without it.
|
|
1342
1353
|
for (const { target, run } of copying) {
|
|
1343
1354
|
try {
|
|
@@ -1350,6 +1361,65 @@ export class FoundryPlanApply {
|
|
|
1350
1361
|
}
|
|
1351
1362
|
return results;
|
|
1352
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* One fan-out pass for the owner org's built-in `app` (`release/fanout.ts`): the other orgs' copies are put on
|
|
1366
|
+
* the commit the owner's copy runs, one after the other, in place, stopping at the first failure. Each state
|
|
1367
|
+
* write takes the owner's lease on its own; no lease is held while a deploy waits.
|
|
1368
|
+
*/
|
|
1369
|
+
async runBuiltinFanOut(app) {
|
|
1370
|
+
const store = this.releaseStore;
|
|
1371
|
+
const platform = await this.platformDeps?.store.get();
|
|
1372
|
+
if (!store || !platform || !this.executor.deployBuiltinAt)
|
|
1373
|
+
return;
|
|
1374
|
+
const ownerOrgId = platform.ownerOrgId;
|
|
1375
|
+
const owner = await this.store.get(ownerOrgId);
|
|
1376
|
+
if (!owner)
|
|
1377
|
+
return;
|
|
1378
|
+
const empty = { orgId: ownerOrgId, steps: [], unmanaged: [] };
|
|
1379
|
+
if ((await this.authz.canApplyPlatform?.(this.actor)) !== true) {
|
|
1380
|
+
await this.refusePlatform(empty, ownerOrgId, 'not authorized to run the release tick');
|
|
1381
|
+
throw new NotAuthorizedError(this.actor, ownerOrgId);
|
|
1382
|
+
}
|
|
1383
|
+
const deploy = this.executor.deployBuiltinAt.bind(this.executor);
|
|
1384
|
+
await runBuiltinFanOut({
|
|
1385
|
+
ownerOrgId,
|
|
1386
|
+
app,
|
|
1387
|
+
ownerDeclares: owner[app]?.blueGreen !== undefined,
|
|
1388
|
+
store,
|
|
1389
|
+
readIds: async () => ({
|
|
1390
|
+
...((await this.store.getAppliedState(ownerOrgId))?.serviceIds ?? {}),
|
|
1391
|
+
}),
|
|
1392
|
+
write: (writes) => this.withMutationLease(companyScope(ownerOrgId), (reason) => this.refuse(empty, reason), async (fence) => {
|
|
1393
|
+
const previous = await this.store.getAppliedState(ownerOrgId);
|
|
1394
|
+
if (!previous)
|
|
1395
|
+
throw new Error('the owner org has no applied state');
|
|
1396
|
+
const serviceIds = { ...previous.serviceIds };
|
|
1397
|
+
for (const [key, value] of Object.entries(writes)) {
|
|
1398
|
+
if (value === null)
|
|
1399
|
+
delete serviceIds[key];
|
|
1400
|
+
else
|
|
1401
|
+
serviceIds[key] = value;
|
|
1402
|
+
}
|
|
1403
|
+
await this.store.putAppliedState({ ...previous, serviceIds }, fence);
|
|
1404
|
+
}),
|
|
1405
|
+
targets: async () => {
|
|
1406
|
+
const others = (await this.store.list()).filter((o) => o.id !== ownerOrgId && o[app] !== undefined);
|
|
1407
|
+
return others.map((o) => ({ orgId: o.id, slug: o.slug }));
|
|
1408
|
+
},
|
|
1409
|
+
// Re-reads the org fresh right before its own deploy, never from a list taken when the pass
|
|
1410
|
+
// started: a deploy can wait many minutes with no lease held on the target org (CLAUDE.md,
|
|
1411
|
+
// "Built-in fan-out"), so an ordinary `apply()` to a later-in-order org can turn its own
|
|
1412
|
+
// blue/green on in that window, and `deployBuiltinAt`'s guard must see that, not a stale copy.
|
|
1413
|
+
deploy: async (target, sha) => {
|
|
1414
|
+
const org = await this.store.get(target.orgId);
|
|
1415
|
+
if (!org || org[app] === undefined)
|
|
1416
|
+
return { ok: false, reason: 'org not found' };
|
|
1417
|
+
const recordedIds = (await this.store.getAppliedState(org.id))?.serviceIds ?? {};
|
|
1418
|
+
return deploy({ app, org, recordedIds, sha });
|
|
1419
|
+
},
|
|
1420
|
+
now: this.now,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1353
1423
|
/**
|
|
1354
1424
|
* Phase 2 for one org: takes the copy lock of each stage copy request under the org's lease, then runs the
|
|
1355
1425
|
* copies after the lease is released. While a copy runs, every other tick and apply for the org goes on, a
|
|
@@ -68,6 +68,20 @@ export declare const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = "zitadel_plat
|
|
|
68
68
|
export declare const AUTH_TUNNEL_ROUTES_VERSION = "2";
|
|
69
69
|
/** The {@link AUTH_TUNNEL_ROUTES_VERSION} the sign-in tunnel was last written with. */
|
|
70
70
|
export declare const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = "auth_tunnel_routes_applied";
|
|
71
|
+
/**
|
|
72
|
+
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
73
|
+
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
74
|
+
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
75
|
+
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
76
|
+
*/
|
|
77
|
+
export declare const AUTH_LOGIN_ENV_VERSION = "1";
|
|
78
|
+
/**
|
|
79
|
+
* What the login apps' env is written for: the version and the sender address (`LOGIN_MAIL_FROM`).
|
|
80
|
+
* The address is no secret; a new mail domain gives a new marker, so the step is planned again.
|
|
81
|
+
*/
|
|
82
|
+
export declare function authLoginEnvMarker(smtpSender: string): string;
|
|
83
|
+
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
84
|
+
export declare const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = "auth_login_env_applied";
|
|
71
85
|
/**
|
|
72
86
|
* `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
|
|
73
87
|
* sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
|
|
@@ -977,6 +991,8 @@ export declare const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = "edge_platform_k
|
|
|
977
991
|
*/
|
|
978
992
|
export declare const PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = "edge_applied_hostname";
|
|
979
993
|
export declare const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = "edge_applied_git_ref";
|
|
994
|
+
/** foundry#1024: what `provisionPlatformEdge` last wrote for {@link usageTailEdgeEnv}; see {@link usageTailEdgeEnvValue}. */
|
|
995
|
+
export declare const PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY = "edge_applied_usage_tail";
|
|
980
996
|
/**
|
|
981
997
|
* F4.dns: where edge answers on the Coolify network
|
|
982
998
|
* (`http://<custom_internal_name>:3000`), recorded by the edge step once the
|
|
@@ -1206,6 +1222,45 @@ export declare const PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY = "uptime_access_
|
|
|
1206
1222
|
export declare function uptimeAccessTokenConflict(): string;
|
|
1207
1223
|
/** Whether anything of the uptime Worker is recorded that disabling it must remove. */
|
|
1208
1224
|
export declare function uptimeRecorded(recordedIds: Readonly<Record<string, string>>): boolean;
|
|
1225
|
+
/**
|
|
1226
|
+
* foundry#1024: the usage tail Worker that meters every company Worker's
|
|
1227
|
+
* use, one `cloudflare:worker` step keyed {@link PLATFORM_USAGE_TAIL_KEY}.
|
|
1228
|
+
* `runPlatformSteps` records the script name under
|
|
1229
|
+
* `cloudflare:worker:platform:usage-tail` once it succeeds, which is what
|
|
1230
|
+
* stops `planUsageTail` planning it again.
|
|
1231
|
+
*/
|
|
1232
|
+
export declare const PLATFORM_USAGE_TAIL_KEY = "platform:usage-tail";
|
|
1233
|
+
/** The Worker script's name at Cloudflare. */
|
|
1234
|
+
export declare const PLATFORM_USAGE_TAIL_SCRIPT_NAME = "platform-usage-tail";
|
|
1235
|
+
/**
|
|
1236
|
+
* The held ref of the Worker's one secret, `EDGE_USAGE_KEY` -- the narrow
|
|
1237
|
+
* `usage:ingest` credential edge's ingest route accepts. Minted once (rule
|
|
1238
|
+
* 1): the hash is never secret, so (the same pattern `provisionPlatformEdge`
|
|
1239
|
+
* uses for `EDGE_PLATFORM_KEY_HASH`) it doubles as both the undelivered
|
|
1240
|
+
* marker and the final recorded value, with nothing extra to compute or a
|
|
1241
|
+
* live read that could fall back to a blank value.
|
|
1242
|
+
*/
|
|
1243
|
+
export declare const PLATFORM_USAGE_TAIL_KEY_REF = "usage-tail:worker:key";
|
|
1244
|
+
export declare const PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY = "usage_tail_key_undelivered";
|
|
1245
|
+
export declare const PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY = "usage_tail_key_set";
|
|
1246
|
+
export declare const PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY = "usage_tail_applied_tag";
|
|
1247
|
+
export declare const PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY = "usage_tail_applied_sha256";
|
|
1248
|
+
export declare const PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY = "usage_tail_removed";
|
|
1249
|
+
/**
|
|
1250
|
+
* What `platform:edge`'s own env should hold for the usage tail Worker
|
|
1251
|
+
* (`USAGE_TAIL_WORKER`, `EDGE_USAGE_KEY_HASH`), or `null` while the Worker is
|
|
1252
|
+
* not deployed or its key is not held yet. `provisionPlatformEdge` writes
|
|
1253
|
+
* these two names on every run alongside `EDGE_PLATFORM_KEY_HASH`; `planEdge`
|
|
1254
|
+
* reads {@link usageTailEdgeEnvValue} to replan edge once this changes.
|
|
1255
|
+
*/
|
|
1256
|
+
export declare function usageTailEdgeEnv(recordedIds: Readonly<Record<string, string>>): {
|
|
1257
|
+
USAGE_TAIL_WORKER: string;
|
|
1258
|
+
EDGE_USAGE_KEY_HASH: string;
|
|
1259
|
+
} | null;
|
|
1260
|
+
/** The value `provisionPlatformEdge` records once it has written {@link usageTailEdgeEnv} to edge's env; empty while there is nothing to write. */
|
|
1261
|
+
export declare function usageTailEdgeEnvValue(recordedIds: Readonly<Record<string, string>>): string;
|
|
1262
|
+
/** Whether anything of the usage tail Worker is recorded that disabling it must remove. */
|
|
1263
|
+
export declare function usageTailRecorded(recordedIds: Readonly<Record<string, string>>): boolean;
|
|
1209
1264
|
/**
|
|
1210
1265
|
* Options for {@link planPlatform}, replacing four positional arguments that
|
|
1211
1266
|
* shifted on every merge that added a platform service (#186, #187, #188) --
|
|
@@ -1265,6 +1320,11 @@ export interface PlanPlatformOptions {
|
|
|
1265
1320
|
* these. Default `[]`.
|
|
1266
1321
|
*/
|
|
1267
1322
|
authOtherInputsMissing?: string[];
|
|
1323
|
+
/**
|
|
1324
|
+
* Whether the sealed Lettermint key resolved. `provisionAuthStack` records the login env marker only
|
|
1325
|
+
* then, so `planAuth` asks for the marker only when this is not false. Unset: asked for.
|
|
1326
|
+
*/
|
|
1327
|
+
authMailKeyHeld?: boolean;
|
|
1268
1328
|
/** C3: why the owner's invite cannot be made (no work address, no mail port). The rest of the step still plans. */
|
|
1269
1329
|
authOwnerInvite?: string;
|
|
1270
1330
|
files?: string;
|
|
@@ -1341,6 +1401,8 @@ export interface PlanPlatformOptions {
|
|
|
1341
1401
|
pending: boolean;
|
|
1342
1402
|
blocked?: string;
|
|
1343
1403
|
};
|
|
1404
|
+
/** foundry#1024: why the usage tail Worker is blocked, or absent when it is not. */
|
|
1405
|
+
usageTail?: string;
|
|
1344
1406
|
/** Why the organisation's release webhook step cannot run (the token is not held), or absent. */
|
|
1345
1407
|
githubReleaseHook?: string;
|
|
1346
1408
|
/**
|