@wtfalch/foundry 0.61.0 → 0.62.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/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/sign-in.js +1 -1
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +22 -3
- package/dist/org.js +25 -2
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/blue-green.d.ts +19 -7
- package/dist/plan/blue-green.js +35 -8
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +4 -1
- package/dist/plan/diff.js +180 -63
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/executor.d.ts +40 -18
- package/dist/plan/executor.js +153 -49
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/locked-hostnames.js +5 -2
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -1
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/platform-providers.js +6 -0
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +5 -2
- package/dist/plan/providers-readers.js +20 -1
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/requested-changes.js +2 -1
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +11 -2
- package/dist/store/postgres.js +13 -0
- package/dist/store/schema.d.ts +36 -0
- package/dist/store/schema.js +6 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, HostingGithub, BlueGreenConfig, OrgBuiltinApp, BouleApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryAuthRoom, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, EntryWorkspace, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
1
|
+
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, HostingGithub, BlueGreenConfig, OrgBuiltinApp, BouleApp, KosmosApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryAuthRoom, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, EntryWorkspace, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
2
2
|
export { APP_ENTRY_KEY, APPS_DOMAIN, DEFAULT_GATE_LIMIT, MAX_WORKSPACE_TESTERS, isLiveEntry, normalizeTesters, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, companyGithubFor, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, normalizeHostingRepos, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
3
3
|
export { refuseOrg } from './refuse-org.js';
|
|
4
4
|
export { workerAppHasBuild } from './worker-app-config.js';
|
|
@@ -15,6 +15,8 @@ export type { SlugRejectionReason, SlugRejection } from './slug.js';
|
|
|
15
15
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
16
16
|
export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
|
|
17
17
|
export type { PlanStep, UnmanagedItem, PlanNote, Plan, VersionChangeItem } from './plan.js';
|
|
18
|
+
export type { NativeBuildDispatchDeps, NativeBuildKeyEnvPort, } from './native-build.js';
|
|
19
|
+
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, mintNativeBuildKey, } from './native-build.js';
|
|
18
20
|
export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
|
|
19
21
|
export { StaleLeaseError } from './store.js';
|
|
20
22
|
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
@@ -23,6 +25,8 @@ export { createPostgresAuditLog } from './store/audit.js';
|
|
|
23
25
|
export type { Color, ReleaseAction, ReleaseSignal, ReleaseState, WindowState, } from './release/types.js';
|
|
24
26
|
export type { ReleaseStore } from './release/state.js';
|
|
25
27
|
export { readReleaseState, requestReleaseAction } from './release/state.js';
|
|
28
|
+
export type { DeclaredClassRemoval, MigrationCompatibilityInput, MigrationCompatibilityProblem, MigrationCompatibilityResult, } from './release/migration-compatibility.js';
|
|
29
|
+
export { checkMigrationCompatibility } from './release/migration-compatibility.js';
|
|
26
30
|
export { createPostgresReleaseStore } from './store/postgres.js';
|
|
27
31
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
28
32
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, 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';
|
package/dist/index.js
CHANGED
|
@@ -10,10 +10,12 @@ export const PINNED_VERSIONS = VERSIONS;
|
|
|
10
10
|
export { buildJsonSchema, parseBuildJson, BuildJsonError } from './build-json.js';
|
|
11
11
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
12
12
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
13
|
+
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, mintNativeBuildKey, } from './native-build.js';
|
|
13
14
|
export { StaleLeaseError } from './store.js';
|
|
14
15
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
15
16
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
16
17
|
export { readReleaseState, requestReleaseAction } from './release/state.js';
|
|
18
|
+
export { checkMigrationCompatibility } from './release/migration-compatibility.js';
|
|
17
19
|
export { createPostgresReleaseStore } from './store/postgres.js';
|
|
18
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, foundryRequestedChanges, } from './store/schema.js';
|
|
19
21
|
export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- @wtfalch/foundry: a company's native app settings and its native build runs (foundry#948, estate
|
|
2
|
+
-- `.plans/2026-10-22-app-shells.md`). Additive, like 0060_hosting_repos.sql and
|
|
3
|
+
-- 0055_release_requests.sql: a column with a default plus a new table, so a row written before this
|
|
4
|
+
-- reads back `[]` and an older reader ignores both. Safe on live rows when the container starts.
|
|
5
|
+
--
|
|
6
|
+
-- foundry#951: Boule owns the native settings, build runs and store keys now (estate#262) -- the
|
|
7
|
+
-- column and the table below are unused by this package's code from here on, but foundry#951 also
|
|
8
|
+
-- establishes that a migration only adds in a release where idle and live share a database, so
|
|
9
|
+
-- neither is dropped here. A later release drops both.
|
|
10
|
+
|
|
11
|
+
alter table foundry_declared_orgs
|
|
12
|
+
add column if not exists hosting_native_apps jsonb not null default '[]'::jsonb;
|
|
13
|
+
|
|
14
|
+
create table if not exists foundry_native_build_runs (
|
|
15
|
+
id bigserial primary key,
|
|
16
|
+
org_id text not null,
|
|
17
|
+
app text not null,
|
|
18
|
+
state text not null default 'queued',
|
|
19
|
+
log text,
|
|
20
|
+
created_at timestamptz not null default now(),
|
|
21
|
+
updated_at timestamptz not null default now()
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
alter table foundry_native_build_runs
|
|
25
|
+
drop constraint if exists foundry_native_build_runs_state_check;
|
|
26
|
+
alter table foundry_native_build_runs
|
|
27
|
+
add constraint foundry_native_build_runs_state_check
|
|
28
|
+
check (state in ('queued', 'building', 'uploaded', 'failed'));
|
|
29
|
+
|
|
30
|
+
-- Listing one app's runs, and (while foundry still checked it, before foundry#951) the `uploaded` look-up.
|
|
31
|
+
create index if not exists foundry_native_build_runs_app_idx
|
|
32
|
+
on foundry_native_build_runs (org_id, app, id desc);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- @wtfalch/foundry: `kosmos_enabled` is `org.kosmos.enabled` (foundry#949): the company's own kosmos is set up
|
|
2
|
+
-- only when it asks. `kosmos_blue_green` is `org.kosmos.blueGreen` ({ steps?, stepHoldMinutes? }, as the other
|
|
3
|
+
-- two built-ins' own columns hold it) -- kept even though kosmos always runs blue/green once enabled, so tuning
|
|
4
|
+
-- its steps needs no new column later. Both default to off. Additive like 0059_org_builtin_blue_green.sql and
|
|
5
|
+
-- 0063_boule_apps.sql: `if not exists`, safe to run at container start on live rows.
|
|
6
|
+
-- Mirrors src/store/schema.ts's `declaredOrgs`.
|
|
7
|
+
|
|
8
|
+
alter table foundry_declared_orgs
|
|
9
|
+
add column if not exists kosmos_enabled boolean not null default false,
|
|
10
|
+
add column if not exists kosmos_blue_green jsonb;
|
package/dist/modules/sign-in.js
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wtfalch/shell`'s `build-app.yml`: the one GitHub Actions workflow that
|
|
3
|
+
* builds and signs a company's native app shell (estate
|
|
4
|
+
* `.plans/2026-10-22-app-shells.md`). Boule now owns the `NativeAppConfig`,
|
|
5
|
+
* the build runs and the company's store keys (estate#262, foundry#951);
|
|
6
|
+
* foundry keeps only two things, both in this file: dispatching the
|
|
7
|
+
* workflow, and the key Boule presents when it asks the host (Archon) to
|
|
8
|
+
* dispatch it.
|
|
9
|
+
*
|
|
10
|
+
* Neither function here is wired into `plan/diff.ts` or `plan/executor.ts`:
|
|
11
|
+
* `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
|
|
12
|
+
* route, not foundry's apply loop, and `mintNativeBuildKey` is left for the
|
|
13
|
+
* host to call from wherever it writes Boule's env (`executor.ts`'s
|
|
14
|
+
* `coolifyBoule`, the same way it already calls `mintIfAbsent` for
|
|
15
|
+
* `BOULE_OWNER_SEED_SECRET_ENV`) once that wiring has its own review --
|
|
16
|
+
* adding it here would touch every one of this package's Boule-convergence
|
|
17
|
+
* fixtures for one new env var, which is a larger change than this issue
|
|
18
|
+
* asks for.
|
|
19
|
+
*/
|
|
20
|
+
export declare const NATIVE_BUILD_REPO = "wtfalch/shell";
|
|
21
|
+
export declare const NATIVE_BUILD_WORKFLOW_FILE = "build-app.yml";
|
|
22
|
+
export declare const NATIVE_BUILD_WORKFLOW_REF = "main";
|
|
23
|
+
/** Every GitHub call this module makes is cut off after this long. */
|
|
24
|
+
export declare const NATIVE_BUILD_DISPATCH_TIMEOUT_MS = 20000;
|
|
25
|
+
/** A `build-app.yml` dispatch that failed. The text is fixed: it never holds the token or a response body. */
|
|
26
|
+
export declare class NativeBuildDispatchError extends Error {
|
|
27
|
+
readonly status: number | null;
|
|
28
|
+
constructor(status: number | null);
|
|
29
|
+
}
|
|
30
|
+
export interface NativeBuildDispatchDeps {
|
|
31
|
+
/** A GitHub token that may dispatch workflows in {@link NATIVE_BUILD_REPO}. */
|
|
32
|
+
token: string;
|
|
33
|
+
/** Global fetch by default; tests give their own. */
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Dispatches `build-app.yml` with exactly `{ runId, origin }` as its inputs:
|
|
39
|
+
* `runId` is the run the caller (Archon, behind its own `/api/native/builds`
|
|
40
|
+
* route) already made, `origin` is the company's Boule address the job
|
|
41
|
+
* reports its status back to and trades its OIDC token for keys at. Foundry
|
|
42
|
+
* keeps no run store of its own any more -- Boule does -- so this is the one
|
|
43
|
+
* thing left for foundry to do with a run: start it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function dispatchNativeBuild(deps: NativeBuildDispatchDeps, run: {
|
|
46
|
+
runId: string;
|
|
47
|
+
origin: string;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
/** The env name Boule presents this key under, to Archon's native-build route. */
|
|
50
|
+
export declare const NATIVE_BUILD_KEY_ENV = "NATIVE_BUILD_KEY";
|
|
51
|
+
export declare const NATIVE_BUILD_KEY_BYTES = 32;
|
|
52
|
+
/** The one thing {@link mintNativeBuildKey} needs from Coolify: a live app's env, to check whether it already has a key. */
|
|
53
|
+
export interface NativeBuildKeyEnvPort {
|
|
54
|
+
listEnvs(appId: string): Promise<ReadonlyArray<{
|
|
55
|
+
key: string;
|
|
56
|
+
value: string;
|
|
57
|
+
is_preview: boolean;
|
|
58
|
+
}>>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Mints the per-company key Boule presents to Archon's native-build route
|
|
62
|
+
* (estate#262), the same way every other per-company key in this codebase is
|
|
63
|
+
* minted (estate#207): a random value, written to Boule's env once and never
|
|
64
|
+
* rotated, kept unchanged on every later apply once it is there. Returns
|
|
65
|
+
* `null` when Boule's env already holds one -- the caller then writes
|
|
66
|
+
* nothing, so the live value stands. `onSecret`, when given, is told the
|
|
67
|
+
* minted value so the caller can keep it out of its own logs and error
|
|
68
|
+
* messages.
|
|
69
|
+
*/
|
|
70
|
+
export declare function mintNativeBuildKey(coolify: NativeBuildKeyEnvPort, appId: string, onSecret?: (value: string) => void): Promise<string | null>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* `wtfalch/shell`'s `build-app.yml`: the one GitHub Actions workflow that
|
|
4
|
+
* builds and signs a company's native app shell (estate
|
|
5
|
+
* `.plans/2026-10-22-app-shells.md`). Boule now owns the `NativeAppConfig`,
|
|
6
|
+
* the build runs and the company's store keys (estate#262, foundry#951);
|
|
7
|
+
* foundry keeps only two things, both in this file: dispatching the
|
|
8
|
+
* workflow, and the key Boule presents when it asks the host (Archon) to
|
|
9
|
+
* dispatch it.
|
|
10
|
+
*
|
|
11
|
+
* Neither function here is wired into `plan/diff.ts` or `plan/executor.ts`:
|
|
12
|
+
* `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
|
|
13
|
+
* route, not foundry's apply loop, and `mintNativeBuildKey` is left for the
|
|
14
|
+
* host to call from wherever it writes Boule's env (`executor.ts`'s
|
|
15
|
+
* `coolifyBoule`, the same way it already calls `mintIfAbsent` for
|
|
16
|
+
* `BOULE_OWNER_SEED_SECRET_ENV`) once that wiring has its own review --
|
|
17
|
+
* adding it here would touch every one of this package's Boule-convergence
|
|
18
|
+
* fixtures for one new env var, which is a larger change than this issue
|
|
19
|
+
* asks for.
|
|
20
|
+
*/
|
|
21
|
+
export const NATIVE_BUILD_REPO = 'wtfalch/shell';
|
|
22
|
+
export const NATIVE_BUILD_WORKFLOW_FILE = 'build-app.yml';
|
|
23
|
+
export const NATIVE_BUILD_WORKFLOW_REF = 'main';
|
|
24
|
+
/** Every GitHub call this module makes is cut off after this long. */
|
|
25
|
+
export const NATIVE_BUILD_DISPATCH_TIMEOUT_MS = 20_000;
|
|
26
|
+
/** A `build-app.yml` dispatch that failed. The text is fixed: it never holds the token or a response body. */
|
|
27
|
+
export class NativeBuildDispatchError extends Error {
|
|
28
|
+
status;
|
|
29
|
+
constructor(status) {
|
|
30
|
+
super(status === null
|
|
31
|
+
? `dispatching ${NATIVE_BUILD_WORKFLOW_FILE} did not answer`
|
|
32
|
+
: `dispatching ${NATIVE_BUILD_WORKFLOW_FILE} failed with status ${status}`);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.name = 'NativeBuildDispatchError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Dispatches `build-app.yml` with exactly `{ runId, origin }` as its inputs:
|
|
39
|
+
* `runId` is the run the caller (Archon, behind its own `/api/native/builds`
|
|
40
|
+
* route) already made, `origin` is the company's Boule address the job
|
|
41
|
+
* reports its status back to and trades its OIDC token for keys at. Foundry
|
|
42
|
+
* keeps no run store of its own any more -- Boule does -- so this is the one
|
|
43
|
+
* thing left for foundry to do with a run: start it.
|
|
44
|
+
*/
|
|
45
|
+
export async function dispatchNativeBuild(deps, run) {
|
|
46
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
47
|
+
const timeoutMs = deps.timeoutMs ?? NATIVE_BUILD_DISPATCH_TIMEOUT_MS;
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await doFetch(`https://api.github.com/repos/${NATIVE_BUILD_REPO}/actions/workflows/${NATIVE_BUILD_WORKFLOW_FILE}/dispatches`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${deps.token}`,
|
|
54
|
+
Accept: 'application/vnd.github+json',
|
|
55
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
ref: NATIVE_BUILD_WORKFLOW_REF,
|
|
60
|
+
inputs: { runId: run.runId, origin: run.origin },
|
|
61
|
+
}),
|
|
62
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`native build: dispatch did not answer (${error instanceof Error ? error.name : 'error'})`);
|
|
67
|
+
throw new NativeBuildDispatchError(null);
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok)
|
|
70
|
+
throw new NativeBuildDispatchError(response.status);
|
|
71
|
+
}
|
|
72
|
+
/** The env name Boule presents this key under, to Archon's native-build route. */
|
|
73
|
+
export const NATIVE_BUILD_KEY_ENV = 'NATIVE_BUILD_KEY';
|
|
74
|
+
export const NATIVE_BUILD_KEY_BYTES = 32;
|
|
75
|
+
/**
|
|
76
|
+
* Mints the per-company key Boule presents to Archon's native-build route
|
|
77
|
+
* (estate#262), the same way every other per-company key in this codebase is
|
|
78
|
+
* minted (estate#207): a random value, written to Boule's env once and never
|
|
79
|
+
* rotated, kept unchanged on every later apply once it is there. Returns
|
|
80
|
+
* `null` when Boule's env already holds one -- the caller then writes
|
|
81
|
+
* nothing, so the live value stands. `onSecret`, when given, is told the
|
|
82
|
+
* minted value so the caller can keep it out of its own logs and error
|
|
83
|
+
* messages.
|
|
84
|
+
*/
|
|
85
|
+
export async function mintNativeBuildKey(coolify, appId, onSecret) {
|
|
86
|
+
const envs = await coolify.listEnvs(appId);
|
|
87
|
+
if (envs.some((e) => e.key === NATIVE_BUILD_KEY_ENV && !e.is_preview))
|
|
88
|
+
return null;
|
|
89
|
+
const value = randomBytes(NATIVE_BUILD_KEY_BYTES).toString('base64url');
|
|
90
|
+
onSecret?.(value);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
package/dist/org.d.ts
CHANGED
|
@@ -301,7 +301,7 @@ export type AccessRule = {
|
|
|
301
301
|
/** The key of the entry that is today's one app: the Coolify app named `<slug>`. */
|
|
302
302
|
export declare const APP_ENTRY_KEY = "app";
|
|
303
303
|
/** Why a validator refused. `code` is stable and never reworded; `message` is for people. */
|
|
304
|
-
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'hosting_github_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'hosted_name_invalid' | 'hosted_name_not_unique' | 'hosted_name_worker_app' | 'entry_workspace_invalid' | 'entry_workspace_unsupported' | 'entry_live_needs_workspace' | 'entry_live_commit_invalid' | 'gate_key_taken_by_workspace' | 'gate_key_taken_by_private_page' | 'private_page_invalid' | 'private_page_path_invalid' | 'private_page_duplicate' | 'private_page_limit' | 'private_page_needs_hosting' | 'private_page_roles_invalid' | 'private_page_people_invalid' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'blue_green_advance_invalid' | 'blue_green_min_requests_invalid' | 'blue_green_error_margin_invalid' | 'blue_green_staff_first_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'workspaces_needs_customers' | 'workspaces_needs_server' | 'workspaces_roles_invalid' | 'workspaces_role_reserved' | 'workspaces_invite_path_invalid' | 'workspaces_env_clash' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_needs_own_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'mail_not_true' | 'mail_needs_hosting' | 'mail_env_clash' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'customer_grants_not_true' | 'customer_grants_needs_customers' | 'customer_grants_env_clash' | 'repo_use_invalid' | 'repo_use_needs_repo' | 'repo_use_env_clash' | 'audit_not_true' | 'audit_needs_hosting' | 'audit_needs_worker_database' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
304
|
+
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'issuer_app_name_reserved_kosmos' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'hosting_github_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'hosted_name_invalid' | 'hosted_name_not_unique' | 'hosted_name_worker_app' | 'entry_workspace_invalid' | 'entry_workspace_unsupported' | 'entry_live_needs_workspace' | 'entry_live_commit_invalid' | 'gate_key_taken_by_workspace' | 'gate_key_taken_by_private_page' | 'private_page_invalid' | 'private_page_path_invalid' | 'private_page_duplicate' | 'private_page_limit' | 'private_page_needs_hosting' | 'private_page_roles_invalid' | 'private_page_people_invalid' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'blue_green_advance_invalid' | 'blue_green_min_requests_invalid' | 'blue_green_error_margin_invalid' | 'blue_green_staff_first_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'workspaces_needs_customers' | 'workspaces_needs_server' | 'workspaces_roles_invalid' | 'workspaces_role_reserved' | 'workspaces_invite_path_invalid' | 'workspaces_env_clash' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_needs_own_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'mail_not_true' | 'mail_needs_hosting' | 'mail_env_clash' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'customer_grants_not_true' | 'customer_grants_needs_customers' | 'customer_grants_env_clash' | 'repo_use_invalid' | 'repo_use_needs_repo' | 'repo_use_env_clash' | 'audit_not_true' | 'audit_needs_hosting' | 'audit_needs_worker_database' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
305
305
|
/**
|
|
306
306
|
* One refusal: a stable `code`, the dotted `path` into {@link DeclaredOrg} it is about
|
|
307
307
|
* (`hosting.entries.terminal.uses.auth.registration`) and the sentence for people.
|
|
@@ -484,6 +484,18 @@ export interface BouleApp extends OrgBuiltinApp {
|
|
|
484
484
|
}
|
|
485
485
|
/** True while the company's Boule runs apps (`org.boule.apps`). */
|
|
486
486
|
export declare const appsOn: (org: Pick<DeclaredOrg, "boule">) => boolean;
|
|
487
|
+
/**
|
|
488
|
+
* The company's kosmos: its own creative workspace (canvas, libraries, photos, pages) that drives the
|
|
489
|
+
* frontend of its apps. Unlike Boule and Agora, which every company runs, kosmos is set up only when the
|
|
490
|
+
* company asks (William, 2026-10-22) -- absent, or `enabled` absent or `false`, plans nothing for it.
|
|
491
|
+
* `enabled: true` plans its ZITADEL client, its database and its live and idle copies; turning it back off
|
|
492
|
+
* parks the app but keeps the database, the same rule every other switched-off service follows.
|
|
493
|
+
*/
|
|
494
|
+
export interface KosmosApp extends OrgBuiltinApp {
|
|
495
|
+
enabled?: boolean;
|
|
496
|
+
}
|
|
497
|
+
/** True while the company's kosmos is turned on (`org.kosmos.enabled`). */
|
|
498
|
+
export declare const kosmosOn: (org: Pick<DeclaredOrg, "kosmos">) => boolean;
|
|
487
499
|
/** The traffic steps (percent to the new color) and the hold at each step. Both default in `plan/blue-green.ts`. */
|
|
488
500
|
export interface BlueGreenConfig {
|
|
489
501
|
/** Ascending, each 1..100, ending at 100. */
|
|
@@ -798,8 +810,13 @@ export declare function refuseHosting(hosting: HostingConfig, names?: HostingNam
|
|
|
798
810
|
export declare function companyGithubFor(hosting: Pick<HostingConfig, 'github'>, repo: string | null): HostingGithub | undefined;
|
|
799
811
|
/** `blueGreen`: steps ascending, each a whole number 1..100, the last 100; a hold of 1..1440 minutes. */
|
|
800
812
|
export declare function validateBlueGreen(value: unknown, at: string): string | null;
|
|
801
|
-
/**
|
|
802
|
-
|
|
813
|
+
/**
|
|
814
|
+
* {@link refuseBlueGreen} for the `blueGreen` of the company's Boule, Agora and kosmos; the same codes.
|
|
815
|
+
* Also refuses an `issuerApps` entry named `kosmos` while kosmos is on -- the same reservation
|
|
816
|
+
* {@link refuseHosting}'s own loop already gives Agora and Boule, but kosmos is opt-in, so it is checked here
|
|
817
|
+
* instead, the same way {@link validateSite} reserves `cms` only while `site.enabled` is true.
|
|
818
|
+
*/
|
|
819
|
+
export declare function refuseBuiltinBlueGreen(org: Pick<DeclaredOrg, 'boule' | 'agora' | 'kosmos' | 'hosting'>): OrgRefusal | null;
|
|
803
820
|
/** The env names `uses.repo` writes; `env` may not name them. */
|
|
804
821
|
export declare const REPO_ENTRY_ENV_NAMES: readonly ["GIT_REPO_URL", "GIT_SSH_KEY"];
|
|
805
822
|
/** The env name `uses.customerGrants` writes; `env` may not name it. */
|
|
@@ -1226,6 +1243,8 @@ export interface DeclaredOrg {
|
|
|
1226
1243
|
boule?: BouleApp;
|
|
1227
1244
|
/** The company's Agora, a static app: its release moves on hard fails and time only. Absent means no blue/green. */
|
|
1228
1245
|
agora?: OrgBuiltinApp;
|
|
1246
|
+
/** The company's kosmos. Absent or `enabled: false` means it is not set up (2026-10-22). */
|
|
1247
|
+
kosmos?: KosmosApp;
|
|
1229
1248
|
/** Absent means `{ enabled: false, signUp: false }`. Optional so every existing literal and fixture compiles unchanged. */
|
|
1230
1249
|
customers?: CustomersConfig;
|
|
1231
1250
|
/** Parts of this company held on their own version. Absent or null follows the platform. Ignored for the owner company. */
|
package/dist/org.js
CHANGED
|
@@ -33,6 +33,8 @@ export function normalizeTesters(testers) {
|
|
|
33
33
|
}
|
|
34
34
|
/** True while the company's Boule runs apps (`org.boule.apps`). */
|
|
35
35
|
export const appsOn = (org) => org.boule?.apps === true;
|
|
36
|
+
/** True while the company's kosmos is turned on (`org.kosmos.enabled`). */
|
|
37
|
+
export const kosmosOn = (org) => org.kosmos?.enabled === true;
|
|
36
38
|
/** The most volumes one entry may declare. */
|
|
37
39
|
export const MAX_ENTRY_VOLUMES = 8;
|
|
38
40
|
/** The env names `uses.auth.workspaces` writes; `env` may not name them. */
|
|
@@ -763,9 +765,14 @@ function refuseBlueGreen(value, at, path) {
|
|
|
763
765
|
}
|
|
764
766
|
return null;
|
|
765
767
|
}
|
|
766
|
-
/**
|
|
768
|
+
/**
|
|
769
|
+
* {@link refuseBlueGreen} for the `blueGreen` of the company's Boule, Agora and kosmos; the same codes.
|
|
770
|
+
* Also refuses an `issuerApps` entry named `kosmos` while kosmos is on -- the same reservation
|
|
771
|
+
* {@link refuseHosting}'s own loop already gives Agora and Boule, but kosmos is opt-in, so it is checked here
|
|
772
|
+
* instead, the same way {@link validateSite} reserves `cms` only while `site.enabled` is true.
|
|
773
|
+
*/
|
|
767
774
|
export function refuseBuiltinBlueGreen(org) {
|
|
768
|
-
for (const app of ['boule', 'agora']) {
|
|
775
|
+
for (const app of ['boule', 'agora', 'kosmos']) {
|
|
769
776
|
const value = org[app];
|
|
770
777
|
if (value === undefined)
|
|
771
778
|
continue;
|
|
@@ -781,6 +788,15 @@ export function refuseBuiltinBlueGreen(org) {
|
|
|
781
788
|
? 'boule.apps must be true or false'
|
|
782
789
|
: `${app} has no apps switch: only the company's Boule runs apps`);
|
|
783
790
|
}
|
|
791
|
+
const enabled = value.enabled;
|
|
792
|
+
if (enabled !== undefined && (app !== 'kosmos' || typeof enabled !== 'boolean')) {
|
|
793
|
+
return refusal('builtin_app_invalid', `${app}.enabled`, app === 'kosmos'
|
|
794
|
+
? 'kosmos.enabled must be true or false'
|
|
795
|
+
: `${app} has no enabled switch: it is always on`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (kosmosOn(org) && org.hosting.issuerApps.some((app) => app.name === 'kosmos')) {
|
|
799
|
+
return refusal('issuer_app_name_reserved_kosmos', 'hosting.issuerApps.kosmos.name', "kosmos.enabled reserves the issuerApps name 'kosmos'");
|
|
784
800
|
}
|
|
785
801
|
return null;
|
|
786
802
|
}
|
|
@@ -899,6 +915,7 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
899
915
|
if (entry.key === 'agora' ||
|
|
900
916
|
entry.key === 'boule' ||
|
|
901
917
|
entry.key === 'cms' ||
|
|
918
|
+
entry.key === 'kosmos' ||
|
|
902
919
|
hosting.issuerApps.some((app) => app.name === entry.key)) {
|
|
903
920
|
return refusal('client_name_taken', `hosting.entries.${entry.key}.key`, `${at} client name '${entry.key}' is already held by another ZITADEL client`);
|
|
904
921
|
}
|
|
@@ -1881,6 +1898,8 @@ function orgServiceSchema(service, schema, legacyKey, split = true) {
|
|
|
1881
1898
|
export const ORG_SERVICE_SCHEMAS = [
|
|
1882
1899
|
orgServiceSchema('Boule', 'boule', () => 'coolify_boule_database'),
|
|
1883
1900
|
orgServiceSchema('CMS', 'cms', () => 'coolify_cms_database'),
|
|
1901
|
+
// kosmos, like CMS, sits behind its own feature switch (`KosmosApp.enabled`); see `orgServiceSchemaEnabled`.
|
|
1902
|
+
orgServiceSchema('kosmos', 'kosmos', () => 'coolify_kosmos_database'),
|
|
1884
1903
|
orgServiceSchema("chat's Synapse homeserver", 'chat_synapse', () => 'coolify_chat_synapse_database', false),
|
|
1885
1904
|
orgServiceSchema("chat's matrix-authentication-service", 'chat_mas', () => 'coolify_chat_mas_database', false),
|
|
1886
1905
|
orgServiceSchema("chat's own service", 'chat_service', () => 'coolify_chat_service_database'),
|
|
@@ -1914,6 +1933,10 @@ export function orgServiceDatabase(org, home) {
|
|
|
1914
1933
|
database = underscored(`${org.slug}-cms-db`);
|
|
1915
1934
|
runtimeRole = 'cms_rt';
|
|
1916
1935
|
break;
|
|
1936
|
+
case 'kosmos':
|
|
1937
|
+
database = underscored(`${org.slug}-kosmos-db`);
|
|
1938
|
+
runtimeRole = 'kosmos_rt';
|
|
1939
|
+
break;
|
|
1917
1940
|
case 'chat_service':
|
|
1918
1941
|
database = 'chat';
|
|
1919
1942
|
break;
|
package/dist/plan/agora.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeclaredOrg, type OrgId } from '../org.js';
|
|
2
2
|
/**
|
|
3
3
|
* #164: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
4
4
|
* need to agree on for an org's Agora -- the hostname, the values the host
|
package/dist/plan/agora.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { kosmosOn } from '../org.js';
|
|
1
2
|
import { chatUpstreams } from './chat-matrix.js';
|
|
2
3
|
import { FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
|
|
4
|
+
import { kosmosHostnameFor } from './kosmos.js';
|
|
3
5
|
import { SIGN_IN_UPSTREAM_ENV, inAppHostApplied, signInUpstream, staleSignInUpstream, } from './sign-in.js';
|
|
4
6
|
/**
|
|
5
7
|
* #164: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
@@ -307,6 +309,8 @@ export function desiredAgoraEnv(org, recorded, rawHost) {
|
|
|
307
309
|
chat: { enabled: org.chat.enabled },
|
|
308
310
|
// Agora draws call buttons only when this is true.
|
|
309
311
|
calls: { enabled: org.calls.enabled },
|
|
312
|
+
// foundry#949: kosmos is opt-in, so Agora is told both whether it is on and where it lives.
|
|
313
|
+
kosmos: { enabled: kosmosOn(org), url: `https://${kosmosHostnameFor(org)}` },
|
|
310
314
|
// In-app sign-in: Agora reads this at runtime (one image serves every company). Set with `SIGNIN_UPSTREAM`
|
|
311
315
|
// only, so Agora never shows the in-app screen before the login can serve it.
|
|
312
316
|
...(inAppHostApplied(org, recorded, agoraHostnameFor(org)) ? { signInInApp: true } : {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BlueGreenConfig, type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
2
2
|
import { type StageKind } from './stage-isolation.js';
|
|
3
3
|
/**
|
|
4
4
|
* Blue/green releases (docs/adr/0001-blue-green-releases.md). A color is a property any app can have
|
|
@@ -164,11 +164,14 @@ export interface BlueGreenSubject {
|
|
|
164
164
|
* (address check, crash loop) and moves up by time.
|
|
165
165
|
*/
|
|
166
166
|
reporting: boolean;
|
|
167
|
-
/** Set for Boule and
|
|
167
|
+
/** Set for Boule, Agora and kosmos, which are not hosting entries. */
|
|
168
168
|
builtin?: BuiltinApp;
|
|
169
169
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
170
|
+
/**
|
|
171
|
+
* The apps foundry runs for a company that can declare blue/green: Boule and Agora, every company's own, plus
|
|
172
|
+
* kosmos once the company turns it on (`org.boule`, `org.agora`, `org.kosmos`; docs/adr/0001, rule 1).
|
|
173
|
+
*/
|
|
174
|
+
export type BuiltinApp = 'boule' | 'agora' | 'kosmos';
|
|
172
175
|
export declare const BUILTIN_APPS: readonly BuiltinApp[];
|
|
173
176
|
/** What foundry builds each built-in app from; `executor.ts` creates the app from the same values. */
|
|
174
177
|
export declare const BUILTIN_APP: {
|
|
@@ -188,6 +191,14 @@ export declare const BUILTIN_APP: {
|
|
|
188
191
|
readonly database: false;
|
|
189
192
|
readonly reporting: false;
|
|
190
193
|
};
|
|
194
|
+
readonly kosmos: {
|
|
195
|
+
readonly repo: "wtfalch/kosmos";
|
|
196
|
+
readonly dockerfile: "/Dockerfile";
|
|
197
|
+
readonly port: 3000;
|
|
198
|
+
readonly healthPath: "/api/health";
|
|
199
|
+
readonly database: true;
|
|
200
|
+
readonly reporting: true;
|
|
201
|
+
};
|
|
191
202
|
};
|
|
192
203
|
/**
|
|
193
204
|
* A hosting entry that declares `blueGreen`, as a subject; `null` when it does not or cannot run.
|
|
@@ -195,11 +206,12 @@ export declare const BUILTIN_APP: {
|
|
|
195
206
|
*/
|
|
196
207
|
export declare function entrySubject(org: DeclaredOrg, entry: HostingEntry, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
|
|
197
208
|
/**
|
|
198
|
-
* The company's Boule or
|
|
199
|
-
* its name `<slug>-<app>` and is blue, like
|
|
209
|
+
* The company's Boule, Agora or kosmos as a subject; `null` when it does not declare blue/green (see
|
|
210
|
+
* {@link builtinDeclaresBlueGreen}). The running app keeps its name `<slug>-<app>` and is blue, like
|
|
211
|
+
* platform-archon. All three run on the primary server only.
|
|
200
212
|
*/
|
|
201
213
|
export declare function builtinSubject(org: DeclaredOrg, app: BuiltinApp, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
|
|
202
|
-
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule and
|
|
214
|
+
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule, Agora and kosmos. */
|
|
203
215
|
export declare function blueGreenSubjects(org: DeclaredOrg, recorded?: Readonly<Record<string, string>>): BlueGreenSubject[];
|
|
204
216
|
/** Whether the org declares blue/green for any of its apps. */
|
|
205
217
|
export declare const declaresBlueGreen: (org: DeclaredOrg) => boolean;
|
package/dist/plan/blue-green.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { VERSIONS, pinnedRef } from '../bootstrap/versions.js';
|
|
3
|
+
import { kosmosOn } from '../org.js';
|
|
3
4
|
import { agoraHostnameFor } from './agora.js';
|
|
4
5
|
import { bouleHostnameFor } from './boule.js';
|
|
5
6
|
import { serverConnectorScopeKey } from './cloudflared-connector.js';
|
|
6
7
|
import { entryHostnames, tunnelIngressHostnames, wildcardHostname } from './hostnames.js';
|
|
8
|
+
import { kosmosHostnameFor } from './kosmos.js';
|
|
7
9
|
import { appNameFor, appStepKey } from './platform-diff.js';
|
|
8
10
|
import { remoteDatabaseAppliedFor } from './remote-database.js';
|
|
9
11
|
import { buildStageEnv } from './stage-env.js';
|
|
@@ -165,7 +167,7 @@ export function reportingEnv(input) {
|
|
|
165
167
|
{ key: REPORTING_ENV.healthToken, value: input.healthToken },
|
|
166
168
|
];
|
|
167
169
|
}
|
|
168
|
-
export const BUILTIN_APPS = ['boule', 'agora'];
|
|
170
|
+
export const BUILTIN_APPS = ['boule', 'agora', 'kosmos'];
|
|
169
171
|
/** What foundry builds each built-in app from; `executor.ts` creates the app from the same values. */
|
|
170
172
|
export const BUILTIN_APP = {
|
|
171
173
|
boule: {
|
|
@@ -185,7 +187,31 @@ export const BUILTIN_APP = {
|
|
|
185
187
|
database: false,
|
|
186
188
|
reporting: false,
|
|
187
189
|
},
|
|
190
|
+
kosmos: {
|
|
191
|
+
repo: 'wtfalch/kosmos',
|
|
192
|
+
dockerfile: '/Dockerfile',
|
|
193
|
+
port: 3000,
|
|
194
|
+
healthPath: '/api/health',
|
|
195
|
+
database: true,
|
|
196
|
+
reporting: true,
|
|
197
|
+
},
|
|
188
198
|
};
|
|
199
|
+
/** `builtinSubject`'s hostname for each built-in app, kept beside {@link BUILTIN_APP} rather than a third switch. */
|
|
200
|
+
function builtinHostnameFor(org, app) {
|
|
201
|
+
if (app === 'boule')
|
|
202
|
+
return bouleHostnameFor(org);
|
|
203
|
+
if (app === 'agora')
|
|
204
|
+
return agoraHostnameFor(org);
|
|
205
|
+
return kosmosHostnameFor(org);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Whether `org` declares blue/green for a built-in app: Boule and Agora opt into it through their own
|
|
209
|
+
* `blueGreen` field (they already run as a single copy otherwise); kosmos has no single-copy mode -- turning
|
|
210
|
+
* it on (`org.kosmos.enabled`) always runs it blue/green, docs/adr/0001 rule 1.
|
|
211
|
+
*/
|
|
212
|
+
function builtinDeclaresBlueGreen(org, app) {
|
|
213
|
+
return app === 'kosmos' ? kosmosOn(org) : org[app]?.blueGreen !== undefined;
|
|
214
|
+
}
|
|
189
215
|
/**
|
|
190
216
|
* A hosting entry that declares `blueGreen`, as a subject; `null` when it does not or cannot run.
|
|
191
217
|
* `recorded` names the blue app: an app that ran before blue/green keeps its own name (see {@link liveAppNameFor}).
|
|
@@ -213,13 +239,14 @@ export function entrySubject(org, entry, recorded) {
|
|
|
213
239
|
};
|
|
214
240
|
}
|
|
215
241
|
/**
|
|
216
|
-
* The company's Boule or
|
|
217
|
-
* its name `<slug>-<app>` and is blue, like
|
|
242
|
+
* The company's Boule, Agora or kosmos as a subject; `null` when it does not declare blue/green (see
|
|
243
|
+
* {@link builtinDeclaresBlueGreen}). The running app keeps its name `<slug>-<app>` and is blue, like
|
|
244
|
+
* platform-archon. All three run on the primary server only.
|
|
218
245
|
*/
|
|
219
246
|
export function builtinSubject(org, app, recorded = {}) {
|
|
220
|
-
|
|
221
|
-
if (!config)
|
|
247
|
+
if (!builtinDeclaresBlueGreen(org, app))
|
|
222
248
|
return null;
|
|
249
|
+
const config = org[app]?.blueGreen ?? {};
|
|
223
250
|
const spec = BUILTIN_APP[app];
|
|
224
251
|
const base = `${org.slug}-${app}`;
|
|
225
252
|
return {
|
|
@@ -233,7 +260,7 @@ export function builtinSubject(org, app, recorded = {}) {
|
|
|
233
260
|
buildPack: 'dockerfile',
|
|
234
261
|
healthPath: spec.healthPath,
|
|
235
262
|
database: spec.database,
|
|
236
|
-
hostnames: [
|
|
263
|
+
hostnames: [builtinHostnameFor(org, app)],
|
|
237
264
|
config,
|
|
238
265
|
blueAppKey: `coolify:${app}:${org.id}`,
|
|
239
266
|
dockerfileLocation: spec.dockerfile,
|
|
@@ -241,7 +268,7 @@ export function builtinSubject(org, app, recorded = {}) {
|
|
|
241
268
|
builtin: app,
|
|
242
269
|
};
|
|
243
270
|
}
|
|
244
|
-
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule and
|
|
271
|
+
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule, Agora and kosmos. */
|
|
245
272
|
export function blueGreenSubjects(org, recorded) {
|
|
246
273
|
const subjects = [];
|
|
247
274
|
for (const entry of org.hosting.entries) {
|
|
@@ -258,7 +285,7 @@ export function blueGreenSubjects(org, recorded) {
|
|
|
258
285
|
}
|
|
259
286
|
/** Whether the org declares blue/green for any of its apps. */
|
|
260
287
|
export const declaresBlueGreen = (org) => org.hosting.entries.some((entry) => entry.blueGreen) ||
|
|
261
|
-
BUILTIN_APPS.some((app) => org
|
|
288
|
+
BUILTIN_APPS.some((app) => builtinDeclaresBlueGreen(org, app));
|
|
262
289
|
/**
|
|
263
290
|
* The Coolify name of the entry's app on the primary. A blue/green entry's app is its blue copy: the recorded
|
|
264
291
|
* name; else the entry's own name when its app is already recorded (it ran before blue/green and is adopted as
|
|
@@ -142,10 +142,17 @@ async function matchesHealth(fetchImpl, health) {
|
|
|
142
142
|
const response = await fetchImpl(health.url, {
|
|
143
143
|
signal: AbortSignal.timeout(HEALTH_REQUEST_TIMEOUT_MS),
|
|
144
144
|
});
|
|
145
|
-
if (!response.ok)
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
// #760: an unread body left undici's parser paused when the socket
|
|
147
|
+
// ended, throwing an uncaught assertion in the host. Every answer this
|
|
148
|
+
// function does not go on to consume gets cancelled here.
|
|
149
|
+
await response.body?.cancel();
|
|
146
150
|
return false;
|
|
147
|
-
|
|
151
|
+
}
|
|
152
|
+
if (health.kind === 'ready') {
|
|
153
|
+
await response.body?.cancel();
|
|
148
154
|
return true;
|
|
155
|
+
}
|
|
149
156
|
const body = await response.json().catch(() => undefined);
|
|
150
157
|
const value = body && typeof body === 'object'
|
|
151
158
|
? body[health.field ?? 'version']
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { agoraHostnameFor } from './agora.js';
|
|
2
2
|
import { bouleHostnameFor } from './boule.js';
|
|
3
3
|
import { appHostname, cmsStudioHostnameFor, entryHostnames } from './hostnames.js';
|
|
4
|
+
import { kosmosHostnameFor } from './kosmos.js';
|
|
4
5
|
import { workspaceAuthEntryOf, workspaceClientHosts } from './workspace-client.js';
|
|
5
6
|
/**
|
|
6
7
|
* #339: the desktop app signs in through the company's web app, so every
|
|
@@ -36,7 +37,9 @@ export function signInRedirectUris(org, appKey) {
|
|
|
36
37
|
? cmsStudioHostnameFor(org)
|
|
37
38
|
: appKey === 'boule'
|
|
38
39
|
? bouleHostnameFor(org)
|
|
39
|
-
:
|
|
40
|
+
: appKey === 'kosmos'
|
|
41
|
+
? kosmosHostnameFor(org)
|
|
42
|
+
: appHostname(org),
|
|
40
43
|
];
|
|
41
44
|
return [
|
|
42
45
|
...hosts.map((host) => `https://${host}/auth/callback`),
|