@wtfalch/foundry 0.33.1 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +1 -1
  2. package/dist/bootstrap/bootstrap.js +49 -11
  3. package/dist/bootstrap/declare.d.ts +12 -5
  4. package/dist/bootstrap/declare.js +33 -18
  5. package/dist/bootstrap/versions.d.ts +24 -0
  6. package/dist/bootstrap/versions.js +46 -0
  7. package/dist/calls-sfu.d.ts +10 -0
  8. package/dist/calls-sfu.js +44 -3
  9. package/dist/index.d.ts +21 -8
  10. package/dist/index.js +11 -4
  11. package/dist/migrations/0051_org_services.sql +13 -0
  12. package/dist/migrations/0052_platform_mail_relay.sql +8 -0
  13. package/dist/migrations/0053_heartbeats_provider.sql +9 -0
  14. package/dist/migrations/0053_org_version_pin.sql +6 -0
  15. package/dist/org.d.ts +76 -12
  16. package/dist/org.js +136 -20
  17. package/dist/plan/backup-gate.d.ts +93 -0
  18. package/dist/plan/backup-gate.js +261 -0
  19. package/dist/plan/broker-entries.d.ts +9 -1
  20. package/dist/plan/broker-entries.js +20 -3
  21. package/dist/plan/build-record.d.ts +56 -0
  22. package/dist/plan/build-record.js +109 -0
  23. package/dist/plan/calls-matrix.d.ts +9 -0
  24. package/dist/plan/calls-matrix.js +18 -1
  25. package/dist/plan/cloudflared-connector.d.ts +7 -1
  26. package/dist/plan/cloudflared-connector.js +15 -2
  27. package/dist/plan/company-backups.d.ts +31 -0
  28. package/dist/plan/company-backups.js +101 -0
  29. package/dist/plan/deploy-health.d.ts +12 -2
  30. package/dist/plan/deploy-health.js +3 -1
  31. package/dist/plan/destinations.d.ts +73 -0
  32. package/dist/plan/destinations.js +217 -0
  33. package/dist/plan/diff.d.ts +46 -8
  34. package/dist/plan/diff.js +367 -126
  35. package/dist/plan/edge-access.d.ts +15 -0
  36. package/dist/plan/edge-access.js +21 -2
  37. package/dist/plan/executor.d.ts +276 -5
  38. package/dist/plan/executor.js +1111 -109
  39. package/dist/plan/health-check-settings.d.ts +28 -0
  40. package/dist/plan/health-check-settings.js +36 -0
  41. package/dist/plan/health-gaps.d.ts +33 -0
  42. package/dist/plan/health-gaps.js +89 -0
  43. package/dist/plan/health-targets.d.ts +1 -3
  44. package/dist/plan/health-targets.js +36 -36
  45. package/dist/plan/held-key-refs.d.ts +15 -0
  46. package/dist/plan/held-key-refs.js +15 -0
  47. package/dist/plan/hostnames.d.ts +10 -0
  48. package/dist/plan/hostnames.js +14 -0
  49. package/dist/plan/index.d.ts +19 -6
  50. package/dist/plan/index.js +10 -3
  51. package/dist/plan/live.d.ts +11 -2
  52. package/dist/plan/live.js +16 -2
  53. package/dist/plan/locked-hostnames.d.ts +7 -3
  54. package/dist/plan/locked-hostnames.js +25 -4
  55. package/dist/plan/login-host.d.ts +61 -12
  56. package/dist/plan/login-host.js +99 -11
  57. package/dist/plan/mail-relay.d.ts +139 -0
  58. package/dist/plan/mail-relay.js +400 -0
  59. package/dist/plan/network-move.d.ts +20 -0
  60. package/dist/plan/network-move.js +41 -0
  61. package/dist/plan/oikos-ingress.d.ts +111 -0
  62. package/dist/plan/oikos-ingress.js +231 -0
  63. package/dist/plan/org-names.d.ts +30 -0
  64. package/dist/plan/org-names.js +108 -0
  65. package/dist/plan/own-diff.d.ts +170 -0
  66. package/dist/plan/own-diff.js +440 -0
  67. package/dist/plan/own-dns.d.ts +6 -0
  68. package/dist/plan/own-dns.js +15 -0
  69. package/dist/plan/own-hostnames.d.ts +27 -0
  70. package/dist/plan/own-hostnames.js +24 -0
  71. package/dist/plan/own-limits.d.ts +213 -0
  72. package/dist/plan/own-limits.js +232 -0
  73. package/dist/plan/own-services.d.ts +193 -0
  74. package/dist/plan/own-services.js +169 -0
  75. package/dist/plan/own-sfu.d.ts +69 -0
  76. package/dist/plan/own-sfu.js +256 -0
  77. package/dist/plan/plan-apply.d.ts +121 -10
  78. package/dist/plan/plan-apply.js +357 -18
  79. package/dist/plan/platform-diff.d.ts +155 -11
  80. package/dist/plan/platform-diff.js +271 -66
  81. package/dist/plan/platform-org.d.ts +107 -4
  82. package/dist/plan/platform-org.js +175 -3
  83. package/dist/plan/platform-providers.d.ts +474 -81
  84. package/dist/plan/platform-providers.js +1407 -560
  85. package/dist/plan/proxy-labels.d.ts +57 -11
  86. package/dist/plan/proxy-labels.js +94 -14
  87. package/dist/plan/run-options.d.ts +19 -0
  88. package/dist/plan/run-options.js +46 -0
  89. package/dist/plan/sign-in-org.d.ts +1 -1
  90. package/dist/plan/sign-in-org.js +20 -2
  91. package/dist/plan/zitadel-login-policy.d.ts +1 -1
  92. package/dist/plan/zitadel-login-policy.js +77 -9
  93. package/dist/plan/zitadel-system-api.d.ts +22 -1
  94. package/dist/plan/zitadel-system-api.js +49 -8
  95. package/dist/platform.d.ts +40 -0
  96. package/dist/platform.js +16 -0
  97. package/dist/provision.d.ts +2 -0
  98. package/dist/provision.js +3 -1
  99. package/dist/slug.d.ts +3 -1
  100. package/dist/slug.js +26 -3
  101. package/dist/store/postgres.js +93 -2
  102. package/dist/store/schema.d.ts +87 -0
  103. package/dist/store/schema.js +9 -0
  104. package/package.json +3 -3
  105. package/versions.digests.json +11 -0
  106. package/versions.json +3 -1
