@wtfalch/foundry 0.60.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.
Files changed (236) hide show
  1. package/dist/apps/cloudflare.d.ts +173 -0
  2. package/dist/apps/cloudflare.js +17 -0
  3. package/dist/apps/deploy.d.ts +116 -0
  4. package/dist/apps/deploy.js +791 -0
  5. package/dist/apps/github.d.ts +47 -0
  6. package/dist/apps/github.js +201 -0
  7. package/dist/apps/index.d.ts +9 -0
  8. package/dist/apps/index.js +9 -0
  9. package/dist/apps/migrations.d.ts +110 -0
  10. package/dist/apps/migrations.js +458 -0
  11. package/dist/audit/connector.d.ts +43 -0
  12. package/dist/audit/connector.js +22 -0
  13. package/dist/audit/evidence.d.ts +49 -0
  14. package/dist/audit/evidence.js +56 -0
  15. package/dist/audit/index.d.ts +9 -0
  16. package/dist/audit/index.js +9 -0
  17. package/dist/audit/migrations.d.ts +5 -0
  18. package/dist/audit/migrations.js +5 -0
  19. package/dist/audit/registry.d.ts +49 -0
  20. package/dist/audit/registry.js +44 -0
  21. package/dist/audit/seal.d.ts +49 -0
  22. package/dist/audit/seal.js +131 -0
  23. package/dist/audit/sealer-role.d.ts +33 -0
  24. package/dist/audit/sealer-role.js +56 -0
  25. package/dist/audit/signer.d.ts +40 -0
  26. package/dist/audit/signer.js +78 -0
  27. package/dist/audit/tick.d.ts +36 -0
  28. package/dist/audit/tick.js +87 -0
  29. package/dist/catalogue/copy-en.d.ts +1 -0
  30. package/dist/catalogue/copy-en.js +129 -0
  31. package/dist/catalogue/index.d.ts +9 -0
  32. package/dist/catalogue/index.js +21 -0
  33. package/dist/catalogue/integrations.d.ts +4 -0
  34. package/dist/catalogue/integrations.js +104 -0
  35. package/dist/catalogue/systems.d.ts +7 -0
  36. package/dist/catalogue/systems.js +31 -0
  37. package/dist/catalogue/types.d.ts +110 -0
  38. package/dist/catalogue/types.js +1 -0
  39. package/dist/entry-rules.d.ts +53 -0
  40. package/dist/entry-rules.js +71 -0
  41. package/dist/hosted-label.d.ts +13 -0
  42. package/dist/hosted-label.js +81 -0
  43. package/dist/index.d.ts +14 -5
  44. package/dist/index.js +7 -3
  45. package/dist/migrations/0062_hosting_github.sql +6 -0
  46. package/dist/migrations/0063_boule_apps.sql +7 -0
  47. package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
  48. package/dist/migrations/0065_native_build_runs.sql +32 -0
  49. package/dist/migrations/0066_org_kosmos.sql +10 -0
  50. package/dist/modules/ai.d.ts +3 -0
  51. package/dist/modules/ai.js +64 -0
  52. package/dist/modules/assistants.d.ts +3 -0
  53. package/dist/modules/assistants.js +72 -0
  54. package/dist/modules/audit-ledger.d.ts +29 -0
  55. package/dist/modules/audit-ledger.js +49 -0
  56. package/dist/modules/audit.d.ts +7 -0
  57. package/dist/modules/audit.js +44 -0
  58. package/dist/modules/database.d.ts +23 -0
  59. package/dist/modules/database.js +189 -0
  60. package/dist/modules/email.d.ts +3 -0
  61. package/dist/modules/email.js +18 -0
  62. package/dist/modules/entry-parts.d.ts +23 -0
  63. package/dist/modules/entry-parts.js +63 -0
  64. package/dist/modules/files.d.ts +3 -0
  65. package/dist/modules/files.js +64 -0
  66. package/dist/modules/index.d.ts +74 -0
  67. package/dist/modules/index.js +774 -0
  68. package/dist/modules/jobs.d.ts +3 -0
  69. package/dist/modules/jobs.js +23 -0
  70. package/dist/modules/organisations.d.ts +3 -0
  71. package/dist/modules/organisations.js +20 -0
  72. package/dist/modules/outputs.d.ts +9 -0
  73. package/dist/modules/outputs.js +38 -0
  74. package/dist/modules/sign-in.d.ts +3 -0
  75. package/dist/modules/sign-in.js +197 -0
  76. package/dist/modules/signed-records.d.ts +3 -0
  77. package/dist/modules/signed-records.js +32 -0
  78. package/dist/modules/types.d.ts +194 -0
  79. package/dist/modules/types.js +1 -0
  80. package/dist/modules/workspaces.d.ts +3 -0
  81. package/dist/modules/workspaces.js +75 -0
  82. package/dist/native-build.d.ts +70 -0
  83. package/dist/native-build.js +92 -0
  84. package/dist/org.d.ts +184 -24
  85. package/dist/org.js +444 -49
  86. package/dist/plan/agora.d.ts +1 -1
  87. package/dist/plan/agora.js +4 -0
  88. package/dist/plan/apps-db-route.d.ts +90 -0
  89. package/dist/plan/apps-db-route.js +168 -0
  90. package/dist/plan/apps-zones.d.ts +56 -0
  91. package/dist/plan/apps-zones.js +118 -0
  92. package/dist/plan/archon-blue-green.js +11 -1
  93. package/dist/plan/blue-green-apply.d.ts +37 -1
  94. package/dist/plan/blue-green-apply.js +55 -15
  95. package/dist/plan/blue-green.d.ts +60 -8
  96. package/dist/plan/blue-green.js +115 -10
  97. package/dist/plan/boule.d.ts +23 -1
  98. package/dist/plan/boule.js +35 -2
  99. package/dist/plan/broker-entries.d.ts +28 -3
  100. package/dist/plan/broker-entries.js +74 -3
  101. package/dist/plan/broker-registrar.d.ts +168 -0
  102. package/dist/plan/broker-registrar.js +440 -0
  103. package/dist/plan/broker-workspaces-database.d.ts +48 -0
  104. package/dist/plan/broker-workspaces-database.js +87 -0
  105. package/dist/plan/deploy-health.js +9 -2
  106. package/dist/plan/desktop-callback.js +15 -7
  107. package/dist/plan/diff.d.ts +8 -2
  108. package/dist/plan/diff.js +552 -104
  109. package/dist/plan/edge-access.d.ts +6 -0
  110. package/dist/plan/edge-access.js +1 -0
  111. package/dist/plan/edge-dns.d.ts +6 -0
  112. package/dist/plan/edge-dns.js +9 -7
  113. package/dist/plan/entry-ai-key.d.ts +1 -1
  114. package/dist/plan/entry-ai-key.js +2 -2
  115. package/dist/plan/entry-audit.d.ts +58 -0
  116. package/dist/plan/entry-audit.js +122 -0
  117. package/dist/plan/entry-customer-grants.d.ts +75 -0
  118. package/dist/plan/entry-customer-grants.js +154 -0
  119. package/dist/plan/entry-edge-key.d.ts +26 -1
  120. package/dist/plan/entry-edge-key.js +42 -3
  121. package/dist/plan/entry-jobs.d.ts +64 -0
  122. package/dist/plan/entry-jobs.js +92 -0
  123. package/dist/plan/entry-repo-key-apply.d.ts +55 -0
  124. package/dist/plan/entry-repo-key-apply.js +185 -0
  125. package/dist/plan/entry-repo-key.d.ts +92 -0
  126. package/dist/plan/entry-repo-key.js +235 -0
  127. package/dist/plan/entry-workspaces.d.ts +55 -0
  128. package/dist/plan/entry-workspaces.js +90 -0
  129. package/dist/plan/executor.d.ts +231 -19
  130. package/dist/plan/executor.js +1648 -129
  131. package/dist/plan/gates.d.ts +1 -1
  132. package/dist/plan/gates.js +62 -9
  133. package/dist/plan/github-deploy-keys.d.ts +78 -0
  134. package/dist/plan/github-deploy-keys.js +305 -0
  135. package/dist/plan/github-source.d.ts +53 -0
  136. package/dist/plan/github-source.js +140 -0
  137. package/dist/plan/held-key-refs.d.ts +12 -0
  138. package/dist/plan/held-key-refs.js +12 -0
  139. package/dist/plan/hostnames.d.ts +9 -0
  140. package/dist/plan/hostnames.js +28 -1
  141. package/dist/plan/index.d.ts +9 -0
  142. package/dist/plan/index.js +6 -0
  143. package/dist/plan/jobs-scheduler-http.js +3 -0
  144. package/dist/plan/keys-registrar-http.d.ts +32 -0
  145. package/dist/plan/keys-registrar-http.js +105 -0
  146. package/dist/plan/keys-registrar.d.ts +67 -0
  147. package/dist/plan/keys-registrar.js +103 -0
  148. package/dist/plan/kosmos.d.ts +56 -0
  149. package/dist/plan/kosmos.js +84 -0
  150. package/dist/plan/live.d.ts +1 -1
  151. package/dist/plan/locked-hostnames.d.ts +2 -2
  152. package/dist/plan/locked-hostnames.js +7 -4
  153. package/dist/plan/login-host.js +11 -5
  154. package/dist/plan/machine-paths.d.ts +36 -0
  155. package/dist/plan/machine-paths.js +137 -0
  156. package/dist/plan/mcp.d.ts +1 -1
  157. package/dist/plan/mcp.js +49 -17
  158. package/dist/plan/org-apps-admin.d.ts +108 -0
  159. package/dist/plan/org-apps-admin.js +434 -0
  160. package/dist/plan/org-names.d.ts +3 -3
  161. package/dist/plan/org-names.js +15 -2
  162. package/dist/plan/org-postgres-migrate.js +1 -0
  163. package/dist/plan/own-diff.d.ts +23 -0
  164. package/dist/plan/own-diff.js +53 -0
  165. package/dist/plan/own-hostnames.d.ts +2 -0
  166. package/dist/plan/own-hostnames.js +1 -0
  167. package/dist/plan/own-jobs-route.d.ts +47 -0
  168. package/dist/plan/own-jobs-route.js +91 -0
  169. package/dist/plan/own-limits.d.ts +6 -0
  170. package/dist/plan/own-limits.js +1 -0
  171. package/dist/plan/own-services.d.ts +4 -0
  172. package/dist/plan/own-services.js +4 -0
  173. package/dist/plan/plan-apply.d.ts +39 -0
  174. package/dist/plan/plan-apply.js +163 -9
  175. package/dist/plan/platform-diff.d.ts +2 -2
  176. package/dist/plan/platform-diff.js +7 -2
  177. package/dist/plan/platform-org.d.ts +30 -0
  178. package/dist/plan/platform-org.js +12 -1
  179. package/dist/plan/platform-providers.d.ts +2 -0
  180. package/dist/plan/platform-providers.js +13 -2
  181. package/dist/plan/post-logout-uris.d.ts +1 -1
  182. package/dist/plan/post-logout-uris.js +12 -3
  183. package/dist/plan/private-pages.d.ts +26 -0
  184. package/dist/plan/private-pages.js +39 -0
  185. package/dist/plan/providers-readers.js +20 -1
  186. package/dist/plan/proxy-labels.d.ts +12 -1
  187. package/dist/plan/proxy-labels.js +32 -2
  188. package/dist/plan/release-host.d.ts +6 -2
  189. package/dist/plan/release-host.js +36 -8
  190. package/dist/plan/stage-env.d.ts +19 -1
  191. package/dist/plan/stage-env.js +25 -1
  192. package/dist/plan/stage-guards.d.ts +20 -0
  193. package/dist/plan/stage-guards.js +24 -0
  194. package/dist/plan/stage-isolation.d.ts +24 -0
  195. package/dist/plan/stage-isolation.js +29 -1
  196. package/dist/plan/worker-app-database.d.ts +46 -42
  197. package/dist/plan/worker-app-database.js +150 -153
  198. package/dist/plan/worker-app.d.ts +5 -65
  199. package/dist/plan/worker-app.js +28 -105
  200. package/dist/plan/worker.d.ts +2 -2
  201. package/dist/plan/worker.js +2 -1
  202. package/dist/plan/workspace-client.d.ts +25 -0
  203. package/dist/plan/workspace-client.js +48 -0
  204. package/dist/plan/workspace-sign-in.d.ts +55 -0
  205. package/dist/plan/workspace-sign-in.js +273 -0
  206. package/dist/plan/workspace-worker.d.ts +4 -0
  207. package/dist/plan/workspace-worker.js +13 -0
  208. package/dist/plan/workspace.d.ts +101 -0
  209. package/dist/plan/workspace.js +305 -0
  210. package/dist/release/decide.d.ts +30 -1
  211. package/dist/release/decide.js +73 -11
  212. package/dist/release/health.d.ts +8 -3
  213. package/dist/release/health.js +58 -5
  214. package/dist/release/migration-compatibility.d.ts +66 -0
  215. package/dist/release/migration-compatibility.js +340 -0
  216. package/dist/release/run.d.ts +2 -5
  217. package/dist/release/run.js +4 -0
  218. package/dist/release/state.d.ts +3 -1
  219. package/dist/release/state.js +6 -0
  220. package/dist/release/types.d.ts +20 -0
  221. package/dist/requested-changes.d.ts +11 -2
  222. package/dist/requested-changes.js +13 -1
  223. package/dist/service-labels.d.ts +4 -0
  224. package/dist/service-labels.js +11 -0
  225. package/dist/slug.d.ts +2 -0
  226. package/dist/slug.js +20 -7
  227. package/dist/store/postgres.js +25 -2
  228. package/dist/store/schema.d.ts +73 -1
  229. package/dist/store/schema.js +10 -0
  230. package/dist/worker-app-config.d.ts +1 -11
  231. package/dist/worker-app-config.js +4 -17
  232. package/dist/worker-app-migrations.d.ts +105 -0
  233. package/dist/worker-app-migrations.js +166 -0
  234. package/dist/worker-app-upload.d.ts +103 -0
  235. package/dist/worker-app-upload.js +128 -0
  236. package/package.json +19 -2
