@wtfalch/foundry 0.33.1 → 0.34.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 (65) hide show
  1. package/dist/bootstrap/versions.d.ts +4 -0
  2. package/dist/calls-sfu.d.ts +10 -0
  3. package/dist/calls-sfu.js +41 -0
  4. package/dist/index.d.ts +14 -6
  5. package/dist/index.js +7 -3
  6. package/dist/migrations/0051_org_services.sql +13 -0
  7. package/dist/migrations/0052_platform_mail_relay.sql +8 -0
  8. package/dist/org.d.ts +18 -0
  9. package/dist/org.js +26 -0
  10. package/dist/plan/calls-matrix.d.ts +9 -0
  11. package/dist/plan/calls-matrix.js +17 -0
  12. package/dist/plan/cloudflared-connector.d.ts +7 -1
  13. package/dist/plan/cloudflared-connector.js +14 -1
  14. package/dist/plan/diff.d.ts +29 -0
  15. package/dist/plan/diff.js +259 -18
  16. package/dist/plan/executor.d.ts +112 -0
  17. package/dist/plan/executor.js +594 -33
  18. package/dist/plan/health-check-settings.d.ts +28 -0
  19. package/dist/plan/health-check-settings.js +36 -0
  20. package/dist/plan/health-gaps.d.ts +33 -0
  21. package/dist/plan/health-gaps.js +89 -0
  22. package/dist/plan/held-key-refs.d.ts +10 -0
  23. package/dist/plan/held-key-refs.js +10 -0
  24. package/dist/plan/index.d.ts +14 -6
  25. package/dist/plan/index.js +7 -3
  26. package/dist/plan/live.d.ts +5 -2
  27. package/dist/plan/live.js +15 -2
  28. package/dist/plan/locked-hostnames.d.ts +5 -2
  29. package/dist/plan/locked-hostnames.js +9 -1
  30. package/dist/plan/login-host.d.ts +62 -2
  31. package/dist/plan/login-host.js +104 -1
  32. package/dist/plan/mail-relay.d.ts +120 -0
  33. package/dist/plan/mail-relay.js +323 -0
  34. package/dist/plan/org-names.d.ts +24 -0
  35. package/dist/plan/org-names.js +52 -0
  36. package/dist/plan/own-diff.d.ts +169 -0
  37. package/dist/plan/own-diff.js +432 -0
  38. package/dist/plan/own-dns.d.ts +6 -0
  39. package/dist/plan/own-dns.js +15 -0
  40. package/dist/plan/own-limits.d.ts +22 -0
  41. package/dist/plan/own-limits.js +101 -0
  42. package/dist/plan/own-services.d.ts +193 -0
  43. package/dist/plan/own-services.js +167 -0
  44. package/dist/plan/own-sfu.d.ts +69 -0
  45. package/dist/plan/own-sfu.js +255 -0
  46. package/dist/plan/plan-apply.d.ts +78 -7
  47. package/dist/plan/plan-apply.js +205 -12
  48. package/dist/plan/platform-diff.d.ts +28 -0
  49. package/dist/plan/platform-diff.js +45 -3
  50. package/dist/plan/platform-org.d.ts +34 -0
  51. package/dist/plan/platform-org.js +94 -2
  52. package/dist/plan/platform-providers.d.ts +356 -8
  53. package/dist/plan/platform-providers.js +661 -353
  54. package/dist/plan/sign-in-org.d.ts +1 -1
  55. package/dist/plan/sign-in-org.js +20 -2
  56. package/dist/plan/zitadel-login-policy.js +1 -1
  57. package/dist/plan/zitadel-system-api.d.ts +22 -1
  58. package/dist/plan/zitadel-system-api.js +49 -8
  59. package/dist/platform.d.ts +8 -0
  60. package/dist/slug.js +1 -0
  61. package/dist/store/postgres.js +51 -2
  62. package/dist/store/schema.d.ts +51 -0
  63. package/dist/store/schema.js +5 -0
  64. package/package.json +1 -1
  65. package/versions.json +3 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The hard limits every container of a company that runs its own services