@@ -1,5 +1,6 @@
1
+ import { type DeclaredOrg, type VersionPin } from '../org.js';
1
2
  import type { DeclaredPlatform } from '../platform.js';
2
- import type { AgoraHostValues } from './agora.js';
3
+ import { type AgoraHostValues } from './agora.js';
3
4
  /**
4
5
  * #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
5
6
  * fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
@@ -75,8 +76,8 @@ export interface OrgPlatformReadiness {
75
76
  /** The AI host (#172), the same always-on shape as {@link filesReady}. */
76
77
  aiReady: boolean;
77
78
  /**
78
- * The AI host's internal address, which an entry with `uses.ai` gets as
79
- * `AI_URL`. `null` or absent until the platform has recorded it.
79
+ * The AI host's public origin (`https://<platform.ai.hostname>`), which an entry with `uses.ai`
80
+ * gets as `AI_URL`. `null` or absent until the platform has applied the AI host.
80
81
  */
81
82
  aiUrl?: string | null;
82
83
  /**
@@ -86,8 +87,30 @@ export interface OrgPlatformReadiness {
86
87
  * read, so "applied" has no separate check.
87
88
  */
88
89
  callsReady: boolean;
90
+ /** The shared oikos.host ingress (tunnel, wildcard record, rate limit) is applied. Absent reads as false. */
91
+ oikosIngressApplied?: boolean;
92
+ /**
93
+ * The broker app Archon last registered on (`PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY`). The
94
+ * owner's own sign-in counts that registration as its broker's first entry: the broker starts
95
+ * on it and the login client is minted once it is there. Absent or `null` when Archon has not
96
+ * registered.
97
+ */
98
+ archonBrokerAppId?: string | null;
99
+ /**
100
+ * The ZITADEL project and client ids the platform made in the owner company's own ZITADEL
101
+ * (Archon's and the Access provider's). The owner company's page counts them as the
102
+ * platform's, not as strangers. Absent when none is recorded.
103
+ */
104
+ platformZitadelIds?: readonly string[];
89
105
  /** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
90
106
  authReady: boolean;
107
+ /**
108
+ * The host of the platform's own shared sign-in when its Coolify app is still recorded
109
+ * (`coolify:app:platform:auth`), whether or not this build declares it. Absent on a platform
110
+ * built from empty. The owner company's own sign-in takes the same name on the apex, so it
111
+ * waits while this is set.
112
+ */
113
+ platformAuthRecordedHost?: string;
91
114
  /** `https://<platform.auth.hostname>`, the platform's own sign-in issuer; `null` until {@link authReady}. The owner invite reads its issuer here, never from Agora's host config. */