@@ -0,0 +1,108 @@
1
+ import { type DeclaredOrg } from '../org.js';
2
+ import type { DraftStep } from './diff.js';
3
+ import type { CoolifyPort } from './executor.js';
4
+ import { type JobRuntime } from './job-app.js';
5
+ import type { StepScope } from './own-services.js';
6
+ /** The role's name on the server: the org's slug plus `_apps_admin`. */
7
+ export declare const appsAdminRoleName: (org: Pick<DeclaredOrg, "slug">) => string;
8
+ /** The job app's name in Coolify. */
9
+ export declare const appsAdminJobName: (org: Pick<DeclaredOrg, "slug">) => string;
10
+ /** Recorded once the role is made, its password set, and the URL delivered. */
11
+ export declare const ORG_APPS_ADMIN_SCOPE_KEY = "org_apps_admin";
12
+ /**
13
+ * An opaque id, set once the password is minted and the job that sets it is about to run, until the URL is
14
+ * delivered. Never the password. A failure before that point leaves no id, so the next apply tries again.
15
+ */
16
+ export declare const ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY = "org_apps_admin_undelivered";
17
+ /** The job app's id while it exists. */
18
+ export declare const ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY = "org_apps_admin_job_app";
19
+ /** Where the host holds the role's connection URL, once delivered -- `deliverSecret`'s own naming convention. */
20
+ export declare const appsAdminUrlRef: (org: Pick<DeclaredOrg, "slug">) => string;
21
+ /** How many times the URL is handed to `deliverSecret`. */
22
+ export declare const ORG_APPS_ADMIN_DELIVERY_ATTEMPTS = 3;
23
+ export interface PlanOrgAppsAdminOptions {
24
+ /** The host can store a secret under a ref (`deliverSecret`). Default true. */
25
+ canHoldSecrets?: boolean;
26
+ }
27
+ /**
28
+ * Pure. Plans `coolify:org-apps-admin` once, after the company's Postgres server is recorded
29
+ * (platform-step rule 2: blocked, not failing, while it is not). A lost mint -- an undelivered id
30
+ * with no done guard -- is reported blocked and never minted again (rule 1); the operator clears the
31
+ * recorded id once the role is confirmed gone from the server.
32
+ */
33
+ export declare function planOrgAppsAdminStep(org: Pick<DeclaredOrg, 'id' | 'slug'>, recordedIds: Readonly<Record<string, string>>, options?: PlanOrgAppsAdminOptions): {
34
+ step: DraftStep | null;
35
+ blocked: string | null;
36
+ };
37
+ export interface PlanAppsAdminOptions {
38
+ canHoldSecrets: boolean;
39
+ /** `coolify:org-postgres` is planned in this same plan, ahead of this step. */
40
+ postgresPlanned: boolean;
41
+ }
42
+ /**
43
+ * Pure. F4 (#925): the wiring of {@link planOrgAppsAdminStep} to `org.boule.apps`. Apps on: the mint
44
+ * step, once Boule (which receives the URL) and the Postgres server are recorded or planned. Apps off with
45
+ * the role recorded (or a mint never delivered): the retire step, which needs no secret and drops nothing.
46
+ */
47
+ export declare function planAppsAdmin(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], options: PlanAppsAdminOptions): {
48
+ steps: DraftStep[];
49
+ blocked: Array<{
50
+ description: string;
51
+ key: string;
52
+ }>;
53
+ };
54
+ /**
55
+ * The job's script: makes the role (idempotent, kept if it already exists), sets its password and grants
56
+ * it CONNECT on `postgres`. It revokes nothing from PUBLIC (see the file comment). Roles are made by
57
+ * `format('%I')` and the password by `format('%L')`, so no value is spliced into SQL by the shell. Setting
58
+ * the password again is harmless, so a mint that is repeated simply replaces it. It never prints a URL or a password: psql's output is discarded and the one
59
+ * error line names neither.
60
+ */
61
+ export declare function orgAppsAdminScript(role: string, mode?: 'mint' | 'retire'): string[];
62
+ /** The job: the same pinned image as the other Postgres jobs, so `psql` matches the server's major version. */
63
+ export declare function orgAppsAdminDockerfile(role: string, mode?: 'mint' | 'retire'): string;
64
+ type AppsAdminCoolify = Pick<CoolifyPort, 'findOrCreateEnvironment' | 'findApplicationByName' | 'getApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'deploy' | 'raw'>;
65
+ /** Everything the step takes from the executor. New ones go here, never as positional arguments. */
66
+ export interface OrgAppsAdminDeps {
67
+ coolify: AppsAdminCoolify;
68
+ ensureProject(): Promise<string>;
69
+ serverUuid(): Promise<string>;
70
+ /** Starts the company's Postgres server when it is not running and returns its live record. */
71
+ startDatabase(uuid: string): Promise<{
72
+ internal_db_url?: string;
73
+ status?: string;
74
+ }>;
75
+ deliverSecret(ref: string, secret: string): Promise<void>;
76
+ /** Registers a value for redaction. */
77
+ onSecret?(value: string): void;
78
+ /** Test seam: a password. Default: 24 random bytes, base64url. */
79
+ mintPassword?(): string;
80
+ /** Test seam: an opaque id. */
81
+ mintId?(): string;
82
+ runtime?: JobRuntime;
83
+ }
84
+ /**
85
+ * Makes the role true and delivers its connection URL. The password is minted in memory. The opaque id
86
+ * {@link ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY} is recorded and persisted only once the job that sets the
87
+ * password is about to be deployed ("password minted and role touched"); a failure before that leaves no
88
+ * id and the next apply simply retries, since setting a password again is harmless. The done guard is
89
+ * recorded only once the URL is delivered. A run that dies between the two finds the id and stops (the
90
+ * planner reports it blocked); retiring the role (apps off) clears it.
91
+ */
92
+ export declare function provisionOrgAppsAdmin(scope: StepScope, deps: OrgAppsAdminDeps, org: DeclaredOrg): Promise<void>;
93
+ /** What the retire form takes from the executor: the mint form's deps, and the held-key blanking. */
94
+ export interface RetireOrgAppsAdminDeps extends Omit<OrgAppsAdminDeps, 'deliverSecret'> {
95
+ /** Blanks the held URL (the store has no delete: an empty value reads as not held). */
96
+ clearHeldUrl(ref: string): Promise<void>;
97
+ /** Takes `APPS_DATABASE_ADMIN_URL` out of Boule's env. A row already gone is not an error. */
98
+ removeBouleEnv(): Promise<void>;
99
+ }
100
+ /**
101
+ * Apps went off (F4, #925): the role stops logging in, the URL leaves Boule's env and the held store,
102
+ * and the recorded ids are cleared last so a failed run repeats. Never drops the role, a database or
103
+ * a role an app made (CLAUDE.md: never drop data); turning apps on again mints a new password for the
104
+ * same role. A recorded mint that was never delivered is retired the same way, which is also how an
105
+ * operator unblocks a lost mint. The job needs the server's admin URL and nothing else secret.
106
+ */
107
+ export declare function retireOrgAppsAdmin(scope: StepScope, deps: RetireOrgAppsAdminDeps, org: DeclaredOrg): Promise<void>;
108
+ export {};
@@ -0,0 +1,434 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { pinnedRef } from '../bootstrap/versions.js';
3
+ import { ORG_POSTGRES_SERVER_SCOPE_KEY, appsOn } from '../org.js';
4
+ import { JobFailure, waitForJob } from './job-app.js';
5
+ import { orgPostgresServerName } from './org-postgres.js';
6
+ import { boundedCall, withCallTimeout } from './signin-db.js';
7
+ /**
8
+ * estate#210: a narrow role on the company's one Postgres server (`org-postgres.ts`) that Boule uses
9
+ * to create a database per app copy itself -- `LOGIN CREATEDB CREATEROLE` only, never superuser,
10
+ * never able to touch another role's data or database (estate#215, tested on Postgres 16.15). Its
11
+ * connection URL is the one credential Boule is handed; it never sees the server's own admin URL.
12
+ *
13
+ * This step does NOT touch CONNECT on any database. `org-postgres-job.ts` deliberately leaves CONNECT open
14
+ * on PUBLIC (a template app creates its own runtime role on first boot and relies on it), so a server-wide
15
+ * `REVOKE CONNECT ... FROM PUBLIC` here would lock out every existing home. Isolation between app databases
16
+ * is done by Boule instead (contract with the Boule builder): on each `app_*` database it creates, Boule
17
+ * revokes CONNECT from PUBLIC and grants it to that database's own role only. The admin role holds CONNECT
18
+ * on `postgres` alone.
19
+ *
20
+ * Wired by F4 (#925): the step is planned only for a company whose Boule runs apps (`appsOn`,
21
+ * `org.boule.apps`), after the company's Postgres server and Boule are recorded or planned. The URL is
22
+ * held at {@link appsAdminUrlRef} and written to Boule's env as `APPS_DATABASE_ADMIN_URL`. Turning apps
23
+ * off plans the retire form ({@link retireOrgAppsAdmin}): the env row goes, the held URL is blanked, and
24
+ * the role is set NOLOGIN and its sessions ended. Nothing is dropped: the role, the `app_*` databases
25
+ * and their roles stay, so turning apps on again mints a new password for the same role.
26
+ */
27
+ const NAME = /^[a-z][a-z0-9_]{0,62}$/;
28
+ function checked(name) {
29
+ if (!NAME.test(name))
30
+ throw new Error(`Postgres name is not valid: ${name}`);
31
+ return name;
32
+ }
33
+ /** The role's name on the server: the org's slug plus `_apps_admin`. */
34
+ export const appsAdminRoleName = (org) => `${org.slug}_apps_admin`;
35
+ /** The job app's name in Coolify. */
36
+ export const appsAdminJobName = (org) => `${org.slug}-apps-admin-job`;
37
+ /** Recorded once the role is made, its password set, and the URL delivered. */
38
+ export const ORG_APPS_ADMIN_SCOPE_KEY = 'org_apps_admin';
39
+ /**
40
+ * An opaque id, set once the password is minted and the job that sets it is about to run, until the URL is
41
+ * delivered. Never the password. A failure before that point leaves no id, so the next apply tries again.
42
+ */
43
+ export const ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY = 'org_apps_admin_undelivered';
44
+ /** The job app's id while it exists. */
45
+ export const ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY = 'org_apps_admin_job_app';
46
+ /** Where the host holds the role's connection URL, once delivered -- `deliverSecret`'s own naming convention. */
47
+ export const appsAdminUrlRef = (org) => `org-apps-admin:${org.slug}:adminUrl`;
48
+ /** How many times the URL is handed to `deliverSecret`. */
49
+ export const ORG_APPS_ADMIN_DELIVERY_ATTEMPTS = 3;
50
+ /**
51
+ * Pure. Plans `coolify:org-apps-admin` once, after the company's Postgres server is recorded
52
+ * (platform-step rule 2: blocked, not failing, while it is not). A lost mint -- an undelivered id
53
+ * with no done guard -- is reported blocked and never minted again (rule 1); the operator clears the
54
+ * recorded id once the role is confirmed gone from the server.
55
+ */
56
+ export function planOrgAppsAdminStep(org, recordedIds, options = {}) {
57
+ if (recordedIds[ORG_APPS_ADMIN_SCOPE_KEY])
58
+ return { step: null, blocked: null };
59
+ if (!recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY]) {
60
+ return {
61
+ step: null,
62
+ blocked: `${org.slug}'s apps admin role is blocked, not planned: its Postgres server is not provisioned yet`,
63
+ };
64
+ }
65
+ const lost = recordedIds[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY];
66
+ if (lost) {
67
+ return {
68
+ step: null,
69
+ blocked: `${org.slug}'s apps admin role (note ${lost}): a run set a new password on the role but its connection URL was never saved. Nothing is lost. To start over, turn apps off and apply (that retires the role and clears this note), then turn apps on again`,
70
+ };
71
+ }
72
+ if (options.canHoldSecrets === false) {
73
+ return {
74
+ step: null,
75
+ blocked: `${org.slug}'s apps admin role is blocked, not planned: the host cannot hold secrets`,
76
+ };
77
+ }
78
+ return {
79
+ step: {
80
+ action: { system: 'coolify', kind: 'org-apps-admin', key: org.id, desired: {} },
81
+ step: {
82
+ description: `mint ${org.slug}'s database admin role (${appsAdminRoleName(org)}); it can create databases and roles but touches no existing database`,
83
+ destroys: false,
84
+ migrates: false,
85
+ },
86
+ },
87
+ blocked: null,
88
+ };
89
+ }
90
+ /**
91
+ * Pure. F4 (#925): the wiring of {@link planOrgAppsAdminStep} to `org.boule.apps`. Apps on: the mint
92
+ * step, once Boule (which receives the URL) and the Postgres server are recorded or planned. Apps off with
93
+ * the role recorded (or a mint never delivered): the retire step, which needs no secret and drops nothing.
94
+ */
95
+ export function planAppsAdmin(org, recordedIds, plannedSteps, options) {
96
+ const key = `coolify:org-apps-admin:${org.id}`;
97
+ const blocked = [];
98
+ if (!appsOn(org)) {
99
+ if (!recordedIds[ORG_APPS_ADMIN_SCOPE_KEY] &&
100
+ !recordedIds[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY]) {
101
+ return { steps: [], blocked };
102
+ }
103
+ if (!recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY]) {
104
+ blocked.push({
105
+ description: `${org.slug}'s apps admin role is not retired, not planned: its Postgres server is not recorded`,
106
+ key,
107
+ });
108
+ return { steps: [], blocked };
109
+ }
110
+ return {
111
+ steps: [
112
+ {
113
+ action: {
114
+ system: 'coolify',
115
+ kind: 'org-apps-admin',
116
+ key: org.id,
117
+ desired: { mode: 'retire' },
118
+ },
119
+ step: {
120
+ description: `retire ${org.slug}'s database admin role (${appsAdminRoleName(org)}): no login, out of Boule's env; no database is dropped`,
121
+ destroys: true,
122
+ migrates: false,
123
+ },
124
+ },
125
+ ],
126
+ blocked,
127
+ };
128
+ }
129
+ const bouleReady = Boolean(recordedIds[`coolify:boule:${org.id}`]) ||
130
+ plannedSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'boule' && p.action.key === org.id);
131
+ if (!bouleReady && !recordedIds[ORG_APPS_ADMIN_SCOPE_KEY]) {
132
+ blocked.push({
133
+ description: `${org.slug}'s apps admin role is blocked, not planned: Boule is not recorded yet`,
134
+ key,
135
+ });
136
+ return { steps: [], blocked };
137
+ }
138
+ const seen = options.postgresPlanned && !recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY]
139
+ ? { ...recordedIds, [ORG_POSTGRES_SERVER_SCOPE_KEY]: 'planned' }
140
+ : recordedIds;
141
+ const planned = planOrgAppsAdminStep(org, seen, { canHoldSecrets: options.canHoldSecrets });
142
+ if (planned.blocked)
143
+ blocked.push({ description: planned.blocked, key });
144
+ return { steps: planned.step ? [planned.step] : [], blocked };
145
+ }
146
+ /**
147
+ * The job's script: makes the role (idempotent, kept if it already exists), sets its password and grants
148
+ * it CONNECT on `postgres`. It revokes nothing from PUBLIC (see the file comment). Roles are made by
149
+ * `format('%I')` and the password by `format('%L')`, so no value is spliced into SQL by the shell. Setting
150
+ * the password again is harmless, so a mint that is repeated simply replaces it. It never prints a URL or a password: psql's output is discarded and the one
151
+ * error line names neither.
152
+ */
153
+ export function orgAppsAdminScript(role, mode = 'mint') {
154
+ const owner = checked(role);
155
+ if (mode === 'retire')
156
+ return orgAppsAdminRetireScript(owner);
157
+ const JOB_DONE_FILE = '/tmp/done';
158
+ const JOB_FAILED_FILE = '/tmp/failed';
159
+ return [
160
+ '#!/bin/sh',
161
+ 'set -eu',
162
+ `trap '[ -f ${JOB_DONE_FILE} ] || touch ${JOB_FAILED_FILE}' EXIT`,
163
+ "trap 'exit 1' HUP INT TERM",
164
+ `[ ! -f ${JOB_FAILED_FILE} ] || exit 1`,
165
+ `rm -f ${JOB_DONE_FILE}`,
166
+ 'fail() { echo "org-apps-admin: $1 failed"; exit 1; }',
167
+ '[ -n "${PW:-}" ] || fail "role"',
168
+ 'psql "$ADMIN_DATABASE_URL" -X -q -v ON_ERROR_STOP=1 > /dev/null 2>&1 <<\'SQL\' || fail "role"',
169
+ `\\set role '${owner}'`,
170
+ '\\getenv pw PW',
171
+ "SELECT format('CREATE ROLE %I LOGIN CREATEDB CREATEROLE', :'role') WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'role') \\gexec",
172
+ "SELECT format('ALTER ROLE %I LOGIN CREATEDB CREATEROLE PASSWORD %L', :'role', :'pw') \\gexec",
173
+ "SELECT format('GRANT CONNECT ON DATABASE postgres TO %I', :'role') \\gexec",
174
+ 'SQL',
175
+ 'echo "org-apps-admin: role ready"',
176
+ `touch ${JOB_DONE_FILE}`,
177
+ 'exec sleep infinity',
178
+ ];
179
+ }
180
+ /**
181
+ * The retire script: the role stops being able to log in and its open sessions end. It drops nothing and
182
+ * grants nothing; CONNECT stays closed to PUBLIC. A role that is not there is left alone.
183
+ */
184
+ function orgAppsAdminRetireScript(owner) {
185
+ const JOB_DONE_FILE = '/tmp/done';
186
+ const JOB_FAILED_FILE = '/tmp/failed';
187
+ return [
188
+ '#!/bin/sh',
189
+ 'set -eu',
190
+ `trap '[ -f ${JOB_DONE_FILE} ] || touch ${JOB_FAILED_FILE}' EXIT`,
191
+ "trap 'exit 1' HUP INT TERM",
192
+ `[ ! -f ${JOB_FAILED_FILE} ] || exit 1`,
193
+ `rm -f ${JOB_DONE_FILE}`,
194
+ 'fail() { echo "org-apps-admin: $1 failed"; exit 1; }',
195
+ 'psql "$ADMIN_DATABASE_URL" -X -q -v ON_ERROR_STOP=1 > /dev/null 2>&1 <<\'SQL\' || fail "retire"',
196
+ `\\set role '${owner}'`,
197
+ "SELECT format('ALTER ROLE %I NOLOGIN', :'role') WHERE EXISTS (SELECT FROM pg_roles WHERE rolname = :'role') \\gexec",
198
+ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = :'role'",
199
+ 'SQL',
200
+ 'echo "org-apps-admin: role retired"',
201
+ `touch ${JOB_DONE_FILE}`,
202
+ 'exec sleep infinity',
203
+ ];
204
+ }
205
+ /** The job: the same pinned image as the other Postgres jobs, so `psql` matches the server's major version. */
206
+ export function orgAppsAdminDockerfile(role, mode = 'mint') {
207
+ const encoded = Buffer.from(`${orgAppsAdminScript(role, mode).join('\n')}\n`, 'utf8').toString('base64');
208
+ return [
209
+ `FROM postgres:${pinnedRef('postgres')}`,
210
+ `RUN echo '${encoded}' | base64 -d > /usr/local/bin/org-apps-admin && chmod 0755 /usr/local/bin/org-apps-admin`,
211
+ 'ENTRYPOINT ["/usr/local/bin/org-apps-admin"]',
212
+ `HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 CMD ["test", "-f", "/tmp/done"]`,
213
+ ].join('\n');
214
+ }
215
+ const mint = () => randomBytes(24).toString('base64url');
216
+ const mintOpaque = () => randomBytes(12).toString('hex');
217
+ /**
218
+ * Makes the role true and delivers its connection URL. The password is minted in memory. The opaque id
219
+ * {@link ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY} is recorded and persisted only once the job that sets the
220
+ * password is about to be deployed ("password minted and role touched"); a failure before that leaves no
221
+ * id and the next apply simply retries, since setting a password again is harmless. The done guard is
222
+ * recorded only once the URL is delivered. A run that dies between the two finds the id and stops (the
223
+ * planner reports it blocked); retiring the role (apps off) clears it.
224
+ */
225
+ export async function provisionOrgAppsAdmin(scope, deps, org) {
226
+ const label = `${org.slug}'s apps admin role`;
227
+ const coolify = withCallTimeout(deps.coolify);
228
+ const recorded = scope.recordedIds();
229
+ if (recorded[ORG_APPS_ADMIN_SCOPE_KEY])
230
+ return;
231
+ const serverId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
232
+ if (!serverId)
233
+ throw new Error(`${label}: the Postgres server is not recorded yet`);
234
+ if (recorded[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY]) {
235
+ throw new Error(`${label}: an earlier run set a new password on the role but never saved its connection URL. Nothing is lost. To start over, turn apps off and apply (that retires the role and clears this note), then turn apps on again`);
236
+ }
237
+ const onSecret = (value) => deps.onSecret?.(value);
238
+ const server = await boundedCall(deps.startDatabase(serverId), 'Coolify startDatabase');
239
+ if (!server.internal_db_url || !URL.canParse(server.internal_db_url)) {
240
+ throw new Error(`${label}: the Postgres server has no usable internal URL`);
241
+ }
242
+ const adminUrl = server.internal_db_url;
243
+ onSecret(adminUrl);
244
+ const adminParsed = new URL(adminUrl);
245
+ const host = adminParsed.hostname;
246
+ const port = adminParsed.port || '5432';
247
+ if (!host)
248
+ throw new Error(`${label}: the Postgres server's internal URL has no host`);
249
+ const adminPassword = decodeURIComponent(adminParsed.password);
250
+ if (adminPassword)
251
+ onSecret(adminPassword);
252
+ const role = appsAdminRoleName(org);
253
+ const password = (deps.mintPassword ?? mint)();
254
+ onSecret(password);
255
+ const jobName = appsAdminJobName(org);
256
+ let jobAppId;
257
+ try {
258
+ const projectUuid = await deps.ensureProject();
259
+ const { environment } = await coolify.findOrCreateEnvironment(projectUuid, 'production');
260
+ const leftover = await coolify.findApplicationByName(jobName, {
261
+ environmentIds: [environment.id],
262
+ });
263
+ if (leftover)
264
+ await coolify.raw('DELETE', `/applications/${leftover.uuid}`);
265
+ const created = await coolify.createDockerfileApplication({
266
+ project_uuid: projectUuid,
267
+ server_uuid: await deps.serverUuid(),
268
+ environment_name: environment.name,
269
+ environment_uuid: environment.uuid,
270
+ dockerfile: orgAppsAdminDockerfile(role),
271
+ name: jobName,
272
+ description: `${org.slug}'s apps admin role job`,
273
+ instant_deploy: false,
274
+ autogenerate_domain: false,
275
+ });
276
+ jobAppId = created.uuid;
277
+ scope.recordScopeId(ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY, jobAppId);
278
+ await scope.persist();
279
+ await coolify.updateApplication(jobAppId, { health_check_enabled: false });
280
+ await coolify.bulkUpsertEnvs(jobAppId, [
281
+ { key: 'ADMIN_DATABASE_URL', value: adminUrl },
282
+ { key: 'PW', value: password },
283
+ ]);
284
+ // From here the job can set the password: the marker is recorded now, not earlier.
285
+ scope.recordScopeId(ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY, (deps.mintId ?? mintOpaque)());
286
+ await scope.persist();
287
+ await coolify.deploy(jobAppId, true);
288
+ await waitForJob(coolify, jobAppId, deps.runtime ?? {}, {
289
+ job: 'the apps admin job',
290
+ stopped: 'the apps admin job stopped before it finished',
291
+ });
292
+ }
293
+ catch (error) {
294
+ const why = error instanceof JobFailure ? error.message : 'a Coolify call failed or timed out';
295
+ let cleaned = '';
296
+ if (jobAppId) {
297
+ const id = jobAppId;
298
+ try {
299
+ await boundedCall(coolify.raw('POST', `/applications/${id}/stop`), 'stop the job');
300
+ }
301
+ catch {
302
+ cleaned += '; the job app could not be stopped, stop it in Coolify';
303
+ }
304
+ try {
305
+ await boundedCall(coolify.bulkUpsertEnvs(id, [
306
+ { key: 'ADMIN_DATABASE_URL', value: '' },
307
+ { key: 'PW', value: '' },
308
+ ]), 'clear the job env');
309
+ }
310
+ catch {
311
+ cleaned +=
312
+ '; its env could not be cleared, delete the app in Coolify (it holds the admin URL)';
313
+ }
314
+ }
315
+ throw new Error(`${label}: ${why}${jobAppId ? `; the job app ${jobAppId} is left stopped without secrets for inspection and the next apply replaces it${cleaned}` : ''}; apply again`);
316
+ }
317
+ try {
318
+ await coolify.raw('DELETE', `/applications/${jobAppId}`);
319
+ }
320
+ catch {
321
+ throw new Error(`${label}: the role was made, but its job app ${jobAppId} could not be deleted; delete it in Coolify (it holds the admin URL), then apply again`);
322
+ }
323
+ const url = `postgres://${encodeURIComponent(role)}:${encodeURIComponent(password)}@${host}:${port}/postgres`;
324
+ onSecret(url);
325
+ let delivered = false;
326
+ for (let attempt = 1; attempt <= ORG_APPS_ADMIN_DELIVERY_ATTEMPTS && !delivered; attempt++) {
327
+ try {
328
+ await boundedCall(deps.deliverSecret(appsAdminUrlRef(org), url), 'deliverSecret');
329
+ delivered = true;
330
+ }
331
+ catch {
332
+ // Retried; the cause may echo the secret and is dropped.
333
+ }
334
+ }
335
+ if (!delivered) {
336
+ throw new Error(`${label}: the role was made, but its connection URL could not be saved after ${ORG_APPS_ADMIN_DELIVERY_ATTEMPTS} attempts. Nothing is lost. Turn apps off and apply (that retires the role and clears the note), then turn apps on again`);
337
+ }
338
+ scope.recordScopeId(ORG_APPS_ADMIN_SCOPE_KEY, orgPostgresServerName(org));
339
+ scope.recordScopeId(ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY, '');
340
+ await scope.persist();
341
+ scope.recordScopeId(ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY, '');
342
+ }
343
+ /**
344
+ * Apps went off (F4, #925): the role stops logging in, the URL leaves Boule's env and the held store,
345
+ * and the recorded ids are cleared last so a failed run repeats. Never drops the role, a database or
346
+ * a role an app made (CLAUDE.md: never drop data); turning apps on again mints a new password for the
347
+ * same role. A recorded mint that was never delivered is retired the same way, which is also how an
348
+ * operator unblocks a lost mint. The job needs the server's admin URL and nothing else secret.
349
+ */
350
+ export async function retireOrgAppsAdmin(scope, deps, org) {
351
+ const label = `${org.slug}'s apps admin role`;
352
+ const coolify = withCallTimeout(deps.coolify);
353
+ const recorded = scope.recordedIds();
354
+ const serverId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
355
+ const hadRole = Boolean(recorded[ORG_APPS_ADMIN_SCOPE_KEY] || recorded[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY]);
356
+ if (hadRole) {
357
+ if (!serverId)
358
+ throw new Error(`${label}: the Postgres server is not recorded; cannot retire it`);
359
+ const onSecret = (value) => deps.onSecret?.(value);
360
+ const server = await boundedCall(deps.startDatabase(serverId), 'Coolify startDatabase');
361
+ if (!server.internal_db_url || !URL.canParse(server.internal_db_url)) {
362
+ throw new Error(`${label}: the Postgres server has no usable internal URL`);
363
+ }
364
+ onSecret(server.internal_db_url);
365
+ const password = decodeURIComponent(new URL(server.internal_db_url).password);
366
+ if (password)
367
+ onSecret(password);
368
+ const jobName = appsAdminJobName(org);
369
+ let jobAppId;
370
+ try {
371
+ const projectUuid = await deps.ensureProject();
372
+ const { environment } = await coolify.findOrCreateEnvironment(projectUuid, 'production');
373
+ const leftover = await coolify.findApplicationByName(jobName, {
374
+ environmentIds: [environment.id],
375
+ });
376
+ if (leftover)
377
+ await coolify.raw('DELETE', `/applications/${leftover.uuid}`);
378
+ const created = await coolify.createDockerfileApplication({
379
+ project_uuid: projectUuid,
380
+ server_uuid: await deps.serverUuid(),
381
+ environment_name: environment.name,
382
+ environment_uuid: environment.uuid,
383
+ dockerfile: orgAppsAdminDockerfile(appsAdminRoleName(org), 'retire'),
384
+ name: jobName,
385
+ description: `${org.slug}'s apps admin role retire job`,
386
+ instant_deploy: false,
387
+ autogenerate_domain: false,
388
+ });
389
+ jobAppId = created.uuid;
390
+ scope.recordScopeId(ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY, jobAppId);
391
+ await scope.persist();
392
+ await coolify.updateApplication(jobAppId, { health_check_enabled: false });
393
+ await coolify.bulkUpsertEnvs(jobAppId, [
394
+ { key: 'ADMIN_DATABASE_URL', value: server.internal_db_url },
395
+ ]);
396
+ await coolify.deploy(jobAppId, true);
397
+ await waitForJob(coolify, jobAppId, deps.runtime ?? {}, {
398
+ job: 'the apps admin retire job',
399
+ stopped: 'the apps admin retire job stopped before it finished',
400
+ });
401
+ }
402
+ catch (error) {
403
+ const why = error instanceof JobFailure ? error.message : 'a Coolify call failed or timed out';
404
+ let cleaned = '';
405
+ if (jobAppId) {
406
+ const id = jobAppId;
407
+ try {
408
+ await boundedCall(coolify.raw('POST', `/applications/${id}/stop`), 'stop the job');
409
+ await boundedCall(coolify.bulkUpsertEnvs(id, [{ key: 'ADMIN_DATABASE_URL', value: '' }]), 'clear the job env');
410
+ }
411
+ catch {
412
+ cleaned = '; delete the job app in Coolify (it holds the admin URL)';
413
+ }
414
+ }
415
+ throw new Error(`${label}: ${why}${cleaned}; apply again`);
416
+ }
417
+ try {
418
+ await coolify.raw('DELETE', `/applications/${jobAppId}`);
419
+ }
420
+ catch {
421
+ throw new Error(`${label}: the role was retired, but its job app ${jobAppId} could not be deleted; delete it in Coolify (it holds the admin URL), then apply again`);
422
+ }
423
+ }
424
+ await deps.removeBouleEnv();
425
+ await deps.clearHeldUrl(appsAdminUrlRef(org));
426
+ for (const key of [
427
+ ORG_APPS_ADMIN_SCOPE_KEY,
428
+ ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY,
429
+ ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY,
430
+ ]) {
431
+ scope.recordScopeId(key, '');
432
+ }
433
+ await scope.persist();
434
+ }
@@ -6,13 +6,13 @@ export type OrgNameSource = 'own' | 'hosting';
6
6
  * hosting entries: Agora, Boule, the tunnel and its connector, and chat,
