@wtfalch/foundry 0.41.0 → 0.41.2

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.
@@ -5975,6 +5975,7 @@ export class FoundryStepExecutor {
5975
5975
  apex: ownHostnames(org).zone,
5976
5976
  version: action.desired.version,
5977
5977
  project: { ensure: () => this.ensureCoolifyProject(org, context) },
5978
+ ...(context.serverIpv4 ? { serverIpv4: context.serverIpv4 } : {}),
5978
5979
  });
5979
5980
  }
5980
5981
  async jobsRegistration(action, org, context) {
@@ -135,7 +135,7 @@ export interface PlanOwnSfuOptions {
135
135
  /**
136
136
  * An own org (`services.own`) with calls on runs its own LiveKit media server
137
137
  * as a Coolify app. Applied once, and again when the pinned LiveKit version,
138
- * the slot or the hostname changed, or an earlier run died before the deploy
138
+ * the slot or the hostname changed, no `node_ip` is recorded (applied before 0.41.2), or an earlier run died before the deploy
139
139
  * finished. `firewall` is the one line that says which ports the server must
140
140
  * have open: foundry opens none, so it stays until the host confirms them.
141
141
  * Every line is fixed text: it is part of the stale-plan comparison.
@@ -9,7 +9,7 @@ import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMT
9
9
  import { mailStoreIsLegacy, mailStoreSwitchDescription } from './mail-store.js';
10
10
  import { ownHostnames } from './own-hostnames.js';
11
11
  import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
12
- import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
12
+ import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
13
13
  import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
14
14
  import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
15
15
  import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
@@ -312,7 +312,7 @@ export const OWN_SFU_FIREWALL_KEY = 'own-sfu:firewall';
312
312
  /**
313
313
  * An own org (`services.own`) with calls on runs its own LiveKit media server
314
314
  * as a Coolify app. Applied once, and again when the pinned LiveKit version,
315
- * the slot or the hostname changed, or an earlier run died before the deploy
315
+ * the slot or the hostname changed, no `node_ip` is recorded (applied before 0.41.2), or an earlier run died before the deploy
316
316
  * finished. `firewall` is the one line that says which ports the server must
317
317
  * have open: foundry opens none, so it stays until the host confirms them.
318
318
  * Every line is fixed text: it is part of the stale-plan comparison.
@@ -361,6 +361,7 @@ export function planOwnSfu(options) {
361
361
  recordedIds[OWN_SFU_APPLIED_VERSION_SCOPE_KEY] === version &&
362
362
  recordedIds[OWN_SFU_APPLIED_SLOT_SCOPE_KEY] === String(slot) &&
363
363
  recordedIds[OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
364
+ Boolean(recordedIds[OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY]) &&
364
365
  !recordedIds[OWN_SFU_DEPLOY_PENDING_SCOPE_KEY] &&
365
366
  Boolean(recordedIds[SFU_INTERNAL_URL_SCOPE_KEY]) &&
366
367
  Boolean(recordedIds[SFU_PUBLIC_URL_SCOPE_KEY]);
@@ -21,6 +21,8 @@ export declare const OWN_SFU_DEPLOY_PENDING_SCOPE_KEY = "own_sfu_deploy_pending"
21
21
  export declare const OWN_SFU_APPLIED_VERSION_SCOPE_KEY = "own_sfu_applied_version";
22
22
  export declare const OWN_SFU_APPLIED_SLOT_SCOPE_KEY = "own_sfu_applied_slot";
23
23
  export declare const OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY = "own_sfu_applied_hostname";
24
+ /** LiveKit's `node_ip` as last applied. Absent: applied before 0.41.2, when it could be `host.docker.internal`. */
25
+ export declare const OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY = "own_sfu_applied_node_ip";
24
26
  export declare const SFU_INTERNAL_URL_SCOPE_KEY = "sfu_internal_url";
25
27
  export declare const SFU_PUBLIC_URL_SCOPE_KEY = "sfu_public_url";
26
28
  export interface SfuAppDeps {
@@ -45,6 +47,8 @@ export interface SfuAppInput {
45
47
  /** The pinned LiveKit release tag. */
46
48
  version: string;
47
49
  project: ProjectSource;
50
+ /** The platform's declared public IPv4: LiveKit's `node_ip`. Absent: the Coolify server's ip, when it is an IPv4. */
51
+ serverIpv4?: string;
48
52
  }
49
53
  /** The compose `ports` lines for a slot: the UDP range and the TCP fallback port, one to one. */
50
54
  export declare function sfuComposePorts(slot: number): string[];
@@ -1,3 +1,4 @@
1
+ import { isIPv4 } from 'node:net';
1
2
  import { DIGESTS, VERSIONS } from '../bootstrap/versions.js';
2
3
  import { mintLivekitCredentials } from '../calls-sfu-keys.js';
3
4
  import { LIVEKIT_SIGNALLING_PORT, sfuSlotPorts } from '../calls-sfu.js';
@@ -22,6 +23,8 @@ export const OWN_SFU_DEPLOY_PENDING_SCOPE_KEY = 'own_sfu_deploy_pending';
22
23
  export const OWN_SFU_APPLIED_VERSION_SCOPE_KEY = 'own_sfu_applied_version';
23
24
  export const OWN_SFU_APPLIED_SLOT_SCOPE_KEY = 'own_sfu_applied_slot';
24
25
  export const OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY = 'own_sfu_applied_hostname';
26
+ /** LiveKit's `node_ip` as last applied. Absent: applied before 0.41.2, when it could be `host.docker.internal`. */
27
+ export const OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY = 'own_sfu_applied_node_ip';
25
28
  export const SFU_INTERNAL_URL_SCOPE_KEY = 'sfu_internal_url';
26
29
  export const SFU_PUBLIC_URL_SCOPE_KEY = 'sfu_public_url';
27
30
  const KEYS_DELIVERY_ATTEMPTS = 3;
@@ -132,8 +135,12 @@ export async function provisionSfuApp(scope, deps, target, input) {
132
135
  const server = servers.find((s) => s.is_usable && s.is_reachable) ?? servers[0];
133
136
  if (!server)
134
137
  throw new Error(`${label}: no usable Coolify server is registered`);
135
- if (!server.ip)
136
- throw new Error(`${label}: the Coolify server has no address to give LiveKit`);
138
+ // Coolify reports its localhost server as `host.docker.internal`, which LiveKit refuses as `node_ip`
139
+ // and then restarts forever; the declared address wins, the same rule as the platform's mail server.
140
+ const nodeIp = input.serverIpv4 ?? (server.ip && isIPv4(server.ip) ? server.ip : undefined);
141
+ if (!nodeIp) {
142
+ throw new Error(`${label}: no public IPv4 to give LiveKit; declare the server's public IPv4 as serverIpv4 in the platform config`);
143
+ }
137
144
  const compose = sfuCompose({
138
145
  serviceName: target.internal.sfu,
139
146
  slot: input.slot,
@@ -213,7 +220,7 @@ export async function provisionSfuApp(scope, deps, target, input) {
213
220
  keysJustSet = true;
214
221
  }
215
222
  }
216
- const config = sfuConfig({ slot: input.slot, nodeIp: server.ip, keys });
223
+ const config = sfuConfig({ slot: input.slot, nodeIp, keys });
217
224
  deps.onSecret?.(config);
218
225
  const envChanged = liveConfig !== config;
219
226
  const pending = Boolean(scope.recordedIds()[OWN_SFU_DEPLOY_PENDING_SCOPE_KEY]) || changed || envChanged;
@@ -278,6 +285,7 @@ export async function provisionSfuApp(scope, deps, target, input) {
278
285
  scope.recordScopeId(OWN_SFU_APPLIED_VERSION_SCOPE_KEY, input.version);
279
286
  scope.recordScopeId(OWN_SFU_APPLIED_SLOT_SCOPE_KEY, String(input.slot));
280
287
  scope.recordScopeId(OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, input.hostname);
288
+ scope.recordScopeId(OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, nodeIp);
281
289
  scope.recordScopeId(SFU_INTERNAL_URL_SCOPE_KEY, signalling);
282
290
  scope.recordScopeId(SFU_PUBLIC_URL_SCOPE_KEY, `https://${input.hostname}`);
283
291
  return { resourceId: serviceId };
@@ -130,6 +130,12 @@ export interface StepExecutionContext {
130
130
  * platform's Docker network, not on `co-<slug>`, so Archon reaches its database. Absent: false.
131
131
  */
132
132
  readonly ownerCompany?: boolean;
133
+ /**
134
+ * The platform's declared public IPv4 (`DeclaredPlatform.serverIpv4`). A company's media server
135
+ * gives it to LiveKit as `node_ip`: Coolify reports its localhost server as `host.docker.internal`,
136
+ * which LiveKit refuses. Absent: none declared.
137
+ */
138
+ readonly serverIpv4?: string;
133
139
  /**
134
140
  * Every id foundry has recorded for this org so far — both `system:kind:key`
135
141
  * resource ids (`runSteps` records these itself from `execute`'s own return
@@ -908,10 +908,13 @@ export class FoundryPlanApply {
908
908
  const checkpoints = [];
909
909
  // Backed by the very `serviceIds` map this method persists once, at the
910
910
  // end — see `StepExecutionContext`'s doc comment for why.
911
- const ownerCompany = (await this.platformDeps?.store.get())?.ownerOrgId === orgId;
911
+ const declaredPlatform = await this.platformDeps?.store.get();
912
+ const ownerCompany = declaredPlatform?.ownerOrgId === orgId;
913
+ const serverIpv4 = declaredPlatform?.serverIpv4 ?? undefined;
912
914
  const context = {
913
915
  org,
914
916
  ownerCompany,
917
+ ...(serverIpv4 ? { serverIpv4 } : {}),
915
918
  recordedIds: () => ({ ...serviceIds }),
916
919
  now: () => this.now().getTime(),
917
920
  recordScopeId: (key, id) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",