92
115
  authIssuer: string | null;
93
116
  /** The Access provider id the platform recorded (`ACCESS_PROVIDER_RECORD_KEY`), which every gate requires. `null` or absent until the platform has recorded it. */
@@ -141,7 +164,7 @@ export interface OrgPlatformReadiness {
141
164
  };
142
165
  /**
143
166
  * The platform's Lettermint key (`PLATFORM_AUTH_BROKER_MAIL_KEY_REF`) is held,
144
- * so an entry env from `platformMailKey` can be filled. Absent reads as not held.
167
+ * so the owner company's mail can be planned. Absent reads as not held.
145
168
  */
146
169
  mailKeyHeld?: boolean;
147
170
  /** The host's held keys can `put`, so an entry env from `sharedSecret` can be filled. Absent reads as false. */
@@ -151,6 +174,19 @@ export interface OrgPlatformReadiness {
151
174
  * A missing key is then reported as "no reader given" instead of "not held".
152
175
  */
153
176
  noKeyReader?: boolean;
177
+ /** The platform's declared git ref for each service, which an own org's services deploy. */
178
+ serviceGitRefs?: {
179
+ auth: string;
180
+ mail: string;
181
+ files: string;
182
+ jobs: string;
183
+ };
184
+ /**
185
+ * The user id an own org's files host takes as `AUTHZ_BOOTSTRAP_OPERATOR`: the owner company's
186
+ * owner in the owner company's own ZITADEL ({@link platformOwnerUserIdFrom}), or `'none'` when
187
+ * the owner company has no owner to invite. `null` or absent until recorded.
188
+ */
189
+ platformOwnerUserId?: string | null;
154
190
  }
155
191
  /**
156
192
  * What an org's plan reads when no platform is declared: nothing is ready, so
@@ -207,6 +243,26 @@ heldRefs?: ReadonlySet<string>,
207
243
  noKeyReader?: boolean,
208
244
  /** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
209
245
  edgePortPresent?: boolean): OrgPlatformReadiness;
246
+ /** The git refs the platform declares for the services an own org deploys; a company's `pin` wins per part. */
247
+ export declare function serviceGitRefsOf(platform: DeclaredPlatform, pin?: VersionPin | null): {
248
+ auth: string;
249
+ mail: string;
250
+ files: string;
251
+ jobs: string;
252
+ };
253
+ /** The runner app both the platform's and an own org's files step record. */
254
+ export declare const OWN_FILES_RUNNER_SCOPE_KEY = "coolify_platform_files_runner_app";
255
+ /**
256
+ * An own org (`services.own`) plans against its own services, so its readiness
257
+ * reads only its own recorded ids and its own held keys; nothing the shared
258
+ * services recorded reaches it. AI, edge and Access stay shared and come from
259
+ * `base`. Any other org gets `base` back unchanged.
260
+ */
261
+ export declare function orgPlatformReadinessFor(org: DeclaredOrg, base: OrgPlatformReadiness, orgRecordedIds: Readonly<Record<string, string>>, heldRefs: ReadonlySet<string>): OrgPlatformReadiness;
262
+ /** The own org's held-key refs `heldKeys` resolves right now. A resolver that throws counts as not held, and its error goes nowhere. */
263
+ export declare function heldOwnKeyRefs(heldKeys: HeldKeyResolver, org: Pick<DeclaredOrg, 'slug'>): Promise<Set<string>>;
264
+ /** {@link agoraHostValuesFromPlatform}'s counterpart for an own org: every value is the org's own, null until recorded. */
265
+ export declare function agoraHostValuesForOwn(org: DeclaredOrg, orgRecordedIds: Readonly<Record<string, string>>): AgoraHostValues;
210
266
  /**
211
267
  * #177: {@link AgoraHostValues} derived from the same platform state, rather
212
268
  * than requiring a bespoke host-supplied `AgoraHostPort` for values the
@@ -223,3 +279,50 @@ edgePortPresent?: boolean): OrgPlatformReadiness;
223
279
  * host-supplied `agora` port overrides it.
224
280
  */
225
281
  export declare function agoraHostValuesFromPlatform(platform: DeclaredPlatform, platformRecordedIds: Readonly<Record<string, string>>): AgoraHostValues;