7
7
  * calls and the cms site while they are on.
8
8
  */
9
- export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site'>): string[];
9
+ export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos'>): string[];
10
10
  /**
11
11
  * Every container name, network alias, database, tunnel and bucket name that
12
12
  * foundry derives for an org: its own-service names when it owns services,
13
13
  * and its hosting-entry and database names.
14
14
  */
15
- export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>): {
15
+ export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>): {
16
16
  name: string;
17
17
  source: OrgNameSource;
18
18
  }[];
@@ -26,5 +26,5 @@ export interface NameClash {
26
26
  source: OrgNameSource;
27
27
  }
28
28
  /** Every name `org` derives that another org or the platform also derives. */
29
- export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>[]): NameClash[];
29
+ export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>[]): NameClash[];
30
30
  export declare function nameClashMessage(slug: string, clash: NameClash): string;
@@ -1,7 +1,7 @@
1
- import { ownsServices } from '../org.js';
1
+ import { kosmosOn, ownsServices } from '../org.js';
2
2
  import { entryHasDatabase } from '../worker-app-config.js';
3
3
  import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
4
- import { workerAppHostname } from './hostnames.js';
4
+ import { workerAppHostname, workspaceHost, workspaceInboxHost } from './hostnames.js';
5
5
  import { orgTarget, platformTarget } from './own-services.js';
