@wtfalch/foundry 0.28.0 → 0.30.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/calls-sfu-ssh-client.d.ts +11 -0
- package/dist/calls-sfu-ssh-client.js +65 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/org.d.ts +88 -0
- package/dist/org.js +68 -0
- package/dist/plan/agora.d.ts +13 -0
- package/dist/plan/agora.js +15 -0
- package/dist/plan/boule.d.ts +27 -3
- package/dist/plan/boule.js +39 -1
- package/dist/plan/chat-matrix.d.ts +31 -2
- package/dist/plan/chat-matrix.js +40 -6
- package/dist/plan/diff.d.ts +45 -2
- package/dist/plan/diff.js +211 -16
- package/dist/plan/executor.d.ts +132 -0
- package/dist/plan/executor.js +510 -31
- package/dist/plan/index.d.ts +5 -2
- package/dist/plan/index.js +3 -1
- package/dist/plan/plan-apply.js +1 -0
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +15 -3
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +79 -22
- package/dist/plan/providers-readers.d.ts +15 -2
- package/dist/plan/providers-readers.js +44 -6
- package/dist/plan/step-system.js +1 -0
- package/dist/plan/version-status.d.ts +200 -0
- package/dist/plan/version-status.js +213 -0
- package/dist/plan/zitadel-login-policy.d.ts +23 -0
- package/dist/plan/zitadel-login-policy.js +103 -0
- package/dist/plan/zitadel-machine-user.d.ts +1 -1
- package/dist/plan/zitadel-machine-user.js +34 -0
- package/dist/platform.d.ts +1 -1
- package/package.json +3 -2
|
@@ -18,12 +18,14 @@ interface SshStream {
|
|
|
18
18
|
stderr: {
|
|
19
19
|
on(event: string, listener: (...args: never[]) => void): unknown;
|
|
20
20
|
};
|
|
21
|
+
close(): void;
|
|
21
22
|
}
|
|
22
23
|
interface SshSftp {
|
|
23
24
|
writeFile(path: string, contents: string, options: {
|
|
24
25
|
mode: number;
|
|
25
26
|
} | undefined, cb: (err?: Error | null) => void): void;
|
|
26
27
|
chmod(path: string, mode: number, cb: (err?: Error | null) => void): void;
|
|
28
|
+
end(): void;
|
|
27
29
|
}
|
|
28
30
|
interface SshClient {
|
|
29
31
|
on(event: string, listener: (...args: never[]) => void): SshClient;
|
|
@@ -41,6 +43,15 @@ export interface CallsSfuSshConnectOptions {
|
|
|
41
43
|
/** `SHA256:...`. Without it every host key is refused. */
|
|
42
44
|
sshHostFingerprint: string | undefined;
|
|
43
45
|
}
|
|
46
|
+
/** `docker compose up -d` pulls images, so a command gets two minutes. */
|
|
47
|
+
export declare const SSH_COMMAND_TIMEOUT_MS = 120000;
|
|
48
|
+
/** One SFTP write (open, write, chmod) of a small config file. */
|
|
49
|
+
export declare const SSH_WRITE_TIMEOUT_MS = 30000;
|
|
50
|
+
/** Handshake limit; ssh2's default is 20 s, set here so it is explicit. */
|
|
51
|
+
export declare const SSH_READY_TIMEOUT_MS = 20000;
|
|
52
|
+
/** A dead link ends the session after 3 missed keepalives, 10 s apart. */
|
|
53
|
+
export declare const SSH_KEEPALIVE_INTERVAL_MS = 10000;
|
|
54
|
+
export declare const SSH_KEEPALIVE_COUNT_MAX = 3;
|
|
44
55
|
export declare function connectCallsSfuSsh(options: CallsSfuSshConnectOptions, deps?: {
|
|
45
56
|
Client?: SshClientConstructor;
|
|
46
57
|
}): Promise<CallsSfuSshSession & {
|
|
@@ -13,36 +13,87 @@
|
|
|
13
13
|
* hands in a fake and never opens a socket.
|
|
14
14
|
*/
|
|
15
15
|
import { verifyHostKey } from './calls-sfu-deploy-support.js';
|
|
16
|
-
|
|
16
|
+
/** `docker compose up -d` pulls images, so a command gets two minutes. */
|
|
17
|
+
export const SSH_COMMAND_TIMEOUT_MS = 120_000;
|
|
18
|
+
/** One SFTP write (open, write, chmod) of a small config file. */
|
|
19
|
+
export const SSH_WRITE_TIMEOUT_MS = 30_000;
|
|
20
|
+
/** Handshake limit; ssh2's default is 20 s, set here so it is explicit. */
|
|
21
|
+
export const SSH_READY_TIMEOUT_MS = 20_000;
|
|
22
|
+
/** A dead link ends the session after 3 missed keepalives, 10 s apart. */
|
|
23
|
+
export const SSH_KEEPALIVE_INTERVAL_MS = 10_000;
|
|
24
|
+
export const SSH_KEEPALIVE_COUNT_MAX = 3;
|
|
25
|
+
/** `sshExec` keeps the last 64 KiB of each of stdout and stderr. */
|
|
26
|
+
const SSH_OUTPUT_MAX_CHARS = 64 * 1024;
|
|
27
|
+
// The timeout messages name the command or the file path and nothing else.
|
|
28
|
+
// Every caller passes a fixed command or a fixed path (`calls-sfu.ts`,
|
|
29
|
+
// `calls-sfu-ssh-adapter.ts`); the cloudflared token and the LiveKit keys
|
|
30
|
+
// only ever travel as file contents, which are never named here.
|
|
31
|
+
function sshExec(conn, command, timeoutMs = SSH_COMMAND_TIMEOUT_MS) {
|
|
17
32
|
return new Promise((resolve, reject) => {
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
let stream;
|
|
34
|
+
let timedOut = false;
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
timedOut = true;
|
|
37
|
+
stream?.close();
|
|
38
|
+
reject(new Error(`ssh command timed out after ${timeoutMs / 1000}s: ${command}`));
|
|
39
|
+
}, timeoutMs);
|
|
40
|
+
conn.exec(command, (err, execStream) => {
|
|
41
|
+
if (timedOut)
|
|
42
|
+
return execStream?.close();
|
|
43
|
+
if (err) {
|
|
44
|
+
clearTimeout(timer);
|
|
20
45
|
return reject(err);
|
|
46
|
+
}
|
|
47
|
+
stream = execStream;
|
|
21
48
|
let stdout = '';
|
|
22
49
|
let stderr = '';
|
|
23
|
-
stream.on('close', (code) =>
|
|
50
|
+
stream.on('close', (code) => {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
resolve({ exitCode: code ?? 0, stdout, stderr });
|
|
53
|
+
});
|
|
54
|
+
stream.on('error', (streamErr) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
reject(streamErr);
|
|
57
|
+
});
|
|
24
58
|
stream.on('data', (chunk) => {
|
|
25
|
-
stdout
|
|
59
|
+
stdout = (stdout + chunk).slice(-SSH_OUTPUT_MAX_CHARS);
|
|
26
60
|
});
|
|
27
61
|
stream.stderr.on('data', (chunk) => {
|
|
28
|
-
stderr
|
|
62
|
+
stderr = (stderr + chunk).slice(-SSH_OUTPUT_MAX_CHARS);
|
|
29
63
|
});
|
|
30
64
|
});
|
|
31
65
|
});
|
|
32
66
|
}
|
|
33
|
-
function sshWriteFile(conn, path, contents, mode) {
|
|
67
|
+
function sshWriteFile(conn, path, contents, mode, timeoutMs = SSH_WRITE_TIMEOUT_MS) {
|
|
34
68
|
return new Promise((resolve, reject) => {
|
|
69
|
+
let session;
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
timedOut = true;
|
|
73
|
+
session?.end();
|
|
74
|
+
reject(new Error(`sftp write timed out after ${timeoutMs / 1000}s: ${path}`));
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
const settle = (err) => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
if (err)
|
|
79
|
+
reject(err);
|
|
80
|
+
else
|
|
81
|
+
resolve();
|
|
82
|
+
};
|
|
35
83
|
conn.sftp((err, sftp) => {
|
|
84
|
+
if (timedOut)
|
|
85
|
+
return sftp?.end();
|
|
36
86
|
if (err)
|
|
37
|
-
return
|
|
87
|
+
return settle(err);
|
|
88
|
+
session = sftp;
|
|
38
89
|
sftp.writeFile(path, contents, mode ? { mode } : undefined, (writeErr) => {
|
|
39
90
|
if (writeErr)
|
|
40
|
-
return
|
|
91
|
+
return settle(writeErr);
|
|
41
92
|
// `mode` on writeFile only applies when the file is created; chmod
|
|
42
93
|
// repairs a file that already existed with looser permissions.
|
|
43
94
|
if (!mode)
|
|
44
|
-
return
|
|
45
|
-
sftp.chmod(path, mode,
|
|
95
|
+
return settle();
|
|
96
|
+
sftp.chmod(path, mode, settle);
|
|
46
97
|
});
|
|
47
98
|
});
|
|
48
99
|
});
|
|
@@ -64,6 +115,9 @@ export async function connectCallsSfuSsh(options, deps = {}) {
|
|
|
64
115
|
port: options.sshPort,
|
|
65
116
|
username: options.sshUser,
|
|
66
117
|
privateKey: options.sshPrivateKey,
|
|
118
|
+
readyTimeout: SSH_READY_TIMEOUT_MS,
|
|
119
|
+
keepaliveInterval: SSH_KEEPALIVE_INTERVAL_MS,
|
|
120
|
+
keepaliveCountMax: SSH_KEEPALIVE_COUNT_MAX,
|
|
67
121
|
// One argument: ssh2 treats the return value as the verdict.
|
|
68
122
|
hostVerifier: (key) => {
|
|
69
123
|
hostKey = key;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, } from './org.js';
|
|
2
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
1
|
+
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, } from './org.js';
|
|
2
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
3
3
|
export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, } from './platform.js';
|
|
4
4
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
5
5
|
import { type PinnedVersions } from './bootstrap/versions.js';
|
|
@@ -18,7 +18,7 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
|
|
|
18
18
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
19
19
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
|
-
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
21
|
+
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
23
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
24
24
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
@@ -33,14 +33,14 @@ export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
|
33
33
|
export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './plan/index.js';
|
|
34
34
|
export { healthTargets, platformHealthTargets } from './plan/index.js';
|
|
35
35
|
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './plan/index.js';
|
|
36
|
-
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
36
|
+
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
37
37
|
export type { ZitadelListAppsRawApp } from './plan/index.js';
|
|
38
38
|
export { adaptZitadelListApps } from './plan/index.js';
|
|
39
39
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js';
|
|
40
40
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
41
41
|
export type { MailAllowlistApp } from './plan/index.js';
|
|
42
42
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
43
|
-
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
43
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
44
44
|
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
45
45
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
46
46
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
@@ -60,6 +60,8 @@ export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCop
|
|
|
60
60
|
export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
|
|
61
61
|
export type { BootstrapCompany, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
|
|
62
62
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
63
|
+
export type { OrgAppVersionRef, OrgAppVersionStatus, WtfalchPackageDrift, PlatformVersionStatus, ReadPlatformVersionStatusOptions, StatusCoolifyPort, StatusGithubPort, StatusGhExec, } from './plan/index.js';
|
|
64
|
+
export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './plan/index.js';
|
|
63
65
|
export type { RunnerDb } from './runner/db.js';
|
|
64
66
|
export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
|
|
65
67
|
export { createJobRunner } from './runner/runner.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
1
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
2
2
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
3
3
|
// archon#273: the pinned platform service versions the planner deploys, so a
|
|
4
4
|
// host can show declared against running. Readonly: it is the planner's own
|
|
@@ -19,11 +19,11 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
|
19
19
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
20
20
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
21
21
|
export { healthTargets, platformHealthTargets } from './plan/index.js';
|
|
22
|
-
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
22
|
+
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
23
23
|
export { adaptZitadelListApps } from './plan/index.js';
|
|
24
24
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
25
25
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
26
|
-
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
26
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
27
27
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
28
28
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
29
29
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './plan/index.js';
|
|
@@ -36,5 +36,6 @@ export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCop
|
|
|
36
36
|
// imports nothing else.
|
|
37
37
|
export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
|
|
38
38
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
39
|
+
export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './plan/index.js';
|
|
39
40
|
export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
|
|
40
41
|
export { createJobRunner } from './runner/runner.js';
|
package/dist/org.d.ts
CHANGED
|
@@ -843,3 +843,91 @@ export interface DeclaredOrg {
|
|
|
843
843
|
*/
|
|
844
844
|
test: boolean;
|
|
845
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* The one Postgres server an org's own service schemas and roles live on
|
|
848
|
+
* (#444) — the settled replacement for today's one-container-per-service
|
|
849
|
+
* model: `plan/executor.ts`'s `coolifyDatabase` mints Archon's/lokessmie's
|
|
850
|
+
* own database, `ensureCmsDatabase` mints CMS's, and `ensureCallsDatabase`
|
|
851
|
+
* mints Boule's and chat's and calls' own Synapse, matrix-authentication-
|
|
852
|
+
* service and service databases, each as its own Coolify Postgres resource
|
|
853
|
+
* today. Not a field on {@link DeclaredOrg}: an operator never picks a
|
|
854
|
+
* server, the same way nobody picks an invited owner's ZITADEL user id
|
|
855
|
+
* (`plan/diff.ts`'s `planOwners`, idempotent on a *recorded* resource id
|
|
856
|
+
* rather than a declared field) — there is exactly one per org, and this is
|
|
857
|
+
* the single flat key it is recorded under once `plan/executor.ts`
|
|
858
|
+
* provisions it (444b). 444a (this slice) only ever reads this key through
|
|
859
|
+
* `recordedIds`; nothing here writes it.
|
|
860
|
+
*/
|
|
861
|
+
export declare const ORG_POSTGRES_SERVER_SCOPE_KEY = "org_postgres_server";
|
|
862
|
+
/**
|
|
863
|
+
* One service's own slice of the org's shared Postgres server (#444): a
|
|
864
|
+
* dedicated schema, an owner role that runs its migrations and owns every
|
|
865
|
+
* object inside it, and a restricted runtime role the service's own app
|
|
866
|
+
* connects as day to day and cannot create or drop anything with. The `_rt`
|
|
867
|
+
* suffix on {@link OrgServiceSchema.runtimeRole} is the one this estate
|
|
868
|
+
* already uses for exactly this role, everywhere a service splits an owner
|
|
869
|
+
* connection from a runtime one — every `package-template`-descended repo's
|
|
870
|
+
* own `CLAUDE.md` names it "the runtime role `<database>_rt`" — just scoped
|
|
871
|
+
* to a schema instead of a whole database, now that several schemas share
|
|
872
|
+
* one server. `scopeKey` is where `plan/diff.ts` and, later, 444b's
|
|
873
|
+
* `plan/executor.ts` record the schema's existence, the same derived-lookup
|
|
874
|
+
* shape {@link OwnerMailbox.scopeKey} already uses: nothing here is itself
|
|
875
|
+
* stored, {@link ORG_SERVICE_SCHEMAS} is rebuilt fresh every time it is read.
|
|
876
|
+
*
|
|
877
|
+
* Every field below is the **target** name 444b provisions under, not what
|
|
878
|
+
* is live today: today there is no schema at all (each service gets its own
|
|
879
|
+
* whole database), its "owner" is really whatever superuser Coolify's
|
|
880
|
+
* database-creation call happened to name, and a database's role is scoped
|
|
881
|
+
* to that whole database, never to a schema within it. The live runtime
|
|
882
|
+
* role names already in production do not all match {@link runtimeRole}'s
|
|
883
|
+
* `<schema>_rt` shape either — CMS's does (`cms_rt`), but chat's own service
|
|
884
|
+
* connects as `chat_rt`, Boule as `<slug>_boule_db_rt`, and calls' own
|
|
885
|
+
* service as `<dbName>_rt`. 444b must not rename a role a password was
|
|
886
|
+
* already minted for; it maps each of these live names onto the matching
|
|
887
|
+
* target below rather than provisioning a second, differently-named role
|
|
888
|
+
* beside it.
|
|
889
|
+
*/
|
|
890
|
+
export interface OrgServiceSchema {
|
|
891
|
+
/** The service this schema belongs to, for a plan step's or an error's own wording — never part of an identifier. */
|
|
892
|
+
service: string;
|
|
893
|
+
/** The schema's target name on the org's one Postgres server. Unique by construction: {@link ORG_SERVICE_SCHEMAS} names each service's schema exactly once. */
|
|
894
|
+
schema: string;
|
|
895
|
+
/** The target role that owns {@link schema} and runs its migrations — this estate's `DATABASE_URL_OWNER` connection, scoped to one schema rather than a whole database. No live role matches this name yet (see this interface's own doc comment). */
|
|
896
|
+
ownerRole: string;
|
|
897
|
+
/** The target restricted role the service's own app connects as day to day — this estate's `DATABASE_URL` connection. Several services' live runtime roles already carry this exact name; others 444b must map onto it (see this interface's own doc comment). */
|
|
898
|
+
runtimeRole: string;
|
|
899
|
+
/** Where this schema's existence is recorded, once provisioned. */
|
|
900
|
+
scopeKey: string;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The full, fixed schema topology every org's shared Postgres server
|
|
904
|
+
* settles into (#444's acceptance: "the documented service schema/role
|
|
905
|
+
* descriptors") — not a per-org choice, so unlike {@link ownerMailboxes}
|
|
906
|
+
* this takes no `DeclaredOrg` at all. One entry per Postgres database
|
|
907
|
+
* `plan/executor.ts` mints today as its own container, confirmed by reading
|
|
908
|
+
* it rather than assumed from the issue or any triage comment: Boule's own
|
|
909
|
+
* (`coolifyBoule`'s `ensureCallsDatabase` call) and CMS's own
|
|
910
|
+
* (`ensureCmsDatabase`) are each one; chat's and calls' own Matrix stacks
|
|
911
|
+
* are each three — `coolifyChat`/`coolifyCalls` each mint a Synapse and a
|
|
912
|
+
* matrix-authentication-service database, and `coolifyChat`/
|
|
913
|
+
* `coolifyCallsService` each mint a third for the service app beside them
|
|
914
|
+
* — eight; plus the org's own app database, `coolifyDatabase`'s `<slug>-db`,
|
|
915
|
+
* for the one hosting entry that declares `database: true` (Archon's or
|
|
916
|
+
* lokessmie's own data) — nine in total.
|
|
917
|
+
*
|
|
918
|
+
* Boule's own entry is provisioned unconditionally — `validateHosting`
|
|
919
|
+
* already refuses an `issuerApps` entry named `boule` because "every org
|
|
920
|
+
* gets" one — so it, and this constant as a whole, name the *full* topology
|
|
921
|
+
* regardless of which feature switches are on, the same way `ORG_ENTRY_KEY`
|
|
922
|
+
* names `app` whether or not an org has one. Which of these schemas
|
|
923
|
+
* `plan/diff.ts`'s `planOrgPostgres` actually reports as missing on a given
|
|
924
|
+
* org is a narrower question than what this list contains — CMS's,
|
|
925
|
+
* chat's and calls' entries sit alongside their own feature's on/off switch
|
|
926
|
+
* ({@link SiteConfig.enabled}, {@link ChatConfig.enabled},
|
|
927
|
+
* {@link CallsConfig.enabled}) and the app entry beside whether any hosting
|
|
928
|
+
* entry declares `database: true`; `planOrgPostgres`'s own doc comment says
|
|
929
|
+
* which. A schema nothing needs yet simply is not reported missing, the
|
|
930
|
+
* same way an org with no app entry (foundry#447) has no app schema to
|
|
931
|
+
* create.
|
|
932
|
+
*/
|
|
933
|
+
export declare const ORG_SERVICE_SCHEMAS: readonly OrgServiceSchema[];
|
package/dist/org.js
CHANGED
|
@@ -930,3 +930,71 @@ export function validateMail(org, opts) {
|
|
|
930
930
|
}
|
|
931
931
|
return null;
|
|
932
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* The one Postgres server an org's own service schemas and roles live on
|
|
935
|
+
* (#444) — the settled replacement for today's one-container-per-service
|
|
936
|
+
* model: `plan/executor.ts`'s `coolifyDatabase` mints Archon's/lokessmie's
|
|
937
|
+
* own database, `ensureCmsDatabase` mints CMS's, and `ensureCallsDatabase`
|
|
938
|
+
* mints Boule's and chat's and calls' own Synapse, matrix-authentication-
|
|
939
|
+
* service and service databases, each as its own Coolify Postgres resource
|
|
940
|
+
* today. Not a field on {@link DeclaredOrg}: an operator never picks a
|
|
941
|
+
* server, the same way nobody picks an invited owner's ZITADEL user id
|
|
942
|
+
* (`plan/diff.ts`'s `planOwners`, idempotent on a *recorded* resource id
|
|
943
|
+
* rather than a declared field) — there is exactly one per org, and this is
|
|
944
|
+
* the single flat key it is recorded under once `plan/executor.ts`
|
|
945
|
+
* provisions it (444b). 444a (this slice) only ever reads this key through
|
|
946
|
+
* `recordedIds`; nothing here writes it.
|
|
947
|
+
*/
|
|
948
|
+
export const ORG_POSTGRES_SERVER_SCOPE_KEY = 'org_postgres_server';
|
|
949
|
+
/** Builds one {@link OrgServiceSchema}: its owner role, runtime role and scope key all follow from {@link schema} alone, so every caller gets the same shape. */
|
|
950
|
+
function orgServiceSchema(service, schema) {
|
|
951
|
+
return {
|
|
952
|
+
service,
|
|
953
|
+
schema,
|
|
954
|
+
ownerRole: `${schema}_owner`,
|
|
955
|
+
runtimeRole: `${schema}_rt`,
|
|
956
|
+
scopeKey: `org_postgres_schema.${schema}`,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* The full, fixed schema topology every org's shared Postgres server
|
|
961
|
+
* settles into (#444's acceptance: "the documented service schema/role
|
|
962
|
+
* descriptors") — not a per-org choice, so unlike {@link ownerMailboxes}
|
|
963
|
+
* this takes no `DeclaredOrg` at all. One entry per Postgres database
|
|
964
|
+
* `plan/executor.ts` mints today as its own container, confirmed by reading
|
|
965
|
+
* it rather than assumed from the issue or any triage comment: Boule's own
|
|
966
|
+
* (`coolifyBoule`'s `ensureCallsDatabase` call) and CMS's own
|
|
967
|
+
* (`ensureCmsDatabase`) are each one; chat's and calls' own Matrix stacks
|
|
968
|
+
* are each three — `coolifyChat`/`coolifyCalls` each mint a Synapse and a
|
|
969
|
+
* matrix-authentication-service database, and `coolifyChat`/
|
|
970
|
+
* `coolifyCallsService` each mint a third for the service app beside them
|
|
971
|
+
* — eight; plus the org's own app database, `coolifyDatabase`'s `<slug>-db`,
|
|
972
|
+
* for the one hosting entry that declares `database: true` (Archon's or
|
|
973
|
+
* lokessmie's own data) — nine in total.
|
|
974
|
+
*
|
|
975
|
+
* Boule's own entry is provisioned unconditionally — `validateHosting`
|
|
976
|
+
* already refuses an `issuerApps` entry named `boule` because "every org
|
|
977
|
+
* gets" one — so it, and this constant as a whole, name the *full* topology
|
|
978
|
+
* regardless of which feature switches are on, the same way `ORG_ENTRY_KEY`
|
|
979
|
+
* names `app` whether or not an org has one. Which of these schemas
|
|
980
|
+
* `plan/diff.ts`'s `planOrgPostgres` actually reports as missing on a given
|
|
981
|
+
* org is a narrower question than what this list contains — CMS's,
|
|
982
|
+
* chat's and calls' entries sit alongside their own feature's on/off switch
|
|
983
|
+
* ({@link SiteConfig.enabled}, {@link ChatConfig.enabled},
|
|
984
|
+
* {@link CallsConfig.enabled}) and the app entry beside whether any hosting
|
|
985
|
+
* entry declares `database: true`; `planOrgPostgres`'s own doc comment says
|
|
986
|
+
* which. A schema nothing needs yet simply is not reported missing, the
|
|
987
|
+
* same way an org with no app entry (foundry#447) has no app schema to
|
|
988
|
+
* create.
|
|
989
|
+
*/
|
|
990
|
+
export const ORG_SERVICE_SCHEMAS = [
|
|
991
|
+
orgServiceSchema('Boule', 'boule'),
|
|
992
|
+
orgServiceSchema('CMS', 'cms'),
|
|
993
|
+
orgServiceSchema("chat's Synapse homeserver", 'chat_synapse'),
|
|
994
|
+
orgServiceSchema("chat's matrix-authentication-service", 'chat_mas'),
|
|
995
|
+
orgServiceSchema("chat's own service", 'chat_service'),
|
|
996
|
+
orgServiceSchema("calls' Synapse homeserver", 'calls_synapse'),
|
|
997
|
+
orgServiceSchema("calls' matrix-authentication-service", 'calls_mas'),
|
|
998
|
+
orgServiceSchema("calls' own service", 'calls_service'),
|
|
999
|
+
orgServiceSchema("the org's own app", 'app'),
|
|
1000
|
+
];
|
package/dist/plan/agora.d.ts
CHANGED
|
@@ -167,6 +167,19 @@ export declare const CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY = "zitadel_calls_
|
|
|
167
167
|
*/
|
|
168
168
|
export declare const CHAT_INTROSPECTION_SCOPE_KEY = "zitadel_chat_introspection_app";
|
|
169
169
|
export declare const CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY = "zitadel_chat_introspection_undelivered";
|
|
170
|
+
/**
|
|
171
|
+
* The one internal token per org that lets the calls service ask chat who is
|
|
172
|
+
* in a room: `CHAT_INTERNAL_TOKEN` on chat-service, `CALLS_CHAT_TOKEN` on the
|
|
173
|
+
* calls service (platform rule 1). The first holds the token's id once both
|
|
174
|
+
* apps carry it; the second is recorded and persisted before either does.
|
|
175
|
+
* The third is `<id> <chat address>`, recorded once both apps run with it. No
|
|
176
|
+
* `:` in any.
|
|
177
|
+
*/
|
|
178
|
+
export declare const CHAT_CALLS_TOKEN_SCOPE_KEY = "coolify_chat_calls_token";
|
|
179
|
+
export declare const CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY = "coolify_chat_calls_token_undelivered";
|
|
180
|
+
export declare const CHAT_CALLS_APPLIED_SCOPE_KEY = "coolify_chat_calls_applied";
|
|
181
|
+
/** The value {@link CHAT_CALLS_APPLIED_SCOPE_KEY} holds once the token and address are live on both apps. */
|
|
182
|
+
export declare function chatCallsApplied(tokenId: string, chatUrl: string): string;
|
|
170
183
|
/** The chat service's introspection API app name; one per org, exact match only. */
|
|
171
184
|
export declare function chatIntrospectionAppName(org: DeclaredOrg): string;
|
|
172
185
|
/** What the plan hands the calls service step: the two host values the token env needs. */
|
package/dist/plan/agora.js
CHANGED
|
@@ -179,6 +179,21 @@ export const CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY = 'zitadel_calls_introspe
|
|
|
179
179
|
*/
|
|
180
180
|
export const CHAT_INTROSPECTION_SCOPE_KEY = 'zitadel_chat_introspection_app';
|
|
181
181
|
export const CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY = 'zitadel_chat_introspection_undelivered';
|
|
182
|
+
/**
|
|
183
|
+
* The one internal token per org that lets the calls service ask chat who is
|
|
184
|
+
* in a room: `CHAT_INTERNAL_TOKEN` on chat-service, `CALLS_CHAT_TOKEN` on the
|
|
185
|
+
* calls service (platform rule 1). The first holds the token's id once both
|
|
186
|
+
* apps carry it; the second is recorded and persisted before either does.
|
|
187
|
+
* The third is `<id> <chat address>`, recorded once both apps run with it. No
|
|
188
|
+
* `:` in any.
|
|
189
|
+
*/
|
|
190
|
+
export const CHAT_CALLS_TOKEN_SCOPE_KEY = 'coolify_chat_calls_token';
|
|
191
|
+
export const CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY = 'coolify_chat_calls_token_undelivered';
|
|
192
|
+
export const CHAT_CALLS_APPLIED_SCOPE_KEY = 'coolify_chat_calls_applied';
|
|
193
|
+
/** The value {@link CHAT_CALLS_APPLIED_SCOPE_KEY} holds once the token and address are live on both apps. */
|
|
194
|
+
export function chatCallsApplied(tokenId, chatUrl) {
|
|
195
|
+
return `${tokenId} ${chatUrl}`;
|
|
196
|
+
}
|
|
182
197
|
/** The chat service's introspection API app name; one per org, exact match only. */
|
|
183
198
|
export function chatIntrospectionAppName(org) {
|
|
184
199
|
return `chat-introspection-${org.slug}`;
|
package/dist/plan/boule.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
2
|
import type { BrokerEntryInput, BrokerEntryWant } from './broker-entries.js';
|
|
3
3
|
/**
|
|
4
4
|
* #161 (Boule part): what both the planner (`diff.ts`) and the executor
|
|
@@ -36,8 +36,28 @@ export declare const BOULE_DEPLOYED_SCOPE_KEY = "coolify_boule_deployed";
|
|
|
36
36
|
* issuer.
|
|
37
37
|
*/
|
|
38
38
|
export declare function bouleHostnameFor(org: DeclaredOrg): string;
|
|
39
|
+
/** The env name Boule reads its Lettermint token from (boule#266). */
|
|
40
|
+
export declare const BOULE_MAIL_TOKEN_ENV = "LETTERMINT_API_TOKEN";
|
|
41
|
+
/**
|
|
42
|
+
* Whether Boule should mail invites: the org's mail is hosted and
|
|
43
|
+
* `coolify:mail` has recorded its domain. Mail off, received, or no domain
|
|
44
|
+
* yet: no mail env at all, and Boule shows the invite link on screen.
|
|
45
|
+
*/
|
|
46
|
+
export declare function bouleMailWanted(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Boule's `MAIL_FROM`: the org's no-reply address on its recorded mail
|
|
49
|
+
* domain. The local part falls back to `DEFAULT_NO_REPLY_LOCAL_PART`, the
|
|
50
|
+
* same default the hosted app's `MAIL_FROM` uses and the store column holds.
|
|
51
|
+
*/
|
|
52
|
+
export declare function bouleMailFrom(org: DeclaredOrg, mailDomain: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the live Boule's mail token needs writing: it holds none, or the
|
|
55
|
+
* org's hosted app holds a different one (the app's token wins). Compares
|
|
56
|
+
* values in memory only; the result names nothing secret.
|
|
57
|
+
*/
|
|
58
|
+
export declare function bouleMailTokenDrift(liveToken: string | undefined, appToken: string | undefined): boolean;
|
|
39
59
|
/** The env names `desiredBouleEnv` can write, so the live reader knows which to read back. */
|
|
40
|
-
export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "BOULE_ORG_NAME", "AUTHZ_BOOTSTRAP_OPERATOR", "ARCHON_URL"];
|
|
60
|
+
export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER", "AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "BOULE_ORG_NAME", "AUTHZ_BOOTSTRAP_OPERATOR", "ARCHON_URL", "MAIL_FROM", "LETTERMINT_API_TOKEN"];
|
|
41
61
|
/**
|
|
42
62
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
43
63
|
* to build them. Callers that write must refuse on a non-empty `missing`: a
|
|
@@ -55,11 +75,15 @@ export declare const BOULE_MANAGED_ENV_KEYS: readonly ["APP_URL", "AUTH_ISSUER",
|
|
|
55
75
|
* absent: Boule has no platform wired in every test and every org-only
|
|
56
76
|
* caller, same as `issuer` before #177.
|
|
57
77
|
*
|
|
78
|
+
* `mailWanted` (see {@link bouleMailWanted}) adds `MAIL_FROM`. The Lettermint
|
|
79
|
+
* token is never here: it is a secret the executor writes and the plan
|
|
80
|
+
* compares in memory ({@link bouleMailTokenDrift}).
|
|
81
|
+
*
|
|
58
82
|
* Not here, and written by the executor instead: the database URLs
|
|
59
83
|
* (`DATABASE_URL_OWNER`, `DATABASE_URL`, minted once) and `AUTH_COOKIE_SECRET`
|
|
60
84
|
* (generated once, never rotated).
|
|
61
85
|
*/
|
|
62
|
-
export declare function desiredBouleEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, issuer: string | null, archonUrl: string | null): {
|
|
86
|
+
export declare function desiredBouleEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, issuer: string | null, archonUrl: string | null, mailWanted?: boolean): {
|
|
63
87
|
env: Record<string, string>;
|
|
64
88
|
missing: string[];
|
|
65
89
|
};
|
package/dist/plan/boule.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_NO_REPLY_LOCAL_PART } from '../org.js';
|
|
1
2
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
2
3
|
/**
|
|
3
4
|
* #161 (Boule part): what both the planner (`diff.ts`) and the executor
|
|
@@ -37,6 +38,34 @@ export const BOULE_DEPLOYED_SCOPE_KEY = 'coolify_boule_deployed';
|
|
|
37
38
|
export function bouleHostnameFor(org) {
|
|
38
39
|
return org.domain ? `boule.${org.domain}` : `${org.slug}-boule.oikos.host`;
|
|
39
40
|
}
|
|
41
|
+
/** The env name Boule reads its Lettermint token from (boule#266). */
|
|
42
|
+
export const BOULE_MAIL_TOKEN_ENV = 'LETTERMINT_API_TOKEN';
|
|
43
|
+
/** The scope id `coolify:mail` records the org's resolved mail domain under. */
|
|
44
|
+
const MAIL_DOMAIN_RECORDED_KEY = 'mail_domain';
|
|
45
|
+
/**
|
|
46
|
+
* Whether Boule should mail invites: the org's mail is hosted and
|
|
47
|
+
* `coolify:mail` has recorded its domain. Mail off, received, or no domain
|
|
48
|
+
* yet: no mail env at all, and Boule shows the invite link on screen.
|
|
49
|
+
*/
|
|
50
|
+
export function bouleMailWanted(org, recorded) {
|
|
51
|
+
return org.mail.mode === 'hosted' && Boolean(recorded[MAIL_DOMAIN_RECORDED_KEY]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Boule's `MAIL_FROM`: the org's no-reply address on its recorded mail
|
|
55
|
+
* domain. The local part falls back to `DEFAULT_NO_REPLY_LOCAL_PART`, the
|
|
56
|
+
* same default the hosted app's `MAIL_FROM` uses and the store column holds.
|
|
57
|
+
*/
|
|
58
|
+
export function bouleMailFrom(org, mailDomain) {
|
|
59
|
+
return `${org.mail.noReplyLocalPart || DEFAULT_NO_REPLY_LOCAL_PART}@${mailDomain}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether the live Boule's mail token needs writing: it holds none, or the
|
|
63
|
+
* org's hosted app holds a different one (the app's token wins). Compares
|
|
64
|
+
* values in memory only; the result names nothing secret.
|
|
65
|
+
*/
|
|
66
|
+
export function bouleMailTokenDrift(liveToken, appToken) {
|
|
67
|
+
return !liveToken || (Boolean(appToken) && appToken !== liveToken);
|
|
68
|
+
}
|
|
40
69
|
/** The env names `desiredBouleEnv` can write, so the live reader knows which to read back. */
|
|
41
70
|
export const BOULE_MANAGED_ENV_KEYS = [
|
|
42
71
|
'APP_URL',
|
|
@@ -46,6 +75,8 @@ export const BOULE_MANAGED_ENV_KEYS = [
|
|
|
46
75
|
'BOULE_ORG_NAME',
|
|
47
76
|
'AUTHZ_BOOTSTRAP_OPERATOR',
|
|
48
77
|
'ARCHON_URL',
|
|
78
|
+
'MAIL_FROM',
|
|
79
|
+
BOULE_MAIL_TOKEN_ENV,
|
|
49
80
|
];
|
|
50
81
|
/**
|
|
51
82
|
* The env vars an org's Boule app should carry, and every input still missing
|
|
@@ -64,11 +95,15 @@ export const BOULE_MANAGED_ENV_KEYS = [
|
|
|
64
95
|
* absent: Boule has no platform wired in every test and every org-only
|
|
65
96
|
* caller, same as `issuer` before #177.
|
|
66
97
|
*
|
|
98
|
+
* `mailWanted` (see {@link bouleMailWanted}) adds `MAIL_FROM`. The Lettermint
|
|
99
|
+
* token is never here: it is a secret the executor writes and the plan
|
|
100
|
+
* compares in memory ({@link bouleMailTokenDrift}).
|
|
101
|
+
*
|
|
67
102
|
* Not here, and written by the executor instead: the database URLs
|
|
68
103
|
* (`DATABASE_URL_OWNER`, `DATABASE_URL`, minted once) and `AUTH_COOKIE_SECRET`
|
|
69
104
|
* (generated once, never rotated).
|
|
70
105
|
*/
|
|
71
|
-
export function desiredBouleEnv(org, recorded, issuer, archonUrl) {
|
|
106
|
+
export function desiredBouleEnv(org, recorded, issuer, archonUrl, mailWanted = false) {
|
|
72
107
|
const zitadelOrgId = recorded[ZITADEL_ORG_SCOPE_KEY];
|
|
73
108
|
const clientId = recorded[BOULE_CLIENT_ID_SCOPE_KEY];
|
|
74
109
|
const missing = [];
|
|
@@ -96,6 +131,9 @@ export function desiredBouleEnv(org, recorded, issuer, archonUrl) {
|
|
|
96
131
|
: undefined;
|
|
97
132
|
if (ownerUserId)
|
|
98
133
|
env.AUTHZ_BOOTSTRAP_OPERATOR = ownerUserId;
|
|
134
|
+
const mailDomain = recorded[MAIL_DOMAIN_RECORDED_KEY];
|
|
135
|
+
if (mailWanted && mailDomain)
|
|
136
|
+
env.MAIL_FROM = bouleMailFrom(org, mailDomain);
|
|
99
137
|
return { env, missing };
|
|
100
138
|
}
|
|
101
139
|
/** The `AUTH_APPS` name of an org's Boule broker entry, and the scope key suffix it records under. */
|
|
@@ -47,6 +47,20 @@ export declare function chatUpstreams(orgSlug: string): {
|
|
|
47
47
|
matrix: string;
|
|
48
48
|
mas: string;
|
|
49
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Internal names for the org's re-keyed homeserver pair (#463, design note
|
|
52
|
+
* `docs/chat-rekey-design.md` Option 1): a second Synapse and MAS,
|
|
53
|
+
* provisioned beside the original pair under `chatInternalNames` so both
|
|
54
|
+
* ends of the copy are live at once. The chat service and its own database
|
|
55
|
+
* are never re-created — same app, repointed once the copy is done and the
|
|
56
|
+
* original pair is parked.
|
|
57
|
+
*/
|
|
58
|
+
export declare function chatRekeyedInternalNames(orgSlug: string): {
|
|
59
|
+
synapse: string;
|
|
60
|
+
mas: string;
|
|
61
|
+
synapseDb: string;
|
|
62
|
+
masDb: string;
|
|
63
|
+
};
|
|
50
64
|
/**
|
|
51
65
|
* Never resolved by DNS and never federates; the identifier Synapse, MAS and
|
|
52
66
|
* the chat service agree on. A level-1 org's chat lives on this name until a
|
|
@@ -54,13 +68,26 @@ export declare function chatUpstreams(orgSlug: string): {
|
|
|
54
68
|
* not collide with a hostname some org owns.
|
|
55
69
|
*/
|
|
56
70
|
export declare function chatServerName(orgSlug: string): string;
|
|
57
|
-
/**
|
|
58
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The Matrix user the chat service acts as
|
|
73
|
+
* (`chat/apps/chat-service/src/workspace.ts`). `serverName` defaults to the
|
|
74
|
+
* org's level-1 `chatServerName`; a re-key (#463) passes the new one so the
|
|
75
|
+
* gate module's `admin_user` matches the homeserver it is loaded into.
|
|
76
|
+
*/
|
|
77
|
+
export declare function chatAdminUser(orgSlug: string, serverName?: string): string;
|
|
78
|
+
/** A re-key's `server_name` is a lowercase DNS name with at least two labels; anything else would be written into both configs. */
|
|
79
|
+
export declare function assertChatServerName(serverName: string): void;
|
|
59
80
|
export interface ChatSynapseHomeserverInput {
|
|
60
81
|
orgSlug: string;
|
|
61
82
|
db: CallsDbConnection;
|
|
62
83
|
masHost: string;
|
|
63
84
|
masSharedSecret: string;
|
|
85
|
+
/**
|
|
86
|
+
* #463: the `server_name` this homeserver answers to. Defaults to the
|
|
87
|
+
* org's level-1 `chatServerName` so every existing caller is unaffected;
|
|
88
|
+
* a re-key's new Synapse passes the org's own domain instead.
|
|
89
|
+
*/
|
|
90
|
+
serverName?: string;
|
|
64
91
|
}
|
|
65
92
|
/** `homeserver.yaml`: federation off, presence on (stable Synapse features only -- no MSC3026 `busy`, foundry#41), MAS for auth, the chat gate module on. */
|
|
66
93
|
export declare function chatSynapseHomeserverYaml(input: ChatSynapseHomeserverInput): string;
|
|
@@ -76,6 +103,8 @@ export interface ChatMasConfigInput {
|
|
|
76
103
|
encryptionKey: string;
|
|
77
104
|
rsaKeyPem: string;
|
|
78
105
|
ecKeyPem: string;
|
|
106
|
+
/** #463: see {@link ChatSynapseHomeserverInput.serverName} -- the two must agree. */
|
|
107
|
+
serverName?: string;
|
|
79
108
|
}
|
|
80
109
|
/** MAS `config.yaml`: password login and registration off; the chat service is the one client, with admin. */
|
|
81
110
|
export declare function chatMasConfigYaml(input: ChatMasConfigInput): string;
|