282
+ /**
283
+ * The owner company's own sign-in, as the platform plan reads it. `null` (the
284
+ * platform plan's input) until the owner's own sign-in step and its ZITADEL
285
+ * organisation are applied. `bearerRef` is a held-key reference, never a value.
286
+ */
287
+ export interface OwnerSignIn {
288
+ issuerUrl: string;
289
+ organisationId: string;
290
+ brokerAppId: string;
291
+ bearerRef: string;
292
+ hostname: string;
293
+ /**
294
+ * The company's first owner in its own organisation (the CI-side half of
295
+ * Archon's first-owner link): the recorded user id, `'none'` when no owner
296
+ * is declared, absent while the owner step has recorded nothing.
297
+ */
298
+ firstOwnerUserId?: string | null;
299
+ }
300
+ /** The owner company's own files host, as Archon's `FILES_*` env reads it. */
301
+ export interface OwnerFiles {
302
+ internalUrl: string;
303
+ audience: string;
304
+ organisationId: string;
305
+ managementKeyRef: string;
306
+ }
307
+ /** What the platform plan reads about the owner company. */
308
+ export interface PlatformOwnerInput {
309
+ signIn?: OwnerSignIn | null;
310
+ files?: OwnerFiles | null;
311
+ }
312
+ /**
313
+ * The owner's sign-in from its declared org and the ids its own steps recorded.
314
+ * The host and issuer are the host the own-auth step last applied, and only
315
+ * while that is the declared `ownHostnames(org).auth`: a domain edit gives
316
+ * `null` until the step has applied the new host, so the owner's instance
317
+ * token is never sent to a host the step has not set up.
318
+ */
319
+ export declare function ownerSignInFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): OwnerSignIn | null;
320
+ /**
321
+ * The platform's operator identity for a company's files host: the owner company's first owner,
322
+ * as the owner's `zitadel:owner` step recorded it in the owner company's own ZITADEL. `'none'`
323
+ * when the owner company declares no owner; `null` until the step has recorded the id, and
324
+ * when there is no owner company running its own services.
325
+ */
326
+ export declare function platformOwnerUserIdFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): string | null;
327
+ /** The owner's files host once its own files step has applied; `null` before. */
328
+ export declare function ownerFilesFrom(owner: DeclaredOrg | null, recorded: Readonly<Record<string, string>>): OwnerFiles | null;
@@ -1,6 +1,12 @@
1
+ import { ownsServices } from '../org.js';
2
+ import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
1
3
  import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