3
+ * gets: one flat default. Field names are Coolify's (`PATCH /applications/{uuid}`
4
+ * and `PATCH /databases/{uuid}`: `limits_memory`, `limits_cpus`). Coolify has
5
+ * no field for a process limit, and its `custom_docker_run_options` parser drops
6
+ * `--pids-limit`, so 512 processes cannot be set through Coolify.
7
+ */
8
+ export const OWN_LIMITS_MEMORY = '1g';
9
+ export const OWN_LIMITS_CPUS = '1';
10
+ const OWN_LIMITS = { limits_memory: OWN_LIMITS_MEMORY, limits_cpus: OWN_LIMITS_CPUS };
11
+ /** The same bound the neighbouring Coolify calls in this package use for a call that may hang. */
12
+ export const OWN_LIMITS_TIMEOUT_MS = 60_000;
13
+ const lacks = (r) => Boolean(r) && (r?.limits_memory !== OWN_LIMITS_MEMORY || r?.limits_cpus !== OWN_LIMITS_CPUS);
14
+ async function bounded(work, what) {
15
+ let timer;
16
+ try {
17
+ return await Promise.race([
18
+ work,
19
+ new Promise((_, reject) => {
20
+ timer = setTimeout(() => reject(new Error(`${what}: setting the resource limits timed out`)), OWN_LIMITS_TIMEOUT_MS);
21
+ }),
22
+ ]);
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+ /**
29
+ * The Coolify port an own-services step uses, with the limits set on every
30
+ * application and database it creates, and set again on one it finds without
31
+ * them, so a company provisioned before the limits existed converges on its
32
+ * next apply. A failed limits call fails the calling step before it records
33
+ * its applied marker; the next run finds the resource and sets them.
34
+ */
35
+ export function limited(coolify) {
36
+ const patchApp = (uuid) => {
37
+ if (!coolify.updateApplication)
38
+ throw new Error('own limits: the Coolify port has no updateApplication');
39
+ return bounded(coolify.updateApplication(uuid, OWN_LIMITS), `application ${uuid}`);
40
+ };
41
+ const patchDb = (uuid) => {
42
+ if (!coolify.raw)
43
+ throw new Error('own limits: the Coolify port has no raw');
44
+ return bounded(coolify.raw('PATCH', `/databases/${uuid}`, OWN_LIMITS), `database ${uuid}`);
45
+ };
46
+ const afterCreate = (create, patch) => async (input) => {
47
+ const created = await create.call(coolify, input);
48
+ await patch(created.uuid);
49
+ return created;
50
+ };
51
+ const over = {};
52
+ if (coolify.createGithubAppApplication)
53
+ over.createGithubAppApplication = afterCreate(coolify.createGithubAppApplication, patchApp);
54
+ if (coolify.createDockerImageApplication)
55
+ over.createDockerImageApplication = afterCreate(coolify.createDockerImageApplication, patchApp);
56
+ if (coolify.createDockerfileApplication)
57
+ over.createDockerfileApplication = afterCreate(coolify.createDockerfileApplication, patchApp);
58
+ if (coolify.createPostgresDatabase) {
59
+ const create = coolify.createPostgresDatabase;
60
+ over.createPostgresDatabase = async (input) => {
61
+ const created = await create.call(coolify, input);
62
+ await patchDb(created.uuid);
63
+ return created;
64
+ };
65
+ }
66
+ const getApplication = coolify.getApplication;
67
+ if (getApplication) {
68
+ over.getApplication = async (uuid) => {
69
+ const app = await getApplication.call(coolify, uuid);
70
+ if (lacks(app))
71
+ await patchApp(uuid);
72
+ return app;
73
+ };
74
+ }
75
+ const find = coolify.findApplicationByName;
76
+ if (find && getApplication) {
77
+ over.findApplicationByName = async (...args) => {
78
+ const found = await find.apply(coolify, args);
79
+ if (found && lacks(await getApplication.call(coolify, found.uuid)))
80
+ await patchApp(found.uuid);
81
+ return found;
82
+ };
83
+ }
84
+ const getDatabase = coolify.getDatabase;
85
+ if (getDatabase) {
86
+ over.getDatabase = async (uuid) => {
87
+ const db = await getDatabase.call(coolify, uuid);
88
+ if (lacks(db))
89
+ await patchDb(uuid);
90
+ return db;
91
+ };
92
+ }
93
+ return new Proxy(coolify, {
94
+ get(target, prop) {
95
+ if (typeof prop === 'string' && prop in over)
96
+ return over[prop];
97
+ const value = Reflect.get(target, prop, target);
98
+ return typeof value === 'function' ? value.bind(target) : value;
99
+ },
100
+ });
101
+ }
@@ -0,0 +1,193 @@
1
+ import type { LivekitCredentials } from '../calls-sfu-keys.js';
2
+ import type { CallsSfuUrls } from '../calls-sfu.js';
3
+ import type { DeclaredOrg } from '../org.js';
4
+ import type { BrokerPort } from './broker-entries.js';
5
+ import type { DnsPort } from './edge-dns.js';
6
+ import type { ZonePort } from './edge-zones.js';
7
+ import type { CallsSfuPort, CloudflarePort, CoolifyPort, FilesOrganisationPort, FoundryStepExecutorPorts, JobsSchedulerPort, MailPort, ZitadelPort } from './executor.js';
8
+ import type { PlatformStepExecutionContext, StepExecutionContext } from './plan-apply.js';
9
+ import type { FoundryPlatformStepExecutorOptions, PlatformAuthDeps, PlatformDnsFactory, PlatformFilesDeps, PlatformMailDeps, PlatformTunnelPort, PlatformTunnelsFactory } from './platform-providers.js';
10
+ /** Applied-state key under which an own org's provisioned-services marker is recorded. */
11
+ export declare const OWN_SERVICES_APPLIED_SCOPE_KEY = "own_services_applied";
12
+ /**
13
+ * Every name a service step creates, so the platform's steps and an own
14
+ * org's steps take them from one place. Names are unique on the server: each
15
+ * Coolify app name, each `custom_internal_name` on the shared Docker network
16
+ * and each held-key ref carries the owner (`platform` or the org slug).
17
+ */
18
+ export interface ServiceTarget {
19
+ readonly id: string;
20
+ readonly kind: 'platform' | 'org';
21
+ readonly names: {
22
+ authDb: string;
23
+ authZitadel: string;
24
+ authLogin: string;
25
+ authBroker: string;
26
+ authTunnel: string;
27
+ authCloudflared: string;
28
+ mailApp: string;
29
+ mailTunnel: string;
30
+ mailCloudflared: string;
31
+ filesDb: string;
32
+ filesMigrate: string;
33
+ filesRunner: string;
34
+ /** An own org's R2 bucket. The platform's is host-supplied (`PlatformFilesDeps.r2`), so its value here is never read. */
35
+ filesBucket: string;
36
+ jobsApp: string;
37
+ jobsDb: string;
38
+ sfuApp: string;
39
+ sfuTunnel: string;
40
+ sfuCloudflared: string;
41
+ };
42
+ readonly internal: {
43
+ authZitadel: string;
44
+ authLogin: string;
45
+ authBroker: string;
46
+ mail: string;
47
+ files: string;
48
+ jobs: string;
49
+ sfu: string;
50
+ };
51
+ readonly refs: {
52
+ instanceBearer: string;
53
+ authAdmin: string;
54
+ brokerMailKey: string;
55
+ mailAdminKey: string;
56
+ filesManagement: string;
57
+ filesIntrospection: string;
58
+ jobsAdminToken: string;
59
+ livekitKeys: string;
60
+ /** An own files host's derived keys credential, held after the mint. The platform's value is never read. */
61
+ keysCredential: string;
62
+ /** The public half the host verifies with (`KEYS_VERIFY_KEYS`). */
63
+ keysVerifyKeys: string;
64
+ };
65
+ }
66
+ /** The platform's shared services, with the literals the platform steps use today. */
67
+ export declare function platformTarget(): ServiceTarget;
68
+ /** One company's own services: every name and ref carries the org's slug, so two orgs never clash. */
69
+ export declare function orgTarget(org: Pick<DeclaredOrg, 'slug'>): ServiceTarget;
70
+ /** Where a step reads and records its ids: the platform's store or one org's. */
71
+ export interface StepScope {
72
+ /** This scope's own ids (platform store or org store). */
73
+ recordedIds(): Readonly<Record<string, string>>;
74
+ recordScopeId(key: string, id: string): void;
75
+ persist(): Promise<void>;
76
+ /** Read only. For a platform scope this is `recordedIds()`. */
77
+ platformIds(): Readonly<Record<string, string>>;
78
+ }
79
+ export declare function scopeFromPlatform(context: PlatformStepExecutionContext, _platformIds?: never): StepScope;
80
+ export declare function scopeFromOrg(context: StepExecutionContext, platformIds: Readonly<Record<string, string>>): StepScope;
81
+ export interface ServiceNetwork {
82
+ dns(): Promise<DnsPort>;
83
+ tunnels(): Promise<PlatformTunnelPort>;
84
+ zoneId(apex: string): Promise<string | undefined>;
85
+ }
86
+ export declare function platformNetwork(deps: {
87
+ dns: PlatformDnsFactory;
88
+ tunnels: PlatformTunnelsFactory;
89
+ cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
90
+ }, scope: StepScope, step: string): ServiceNetwork;
91
+ /**
92
+ * An own org's DNS and tunnels: the org's edge-routed ports, never the
93
+ * platform's scope, and the zone edge recorded for the org's domain. A port
94
+ * the host did not supply is named in the error, never reached around.
95
+ */
96
+ export declare function orgNetwork(ports: {
97
+ dns?: DnsPort;
98
+ tunnels?: PlatformTunnelPort;
99
+ zones?: Pick<ZonePort, 'findZoneByApex'>;
100
+ }, scope: StepScope, step: string): ServiceNetwork;
101
+ /** Where the project comes from: the platform's 'platform' project, or the org's own. */
102
+ export interface ProjectSource {
103
+ ensure(coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject'>, scope: StepScope): Promise<string>;
104
+ }
105
+ export interface OwnAuthInstance {
106
+ issuerUrl: string;
107
+ bearerRef: string;
108
+ }
109
+ /** Every field optional: absent means "not ready", and the executor throws {@link OwnServicesBlockedError} naming the ref. */
110
+ export interface OwnServicePorts {
111
+ zitadel?: ZitadelPort;
112
+ mail?: MailPort;
113
+ broker?: BrokerPort;
114
+ filesOrganisation?: FilesOrganisationPort;
115
+ jobsScheduler?: JobsSchedulerPort;
116
+ callsSfu?: CallsSfuPort;
117
+ callsSfuUrls?: CallsSfuUrls;
118
+ }
119
+ /** Dependencies of the `coolify:own-*` steps themselves. */
120
+ export interface OwnServicesProvision {
121
+ auth: PlatformAuthDeps;
122
+ mail: PlatformMailDeps;
123
+ files: OwnFilesDeps;
124
+ jobs: NonNullable<FoundryPlatformStepExecutorOptions['coolify']>;
125
+ sfu: OwnSfuDeps;
126
+ }
127
+ /** What the keys worker answers for one company's derived host: the credential and the public keys, plus ids for the record. */
128
+ export interface FilesKeysMinted {
129
+ credential: string;
130
+ verifyKeys: string;
131
+ credentialId: string;
132
+ epoch: number;
133
+ expiresAt: number;
134
+ }
135
+ /** Mints a company's own keys credential on the keys worker. Called with the host id `files-<slug>`. */
136
+ export interface FilesKeysMint {
137
+ workerUrl: string;
138
+ mint(hostId: string, requestId: string): Promise<FilesKeysMinted>;
139
+ }
140
+ /** One S3 pair for one bucket, and the id of the API token it came from. The secret is shown once. */
141
+ export interface OwnFilesR2Key {
142
+ accessKeyId: string;
143
+ secretAccessKey: string;
144
+ tokenId: string;
145
+ }
146
+ /**
147
+ * Mints and removes the storage key of one company bucket. Each call is given 30 seconds. A delete
148
+ * of a token that is already gone (404) is a success.
149
+ */
150
+ export interface OwnFilesR2Keys {
151
+ mintBucketKey(bucket: string): Promise<OwnFilesR2Key>;
152
+ deleteBucketKey(tokenId: string): Promise<void>;
153
+ }
154
+ /** The token id of the storage key delivered to an own files host. Its presence means the step is done. */
155
+ export declare const OWN_FILES_R2_SCOPE_KEY = "files_r2_id";
156
+ /** The token id minted and not yet confirmed delivered: deleted before any new mint. */
157
+ export declare const OWN_FILES_R2_UNDELIVERED_SCOPE_KEY = "files_r2_undelivered";
158
+ /**
159
+ * An own files host takes no `keys.credential`, `keys.auditToken` or `jobsSigningSecrets` from here:
160
+ * those are the platform's and never reach a company's containers. Its keyring comes from `keysMint`.
161
+ */
162
+ export interface OwnFilesDeps extends Omit<PlatformFilesDeps, 'r2'> {
163
+ r2Buckets: {
164
+ ensureBucket(name: string): Promise<{
165
+ bucket: string;
166
+ endpoint: string;
167
+ }>;
168
+ };
169
+ /** Absent: the own files R2 key step is blocked, naming this port. */
170
+ r2Keys?: OwnFilesR2Keys;
171
+ /** Absent: the own files step is blocked, naming this port. */
172
+ keysMint?: FilesKeysMint;
173
+ /** Reads a delivered secret back from the host, so a rerun after a failed run resumes instead of minting. */
174
+ readSecret?: (ref: string) => Promise<string | null>;
175
+ }
176
+ export interface OwnSfuDeps {
177
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'createDockerImageApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'deploy' | 'getApplication' | 'raw' | 'listEnvs' | 'createDockerfileApplication' | 'deployAndWait'>;
178
+ mintLivekitCredentials?: () => LivekitCredentials;
179
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
180
+ }
181
+ /** A held key an own service needs is missing. Names the service and the ref, never a value. */
182
+ export declare class OwnServicesBlockedError extends Error {
183
+ readonly service: string;
184
+ readonly ref: string;
185
+ constructor(service: string, ref: string);
186
+ }
187
+ export interface OwnServicesPort {
188
+ /** Ports scoped to this org's own instances, or null when the org is not own. Throws {@link OwnServicesBlockedError} when a held key is missing. */
189
+ portsFor(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<OwnServicePorts | null>;
190
+ /** Builds the ZITADEL port for one company instance. */
191
+ zitadelFor(instance: OwnAuthInstance): ZitadelPort;
192
+ provision: OwnServicesProvision;
193
+ }
@@ -0,0 +1,167 @@
1
+ import { apexZoneKey, exactZone, platformDns, platformTunnels } from './platform-providers.js';
2
+ /** The key the org's zone step records its Cloudflare zone id under. */
3
+ const ORG_ZONE_SCOPE_KEY = 'cloudflare_zone';
4
+ /** Applied-state key under which an own org's provisioned-services marker is recorded. */
5
+ export const OWN_SERVICES_APPLIED_SCOPE_KEY = 'own_services_applied';
6
+ /** The platform's shared services, with the literals the platform steps use today. */
7
+ export function platformTarget() {
8
+ return {
9
+ id: 'platform',
10
+ kind: 'platform',
11
+ names: {
12
+ authDb: 'platform-auth-db',
13
+ authZitadel: 'platform-auth-zitadel',
14
+ authLogin: 'platform-auth-login',
15
+ authBroker: 'platform-auth-broker',
16
+ authTunnel: 'platform-auth-tunnel',
17
+ authCloudflared: 'platform-auth-cloudflared',
18
+ mailApp: 'platform-mail-stalwart',
19
+ mailTunnel: 'platform-mail-tunnel',
20
+ mailCloudflared: 'platform-mail-cloudflared',
21
+ filesDb: 'platform-files-db',
22
+ filesMigrate: 'files-migrate',
23
+ filesRunner: 'files',
24
+ filesBucket: 'platform-files',
25
+ jobsApp: 'jobs',
26
+ jobsDb: 'jobs-scheduler-db',
27
+ sfuApp: 'platform-sfu',
28
+ sfuTunnel: 'platform-sfu-tunnel',
29
+ sfuCloudflared: 'platform-sfu-cloudflared',
30
+ },
31
+ internal: {
32
+ authZitadel: 'platform-auth-zitadel',
33
+ authLogin: 'platform-auth-login',
34
+ authBroker: 'platform-auth-broker',
35
+ mail: 'platform-mail-stalwart',
36
+ files: 'platform-files',
37
+ jobs: 'platform-jobs',
38
+ sfu: 'platform-sfu',
39
+ },
40
+ refs: {
41
+ instanceBearer: 'zitadel:platform:instanceBearer',
42
+ authAdmin: 'zitadel:platform:admin',
43
+ brokerMailKey: 'lettermint:platform:apiToken',
44
+ mailAdminKey: 'stalwart:platform:admin',
45
+ filesManagement: 'files:platform:managementKey',
46
+ filesIntrospection: 'zitadel:platform:files:introspection',
47
+ jobsAdminToken: 'jobs:scheduler:adminToken',
48
+ livekitKeys: 'livekit:platform:keys',
49
+ keysCredential: 'keys:platform:credential',
50
+ keysVerifyKeys: 'keys:platform:verifyKeys',
51
+ },
52
+ };
53
+ }
54
+ /** One company's own services: every name and ref carries the org's slug, so two orgs never clash. */
55
+ export function orgTarget(org) {
56
+ const s = org.slug;
57
+ return {
58
+ id: s,
59
+ kind: 'org',
60
+ names: {
61
+ authDb: `${s}-auth-db`,
62
+ authZitadel: `${s}-auth-zitadel`,
63
+ authLogin: `${s}-auth-login`,
64
+ authBroker: `${s}-auth-broker`,
65
+ authTunnel: `${s}-auth-tunnel`,
66
+ authCloudflared: `${s}-auth-cloudflared`,
67
+ mailApp: `${s}-mail-stalwart`,
68
+ mailTunnel: `${s}-mail-tunnel`,
69
+ mailCloudflared: `${s}-mail-cloudflared`,
70
+ filesDb: `${s}-files-db`,
71
+ filesMigrate: `${s}-files-migrate`,
72
+ filesRunner: `${s}-files`,
73
+ filesBucket: `${s}-files`,
74
+ jobsApp: `${s}-jobs`,
75
+ jobsDb: `${s}-jobs-db`,
76
+ sfuApp: `${s}-sfu`,
77
+ sfuTunnel: `${s}-sfu-tunnel`,
78
+ sfuCloudflared: `${s}-sfu-cloudflared`,
79
+ },
80
+ internal: {
81
+ authZitadel: `${s}-auth-zitadel`,
82
+ authLogin: `${s}-auth-login`,
83
+ authBroker: `${s}-auth-broker`,
84
+ mail: `${s}-mail-stalwart`,
85
+ files: `${s}-files`,
86
+ jobs: `${s}-jobs`,
87
+ sfu: `${s}-sfu`,
88
+ },
89
+ refs: {
90
+ instanceBearer: `zitadel:org:${s}:instanceBearer`,
91
+ authAdmin: `zitadel:org:${s}:admin`,
92
+ brokerMailKey: `lettermint:org:${s}:apiToken`,
93
+ mailAdminKey: `stalwart:org:${s}:admin`,
94
+ filesManagement: `files:org:${s}:managementKey`,
95
+ filesIntrospection: `zitadel:org:${s}:files:introspection`,
96
+ jobsAdminToken: `jobs:org:${s}:adminToken`,
97
+ livekitKeys: `livekit:org:${s}:keys`,
98
+ keysCredential: `keys:org:${s}:credential`,
99
+ keysVerifyKeys: `keys:org:${s}:verifyKeys`,
100
+ },
101
+ };
102
+ }
103
+ export function scopeFromPlatform(context, _platformIds) {
104
+ return {
105
+ recordedIds: () => context.recordedIds(),
106
+ recordScopeId: (key, id) => context.recordScopeId(key, id),
107
+ persist: async () => {
108
+ await context.persist?.();
109
+ },
110
+ platformIds: () => context.recordedIds(),
111
+ };
112
+ }
113
+ export function scopeFromOrg(context, platformIds) {
114
+ return {
115
+ recordedIds: () => context.recordedIds(),
116
+ recordScopeId: (key, id) => context.recordScopeId(key, id),
117
+ persist: () => context.persistScopeIds(),
118
+ platformIds: () => platformIds,
119
+ };
120
+ }
121
+ export function platformNetwork(deps, scope, step) {
122
+ return {
123
+ dns: () => platformDns(deps, scope.recordedIds(), step),
124
+ tunnels: () => platformTunnels(deps, scope.recordedIds(), step),
125
+ zoneId: async (apex) => scope.recordedIds()[apexZoneKey(apex)] ?? (await exactZone(deps.cloudflare, apex))?.id,
126
+ };
127
+ }
128
+ /**
129
+ * An own org's DNS and tunnels: the org's edge-routed ports, never the
130
+ * platform's scope, and the zone edge recorded for the org's domain. A port
131
+ * the host did not supply is named in the error, never reached around.
132
+ */
133
+ export function orgNetwork(ports, scope, step) {
134
+ return {
135
+ dns: async () => {
136
+ if (!ports.dns)
137
+ throw new Error(`${step}: the host supplies no DNS port for this org`);
138
+ return ports.dns;
139
+ },
140
+ tunnels: async () => {
141
+ if (!ports.tunnels)
142
+ throw new Error(`${step}: the host supplies no tunnel port for this org`);
143
+ return ports.tunnels;
144
+ },
145
+ zoneId: async (apex) => {
146
+ const recorded = scope.recordedIds()[ORG_ZONE_SCOPE_KEY];
147
+ if (recorded)
148
+ return recorded;
149
+ return ports.zones ? (await exactZone(ports.zones, apex))?.id : undefined;
150
+ },
151
+ };
152
+ }
153
+ /** The token id of the storage key delivered to an own files host. Its presence means the step is done. */
154
+ export const OWN_FILES_R2_SCOPE_KEY = 'files_r2_id';
155
+ /** The token id minted and not yet confirmed delivered: deleted before any new mint. */
156
+ export const OWN_FILES_R2_UNDELIVERED_SCOPE_KEY = 'files_r2_undelivered';
157
+ /** A held key an own service needs is missing. Names the service and the ref, never a value. */
158
+ export class OwnServicesBlockedError extends Error {
159
+ service;
160
+ ref;
161
+ constructor(service, ref) {
162
+ super(`own ${service} is blocked: held key ${ref} is missing`);
163
+ this.service = service;
164
+ this.ref = ref;
165
+ this.name = 'OwnServicesBlockedError';
166
+ }
167
+ }
@@ -0,0 +1,69 @@
1
+ import { type LivekitCredentials } from '../calls-sfu-keys.js';
2
+ import { type CallsSfuUrls } from '../calls-sfu.js';
3
+ import { type DeployHealthRuntime } from './deploy-health.js';
4
+ import type { FoundryStepExecutorPorts } from './executor.js';
5
+ import type { OwnSfuDeps, ProjectSource, ServiceNetwork, ServiceTarget, StepScope } from './own-services.js';
6
+ /** The LiveKit image, the same repository the shared SFU runs. */
7
+ export declare const OWN_SFU_IMAGE = "livekit/livekit-server";
8
+ export declare const OWN_SFU_APP_SCOPE_KEY = "coolify_own_sfu_app";
9
+ export declare const OWN_SFU_TUNNEL_SCOPE_KEY = "cloudflare_own_sfu_tunnel";
10
+ export declare const OWN_SFU_CONNECTOR_APP_SCOPE_KEY = "coolify_own_sfu_connector_app";
11
+ /** Set to the app id before the key pair leaves, cleared after the pair is delivered and written to the app. */
12
+ export declare const OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY = "own_sfu_keys_undelivered";
13
+ /** The app id once the company's key pair is delivered and in the app's env. */
14
+ export declare const OWN_SFU_KEYS_SCOPE_KEY = "own_sfu_keys_set";
15
+ export declare const OWN_SFU_DEPLOY_PENDING_SCOPE_KEY = "own_sfu_deploy_pending";
16
+ export declare const OWN_SFU_APPLIED_VERSION_SCOPE_KEY = "own_sfu_applied_version";
17
+ export declare const OWN_SFU_APPLIED_SLOT_SCOPE_KEY = "own_sfu_applied_slot";
18
+ export declare const OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY = "own_sfu_applied_hostname";
19
+ export declare const SFU_INTERNAL_URL_SCOPE_KEY = "sfu_internal_url";
20
+ export declare const SFU_PUBLIC_URL_SCOPE_KEY = "sfu_public_url";
21
+ export interface SfuAppDeps {
22
+ coolify: OwnSfuDeps['coolify'];
23
+ network: ServiceNetwork;
24
+ deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
25
+ /** Reads a delivered secret back from the host. Without it, an undelivered marker cannot be resumed. */
26
+ readSecret?: (ref: string) => Promise<string | null>;
27
+ runtime: DeployHealthRuntime;
28
+ mintLivekitCredentials?: () => LivekitCredentials;
29
+ /** Called with every secret value this step handles, so the caller can scrub it from output. */
30
+ onSecret?: (value: string) => void;
31
+ }
32
+ export interface SfuAppInput {
33
+ slot: number;
34
+ /** `sfu.<domain>`. */
35
+ hostname: string;
36
+ /** The org's domain: the zone the hostname goes in. */
37
+ apex: string;
38
+ /** The pinned LiveKit release tag. */
39
+ version: string;
40
+ project: ProjectSource;
41
+ }
42
+ /** The Docker `ports_mappings` string for a slot: the UDP range and the TCP fallback port, one to one. */
43
+ export declare function sfuPortsMappings(slot: number): string;
44
+ /**
45
+ * LiveKit takes its whole config from one env value. `node_ip` is the
46
+ * server's address: the app is not on the host network, so the media ports
47
+ * reach it through the published mappings.
48
+ */
49
+ export declare function sfuConfig(input: {
50
+ slot: number;
51
+ nodeIp: string;
52
+ keys: LivekitCredentials;
53
+ }): string;
54
+ /**
55
+ * One company's LiveKit media server as a Coolify app: the image, its slot's
56
+ * ports published to the server, and a tunnel for signalling at `sfu.<domain>`.
57
+ * The company's single key pair is minted once, delivered to
58
+ * `target.refs.livekitKeys` as JSON, and written to the app's env; it is never
59
+ * recorded. Rules 1 to 6 of `CLAUDE.md` "Platform steps".
60
+ */
61
+ export declare function provisionSfuApp(scope: StepScope, deps: SfuAppDeps, target: ServiceTarget, input: SfuAppInput): Promise<{
62
+ resourceId?: string;
63
+ }>;
64
+ /**
65
+ * The addresses of the company's media server, from its recorded ids. Before
66
+ * the step is recorded the pair is empty, which `resolveCallsSfuUrls` refuses:
67
+ * an own org's calls service never falls back to the shared SFU's addresses.
68
+ */
69
+ export declare function ownSfuUrls(ids: Readonly<Record<string, string>>): CallsSfuUrls;