@wtfalch/foundry 0.32.0 → 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.
- package/dist/bootstrap/bootstrap.d.ts +18 -1
- package/dist/bootstrap/bootstrap.js +167 -28
- package/dist/bootstrap/declare.d.ts +5 -1
- package/dist/bootstrap/declare.js +3 -1
- package/dist/bootstrap/index.d.ts +1 -1
- package/dist/bootstrap/versions.d.ts +4 -0
- package/dist/calls-sfu.d.ts +10 -0
- package/dist/calls-sfu.js +41 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.js +8 -3
- package/dist/migrations/0051_org_services.sql +13 -0
- package/dist/migrations/0052_platform_mail_relay.sql +8 -0
- package/dist/org.d.ts +24 -2
- package/dist/org.js +42 -0
- package/dist/plan/boule.d.ts +24 -1
- package/dist/plan/boule.js +29 -1
- package/dist/plan/calls-matrix.d.ts +9 -0
- package/dist/plan/calls-matrix.js +17 -0
- package/dist/plan/cloudflared-connector.d.ts +7 -1
- package/dist/plan/cloudflared-connector.js +14 -1
- package/dist/plan/diff.d.ts +32 -1
- package/dist/plan/diff.js +383 -26
- package/dist/plan/executor.d.ts +177 -7
- package/dist/plan/executor.js +738 -51
- package/dist/plan/health-check-settings.d.ts +28 -0
- package/dist/plan/health-check-settings.js +36 -0
- package/dist/plan/health-gaps.d.ts +33 -0
- package/dist/plan/health-gaps.js +89 -0
- package/dist/plan/health-targets.js +1 -1
- package/dist/plan/held-key-refs.d.ts +10 -0
- package/dist/plan/held-key-refs.js +10 -0
- package/dist/plan/index.d.ts +16 -6
- package/dist/plan/index.js +8 -3
- package/dist/plan/live.d.ts +5 -2
- package/dist/plan/live.js +15 -2
- package/dist/plan/locked-hostnames.d.ts +5 -2
- package/dist/plan/locked-hostnames.js +9 -1
- package/dist/plan/login-host.d.ts +62 -2
- package/dist/plan/login-host.js +104 -1
- package/dist/plan/mail-relay.d.ts +120 -0
- package/dist/plan/mail-relay.js +323 -0
- package/dist/plan/org-names.d.ts +24 -0
- package/dist/plan/org-names.js +52 -0
- package/dist/plan/own-diff.d.ts +169 -0
- package/dist/plan/own-diff.js +432 -0
- package/dist/plan/own-dns.d.ts +6 -0
- package/dist/plan/own-dns.js +15 -0
- package/dist/plan/own-limits.d.ts +22 -0
- package/dist/plan/own-limits.js +101 -0
- package/dist/plan/own-services.d.ts +193 -0
- package/dist/plan/own-services.js +167 -0
- package/dist/plan/own-sfu.d.ts +69 -0
- package/dist/plan/own-sfu.js +255 -0
- package/dist/plan/plan-apply.d.ts +101 -7
- package/dist/plan/plan-apply.js +216 -12
- package/dist/plan/platform-diff.d.ts +61 -12
- package/dist/plan/platform-diff.js +140 -34
- package/dist/plan/platform-org.d.ts +36 -0
- package/dist/plan/platform-org.js +96 -2
- package/dist/plan/platform-providers.d.ts +407 -12
- package/dist/plan/platform-providers.js +947 -422
- package/dist/plan/sign-in-org.d.ts +99 -0
- package/dist/plan/sign-in-org.js +151 -0
- package/dist/plan/zitadel-login-policy.js +1 -1
- package/dist/plan/zitadel-system-api.d.ts +22 -1
- package/dist/plan/zitadel-system-api.js +49 -8
- package/dist/plan.d.ts +18 -0
- package/dist/platform.d.ts +8 -0
- package/dist/slug.js +1 -0
- package/dist/store/postgres.js +51 -2
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +5 -0
- package/package.json +1 -1
- package/versions.json +3 -1
|
@@ -45,3 +45,31 @@ export declare const DEFAULT_HEALTH_CHECK_START_PERIOD_SECONDS = 30;
|
|
|
45
45
|
* Dockerfile's `EXPOSE`) names, not a public one.
|
|
46
46
|
*/
|
|
47
47
|
export declare function healthCheckSettings(path: string, port: number, startPeriodSeconds?: number): HealthCheckPatch;
|
|
48
|
+
/**
|
|
49
|
+
* For an app built from a Dockerfile that has its own HEALTHCHECK: Coolify's check off. Coolify then keeps the
|
|
50
|
+
* Dockerfile's check, and its rolling update waits on that check's state (with Coolify's check on it would replace
|
|
51
|
+
* the Dockerfile's, so two checks would never both be live). Coolify reads the Dockerfile only while its own check is
|
|
52
|
+
* off, so this goes on before the first deploy.
|
|
53
|
+
*/
|
|
54
|
+
export declare const IMAGE_HEALTHCHECK_SETTINGS: {
|
|
55
|
+
readonly health_check_enabled: false;
|
|
56
|
+
};
|
|
57
|
+
/** The four `updateApplication` fields that turn on a Coolify health check that runs a command inside the container. */
|
|
58
|
+
export interface CommandHealthCheckPatch {
|
|
59
|
+
health_check_enabled: true;
|
|
60
|
+
health_check_type: 'cmd';
|
|
61
|
+
health_check_command: string;
|
|
62
|
+
health_check_start_period: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The same fields as {@link healthCheckSettings}, for an image whose HTTP
|
|
66
|
+
* check cannot work: no `curl`, or an app that answers 401 on every route.
|
|
67
|
+
* Coolify's own HTTP check calls `localhost`, which `wget` in an Alpine image
|
|
68
|
+
* resolves to `::1` first, so a server that listens on IPv4 only is never
|
|
69
|
+
* reached. A command names `127.0.0.1` itself.
|
|
70
|
+
*/
|
|
71
|
+
export declare function commandHealthCheckSettings(command: string, startPeriodSeconds?: number): CommandHealthCheckPatch;
|
|
72
|
+
/** A GET on `127.0.0.1` that fails on any status of 400 or more; the `wget` of a busybox image. */
|
|
73
|
+
export declare const wgetProbe: (port: number | string, path: string) => string;
|
|
74
|
+
/** A TCP connect to `127.0.0.1`, for a server with no route that answers without a token. */
|
|
75
|
+
export declare const tcpProbe: (port: number | string) => string;
|
|
@@ -45,3 +45,39 @@ export function healthCheckSettings(path, port, startPeriodSeconds = DEFAULT_HEA
|
|
|
45
45
|
health_check_start_period: startPeriodSeconds,
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* For an app built from a Dockerfile that has its own HEALTHCHECK: Coolify's check off. Coolify then keeps the
|
|
50
|
+
* Dockerfile's check, and its rolling update waits on that check's state (with Coolify's check on it would replace
|
|
51
|
+
* the Dockerfile's, so two checks would never both be live). Coolify reads the Dockerfile only while its own check is
|
|
52
|
+
* off, so this goes on before the first deploy.
|
|
53
|
+
*/
|
|
54
|
+
export const IMAGE_HEALTHCHECK_SETTINGS = { health_check_enabled: false };
|
|
55
|
+
/**
|
|
56
|
+
* What Coolify accepts as `health_check_command`: letters, digits, space and
|
|
57
|
+
* `- _ . / : = @ , +`, at most 1000 characters (its `generate_healthcheck_commands`
|
|
58
|
+
* falls back to the HTTP check on anything else, without an error). It runs as
|
|
59
|
+
* `sh -c`, so the image needs a shell and the tool the command names.
|
|
60
|
+
*/
|
|
61
|
+
const COOLIFY_COMMAND = /^[a-zA-Z0-9 \-_./:=@,+]+$/;
|
|
62
|
+
/**
|
|
63
|
+
* The same fields as {@link healthCheckSettings}, for an image whose HTTP
|
|
64
|
+
* check cannot work: no `curl`, or an app that answers 401 on every route.
|
|
65
|
+
* Coolify's own HTTP check calls `localhost`, which `wget` in an Alpine image
|
|
66
|
+
* resolves to `::1` first, so a server that listens on IPv4 only is never
|
|
67
|
+
* reached. A command names `127.0.0.1` itself.
|
|
68
|
+
*/
|
|
69
|
+
export function commandHealthCheckSettings(command, startPeriodSeconds = DEFAULT_HEALTH_CHECK_START_PERIOD_SECONDS) {
|
|
70
|
+
if (!COOLIFY_COMMAND.test(command) || command.length > 1000) {
|
|
71
|
+
throw new Error(`health check command ${JSON.stringify(command)} is not one Coolify accepts`);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
health_check_enabled: true,
|
|
75
|
+
health_check_type: 'cmd',
|
|
76
|
+
health_check_command: command,
|
|
77
|
+
health_check_start_period: startPeriodSeconds,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** A GET on `127.0.0.1` that fails on any status of 400 or more; the `wget` of a busybox image. */
|
|
81
|
+
export const wgetProbe = (port, path) => `wget -q -O /dev/null http://127.0.0.1:${port}${path}`;
|
|
82
|
+
/** A TCP connect to `127.0.0.1`, for a server with no route that answers without a token. */
|
|
83
|
+
export const tcpProbe = (port) => `nc -z 127.0.0.1 ${port}`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** An app as Coolify keeps it: its name and everything create and update sent. */
|
|
2
|
+
export interface StoredApp {
|
|
3
|
+
name: string;
|
|
4
|
+
fields: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Apps that run once and then idle or exit, so Docker has no service to check. Matched by what the app is built
|
|
8
|
+
* from (repo and Dockerfile target), never by its name, so no look-alike name is exempt.
|
|
9
|
+
*/
|
|
10
|
+
export declare const EXIT_JOBS: ReadonlyArray<{
|
|
11
|
+
repo: string;
|
|
12
|
+
target: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Apps built from a Dockerfile in another repo that carries a HEALTHCHECK line. Foundry cannot read that file
|
|
17
|
+
* here, so each entry names the app by repo and Dockerfile and says where the line was seen. With the Coolify
|
|
18
|
+
* check off, Coolify keeps the image's check and its rolling update follows it.
|
|
19
|
+
*/
|
|
20
|
+
export declare const REPO_DOCKERFILE_CHECKED: ReadonlyArray<{
|
|
21
|
+
repo: string;
|
|
22
|
+
dockerfile?: string;
|
|
23
|
+
target?: string;
|
|
24
|
+
seen: string;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* A check that can fail. Either Coolify's own, on and with something to probe (a command that does something,
|
|
28
|
+
* or HTTP on a port; Coolify falls back to the exposed port and to `/`), or Coolify's off over a Dockerfile that
|
|
29
|
+
* carries its own HEALTHCHECK, which Coolify then keeps.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hasRealCheck(fields: Record<string, unknown>): boolean;
|
|
32
|
+
/** The names of the long-running apps with no real check, not counting the exit jobs. */
|
|
33
|
+
export declare function healthGaps(apps: ReadonlyArray<StoredApp>): string[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apps that run once and then idle or exit, so Docker has no service to check. Matched by what the app is built
|
|
3
|
+
* from (repo and Dockerfile target), never by its name, so no look-alike name is exempt.
|
|
4
|
+
*/
|
|
5
|
+
export const EXIT_JOBS = [
|
|
6
|
+
{
|
|
7
|
+
repo: 'wtfalch/files',
|
|
8
|
+
target: 'migration',
|
|
9
|
+
reason: 'runs the migrations once; its image HEALTHCHECK reports done',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
repo: 'wtfalch/ai',
|
|
13
|
+
target: 'migration',
|
|
14
|
+
reason: 'runs the migrations once; its image HEALTHCHECK reports done',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
repo: 'wtfalch/integrations',
|
|
18
|
+
target: 'migration',
|
|
19
|
+
reason: 'runs the migrations once; its image HEALTHCHECK reports done',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
repo: 'wtfalch/integrations',
|
|
23
|
+
target: 'worker',
|
|
24
|
+
reason: 'a finite pass (work-once.ts) that exits 0; nothing waits on it',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Apps built from a Dockerfile in another repo that carries a HEALTHCHECK line. Foundry cannot read that file
|
|
29
|
+
* here, so each entry names the app by repo and Dockerfile and says where the line was seen. With the Coolify
|
|
30
|
+
* check off, Coolify keeps the image's check and its rolling update follows it.
|
|
31
|
+
*/
|
|
32
|
+
export const REPO_DOCKERFILE_CHECKED = [
|
|
33
|
+
{ repo: 'wtfalch/auth', dockerfile: '/login/Dockerfile', seen: 'login image, docker inspect' },
|
|
34
|
+
{ repo: 'wtfalch/auth', dockerfile: '/broker/Dockerfile', seen: 'broker image, docker inspect' },
|
|
35
|
+
{ repo: 'wtfalch/ai', target: 'worker', seen: 'ai-worker image, docker inspect' },
|
|
36
|
+
{ repo: 'wtfalch/integrations', target: 'runner', seen: 'integrations/Dockerfile line 49' },
|
|
37
|
+
{
|
|
38
|
+
repo: 'wtfalch/chat',
|
|
39
|
+
dockerfile: '/apps/chat-service/synapse-module/Dockerfile',
|
|
40
|
+
seen: 'chat Synapse image, docker inspect',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
/** Commands that succeed whatever the container is doing. */
|
|
44
|
+
const NO_OP = /^(?:true|:|exit 0|\/bin\/true|\/usr\/bin\/true|sh -c true)$/;
|
|
45
|
+
/** A `HEALTHCHECK` instruction that is not `HEALTHCHECK NONE`. */
|
|
46
|
+
const DOCKERFILE_HEALTHCHECK = /^\s*HEALTHCHECK\s+(?!NONE\b)/im;
|
|
47
|
+
const same = (a, b) => b === undefined || a === b;
|
|
48
|
+
function exitJob(f) {
|
|
49
|
+
return EXIT_JOBS.some((e) => f.git_repository === e.repo && f.dockerfile_target_build === e.target);
|
|
50
|
+
}
|
|
51
|
+
function repoDockerfileChecked(f) {
|
|
52
|
+
return REPO_DOCKERFILE_CHECKED.some((e) => f.git_repository === e.repo &&
|
|
53
|
+
same(f.dockerfile_location, e.dockerfile) &&
|
|
54
|
+
same(f.dockerfile_target_build, e.target));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A check that can fail. Either Coolify's own, on and with something to probe (a command that does something,
|
|
58
|
+
* or HTTP on a port; Coolify falls back to the exposed port and to `/`), or Coolify's off over a Dockerfile that
|
|
59
|
+
* carries its own HEALTHCHECK, which Coolify then keeps.
|
|
60
|
+
*/
|
|
61
|
+
export function hasRealCheck(fields) {
|
|
62
|
+
if (fields.health_check_enabled === true) {
|
|
63
|
+
if (fields.health_check_type === 'cmd') {
|
|
64
|
+
const command = fields.health_check_command;
|
|
65
|
+
return typeof command === 'string' && command.trim() !== '' && !NO_OP.test(command.trim());
|
|
66
|
+
}
|
|
67
|
+
const exposed = typeof fields.ports_exposes === 'string' && fields.ports_exposes.trim() !== '';
|
|
68
|
+
return typeof fields.health_check_port === 'number' || exposed;
|
|
69
|
+
}
|
|
70
|
+
if (fields.health_check_enabled === false) {
|
|
71
|
+
const dockerfile = fields.dockerfile;
|
|
72
|
+
if (typeof dockerfile === 'string') {
|
|
73
|
+
// An inline Dockerfile may arrive base64 (a PATCH), as plain text on create.
|
|
74
|
+
const text = DOCKERFILE_HEALTHCHECK.test(dockerfile)
|
|
75
|
+
? dockerfile
|
|
76
|
+
: Buffer.from(dockerfile, 'base64').toString('utf8');
|
|
77
|
+
return DOCKERFILE_HEALTHCHECK.test(text);
|
|
78
|
+
}
|
|
79
|
+
return repoDockerfileChecked(fields);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/** The names of the long-running apps with no real check, not counting the exit jobs. */
|
|
84
|
+
export function healthGaps(apps) {
|
|
85
|
+
return apps
|
|
86
|
+
.filter((a) => !exitJob(a.fields) && !hasRealCheck(a.fields))
|
|
87
|
+
.map((a) => a.name)
|
|
88
|
+
.sort();
|
|
89
|
+
}
|
|
@@ -28,7 +28,7 @@ const CALLS_HEALTH_PATH = '/healthz';
|
|
|
28
28
|
function alwaysOnTargets(org, platform) {
|
|
29
29
|
// The platform's own org signs in on the shared instance host, not its own
|
|
30
30
|
// `loginHostnameFor` (#488; `login-host.ts`, `diff.ts`'s `planLoginHost`).
|
|
31
|
-
const loginHost = platform && isPlatformOwnOrg(org, platform.
|
|
31
|
+
const loginHost = platform && isPlatformOwnOrg(org, platform.ownerOrgId)
|
|
32
32
|
? platform.auth.hostname
|
|
33
33
|
: loginHostnameFor(org);
|
|
34
34
|
return [
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The held-key refs the platform's steps deliver under and an org step reads back. A leaf module:
|
|
3
|
+
* platform-providers.ts and platform-org.ts import each other, so a constant read at load lives here.
|
|
4
|
+
*/
|
|
5
|
+
/** The ref the scheduler's admin token is delivered under: the host's real `JobsSchedulerPort` adapter reads it back. */
|
|
6
|
+
export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
|
|
7
|
+
/** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
|
|
8
|
+
export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
|
|
9
|
+
/** The ref the management private key (PKCS#8 PEM) is delivered under. */
|
|
10
|
+
export declare const FILES_MANAGEMENT_KEY_REF = "files:platform:managementKey";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The held-key refs the platform's steps deliver under and an org step reads back. A leaf module:
|
|
3
|
+
* platform-providers.ts and platform-org.ts import each other, so a constant read at load lives here.
|
|
4
|
+
*/
|
|
5
|
+
/** The ref the scheduler's admin token is delivered under: the host's real `JobsSchedulerPort` adapter reads it back. */
|
|
6
|
+
export const JOBS_ADMIN_TOKEN_REF = 'jobs:scheduler:adminToken';
|
|
7
|
+
/** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
|
|
8
|
+
export const PLATFORM_AI_MANAGEMENT_KEY_REF = 'ai:platform:managementKey';
|
|
9
|
+
/** The ref the management private key (PKCS#8 PEM) is delivered under. */
|
|
10
|
+
export const FILES_MANAGEMENT_KEY_REF = 'files:platform:managementKey';
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -4,10 +4,18 @@ export type { StepAction, PlannedStep, PlanDetail, OrgPostgresPlan } from './dif
|
|
|
4
4
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
5
5
|
export type { StepSystem } from './step-system.js';
|
|
6
6
|
export { stepSystemOf } from './step-system.js';
|
|
7
|
-
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
8
|
-
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
7
|
+
export type { PlatformPlanDetail, MailRoute } from './platform-diff.js';
|
|
8
|
+
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
9
9
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
10
|
-
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
10
|
+
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
11
|
+
export type { PlanOwnAuthOptions, PlanOwnFilesOptions, PlanOwnFilesR2Options, PlanOwnJobsOptions, } from './own-diff.js';
|
|
12
|
+
export { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, } from './own-diff.js';
|
|
13
|
+
export type { PlanOwnSfuOptions } from './own-diff.js';
|
|
14
|
+
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu } from './own-diff.js';
|
|
15
|
+
export type { SfuAppDeps, SfuAppInput } from './own-sfu.js';
|
|
16
|
+
export { provisionSfuApp, sfuConfig, sfuPortsMappings } from './own-sfu.js';
|
|
17
|
+
export type { OwnAuthInstance, FilesKeysMint, FilesKeysMinted, OwnFilesDeps, OwnFilesR2Key, OwnFilesR2Keys, OwnServicePorts, OwnServicesPort, OwnServicesProvision, OwnSfuDeps, ProjectSource, ServiceNetwork, ServiceTarget, StepScope, } from './own-services.js';
|
|
18
|
+
export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgNetwork, orgTarget, platformNetwork, platformTarget, scopeFromOrg, scopeFromPlatform, } from './own-services.js';
|
|
11
19
|
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
12
20
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
13
21
|
export type { AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot } from './adopt.js';
|
|
@@ -15,8 +23,8 @@ export { AdoptRefusedError } from './adopt.js';
|
|
|
15
23
|
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, } from './plan-apply.js';
|
|
16
24
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
17
25
|
export type { UnconfirmedUpgrade } from './confirm-upgrade.js';
|
|
18
|
-
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
19
|
-
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
26
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './platform-providers.js';
|
|
27
|
+
export type { FilesHostInput, JobsHostDeps, JobsHostInput, MailServerInput, PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
20
28
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
21
29
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
22
30
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
@@ -33,7 +41,7 @@ export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, Provi
|
|
|
33
41
|
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
34
42
|
export type { BrokerPort } from './broker-entries.js';
|
|
35
43
|
export { PlatformBrokerPort } from './broker-entries.js';
|
|
36
|
-
export type { LoginHostBinding, LoginHostBindingsPort } from './login-host.js';
|
|
44
|
+
export type { LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './login-host.js';
|
|
37
45
|
export { loginHostnameFor } from './login-host.js';
|
|
38
46
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './mail-identity.js';
|
|
39
47
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
@@ -41,6 +49,8 @@ export type { MailAllowlistApp } from './mail-identity-allowlist.js';
|
|
|
41
49
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
42
50
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, } from './executor.js';
|
|
43
51
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
52
|
+
export type { EnsureSignInOrgOptions } from './sign-in-org.js';
|
|
53
|
+
export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './sign-in-org.js';
|
|
44
54
|
export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './health-targets.js';
|
|
45
55
|
export { healthTargets, platformHealthTargets } from './health-targets.js';
|
|
46
56
|
export { createEdgeDns, edgeDnsRecordOwners, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
3
|
export { stepSystemOf } from './step-system.js';
|
|
4
|
-
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
5
|
-
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
4
|
+
export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
5
|
+
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
6
|
+
export { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, } from './own-diff.js';
|
|
7
|
+
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu } from './own-diff.js';
|
|
8
|
+
export { provisionSfuApp, sfuConfig, sfuPortsMappings } from './own-sfu.js';
|
|
9
|
+
export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgNetwork, orgTarget, platformNetwork, platformTarget, scopeFromOrg, scopeFromPlatform, } from './own-services.js';
|
|
6
10
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
7
11
|
export { AdoptRefusedError } from './adopt.js';
|
|
8
12
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
9
|
-
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
13
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './platform-providers.js';
|
|
10
14
|
// #171: how a host signs files management requests with the delivered key.
|
|
11
15
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
12
16
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
@@ -21,6 +25,7 @@ export { loginHostnameFor } from './login-host.js';
|
|
|
21
25
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
22
26
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
23
27
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
28
|
+
export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './sign-in-org.js';
|
|
24
29
|
export { healthTargets, platformHealthTargets } from './health-targets.js';
|
|
25
30
|
export { createEdgeDns, edgeDnsRecordOwners, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
26
31
|
export { createEdgeAccess } from './edge-access.js';
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeclaredOrg, type OrgId } from '../org.js';
|
|
2
|
+
import { type OwnAuthInstance } from './own-services.js';
|
|
2
3
|
/**
|
|
3
4
|
* One resource as seen live in one of foundry's seven systems.
|
|
4
5
|
*
|
|
@@ -87,6 +88,8 @@ export interface CmsReader {
|
|
|
87
88
|
}
|
|
88
89
|
export interface LiveReaders {
|
|
89
90
|
zitadel: ZitadelReader;
|
|
91
|
+
/** The reader for one own org's ZITADEL instance. Without it an own org's ZITADEL is not read. */
|
|
92
|
+
zitadelFor?: (instance: OwnAuthInstance) => ZitadelReader;
|
|
90
93
|
coolify: CoolifyReader;
|
|
91
94
|
cloudflare: CloudflareReader;
|
|
92
95
|
files: FilesReader;
|
|
@@ -94,7 +97,7 @@ export interface LiveReaders {
|
|
|
94
97
|
cms: CmsReader;
|
|
95
98
|
}
|
|
96
99
|
/** Every live resource foundry can see for `orgId`, across all six systems. */
|
|
97
|
-
export declare function readLiveResources(orgId: OrgId, readers: LiveReaders): Promise<LiveResource[]>;
|
|
100
|
+
export declare function readLiveResources(orgId: OrgId, readers: LiveReaders, org?: DeclaredOrg, orgRecordedIds?: Readonly<Record<string, string>>): Promise<LiveResource[]>;
|
|
98
101
|
/**
|
|
99
102
|
* #167: one narrow interface per system reading the shared platform's own
|
|
100
103
|
* resources (`platform.ts`'s `DeclaredPlatform`) -- the platform-scoped
|
package/dist/plan/live.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { ownsServices } from '../org.js';
|
|
2
|
+
import { OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
1
3
|
/** Every live resource foundry can see for `orgId`, across all six systems. */
|
|
2
|
-
export async function readLiveResources(orgId, readers) {
|
|
4
|
+
export async function readLiveResources(orgId, readers, org, orgRecordedIds) {
|
|
3
5
|
const [zitadel, coolify, cloudflare, files, ai, cms] = await Promise.all([
|
|
4
|
-
|
|
6
|
+
org && ownsServices(org)
|
|
7
|
+
? readOwnZitadel(orgId, readers, org, orgRecordedIds ?? {})
|
|
8
|
+
: readers.zitadel.listResources(orgId),
|
|
5
9
|
readers.coolify.listResources(orgId),
|
|
6
10
|
readers.cloudflare.listResources(orgId),
|
|
7
11
|
readers.files.listResources(orgId),
|
|
@@ -10,6 +14,15 @@ export async function readLiveResources(orgId, readers) {
|
|
|
10
14
|
]);
|
|
11
15
|
return [...zitadel, ...coolify, ...cloudflare, ...files, ...ai, ...cms];
|
|
12
16
|
}
|
|
17
|
+
/** An own org's ZITADEL is its own instance: nothing is read until it is applied, and never from the shared one. */
|
|
18
|
+
async function readOwnZitadel(orgId, readers, org, recordedIds) {
|
|
19
|
+
if (!readers.zitadelFor || !org.domain || !recordedIds[OWN_SERVICES_APPLIED_SCOPE_KEY])
|
|
20
|
+
return [];
|
|
21
|
+
const refs = orgTarget(org).refs;
|
|
22
|
+
return readers
|
|
23
|
+
.zitadelFor({ issuerUrl: `https://auth.${org.domain}`, bearerRef: refs.instanceBearer })
|
|
24
|
+
.listResources(orgId);
|
|
25
|
+
}
|
|
13
26
|
/** Every live resource foundry can see for the platform, across every wired reader (#167, #173). */
|
|
14
27
|
export async function readLivePlatformResources(readers) {
|
|
15
28
|
const [cloudflare, coolify] = await Promise.all([
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
|
+
/** The labels an own org's services answer on, so no hosting entry may take them. */
|
|
3
|
+
export declare const OWN_SERVICE_LABELS: readonly ["auth", "mail", "files", "sfu"];
|
|
2
4
|
/**
|
|
3
5
|
* The names under the org's domain that foundry owns, so no hosting entry may
|
|
4
6
|
* take one: Agora, Boule, the cms studio when the site is on, calls when it uses the
|
|
5
|
-
* domain, the sign-in host when the issuer does,
|
|
7
|
+
* domain, the sign-in host when the issuer does, `auth.`, `mail.`, `files.` and
|
|
8
|
+
* `sfu.` for an org that runs its own services, and every platform service
|
|
6
9
|
* hostname that falls under the domain. Empty for an org with no domain.
|
|
7
10
|
*/
|
|
8
11
|
export declare function lockedHostnames(org: DeclaredOrg, platformHostnames: readonly string[]): Set<string>;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { ownsServices } from '../org.js';
|
|
1
2
|
import { agoraHostnameFor } from './agora.js';
|
|
2
3
|
import { bouleHostnameFor } from './boule.js';
|
|
3
4
|
import { loginHostnameFor } from './login-host.js';
|
|
5
|
+
/** The labels an own org's services answer on, so no hosting entry may take them. */
|
|
6
|
+
export const OWN_SERVICE_LABELS = ['auth', 'mail', 'files', 'sfu'];
|
|
4
7
|
/**
|
|
5
8
|
* The names under the org's domain that foundry owns, so no hosting entry may
|
|
6
9
|
* take one: Agora, Boule, the cms studio when the site is on, calls when it uses the
|
|
7
|
-
* domain, the sign-in host when the issuer does,
|
|
10
|
+
* domain, the sign-in host when the issuer does, `auth.`, `mail.`, `files.` and
|
|
11
|
+
* `sfu.` for an org that runs its own services, and every platform service
|
|
8
12
|
* hostname that falls under the domain. Empty for an org with no domain.
|
|
9
13
|
*/
|
|
10
14
|
export function lockedHostnames(org, platformHostnames) {
|
|
@@ -20,6 +24,10 @@ export function lockedHostnames(org, platformHostnames) {
|
|
|
20
24
|
locked.add(`calls.${domain}`);
|
|
21
25
|
if (org.issuer.usesDomain)
|
|
22
26
|
locked.add(loginHostnameFor(org));
|
|
27
|
+
if (ownsServices(org)) {
|
|
28
|
+
for (const label of OWN_SERVICE_LABELS)
|
|
29
|
+
locked.add(`${label}.${domain}`);
|
|
30
|
+
}
|
|
23
31
|
for (const hostname of platformHostnames) {
|
|
24
32
|
if (hostname.endsWith(`.${domain}`))
|
|
25
33
|
locked.add(hostname);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
2
|
/**
|
|
3
3
|
* #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
4
4
|
* need to agree on for an org's own company login host -- the hostname and
|
|
@@ -32,6 +32,66 @@ import type { DeclaredOrg } from '../org.js';
|
|
|
32
32
|
* included (`planLoginHost` and `zitadelLoginHost` skip both for it).
|
|
33
33
|
*/
|
|
34
34
|
export declare function loginHostnameFor(org: DeclaredOrg): string;
|
|
35
|
+
/**
|
|
36
|
+
* The login host the login-host step plans for an org: the shared instance's
|
|
37
|
+
* own host when `sharedHost` is given (the platform's own org), the company's
|
|
38
|
+
* own host otherwise. `planLoginHost` and {@link agoraIssuerHostFor} both
|
|
39
|
+
* read it here, so Agora's issuer and the login host cannot disagree.
|
|
40
|
+
*/
|
|
41
|
+
export declare function orgLoginHost(org: DeclaredOrg, sharedHost: string | null): string;
|
|
42
|
+
/**
|
|
43
|
+
* The login host Agora may take as its issuer, and whether the org's pair is
|
|
44
|
+
* one Agora refuses. Agora accepts an issuer host on its own address's
|
|
45
|
+
* registrable domain, or `<slug>-auth.oikos.host` for `<slug>-agora.oikos.host`.
|
|
46
|
+
* `host` is null for the platform's own org (it keeps the platform issuer) and
|
|
47
|
+
* for a domain with `issuer.usesDomain` off, where the login host
|
|
48
|
+
* `<slug>-auth.oikos.host` and the Agora address `agora.<domain>` are
|
|
49
|
+
* refused together (`mismatch`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function agoraIssuerHostFor(org: DeclaredOrg, isPlatformOrg: boolean): {
|
|
52
|
+
host: string | null;
|
|
53
|
+
mismatch: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** Milliseconds the discovery read may take before the host counts as not serving. */
|
|
56
|
+
export declare const LOGIN_HOST_DISCOVERY_TIMEOUT_MS = 5000;
|
|
57
|
+
/**
|
|
58
|
+
* Host-supplied read of `GET https://<host>/.well-known/openid-configuration`:
|
|
59
|
+
* the status and the body text. Optional on `FoundryPlanApplyOptions`; absent
|
|
60
|
+
* means "not read", so Agora keeps the platform issuer. The port may throw.
|
|
61
|
+
*/
|
|
62
|
+
export interface LoginHostDiscoveryPort {
|
|
63
|
+
discovery(host: string): Promise<{
|
|
64
|
+
status: number;
|
|
65
|
+
body: string;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/** True when the answer is a 200 whose JSON `issuer` is exactly `https://<host>`. */
|
|
69
|
+
export declare function discoveryNamesHost(host: string, answer: {
|
|
70
|
+
status: number;
|
|
71
|
+
body: string;
|
|
72
|
+
}): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Reads the port once, with a {@link LOGIN_HOST_DISCOVERY_TIMEOUT_MS} limit.
|
|
75
|
+
* An absent port, a throw, a timeout or any answer that is not a discovery
|
|
76
|
+
* document for `host` is false. Never throws; nothing is logged, so no value
|
|
77
|
+
* leaves.
|
|
78
|
+
*/
|
|
79
|
+
export declare function readLoginHostDiscovery(port: LoginHostDiscoveryPort | undefined, host: string, timeoutMs?: number): Promise<boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* True once the org's own sign-in step has applied (`coolify:own-auth:<org.id>`
|
|
82
|
+
* in its recorded ids). An own org has no login-host step: its issuer host is
|
|
83
|
+
* the sign-in service's own, so this marker is what Agora and every ZITADEL
|
|
84
|
+
* step of the org wait for.
|
|
85
|
+
*/
|
|
86
|
+
export declare function ownAuthApplied(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* True once the login-host step has applied `host` for the org, and, when
|
|
89
|
+
* Agora's client is already made, the binding it wrote names that client.
|
|
90
|
+
* The binding admits client ids only, so a host whose binding lacks Agora's
|
|
91
|
+
* client cannot sign Agora in. An org with no Agora client yet has nothing
|
|
92
|
+
* to wait for beyond the host itself.
|
|
93
|
+
*/
|
|
94
|
+
export declare function loginHostServesAgora(host: string, org: DeclaredOrg, recorded: Readonly<Record<string, string>>): boolean;
|
|
35
95
|
/**
|
|
36
96
|
* Scope id set to `'1'` once an org's recorded login host is the platform's
|
|
37
97
|
* shared instance host: that host's trusted domain belongs to the shared
|
|
@@ -113,6 +173,6 @@ export declare function pendingRemovalState(hosts: readonly string[]): string;
|
|
|
113
173
|
* it: `auth.<domain>`, only when it is under the org's own domain
|
|
114
174
|
* (`issuer.usesDomain`) and the org is not the platform's own, whose host is
|
|
115
175
|
* the shared instance's and is routed by the platform auth tunnel. Otherwise
|
|
116
|
-
* `null
|
|
176
|
+
* `null`, and always `null` for an own org. The tier 1 host rides the shared `*.oikos.host` tunnel.
|
|
117
177
|
*/
|
|
118
178
|
export declare function companyLoginTunnelHost(org: DeclaredOrg, isPlatformOrg: boolean): string | null;
|
package/dist/plan/login-host.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownsServices } from '../org.js';
|
|
1
2
|
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, } from './agora.js';
|
|
2
3
|
import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
|
|
3
4
|
/**
|
|
@@ -41,6 +42,105 @@ export function loginHostnameFor(org) {
|
|
|
41
42
|
}
|
|
42
43
|
return `${org.slug}-auth.oikos.host`;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The login host the login-host step plans for an org: the shared instance's
|
|
47
|
+
* own host when `sharedHost` is given (the platform's own org), the company's
|
|
48
|
+
* own host otherwise. `planLoginHost` and {@link agoraIssuerHostFor} both
|
|
49
|
+
* read it here, so Agora's issuer and the login host cannot disagree.
|
|
50
|
+
*/
|
|
51
|
+
export function orgLoginHost(org, sharedHost) {
|
|
52
|
+
return sharedHost ?? loginHostnameFor(org);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The login host Agora may take as its issuer, and whether the org's pair is
|
|
56
|
+
* one Agora refuses. Agora accepts an issuer host on its own address's
|
|
57
|
+
* registrable domain, or `<slug>-auth.oikos.host` for `<slug>-agora.oikos.host`.
|
|
58
|
+
* `host` is null for the platform's own org (it keeps the platform issuer) and
|
|
59
|
+
* for a domain with `issuer.usesDomain` off, where the login host
|
|
60
|
+
* `<slug>-auth.oikos.host` and the Agora address `agora.<domain>` are
|
|
61
|
+
* refused together (`mismatch`).
|
|
62
|
+
*/
|
|
63
|
+
export function agoraIssuerHostFor(org, isPlatformOrg) {
|
|
64
|
+
if (isPlatformOrg)
|
|
65
|
+
return { host: null, mismatch: false };
|
|
66
|
+
if (org.domain && !org.issuer.usesDomain)
|
|
67
|
+
return { host: null, mismatch: true };
|
|
68
|
+
return { host: loginHostnameFor(org), mismatch: false };
|
|
69
|
+
}
|
|
70
|
+
/** Milliseconds the discovery read may take before the host counts as not serving. */
|
|
71
|
+
export const LOGIN_HOST_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
72
|
+
/** True when the answer is a 200 whose JSON `issuer` is exactly `https://<host>`. */
|
|
73
|
+
export function discoveryNamesHost(host, answer) {
|
|
74
|
+
if (answer.status !== 200)
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(answer.body);
|
|
78
|
+
return (typeof parsed === 'object' &&
|
|
79
|
+
parsed !== null &&
|
|
80
|
+
parsed.issuer === `https://${host}`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reads the port once, with a {@link LOGIN_HOST_DISCOVERY_TIMEOUT_MS} limit.
|
|
88
|
+
* An absent port, a throw, a timeout or any answer that is not a discovery
|
|
89
|
+
* document for `host` is false. Never throws; nothing is logged, so no value
|
|
90
|
+
* leaves.
|
|
91
|
+
*/
|
|
92
|
+
export async function readLoginHostDiscovery(port, host, timeoutMs = LOGIN_HOST_DISCOVERY_TIMEOUT_MS) {
|
|
93
|
+
if (!port)
|
|
94
|
+
return false;
|
|
95
|
+
let timer;
|
|
96
|
+
try {
|
|
97
|
+
const answer = await Promise.race([
|
|
98
|
+
port.discovery(host),
|
|
99
|
+
new Promise((_, reject) => {
|
|
100
|
+
timer = setTimeout(() => reject(new Error('timeout')), timeoutMs);
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
return discoveryNamesHost(host, answer);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Any failure is "not serving": the plan keeps the platform issuer and says so in a fixed line.
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* True once the org's own sign-in step has applied (`coolify:own-auth:<org.id>`
|
|
115
|
+
* in its recorded ids). An own org has no login-host step: its issuer host is
|
|
116
|
+
* the sign-in service's own, so this marker is what Agora and every ZITADEL
|
|
117
|
+
* step of the org wait for.
|
|
118
|
+
*/
|
|
119
|
+
export function ownAuthApplied(org, recorded) {
|
|
120
|
+
return Boolean(recorded[`coolify:own-auth:${org.id}`]);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* True once the login-host step has applied `host` for the org, and, when
|
|
124
|
+
* Agora's client is already made, the binding it wrote names that client.
|
|
125
|
+
* The binding admits client ids only, so a host whose binding lacks Agora's
|
|
126
|
+
* client cannot sign Agora in. An org with no Agora client yet has nothing
|
|
127
|
+
* to wait for beyond the host itself.
|
|
128
|
+
*/
|
|
129
|
+
export function loginHostServesAgora(host, org, recorded) {
|
|
130
|
+
if (ownsServices(org))
|
|
131
|
+
return ownAuthApplied(org, recorded);
|
|
132
|
+
if (recorded[`zitadel:login-host:${org.id}`] !== host)
|
|
133
|
+
return false;
|
|
134
|
+
const bound = recorded[LOGIN_BINDING_SCOPE_KEY];
|
|
135
|
+
if (!bound?.startsWith(`${host} `))
|
|
136
|
+
return false;
|
|
137
|
+
const clientId = recorded[AGORA_CLIENT_ID_SCOPE_KEY];
|
|
138
|
+
return (!clientId ||
|
|
139
|
+
bound
|
|
140
|
+
.slice(host.length + 1)
|
|
141
|
+
.split(',')
|
|
142
|
+
.includes(clientId));
|
|
143
|
+
}
|
|
44
144
|
/**
|
|
45
145
|
* Scope id set to `'1'` once an org's recorded login host is the platform's
|
|
46
146
|
* shared instance host: that host's trusted domain belongs to the shared
|
|
@@ -109,8 +209,11 @@ export function pendingRemovalState(hosts) {
|
|
|
109
209
|
* it: `auth.<domain>`, only when it is under the org's own domain
|
|
110
210
|
* (`issuer.usesDomain`) and the org is not the platform's own, whose host is
|
|
111
211
|
* the shared instance's and is routed by the platform auth tunnel. Otherwise
|
|
112
|
-
* `null
|
|
212
|
+
* `null`, and always `null` for an own org. The tier 1 host rides the shared `*.oikos.host` tunnel.
|
|
113
213
|
*/
|
|
114
214
|
export function companyLoginTunnelHost(org, isPlatformOrg) {
|
|
215
|
+
// An own org's sign-in stack publishes `auth.<domain>` through its own tunnel.
|
|
216
|
+
if (ownsServices(org))
|
|
217
|
+
return null;
|
|
115
218
|
return org.domain && org.issuer.usesDomain && !isPlatformOrg ? loginHostnameFor(org) : null;
|
|
116
219
|
}
|