6
6
  import { appNameFor } from './platform-diff.js';
7
7
  /**
@@ -20,6 +20,8 @@ export function companyServiceNames(org) {
20
20
  `${s}-tunnel`,
21
21
  `${s}-cloudflared`,
22
22
  ];
23
+ if (kosmosOn(org))
24
+ names.push(`${s}-kosmos`, `${s}-kosmos-db`);
23
25
  if (org.chat?.enabled) {
24
26
  names.push(...Object.values(chatInternalNames(s)), ...Object.values(chatRekeyedInternalNames(s)));
25
27
  }
@@ -61,6 +63,17 @@ export function orgDerivedNames(org) {
61
63
  if (org.hosting.entries.some(entryHasDatabase)) {
62
64
  names.push({ name: `${org.slug}-db`, source: 'hosting' });
63
65
  }
66
+ // A workspace's host and the test inbox's: one label under one zone, so it is compared across orgs like the names above.
67
+ if (org.hosting.enabled) {
68
+ for (const entry of org.hosting.entries) {
69
+ if (entry.workspace) {
70
+ names.push({ name: workspaceHost(org.slug, entry.key), source: 'hosting' });
71
+ }
72
+ }
73
+ if (org.hosting.entries.some((e) => e.workspace && !e.workerApp)) {
74
+ names.push({ name: workspaceInboxHost(org.slug), source: 'hosting' });
75
+ }
76
+ }
64
77
  return names;
65
78
  }
66
79
  /** Names the platform's own steps give apps, tunnels, connectors, databases and projects outside `platformTarget()`. */