2
- import { JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_EDGE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
3
- import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './platform-providers.js';
4
+ import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
5
+ import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
6
+ import { oikosIngressApplied } from './oikos-ingress.js';
7
+ import { ownHostnames } from './own-hostnames.js';
8
+ import { OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget } from './own-services.js';
9
+ import { JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_EDGE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
4
10
  /**
5
11
  * #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
6
12
  * fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
@@ -132,11 +138,27 @@ edgePortPresent = false) {
132
138
  ? (platformRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] ?? null)
133
139
  : null,
134
140
  aiReady: aiApplied && missingKeys.ai === null,
135
- aiUrl: aiApplied ? (platformRecordedIds[PLATFORM_AI_INTERNAL_URL_SCOPE_KEY] ?? null) : null,
141
+ // An entry runs on its company's own network and cannot dial the platform's container name, so it
142
+ // is given the host's public origin.
143
+ // Only once the route (tunnel, connector, DNS record) is recorded: before it the hostname answers nothing.
144
+ aiUrl: aiApplied && platformRecordedIds[PLATFORM_AI_TUNNEL_SCOPE_KEY]
145
+ ? `https://${platform.ai.hostname}`
146
+ : null,
136
147
  callsReady: platform.sfu.enabled,
148
+ oikosIngressApplied: oikosIngressApplied(platformRecordedIds),
149
+ archonBrokerAppId: platformRecordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ?? null,
150
+ platformZitadelIds: [
151
+ PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY,
152
+ PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY,
153
+ PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY,
154
+ PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY,
155
+ ].flatMap((key) => platformRecordedIds[key] || []),
137
156
  ...(platformRecordedIds[ACCESS_PROVIDER_RECORD_KEY]
138
157
  ? { accessProviderId: platformRecordedIds[ACCESS_PROVIDER_RECORD_KEY] }
139
158
  : {}),
159
+ ...(coolifyAppApplied(platformRecordedIds, PLATFORM_AUTH_KEY)
160
+ ? { platformAuthRecordedHost: platform.auth.hostname }
161
+ : {}),
140
162
  authReady,
141
163
  authIssuer: authReady ? `https://${platform.auth.hostname}` : null,
142
164
  jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
@@ -147,6 +169,100 @@ edgePortPresent = false) {
147
169
  ...(noKeyReader ? { noKeyReader } : {}),
148
170
  };
149
171
  }
172
+ /** The git refs the platform declares for the services an own org deploys; a company's `pin` wins per part. */
173
+ export function serviceGitRefsOf(platform, pin) {
174
+ return {
175
+ auth: pin?.auth ?? platform.auth.gitRef,
176
+ mail: pin?.mail ?? platform.mail.gitRef,
177
+ files: pin?.files ?? platform.files.gitRef,
178
+ jobs: pin?.jobs ?? platform.jobs.gitRef,
179
+ };
180
+ }
181
+ /** The runner app both the platform's and an own org's files step record. */
182
+ export const OWN_FILES_RUNNER_SCOPE_KEY = 'coolify_platform_files_runner_app';
183
+ const OWN_MAIL_APP_SCOPE_KEY = 'coolify_platform_mail_app';
184
+ /** Resource id of an own-service step, in `AppliedState.serviceIds`' `system:kind:key` form. */
185
+ function ownStepRecorded(ids, org, kind) {
186
+ return Boolean(ids[`coolify:${kind}:${org.id}`]);
187
+ }
188
+ /**
189
+ * The own org's sign-in is up: applied, so its ZITADEL answers. The broker and the login client
190
+ * come after the org's first broker entry, as the platform's do after its first registration, so
191
+ * waiting for them here would wait for the very steps this gate lets run.
192
+ */
193
+ function ownAuthReady(ids) {
194
+ return Boolean(ids[OWN_SERVICES_APPLIED_SCOPE_KEY]);
195
+ }
196
+ /**
197
+ * An own org (`services.own`) plans against its own services, so its readiness
198
+ * reads only its own recorded ids and its own held keys; nothing the shared
199
+ * services recorded reaches it. AI, edge and Access stay shared and come from
200
+ * `base`. Any other org gets `base` back unchanged.
201
+ */
202
+ export function orgPlatformReadinessFor(org, base, orgRecordedIds, heldRefs) {
203
+ if (!ownsServices(org))
204
+ return base;
205
+ const refs = orgTarget(org).refs;
206
+ const authReady = ownAuthReady(orgRecordedIds);
207
+ const filesApplied = ownStepRecorded(orgRecordedIds, org, 'own-files') &&
208
+ Boolean(orgRecordedIds[OWN_FILES_RUNNER_SCOPE_KEY]);
209
+ const jobsApplied = ownStepRecorded(orgRecordedIds, org, 'own-jobs');
210
+ const mailApplied = ownStepRecorded(orgRecordedIds, org, 'own-mail') &&
211
+ Boolean(orgRecordedIds[OWN_MAIL_APP_SCOPE_KEY]);
212
+ const filesUrl = orgRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY];
213
+ const jobsUrl = orgRecordedIds[JOBS_INTERNAL_URL_SCOPE_KEY];
214
+ const missing = (applied, ref) => applied && !heldRefs.has(ref) ? ref : null;
215
+ const missingKeys = {
216
+ files: missing(filesApplied, refs.filesManagement),
217
+ ai: base.missingKeys.ai,
218
+ jobs: missing(jobsApplied, refs.jobsAdminToken),
219
+ };
220
+ const { jobsUrlMissing: _shared, ...rest } = base;
221
+ return {
222
+ ...rest,
223
+ authReady,
224
+ authIssuer: authReady ? ownHostnames(org).issuerUrl : null,
225
+ filesReady: filesApplied && filesUrl !== undefined && missingKeys.files === null,
226
+ filesUrl: filesApplied ? (filesUrl ?? null) : null,
227
+ jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
228
+ ...(jobsApplied && jobsUrl === undefined ? { jobsUrlMissing: true } : {}),
229
+ callsReady: ownStepRecorded(orgRecordedIds, org, 'own-sfu'),
230
+ // The MX name is the shared relay's, `mx.<platform apex>`: no platform mail step is needed.
231
+ // The apex is the company's mail zone: its domain when mail runs on it, else oikos.host.
232
+ mailHost: mailApplied && base.platformApex
233
+ ? {
234
+ apex: org.domain && org.mail.usesDomain ? org.domain : NO_DOMAIN_MAIL_APEX,
235
+ mailHost: platformMailMxHost(base.platformApex),
236
+ }
237
+ : null,
238
+ missingKeys,
239
+ };
240
+ }
241
+ /** The own org's held-key refs `heldKeys` resolves right now. A resolver that throws counts as not held, and its error goes nowhere. */
242
+ export async function heldOwnKeyRefs(heldKeys, org) {
243
+ const held = new Set();
244
+ for (const ref of Object.values(orgTarget(org).refs)) {
245
+ try {
246
+ if (await heldKeys.resolve(ref))
247
+ held.add(ref);
248
+ }
249
+ catch {
250
+ // not held
251
+ }
252
+ }
253
+ return held;
254
+ }
255
+ /** {@link agoraHostValuesFromPlatform}'s counterpart for an own org: every value is the org's own, null until recorded. */
256
+ export function agoraHostValuesForOwn(org, orgRecordedIds) {
257
+ const authReady = ownAuthReady(orgRecordedIds);
258
+ return {
259
+ issuer: authReady ? ownHostnames(org).issuerUrl : null,
260
+ mailProjectId: orgRecordedIds[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY] ?? null,
261
+ messagingProjectId: null,
262
+ mailUpstream: orgRecordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY] ?? null,
263
+ filesUpstream: orgRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] ?? null,
264
+ };
265
+ }
150
266
  /**
151
267
  * #177: {@link AgoraHostValues} derived from the same platform state, rather
152
268
  * than requiring a bespoke host-supplied `AgoraHostPort` for values the
@@ -180,3 +296,59 @@ export function agoraHostValuesFromPlatform(platform, platformRecordedIds) {
180
296
  : null,
181
297
  };
182
298
  }
299
+ /**
300
+ * The owner's sign-in from its declared org and the ids its own steps recorded.
301
+ * The host and issuer are the host the own-auth step last applied, and only
302
+ * while that is the declared `ownHostnames(org).auth`: a domain edit gives
303
+ * `null` until the step has applied the new host, so the owner's instance
304
+ * token is never sent to a host the step has not set up.
305
+ */
306
+ export function ownerSignInFrom(owner, recorded) {
307
+ if (!owner || !ownsServices(owner) || !ownAuthApplied(owner, recorded))
308
+ return null;
309
+ const organisationId = recorded[ZITADEL_ORG_SCOPE_KEY];
310
+ const brokerAppId = recorded[PLATFORM_AUTH_BROKER_SCOPE_KEY];
311
+ if (!organisationId || !brokerAppId)
312
+ return null;
313
+ const applied = recorded[OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY];
314
+ if (!applied || applied !== ownHostnames(owner).auth)
315
+ return null;
316
+ const hostname = applied;
317
+ return {
318
+ issuerUrl: `https://${applied}`,
319
+ organisationId,
320
+ brokerAppId,
321
+ bearerRef: orgTarget(owner).refs.instanceBearer,
322
+ hostname,
323
+ firstOwnerUserId: platformOwnerUserIdFrom(owner, recorded),
324
+ };
325
+ }
326
+ /**
327
+ * The platform's operator identity for a company's files host: the owner company's first owner,
328
+ * as the owner's `zitadel:owner` step recorded it in the owner company's own ZITADEL. `'none'`
329
+ * when the owner company declares no owner; `null` until the step has recorded the id, and
330
+ * when there is no owner company running its own services.
331
+ */
332
+ export function platformOwnerUserIdFrom(owner, recorded) {
333
+ if (!owner || !ownsServices(owner))
334
+ return null;
335
+ const first = owner.owners[0];
336
+ if (!first)
337
+ return 'none';
338
+ return recorded[`zitadel:owner:${owner.id}:${first.email.toLowerCase()}`] || null;
339
+ }
340
+ /** The owner's files host once its own files step has applied; `null` before. */
341
+ export function ownerFilesFrom(owner, recorded) {
342
+ if (!owner || !ownsServices(owner) || !recorded[`coolify:own-files:${owner.id}`])
343
+ return null;
344
+ const internalUrl = recorded[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY];
345
+ const hostname = recorded[PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY];
346
+ if (!internalUrl || !hostname)
347
+ return null;
348
+ return {
349
+ internalUrl,
350
+ audience: `https://${hostname}`,
351
+ organisationId: owner.id,
352
+ managementKeyRef: orgTarget(owner).refs.filesManagement,
353
+ };
354
+ }