@@ -64,6 +64,7 @@ export function parseMigrationConfirmation(raw) {
64
64
  const MIGRATION_ORDER = [
65
65
  'app',
66
66
  'cms',
67
+ 'kosmos',
67
68
  'boule',
68
69
  'chat_service',
69
70
  'calls_service',
@@ -24,6 +24,29 @@ export declare function planOwnJobs(options: PlanOwnJobsOptions): {
24
24
  step: DraftStep | null;
25
25
  blocked: string | null;
26
26
  };
27
+ /** `unmanaged` key of the one line `planOwnJobsRoute` reports. */
28
+ export declare const OWN_JOBS_ROUTE_BLOCKED_KEY = "own-jobs-route";
29
+ export interface PlanOwnJobsRouteOptions {
30
+ org: DeclaredOrg;
31
+ recordedIds: Readonly<Record<string, string>>;
32
+ readiness: OrgPlatformReadiness;
33
+ /** The executor was built with an `ownServices` port. */
34
+ ownServices: boolean;
35
+ /** The org's zone is live and active: none is planned and none is waiting. */
36
+ zoneActive: boolean;
37
+ /** The org's own scheduler step is planned in this same plan, so its address is recorded by the time the route runs. */
38
+ schedulerPlanned: boolean;
39
+ }
40
+ /**
41
+ * The public route of an own org's jobs scheduler: `jobs.<domain>`, serving only what an app calls. The
42
+ * owner company always publishes its scheduler, since every company on shared services registers there; any
43
+ * other own org publishes its own only while an entry declares `uses.jobs`. Planned again when the hostname
44
+ * moved. Every blocked line is fixed text: it is part of the stale-plan comparison.
45
+ */
46
+ export declare function planOwnJobsRoute(options: PlanOwnJobsRouteOptions): {
47
+ step: DraftStep | null;
48
+ blocked: string | null;
49
+ };
27
50
  /** `unmanaged` key of the one line `planOwnAuth` reports. */
28
51
  export declare const OWN_AUTH_BLOCKED_KEY = "own-auth";
29
52
  export { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY };