@wtfalch/foundry 0.48.0 → 0.49.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/declare.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/migrations/0060_hosting_repos.sql +7 -0
- package/dist/org.d.ts +9 -1
- package/dist/org.js +27 -0
- package/dist/plan/github-release-hook.d.ts +81 -0
- package/dist/plan/github-release-hook.js +159 -0
- package/dist/plan/index.d.ts +2 -1
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/platform-diff.d.ts +59 -0
- package/dist/plan/platform-diff.js +98 -1
- package/dist/plan/platform-providers.d.ts +10 -0
- package/dist/plan/platform-providers.js +71 -1
- package/dist/store/postgres.js +13 -2
- package/dist/store/schema.d.ts +19 -0
- package/dist/store/schema.js +2 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, BlueGreenConfig, OrgBuiltinApp, OrgRefusal, OrgRefusalCode, WorkerAppConfig, EntryGate, EntryVolume, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, EnvHeldSecret, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceDatabase, OrgServiceSchema, ServicesConfig, SignInConfig, VersionPin, VersionPinPart, } from './org.js';
|
|
2
|
-
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
2
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, normalizeHostingRepos, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
3
3
|
export { refuseOrg } from './refuse-org.js';
|
|
4
4
|
export { workerAppHasBuild } from './worker-app-config.js';
|
|
5
5
|
export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, HeartbeatsProvider, } from './platform.js';
|
|
@@ -37,7 +37,7 @@ export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan
|
|
|
37
37
|
export type { BackupGatePorts } from './plan/index.js';
|
|
38
38
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
39
39
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
40
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, 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_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_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, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, stepEntryKey, } from './plan/index.js';
|
|
40
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, 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_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_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, GITHUB_RELEASE_HOOK_ORG, GITHUB_RELEASE_HOOK_PATH, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, stepEntryKey, } from './plan/index.js';
|
|
41
41
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
42
42
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
43
43
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
|
@@ -66,7 +66,7 @@ export { checkMailIdentityAudience } from './plan/index.js';
|
|
|
66
66
|
export type { MailAllowlistApp } from './plan/index.js';
|
|
67
67
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
68
68
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelOrgMetadata, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
69
|
-
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeChatDeps, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
69
|
+
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeChatDeps, PlatformUptimeDeps, PlatformGithubReleaseHookDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
70
70
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
71
71
|
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';
|
|
72
72
|
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';
|
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, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
1
|
+
export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, orgServiceDatabase, orgServiceDatabases, SFU_SLOT_MAX, VERSION_PIN_PARTS, isMainEntry, mainEntry, heldEnvRef, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, normalizeHostingRepos, validateMail, validateOwners, validateServices, validateCustomers, validateSignIn, validateVersionPin, validateSite, } from './org.js';
|
|
2
2
|
export { refuseOrg } from './refuse-org.js';
|
|
3
3
|
export { workerAppHasBuild } from './worker-app-config.js';
|
|
4
4
|
export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
@@ -22,7 +22,7 @@ export { DumpError, dumpNow } from './plan/index.js';
|
|
|
22
22
|
export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
|
|
23
23
|
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
24
24
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
25
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, 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_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_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, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, stepEntryKey, } from './plan/index.js';
|
|
25
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, 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_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_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, GITHUB_RELEASE_HOOK_ORG, GITHUB_RELEASE_HOOK_PATH, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, stepEntryKey, } from './plan/index.js';
|
|
26
26
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
27
27
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
28
28
|
export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgTarget, } from './plan/index.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the repos a company's apps may be built from (`HostingConfig.repos`), persisted.
|
|
2
|
+
-- Mirrors `src/store/schema.ts`'s `hostingRepos`. Additive, like 0048_hosting_gate_limit.sql: a new
|
|
3
|
+
-- column with a default, so a row written before it reads `[]` and an older reader ignores it. Safe
|
|
4
|
+
-- on live rows when the container starts.
|
|
5
|
+
|
|
6
|
+
alter table foundry_declared_orgs
|
|
7
|
+
add column if not exists hosting_repos jsonb not null default '[]'::jsonb;
|
package/dist/org.d.ts
CHANGED
|
@@ -253,6 +253,12 @@ export interface HostingConfig {
|
|
|
253
253
|
access?: AccessRule[];
|
|
254
254
|
/** How many gates the org may declare across all its entries. Only the platform operator sets it. Default: `3`. */
|
|
255
255
|
gateLimit: number;
|
|
256
|
+
/**
|
|
257
|
+
* The repos under `wtfalch` the org's apps may be built from, each `wtfalch/<name>`. Stored lowercase and
|
|
258
|
+
* unique ({@link normalizeHostingRepos}). Only a person who declares the company sets it; the planner
|
|
259
|
+
* and the diff ignore it. Default: `[]`.
|
|
260
|
+
*/
|
|
261
|
+
repos: string[];
|
|
256
262
|
}
|
|
257
263
|
/** The two Cloudflare Access rule kinds {@link HostingConfig.access} supports (#246). */
|
|
258
264
|
export type AccessRuleKind = 'bypass' | 'service-token';
|
|
@@ -283,7 +289,7 @@ export type AccessRule = {
|
|
|
283
289
|
/** The key of the entry that is today's one app: the Coolify app named `<slug>`. */
|
|
284
290
|
export declare const APP_ENTRY_KEY = "app";
|
|
285
291
|
/** Why a validator refused. `code` is stable and never reworded; `message` is for people. */
|
|
286
|
-
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'client_name_taken' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
292
|
+
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'client_name_taken' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
287
293
|
/**
|
|
288
294
|
* One refusal: a stable `code`, the dotted `path` into {@link DeclaredOrg} it is about
|
|
289
295
|
* (`hosting.entries.terminal.uses.auth.registration`) and the sentence for people.
|
|
@@ -626,6 +632,8 @@ export interface HostingNames {
|
|
|
626
632
|
/** Boule or Agora declares `blueGreen`: the blue/green key names are reserved for the entries too. */
|
|
627
633
|
builtinBlueGreen?: boolean;
|
|
628
634
|
}
|
|
635
|
+
/** `repos` as the store keeps them: lowercase, in the order given. Absent reads as none. */
|
|
636
|
+
export declare function normalizeHostingRepos(repos: readonly string[] | undefined): string[];
|
|
629
637
|
/** {@link validateHosting} with the code and path of the first refusal. */
|
|
630
638
|
export declare function refuseHosting(hosting: HostingConfig, names?: HostingNames): OrgRefusal | null;
|
|
631
639
|
/** `blueGreen`: steps ascending, each a whole number 1..100, the last 100; a hold of 1..1440 minutes. */
|
package/dist/org.js
CHANGED
|
@@ -123,8 +123,34 @@ const HOSTING_BUILD_PACKS = new Set([
|
|
|
123
123
|
export function validateHosting(hosting, names) {
|
|
124
124
|
return refuseHosting(hosting, names)?.message ?? null;
|
|
125
125
|
}
|
|
126
|
+
/** `repos` as the store keeps them: lowercase, in the order given. Absent reads as none. */
|
|
127
|
+
export function normalizeHostingRepos(repos) {
|
|
128
|
+
return (repos ?? []).map((repo) => repo.toLowerCase());
|
|
129
|
+
}
|
|
130
|
+
/** The rules for `hosting.repos`: a list of `wtfalch/<name>` (name 1-100 of letters, digits, `.`, `_`, `-`), unique ignoring case. */
|
|
131
|
+
function refuseHostingRepos(repos) {
|
|
132
|
+
const invalid = (message) => refusal('hosting_repo_invalid', 'hosting.repos', message);
|
|
133
|
+
if (repos === undefined)
|
|
134
|
+
return null;
|
|
135
|
+
if (!Array.isArray(repos))
|
|
136
|
+
return invalid('hosting.repos must be a list');
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
for (const repo of repos) {
|
|
139
|
+
if (typeof repo !== 'string' || !HOSTING_REPO.test(repo)) {
|
|
140
|
+
return invalid('hosting.repos must each be wtfalch/<name>, the name 1-100 letters, digits, dots, underscores or hyphens');
|
|
141
|
+
}
|
|
142
|
+
const lower = repo.toLowerCase();
|
|
143
|
+
if (seen.has(lower))
|
|
144
|
+
return invalid(`hosting.repos names ${lower} twice`);
|
|
145
|
+
seen.add(lower);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
126
149
|
/** {@link validateHosting} with the code and path of the first refusal. */
|
|
127
150
|
export function refuseHosting(hosting, names) {
|
|
151
|
+
const badRepos = refuseHostingRepos(hosting.repos);
|
|
152
|
+
if (badRepos)
|
|
153
|
+
return badRepos;
|
|
128
154
|
if (hosting.tunnel && !hosting.enabled) {
|
|
129
155
|
return refusal('hosting_tunnel_needs_hosting', 'hosting.tunnel', 'hosting.tunnel requires hosting.enabled');
|
|
130
156
|
}
|
|
@@ -381,6 +407,7 @@ const VOLUME_FORBIDDEN_ROOTS = [
|
|
|
381
407
|
];
|
|
382
408
|
const VOLUME_MOUNT_PATH_MAX = 255;
|
|
383
409
|
const REPO = /^wtfalch\/[A-Za-z0-9._-]+$/;
|
|
410
|
+
const HOSTING_REPO = /^wtfalch\/[A-Za-z0-9._-]{1,100}$/;
|
|
384
411
|
// The env names `pushAppEnv` and `pushDatabaseUrl` set themselves.
|
|
385
412
|
const FOUNDRY_ENV_NAME = /^(APP_URL|LOG_DIR|OWNER_USER_ID|AUTH_.*|DATABASE_URL.*)$/;
|
|
386
413
|
// Set only on the colors of a blueGreen entry; any other entry may declare them.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CoolifyPort } from './executor.js';
|
|
2
|
+
/** Every GitHub call is cut off after this long. */
|
|
3
|
+
export declare const GITHUB_API_TIMEOUT_MS = 20000;
|
|
4
|
+
/** A GitHub call that failed. The text is fixed: it never holds the token, the secret or a response body. */
|
|
5
|
+
export declare class GithubApiError extends Error {
|
|
6
|
+
readonly operation: string;
|
|
7
|
+
readonly status: number | null;
|
|
8
|
+
constructor(operation: string, status: number | null);
|
|
9
|
+
}
|
|
10
|
+
/** The shape of an organisation hook foundry reads. */
|
|
11
|
+
export interface GithubOrgHook {
|
|
12
|
+
id: number;
|
|
13
|
+
active: boolean;
|
|
14
|
+
events: string[];
|
|
15
|
+
config: {
|
|
16
|
+
url?: string;
|
|
17
|
+
content_type?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface GithubReleaseHookBody {
|
|
21
|
+
name: 'web';
|
|
22
|
+
active: true;
|
|
23
|
+
events: ['release'];
|
|
24
|
+
config: {
|
|
25
|
+
url: string;
|
|
26
|
+
content_type: 'json';
|
|
27
|
+
secret: string;
|
|
28
|
+
insecure_ssl: '0';
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface GithubHooksClient {
|
|
32
|
+
list(org: string): Promise<GithubOrgHook[]>;
|
|
33
|
+
create(org: string, body: GithubReleaseHookBody): Promise<GithubOrgHook>;
|
|
34
|
+
update(org: string, id: number, body: GithubReleaseHookBody): Promise<GithubOrgHook>;
|
|
35
|
+
}
|
|
36
|
+
/** The GitHub REST organisation-hooks calls, over `fetchImpl` (global fetch by default) with a timeout on each. */
|
|
37
|
+
export declare function createGithubHooksClient(options: {
|
|
38
|
+
token: string;
|
|
39
|
+
fetchImpl?: typeof fetch;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}): GithubHooksClient;
|
|
42
|
+
export interface PlatformGithubReleaseHookDeps {
|
|
43
|
+
coolify: Pick<CoolifyPort, 'listEnvs' | 'bulkUpsertEnvs'>;
|
|
44
|
+
heldKeys: {
|
|
45
|
+
resolve(ref: string): Promise<string | null>;
|
|
46
|
+
};
|
|
47
|
+
/** Global fetch by default; tests give their own. */
|
|
48
|
+
fetchImpl?: typeof fetch;
|
|
49
|
+
/** Told of the token and the secret, so the host can scrub them from logs and audit rows. */
|
|
50
|
+
onSecret?: (value: string) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What the planner needs: why the step is blocked (the token is not held), or `ready` (the token is
|
|
54
|
+
* held) with the hash of the hook's url and Archon's live secret when Archon already holds one. With no
|
|
55
|
+
* secret yet the step still plans: `platform:archon` mints it earlier in the same apply.
|
|
56
|
+
*/
|
|
57
|
+
export declare function githubReleaseHookPlanInputs(deps: PlatformGithubReleaseHookDeps, hostname: string, recorded: Readonly<Record<string, string>>): Promise<{
|
|
58
|
+
blocked?: string;
|
|
59
|
+
ready?: true;
|
|
60
|
+
hash?: string;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Makes the organisation's release webhook. Adopts the hook whose url is Archon's, else creates one;
|
|
64
|
+
* an adopted hook is always written again, because GitHub never returns its secret.
|
|
65
|
+
*
|
|
66
|
+
* The secret is made here when Archon's app does not hold one yet (`ensureSecret`, which keeps a live
|
|
67
|
+
* value), so the step can run before `platform:archon`; that step then keeps the same value.
|
|
68
|
+
*
|
|
69
|
+
* A failure halfway: a hook made and not recorded is adopted by its url on the next run; a hook
|
|
70
|
+
* written and not recorded is written again. The id and the hash are recorded together, after the write.
|
|
71
|
+
*/
|
|
72
|
+
export declare function provisionGithubReleaseHook(context: {
|
|
73
|
+
platform: {
|
|
74
|
+
archon: {
|
|
75
|
+
hostname: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
recordedIds(): Record<string, string>;
|
|
79
|
+
recordScopeId(key: string, id: string): void;
|
|
80
|
+
persist?(): Promise<void>;
|
|
81
|
+
}, deps: PlatformGithubReleaseHookDeps, ensureSecret: (appId: string) => Promise<string>): Promise<undefined>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { GITHUB_RELEASE_HOOK_COOLIFY_READ_FAILED, GITHUB_RELEASE_HOOK_ORG, GITHUB_RELEASE_HOOK_TOKEN_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, PLATFORM_ARCHON_KEY, PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF, PLATFORM_GITHUB_RELEASE_HOOK_HASH_SCOPE_KEY, PLATFORM_GITHUB_RELEASE_HOOK_ID_SCOPE_KEY, githubReleaseHookHash, githubReleaseHookUrl, } from './platform-diff.js';
|
|
2
|
+
/** Every GitHub call is cut off after this long. */
|
|
3
|
+
export const GITHUB_API_TIMEOUT_MS = 20_000;
|
|
4
|
+
const GITHUB_API = 'https://api.github.com';
|
|
5
|
+
const PAGE_SIZE = 100;
|
|
6
|
+
const MAX_PAGES = 20;
|
|
7
|
+
/** A GitHub call that failed. The text is fixed: it never holds the token, the secret or a response body. */
|
|
8
|
+
export class GithubApiError extends Error {
|
|
9
|
+
operation;
|
|
10
|
+
status;
|
|
11
|
+
constructor(operation, status) {
|
|
12
|
+
super(status === null
|
|
13
|
+
? `GitHub API ${operation} did not answer`
|
|
14
|
+
: `GitHub API ${operation} failed with status ${status}`);
|
|
15
|
+
this.operation = operation;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.name = 'GithubApiError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The GitHub REST organisation-hooks calls, over `fetchImpl` (global fetch by default) with a timeout on each. */
|
|
21
|
+
export function createGithubHooksClient(options) {
|
|
22
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
23
|
+
const timeoutMs = options.timeoutMs ?? GITHUB_API_TIMEOUT_MS;
|
|
24
|
+
const call = async (operation, method, path, body) => {
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await doFetch(`${GITHUB_API}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${options.token}`,
|
|
31
|
+
Accept: 'application/vnd.github+json',
|
|
32
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
33
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
34
|
+
},
|
|
35
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
36
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error(`github release hook: ${operation} did not answer (${error instanceof Error ? error.name : 'error'})`);
|
|
41
|
+
throw new GithubApiError(operation, null);
|
|
42
|
+
}
|
|
43
|
+
if (!response.ok)
|
|
44
|
+
throw new GithubApiError(operation, response.status);
|
|
45
|
+
try {
|
|
46
|
+
return await response.json();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`github release hook: ${operation} answered with unreadable JSON (${error instanceof Error ? error.name : 'error'})`);
|
|
50
|
+
throw new GithubApiError(operation, response.status);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const asHook = (operation, value) => {
|
|
54
|
+
const hook = value;
|
|
55
|
+
if (!hook || typeof hook.id !== 'number')
|
|
56
|
+
throw new GithubApiError(operation, 200);
|
|
57
|
+
return {
|
|
58
|
+
id: hook.id,
|
|
59
|
+
active: hook.active === true,
|
|
60
|
+
events: Array.isArray(hook.events) ? hook.events.map(String) : [],
|
|
61
|
+
config: {
|
|
62
|
+
...(typeof hook.config?.url === 'string' ? { url: hook.config.url } : {}),
|
|
63
|
+
...(typeof hook.config?.content_type === 'string'
|
|
64
|
+
? { content_type: hook.config.content_type }
|
|
65
|
+
: {}),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
async list(org) {
|
|
71
|
+
const hooks = [];
|
|
72
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
73
|
+
const value = await call('list hooks', 'GET', `/orgs/${org}/hooks?per_page=${PAGE_SIZE}&page=${page}`);
|
|
74
|
+
if (!Array.isArray(value))
|
|
75
|
+
throw new GithubApiError('list hooks', 200);
|
|
76
|
+
hooks.push(...value.map((v) => asHook('list hooks', v)));
|
|
77
|
+
if (value.length < PAGE_SIZE)
|
|
78
|
+
return hooks;
|
|
79
|
+
}
|
|
80
|
+
return hooks;
|
|
81
|
+
},
|
|
82
|
+
async create(org, body) {
|
|
83
|
+
return asHook('create hook', await call('create hook', 'POST', `/orgs/${org}/hooks`, body));
|
|
84
|
+
},
|
|
85
|
+
async update(org, id, body) {
|
|
86
|
+
return asHook('update hook', await call('update hook', 'PATCH', `/orgs/${org}/hooks/${id}`, body));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function liveWebhookSecret(deps, recorded) {
|
|
91
|
+
const appId = recorded[`coolify:app:${PLATFORM_ARCHON_KEY}`];
|
|
92
|
+
if (!appId)
|
|
93
|
+
return undefined;
|
|
94
|
+
const envs = await deps.coolify.listEnvs(appId);
|
|
95
|
+
const value = envs.find((e) => e.key === GITHUB_RELEASE_WEBHOOK_SECRET_ENV && !e.is_preview)?.value;
|
|
96
|
+
return value || undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What the planner needs: why the step is blocked (the token is not held), or `ready` (the token is
|
|
100
|
+
* held) with the hash of the hook's url and Archon's live secret when Archon already holds one. With no
|
|
101
|
+
* secret yet the step still plans: `platform:archon` mints it earlier in the same apply.
|
|
102
|
+
*/
|
|
103
|
+
export async function githubReleaseHookPlanInputs(deps, hostname, recorded) {
|
|
104
|
+
const token = await deps.heldKeys.resolve(PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF);
|
|
105
|
+
if (!token)
|
|
106
|
+
return { blocked: GITHUB_RELEASE_HOOK_TOKEN_MISSING };
|
|
107
|
+
let secret;
|
|
108
|
+
try {
|
|
109
|
+
secret = await liveWebhookSecret(deps, recorded);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error(`github release hook: could not read Archon's env from Coolify (${error instanceof Error ? error.name : 'error'})`);
|
|
113
|
+
return { blocked: GITHUB_RELEASE_HOOK_COOLIFY_READ_FAILED };
|
|
114
|
+
}
|
|
115
|
+
return secret
|
|
116
|
+
? { ready: true, hash: githubReleaseHookHash(githubReleaseHookUrl(hostname), secret) }
|
|
117
|
+
: { ready: true };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Makes the organisation's release webhook. Adopts the hook whose url is Archon's, else creates one;
|
|
121
|
+
* an adopted hook is always written again, because GitHub never returns its secret.
|
|
122
|
+
*
|
|
123
|
+
* The secret is made here when Archon's app does not hold one yet (`ensureSecret`, which keeps a live
|
|
124
|
+
* value), so the step can run before `platform:archon`; that step then keeps the same value.
|
|
125
|
+
*
|
|
126
|
+
* A failure halfway: a hook made and not recorded is adopted by its url on the next run; a hook
|
|
127
|
+
* written and not recorded is written again. The id and the hash are recorded together, after the write.
|
|
128
|
+
*/
|
|
129
|
+
export async function provisionGithubReleaseHook(context, deps, ensureSecret) {
|
|
130
|
+
const appId = context.recordedIds()[`coolify:app:${PLATFORM_ARCHON_KEY}`];
|
|
131
|
+
if (!appId) {
|
|
132
|
+
throw new Error("platform:github-release-hook: Archon's app is not recorded yet; platform:archon runs first");
|
|
133
|
+
}
|
|
134
|
+
const token = await deps.heldKeys.resolve(PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF);
|
|
135
|
+
if (!token)
|
|
136
|
+
throw new Error(`platform:github-release-hook: ${GITHUB_RELEASE_HOOK_TOKEN_MISSING}`);
|
|
137
|
+
const secret = await ensureSecret(appId);
|
|
138
|
+
deps.onSecret?.(secret);
|
|
139
|
+
deps.onSecret?.(token);
|
|
140
|
+
const url = githubReleaseHookUrl(context.platform.archon.hostname);
|
|
141
|
+
const body = {
|
|
142
|
+
name: 'web',
|
|
143
|
+
active: true,
|
|
144
|
+
events: ['release'],
|
|
145
|
+
config: { url, content_type: 'json', secret, insecure_ssl: '0' },
|
|
146
|
+
};
|
|
147
|
+
const client = createGithubHooksClient({
|
|
148
|
+
token,
|
|
149
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
150
|
+
});
|
|
151
|
+
const existing = (await client.list(GITHUB_RELEASE_HOOK_ORG)).find((h) => h.config.url === url);
|
|
152
|
+
const hook = existing
|
|
153
|
+
? await client.update(GITHUB_RELEASE_HOOK_ORG, existing.id, body)
|
|
154
|
+
: await client.create(GITHUB_RELEASE_HOOK_ORG, body);
|
|
155
|
+
context.recordScopeId(PLATFORM_GITHUB_RELEASE_HOOK_ID_SCOPE_KEY, String(hook.id));
|
|
156
|
+
context.recordScopeId(PLATFORM_GITHUB_RELEASE_HOOK_HASH_SCOPE_KEY, githubReleaseHookHash(url, secret));
|
|
157
|
+
await context.persist?.();
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { computePlanDetail, toPlan } from './diff.js';
|
|
|
5
5
|
export type { StepSystem } from './step-system.js';
|
|
6
6
|
export { stepSystemOf } from './step-system.js';
|
|
7
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';
|
|
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, GITHUB_RELEASE_HOOK_ORG, GITHUB_RELEASE_HOOK_PATH, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
9
9
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
10
10
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
11
11
|
export type { PlanOwnAuthOptions, PlanOwnFilesOptions, PlanOwnFilesR2Options, PlanOwnJobsOptions, } from './own-diff.js';
|
|
@@ -70,6 +70,7 @@ export { createEdgeAccess } from './edge-access.js';
|
|
|
70
70
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, EdgePlatformAccess, } from './edge-access.js';
|
|
71
71
|
export { createEdgeWorkers } from './edge-workers.js';
|
|
72
72
|
export type { EdgePlatformWorkers, EdgeWorkersOptions, PlatformWorkersPort, } from './edge-workers.js';
|
|
73
|
+
export type { PlatformGithubReleaseHookDeps } from './github-release-hook.js';
|
|
73
74
|
export type { PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeChatDeps, PlatformUptimeDeps, PlatformWorkersFactory, } from './uptime-worker.js';
|
|
74
75
|
export { createEdgeRateLimits } from './edge-ratelimits.js';
|
|
75
76
|
export type { EdgePlatformRateLimits, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, } from './edge-ratelimits.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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_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';
|
|
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, GITHUB_RELEASE_HOOK_ORG, GITHUB_RELEASE_HOOK_PATH, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, } from './platform-diff.js';
|
|
5
5
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
6
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
7
|
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu } from './own-diff.js';
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface LiveResource {
|
|
|
42
42
|
* see that function's own doc comment for why "always planned" broke
|
|
43
43
|
* `bootstrapPlatform`.
|
|
44
44
|
*/
|
|
45
|
-
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'edge' | 'heartbeats';
|
|
45
|
+
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'edge' | 'heartbeats' | 'github';
|
|
46
46
|
kind: string;
|
|
47
47
|
key: string;
|
|
48
48
|
config: Record<string, unknown>;
|
|
@@ -151,6 +151,13 @@ export declare const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = "archon
|
|
|
151
151
|
* values until someone redeploys by hand (#467).
|
|
152
152
|
*/
|
|
153
153
|
export declare const PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY = "archon_envs_deployed_hash";
|
|
154
|
+
/**
|
|
155
|
+
* The sha256 of the `GITHUB_RELEASE_WEBHOOK_SECRET` Archon last deployed with, recorded by
|
|
156
|
+
* `platform:archon` after its deploy (never the secret). `driftedSteps` plans `platform:archon` again
|
|
157
|
+
* while the live secret is missing or hashes differently: the release hook step writes the secret into
|
|
158
|
+
* Coolify and nothing else redeploys an applied Archon.
|
|
159
|
+
*/
|
|
160
|
+
export declare const PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY = "archon_webhook_secret_deployed_sha256";
|
|
154
161
|
/**
|
|
155
162
|
* The uuid of the deployment Archon's step triggered without waiting
|
|
156
163
|
* (`PlatformArchonDeps.runsInsideApp`), `pending` between the trigger and the
|
|
@@ -412,6 +419,29 @@ export declare const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = "platform:mas
|
|
|
412
419
|
* already holds the same panel's address.
|
|
413
420
|
*/
|
|
414
421
|
export declare const PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF = "platform:archon:coolify-read-token";
|
|
422
|
+
/**
|
|
423
|
+
* The held key (tenant `platform`) of a fine-grained GitHub token for the `wtfalch`
|
|
424
|
+
* organisation with Webhooks read and write. Foundry only reads it back; a person holds it in
|
|
425
|
+
* Archon, Shared, Keys. Until it is held, the `platform:github-release-hook` step is reported
|
|
426
|
+
* blocked and nothing else is.
|
|
427
|
+
*/
|
|
428
|
+
export declare const PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF = "platform:github:org-hooks-token";
|
|
429
|
+
/** Archon's env name for the secret GitHub signs release webhooks with. Minted once by `platform:archon`, never rotated. */
|
|
430
|
+
export declare const GITHUB_RELEASE_WEBHOOK_SECRET_ENV = "GITHUB_RELEASE_WEBHOOK_SECRET";
|
|
431
|
+
/** The GitHub organisation whose release webhook `platform:github-release-hook` makes. */
|
|
432
|
+
export declare const GITHUB_RELEASE_HOOK_ORG = "wtfalch";
|
|
433
|
+
/** Archon's route that takes release webhooks, under its public host. */
|
|
434
|
+
export declare const GITHUB_RELEASE_HOOK_PATH = "/api/github/releases";
|
|
435
|
+
/** The step key. */
|
|
436
|
+
export declare const PLATFORM_GITHUB_RELEASE_HOOK_KEY = "platform:github-release-hook";
|
|
437
|
+
/** Scope key: the id of the organisation hook the step made or adopted. */
|
|
438
|
+
export declare const PLATFORM_GITHUB_RELEASE_HOOK_ID_SCOPE_KEY = "github_release_hook_id";
|
|
439
|
+
/** Scope key: the hash of the hook's url and secret as last written. The secret itself is never recorded. */
|
|
440
|
+
export declare const PLATFORM_GITHUB_RELEASE_HOOK_HASH_SCOPE_KEY = "github_release_hook_hash";
|
|
441
|
+
/** Why the step is blocked while Archon's env cannot be read from Coolify. */
|
|
442
|
+
export declare const GITHUB_RELEASE_HOOK_COOLIFY_READ_FAILED = "the GitHub release webhook is blocked, not planned: could not read Archon's env from Coolify";
|
|
443
|
+
/** Why the step is blocked while the token is not held. */
|
|
444
|
+
export declare const GITHUB_RELEASE_HOOK_TOKEN_MISSING = "the GitHub release webhook is blocked, not planned: hold the GitHub organisation webhooks token in Archon, Shared, Keys";
|
|
415
445
|
/**
|
|
416
446
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
417
447
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -1248,6 +1278,14 @@ export interface PlanPlatformOptions {
|
|
|
1248
1278
|
pending: boolean;
|
|
1249
1279
|
blocked?: string;
|
|
1250
1280
|
};
|
|
1281
|
+
/** Why the organisation's release webhook step cannot run (the token is not held), or absent. */
|
|
1282
|
+
githubReleaseHook?: string;
|
|
1283
|
+
/**
|
|
1284
|
+
* The token is held: the step may plan, also in the apply that makes Archon.
|
|
1285
|
+
*/
|
|
1286
|
+
githubReleaseHookReady?: boolean;
|
|
1287
|
+
/** The hash of the hook's url and Archon's live webhook secret now (`githubReleaseHookHash`). Absent while Archon holds no secret. */
|
|
1288
|
+
githubReleaseHookHash?: string;
|
|
1251
1289
|
/** Why Archon's step must wait: the deployment it triggered is still running. Blocks even an applied step. */
|
|
1252
1290
|
archonDeployRunning?: string;
|
|
1253
1291
|
/**
|
|
@@ -1448,6 +1486,27 @@ export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = "in
|
|
|
1448
1486
|
* `''`) as soon as the mint is recorded, so one request is one rotation.
|
|
1449
1487
|
*/
|
|
1450
1488
|
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = "integrations_management_key_rotation_requested";
|
|
1489
|
+
/**
|
|
1490
|
+
* foundry#542: the uptime Worker, one `cloudflare:worker` step. Blocked, not
|
|
1491
|
+
* planned (rule 2), while no artifact is pinned, Archon is not applied, the
|
|
1492
|
+
* executor lacks its dependencies, a Worker or Access token of the name exists
|
|
1493
|
+
* that foundry did not make. A lost Worker token or Access pair is not a
|
|
1494
|
+
* block: the step is planned and repairs it (rule 4). Planned again when the
|
|
1495
|
+
* pinned tag or sha256, the Archon hostname (`ARCHON_BASE_URL`), the plain
|
|
1496
|
+
* text variables or the set of secrets that can be set changed (rule 3), or
|
|
1497
|
+
* when a half-made deploy never recorded its applied values; it mints nothing
|
|
1498
|
+
* that is already held. When the platform no longer enables the Worker, a
|
|
1499
|
+
* destructive step removes whatever of it is recorded (edge removes its KV
|
|
1500
|
+
* namespace with the script).
|
|
1501
|
+
*
|
|
1502
|
+
* The returned `blocked` lines are reported in `unmanaged`. A secret whose
|
|
1503
|
+
* source does not exist yet blocks nothing: the step still delivers
|
|
1504
|
+
* everything that exists, so it comes back in `notes`.
|
|
1505
|
+
*/
|
|
1506
|
+
/** The hash of a hook's url and secret. The planner and the step both compute it, and it is the only trace of the secret that is recorded. */
|
|
1507
|
+
export declare function githubReleaseHookHash(url: string, secret: string): string;
|
|
1508
|
+
/** The url GitHub delivers release webhooks to. */
|
|
1509
|
+
export declare function githubReleaseHookUrl(hostname: string): string;
|
|
1451
1510
|
/**
|
|
1452
1511
|
* #175: Archon (https://github.com/wtfalch/archon), the one shared console,
|
|
1453
1512
|
* built fresh at `platform.archon.hostname` (`archon.<apex>`) rather than the
|
|
@@ -190,6 +190,13 @@ export const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = 'archon_integra
|
|
|
190
190
|
* values until someone redeploys by hand (#467).
|
|
191
191
|
*/
|
|
192
192
|
export const PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY = 'archon_envs_deployed_hash';
|
|
193
|
+
/**
|
|
194
|
+
* The sha256 of the `GITHUB_RELEASE_WEBHOOK_SECRET` Archon last deployed with, recorded by
|
|
195
|
+
* `platform:archon` after its deploy (never the secret). `driftedSteps` plans `platform:archon` again
|
|
196
|
+
* while the live secret is missing or hashes differently: the release hook step writes the secret into
|
|
197
|
+
* Coolify and nothing else redeploys an applied Archon.
|
|
198
|
+
*/
|
|
199
|
+
export const PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY = 'archon_webhook_secret_deployed_sha256';
|
|
193
200
|
/**
|
|
194
201
|
* The uuid of the deployment Archon's step triggered without waiting
|
|
195
202
|
* (`PlatformArchonDeps.runsInsideApp`), `pending` between the trigger and the
|
|
@@ -464,6 +471,29 @@ export const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = 'platform:master:clou
|
|
|
464
471
|
* already holds the same panel's address.
|
|
465
472
|
*/
|
|
466
473
|
export const PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF = 'platform:archon:coolify-read-token';
|
|
474
|
+
/**
|
|
475
|
+
* The held key (tenant `platform`) of a fine-grained GitHub token for the `wtfalch`
|
|
476
|
+
* organisation with Webhooks read and write. Foundry only reads it back; a person holds it in
|
|
477
|
+
* Archon, Shared, Keys. Until it is held, the `platform:github-release-hook` step is reported
|
|
478
|
+
* blocked and nothing else is.
|
|
479
|
+
*/
|
|
480
|
+
export const PLATFORM_GITHUB_ORG_HOOKS_TOKEN_REF = 'platform:github:org-hooks-token';
|
|
481
|
+
/** Archon's env name for the secret GitHub signs release webhooks with. Minted once by `platform:archon`, never rotated. */
|
|
482
|
+
export const GITHUB_RELEASE_WEBHOOK_SECRET_ENV = 'GITHUB_RELEASE_WEBHOOK_SECRET';
|
|
483
|
+
/** The GitHub organisation whose release webhook `platform:github-release-hook` makes. */
|
|
484
|
+
export const GITHUB_RELEASE_HOOK_ORG = 'wtfalch';
|
|
485
|
+
/** Archon's route that takes release webhooks, under its public host. */
|
|
486
|
+
export const GITHUB_RELEASE_HOOK_PATH = '/api/github/releases';
|
|
487
|
+
/** The step key. */
|
|
488
|
+
export const PLATFORM_GITHUB_RELEASE_HOOK_KEY = 'platform:github-release-hook';
|
|
489
|
+
/** Scope key: the id of the organisation hook the step made or adopted. */
|
|
490
|
+
export const PLATFORM_GITHUB_RELEASE_HOOK_ID_SCOPE_KEY = 'github_release_hook_id';
|
|
491
|
+
/** Scope key: the hash of the hook's url and secret as last written. The secret itself is never recorded. */
|
|
492
|
+
export const PLATFORM_GITHUB_RELEASE_HOOK_HASH_SCOPE_KEY = 'github_release_hook_hash';
|
|
493
|
+
/** Why the step is blocked while Archon's env cannot be read from Coolify. */
|
|
494
|
+
export const GITHUB_RELEASE_HOOK_COOLIFY_READ_FAILED = "the GitHub release webhook is blocked, not planned: could not read Archon's env from Coolify";
|
|
495
|
+
/** Why the step is blocked while the token is not held. */
|
|
496
|
+
export const GITHUB_RELEASE_HOOK_TOKEN_MISSING = 'the GitHub release webhook is blocked, not planned: hold the GitHub organisation webhooks token in Archon, Shared, Keys';
|
|
467
497
|
/**
|
|
468
498
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
469
499
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -1441,8 +1471,25 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1441
1471
|
step: { description: archonBg.step.description, destroys: false, migrates: false },
|
|
1442
1472
|
});
|
|
1443
1473
|
}
|
|
1444
|
-
|
|
1474
|
+
let releaseHook;
|
|
1475
|
+
// The hook step makes the secret itself, so with the app recorded it goes before a last-placed
|
|
1476
|
+
// platform:archon (the host redeploys itself there and would stop before a later step). Without the
|
|
1477
|
+
// app it goes after platform:archon, which makes the app.
|
|
1478
|
+
if (archon.step && blocked.archonRunsInsideApp) {
|
|
1479
|
+
const hookFirst = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY)]);
|
|
1480
|
+
const hookStep = () => planGithubReleaseHook(platform, recordedIds, blocked.githubReleaseHook, blocked.githubReleaseHookReady ?? false, blocked.githubReleaseHookHash, true);
|
|
1481
|
+
releaseHook = hookStep();
|
|
1482
|
+
if (hookFirst && releaseHook.step)
|
|
1483
|
+
plannedSteps.push(releaseHook.step);
|
|
1445
1484
|
plannedSteps.push(archon.step);
|
|
1485
|
+
if (!hookFirst && releaseHook.step)
|
|
1486
|
+
plannedSteps.push(releaseHook.step);
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
releaseHook = planGithubReleaseHook(platform, recordedIds, blocked.githubReleaseHook, blocked.githubReleaseHookReady ?? false, blocked.githubReleaseHookHash, Boolean(archon.step));
|
|
1490
|
+
if (releaseHook.step)
|
|
1491
|
+
plannedSteps.push(releaseHook.step);
|
|
1492
|
+
}
|
|
1446
1493
|
// After Archon's own step: a repair that fails must not hold up the work before it.
|
|
1447
1494
|
const idTokenUserinfo = planArchonIdTokenUserinfo(platform, recordedIds, blocked.archonIdTokenUserinfo ?? false);
|
|
1448
1495
|
if (idTokenUserinfo)
|
|
@@ -1575,6 +1622,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1575
1622
|
unmanaged.push({ description: heartbeats.blocked, key: 'heartbeats' });
|
|
1576
1623
|
for (const description of uptime.blocked)
|
|
1577
1624
|
unmanaged.push({ description, key: 'uptime' });
|
|
1625
|
+
if (releaseHook.blocked) {
|
|
1626
|
+
unmanaged.push({ description: releaseHook.blocked, key: 'github-release-hook' });
|
|
1627
|
+
}
|
|
1578
1628
|
return {
|
|
1579
1629
|
plannedSteps: plannedSteps.map(withStepSystem),
|
|
1580
1630
|
unmanaged,
|
|
@@ -2932,6 +2982,53 @@ function planHeartbeats(platform, recordedIds, hcReady, executorMissing, apiKeyM
|
|
|
2932
2982
|
* source does not exist yet blocks nothing: the step still delivers
|
|
2933
2983
|
* everything that exists, so it comes back in `notes`.
|
|
2934
2984
|
*/
|
|
2985
|
+
/** The hash of a hook's url and secret. The planner and the step both compute it, and it is the only trace of the secret that is recorded. */
|
|
2986
|
+
export function githubReleaseHookHash(url, secret) {
|
|
2987
|
+
return createHash('sha256').update(`${url}\n${secret}`).digest('hex');
|
|
2988
|
+
}
|
|
2989
|
+
/** The url GitHub delivers release webhooks to. */
|
|
2990
|
+
export function githubReleaseHookUrl(hostname) {
|
|
2991
|
+
return `https://${hostname}${GITHUB_RELEASE_HOOK_PATH}`;
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* The organisation's release webhook, once the token is held and Archon is applied or planned in this
|
|
2995
|
+
* same plan (`platform:archon` runs first and mints the secret). Planned when no hook is recorded, or the
|
|
2996
|
+
* recorded hash of (url, secret) differs from the one now.
|
|
2997
|
+
* Blocked, not planned, while the token is not held; nothing else is held back by that.
|
|
2998
|
+
*/
|
|
2999
|
+
function planGithubReleaseHook(platform, recordedIds, executorBlocked, tokenHeld, currentHash, archonPlanned) {
|
|
3000
|
+
if (!platform.archon.enabled)
|
|
3001
|
+
return { step: null, blocked: null };
|
|
3002
|
+
if (!recordedIds[resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY)] && !archonPlanned) {
|
|
3003
|
+
return { step: null, blocked: null };
|
|
3004
|
+
}
|
|
3005
|
+
if (executorBlocked)
|
|
3006
|
+
return { step: null, blocked: executorBlocked };
|
|
3007
|
+
if (!tokenHeld)
|
|
3008
|
+
return { step: null, blocked: null };
|
|
3009
|
+
if (currentHash &&
|
|
3010
|
+
recordedIds[PLATFORM_GITHUB_RELEASE_HOOK_ID_SCOPE_KEY] &&
|
|
3011
|
+
recordedIds[PLATFORM_GITHUB_RELEASE_HOOK_HASH_SCOPE_KEY] === currentHash) {
|
|
3012
|
+
return { step: null, blocked: null };
|
|
3013
|
+
}
|
|
3014
|
+
const url = githubReleaseHookUrl(platform.archon.hostname);
|
|
3015
|
+
return {
|
|
3016
|
+
step: {
|
|
3017
|
+
action: {
|
|
3018
|
+
system: 'github',
|
|
3019
|
+
kind: 'release-hook',
|
|
3020
|
+
key: PLATFORM_GITHUB_RELEASE_HOOK_KEY,
|
|
3021
|
+
desired: { org: GITHUB_RELEASE_HOOK_ORG, url, events: ['release'] },
|
|
3022
|
+
},
|
|
3023
|
+
step: {
|
|
3024
|
+
description: `make the ${GITHUB_RELEASE_HOOK_ORG} organisation's release webhook to ${url}`,
|
|
3025
|
+
destroys: false,
|
|
3026
|
+
migrates: false,
|
|
3027
|
+
},
|
|
3028
|
+
},
|
|
3029
|
+
blocked: null,
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
2935
3032
|
function planUptime(platform, recordedIds, executorBlocked, heldOptional, archonApplied, uptimeHeld = {
|
|
2936
3033
|
token: true,
|
|
2937
3034
|
access: true,
|
|
@@ -21,6 +21,7 @@ import { type BrokerDeployRecord, PLATFORM_ZITADEL_ORG_SCOPE_KEY } from './platf
|
|
|
21
21
|
import type { PlatformOwnerInput } from './platform-org.js';
|
|
22
22
|
import type { SigninDbBackupPort } from './signin-db-backup.js';
|
|
23
23
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
24
|
+
import { type PlatformGithubReleaseHookDeps } from './github-release-hook.js';
|
|
24
25
|
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
25
26
|
import { type PlatformUptimeDeps } from './uptime-worker.js';
|
|
26
27
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -324,6 +325,11 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
324
325
|
* unset, the step is reported blocked.
|
|
325
326
|
*/
|
|
326
327
|
oikosIngress?: PlatformOikosIngressDeps;
|
|
328
|
+
/**
|
|
329
|
+
* The `github:release-hook` step. Optional like `auth`; without it the step is not planned and
|
|
330
|
+
* nothing is reported.
|
|
331
|
+
*/
|
|
332
|
+
githubReleaseHook?: PlatformGithubReleaseHookDeps;
|
|
327
333
|
}
|
|
328
334
|
export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
|
|
329
335
|
private readonly cloudflare;
|
|
@@ -345,6 +351,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
345
351
|
private readonly uptime?;
|
|
346
352
|
private readonly accessProvider?;
|
|
347
353
|
private readonly oikosIngress?;
|
|
354
|
+
private readonly githubReleaseHook?;
|
|
348
355
|
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
|
|
349
356
|
private readonly platformPorts;
|
|
350
357
|
/** The port placed on the platform's destination, made once per port so a server is listed once. */
|
|
@@ -388,6 +395,9 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
388
395
|
pending: boolean;
|
|
389
396
|
blocked?: string;
|
|
390
397
|
};
|
|
398
|
+
githubReleaseHook?: string;
|
|
399
|
+
githubReleaseHookReady?: boolean;
|
|
400
|
+
githubReleaseHookHash?: string;
|
|
391
401
|
archonRunsInsideApp?: boolean;
|
|
392
402
|
archonDeployRunning?: string;
|
|
393
403
|
archonIdTokenUserinfo?: boolean;
|
|
@@ -28,7 +28,7 @@ import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAI
|
|
|
28
28
|
import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
|
|
29
29
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
30
30
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
31
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
31
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_GITHUB_RELEASE_HOOK_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
32
32
|
import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
|
|
33
33
|
import { LOGIN_HOST_BINDINGS_ENV } from './sign-in-bindings.js';
|
|
34
34
|
import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
|
|
@@ -36,6 +36,7 @@ import { SIGN_IN_BASE_PATH, SIGN_IN_BUILD_ARG_ENV, SIGN_IN_LOGIN_APP_SCOPE_KEY,
|
|
|
36
36
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
37
37
|
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
38
38
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
39
|
+
import { githubReleaseHookPlanInputs, provisionGithubReleaseHook, } from './github-release-hook.js';
|
|
39
40
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
40
41
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
41
42
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, waitForSystemApi, } from './zitadel-system-api.js';
|
|
@@ -144,6 +145,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
144
145
|
uptime;
|
|
145
146
|
accessProvider;
|
|
146
147
|
oikosIngress;
|
|
148
|
+
githubReleaseHook;
|
|
147
149
|
constructor(cloudflare, declaredOrgStore, options = {}) {
|
|
148
150
|
this.cloudflare = cloudflare;
|
|
149
151
|
this.declaredOrgStore = declaredOrgStore;
|
|
@@ -164,6 +166,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
164
166
|
this.uptime = options.uptime;
|
|
165
167
|
this.accessProvider = options.accessProvider;
|
|
166
168
|
this.oikosIngress = options.oikosIngress;
|
|
169
|
+
this.githubReleaseHook = options.githubReleaseHook;
|
|
167
170
|
}
|
|
168
171
|
platformPorts = new WeakMap();
|
|
169
172
|
/** The port placed on the platform's destination, made once per port so a server is listed once. */
|
|
@@ -470,6 +473,15 @@ export class FoundryPlatformStepExecutor {
|
|
|
470
473
|
blocked.heartbeatsApiKeyMissing = true;
|
|
471
474
|
}
|
|
472
475
|
await this.uptimeBlocked(platform, recordedIds, blocked, owner);
|
|
476
|
+
if (this.githubReleaseHook && platform.archon.enabled) {
|
|
477
|
+
const inputs = await githubReleaseHookPlanInputs(this.githubReleaseHook, platform.archon.hostname, recordedIds);
|
|
478
|
+
if (inputs.blocked)
|
|
479
|
+
blocked.githubReleaseHook = inputs.blocked;
|
|
480
|
+
if (inputs.ready)
|
|
481
|
+
blocked.githubReleaseHookReady = true;
|
|
482
|
+
if (inputs.hash)
|
|
483
|
+
blocked.githubReleaseHookHash = inputs.hash;
|
|
484
|
+
}
|
|
473
485
|
// #604: only once edge and sign-in are applied (the planner reports
|
|
474
486
|
// that case itself) and no provider is recorded yet.
|
|
475
487
|
if (platform.edge.enabled &&
|
|
@@ -693,12 +705,15 @@ export class FoundryPlatformStepExecutor {
|
|
|
693
705
|
const redeployFailed = this.archon.runsInsideApp === true &&
|
|
694
706
|
Boolean(deployment) &&
|
|
695
707
|
(await archonDeploymentState(this.archon.coolify, deployment)) === 'failed';
|
|
708
|
+
const webhookSecretDrifted = !rollingUpdateDrifted &&
|
|
709
|
+
(await archonWebhookSecretDrifted(this.archon.coolify, recordedIds[`coolify:app:${PLATFORM_ARCHON_KEY}`], recordedIds));
|
|
696
710
|
if (accessDrifted ||
|
|
697
711
|
brokerDrifted ||
|
|
698
712
|
authOrgDrifted ||
|
|
699
713
|
authzDrifted ||
|
|
700
714
|
bridgeDrifted ||
|
|
701
715
|
rollingUpdateDrifted ||
|
|
716
|
+
webhookSecretDrifted ||
|
|
702
717
|
redeployFailed)
|
|
703
718
|
result.archon = true;
|
|
704
719
|
}
|
|
@@ -923,6 +938,28 @@ export class FoundryPlatformStepExecutor {
|
|
|
923
938
|
}
|
|
924
939
|
return provisionPlatformAccessProvider(context, this.accessProvider);
|
|
925
940
|
}
|
|
941
|
+
if (action.system === 'github' &&
|
|
942
|
+
action.kind === 'release-hook' &&
|
|
943
|
+
action.key === PLATFORM_GITHUB_RELEASE_HOOK_KEY) {
|
|
944
|
+
if (!this.githubReleaseHook) {
|
|
945
|
+
throw new Error('platform:github-release-hook needs FoundryPlatformStepExecutor to be constructed with its github release hook dependencies');
|
|
946
|
+
}
|
|
947
|
+
const hookDeps = this.githubReleaseHook;
|
|
948
|
+
return provisionGithubReleaseHook(context, hookDeps, async (appId) => {
|
|
949
|
+
const minted = await ensureGeneratedEnv(hookDeps.coolify, appId, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, () => randomBytes(32).toString('hex'));
|
|
950
|
+
if (minted) {
|
|
951
|
+
await hookDeps.coolify.bulkUpsertEnvs(appId, [
|
|
952
|
+
{ key: GITHUB_RELEASE_WEBHOOK_SECRET_ENV, value: minted },
|
|
953
|
+
]);
|
|
954
|
+
return minted;
|
|
955
|
+
}
|
|
956
|
+
const live = await currentEnvValue(hookDeps.coolify, appId, GITHUB_RELEASE_WEBHOOK_SECRET_ENV);
|
|
957
|
+
if (!live) {
|
|
958
|
+
throw new Error(`platform:github-release-hook: Archon lost its ${GITHUB_RELEASE_WEBHOOK_SECRET_ENV} between two reads`);
|
|
959
|
+
}
|
|
960
|
+
return live;
|
|
961
|
+
});
|
|
962
|
+
}
|
|
926
963
|
if (action.system === 'heartbeats' && action.kind === 'reconcile') {
|
|
927
964
|
if (!this.heartbeats) {
|
|
928
965
|
throw new Error('heartbeats:reconcile needs FoundryPlatformStepExecutor to be constructed with its heartbeats dependencies (#540)');
|
|
@@ -4458,6 +4495,10 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4458
4495
|
const cookieSecret = await ensureGeneratedEnv(deps.coolify, appId, 'AUTH_COOKIE_SECRET', () => randomBytes(32).toString('base64'));
|
|
4459
4496
|
if (cookieSecret)
|
|
4460
4497
|
envs.push({ key: 'AUTH_COOKIE_SECRET', value: cookieSecret });
|
|
4498
|
+
// The secret of the organisation's release webhook: minted once, never rotated, a hand-set value kept.
|
|
4499
|
+
const webhookSecret = await ensureGeneratedEnv(deps.coolify, appId, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, () => randomBytes(32).toString('hex'));
|
|
4500
|
+
if (webhookSecret)
|
|
4501
|
+
envs.push({ key: GITHUB_RELEASE_WEBHOOK_SECRET_ENV, value: webhookSecret });
|
|
4461
4502
|
const runtimeUrl = await ensureArchonRuntimeUrl(deps.coolify, appId, db.internal_db_url);
|
|
4462
4503
|
if (runtimeUrl)
|
|
4463
4504
|
envs.push({ key: 'DATABASE_URL', value: runtimeUrl });
|
|
@@ -4494,6 +4535,12 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4494
4535
|
if (effectiveCookieSecret) {
|
|
4495
4536
|
archonEnvHashOverrides.set('AUTH_COOKIE_SECRET', effectiveCookieSecret);
|
|
4496
4537
|
}
|
|
4538
|
+
const effectiveWebhookSecret = webhookSecret ??
|
|
4539
|
+
(await currentEnvValue(deps.coolify, appId, GITHUB_RELEASE_WEBHOOK_SECRET_ENV));
|
|
4540
|
+
if (effectiveWebhookSecret) {
|
|
4541
|
+
archonEnvHashOverrides.set(GITHUB_RELEASE_WEBHOOK_SECRET_ENV, effectiveWebhookSecret);
|
|
4542
|
+
runtime.onSecret?.(effectiveWebhookSecret);
|
|
4543
|
+
}
|
|
4497
4544
|
const effectiveRuntimeUrl = runtimeUrl ?? (await currentEnvValue(deps.coolify, appId, 'DATABASE_URL'));
|
|
4498
4545
|
if (effectiveRuntimeUrl) {
|
|
4499
4546
|
archonEnvHashOverrides.set('DATABASE_URL', effectiveRuntimeUrl);
|
|
@@ -4941,6 +4988,9 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4941
4988
|
// when no deploy was needed this run, left exactly as it already was --
|
|
4942
4989
|
// `envsHashChanged` false means it already equals `envsHash`).
|
|
4943
4990
|
await record(PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, envsHash);
|
|
4991
|
+
if (effectiveWebhookSecret) {
|
|
4992
|
+
await record(PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, createHash('sha256').update(effectiveWebhookSecret).digest('hex'));
|
|
4993
|
+
}
|
|
4944
4994
|
// Last, after every write the applied values depend on, so a run that failed
|
|
4945
4995
|
// halfway plans again.
|
|
4946
4996
|
await record(PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, hostname);
|
|
@@ -5726,6 +5776,26 @@ async function authAppsNotRunning(coolify, recordedIds) {
|
|
|
5726
5776
|
}
|
|
5727
5777
|
return false;
|
|
5728
5778
|
}
|
|
5779
|
+
/**
|
|
5780
|
+
* Archon's live release webhook secret is missing, or is not the one its last deploy recorded
|
|
5781
|
+
* (`PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY`): the release hook step wrote it after Archon's
|
|
5782
|
+
* last deploy, so the running Archon does not have it. A Coolify read error counts as not drifted.
|
|
5783
|
+
*/
|
|
5784
|
+
async function archonWebhookSecretDrifted(coolify, appId, recorded) {
|
|
5785
|
+
if (!appId)
|
|
5786
|
+
return false;
|
|
5787
|
+
try {
|
|
5788
|
+
const live = await currentEnvValue(coolify, appId, GITHUB_RELEASE_WEBHOOK_SECRET_ENV);
|
|
5789
|
+
if (!live)
|
|
5790
|
+
return true;
|
|
5791
|
+
return (recorded[PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY] !==
|
|
5792
|
+
createHash('sha256').update(live).digest('hex'));
|
|
5793
|
+
}
|
|
5794
|
+
catch (error) {
|
|
5795
|
+
console.error(`platform:archon: could not read Archon's webhook secret from Coolify (${error instanceof Error ? error.name : 'error'})`);
|
|
5796
|
+
return false;
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5729
5799
|
async function archonRollingUpdateDrifted(coolify, appId) {
|
|
5730
5800
|
if (!appId)
|
|
5731
5801
|
return false;
|
package/dist/store/postgres.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, asc, desc, eq, inArray, isNull, ne, sql } from 'drizzle-orm';
|
|
2
|
-
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, refuseBuiltinBlueGreen, refuseCustomers, refuseHosting, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, normalizeHostingRepos, refuseBuiltinBlueGreen, refuseCustomers, refuseHosting, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
|
|
3
3
|
import { hostingNames } from '../plan/locked-hostnames.js';
|
|
4
4
|
import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
|
|
5
5
|
import { validatePlatform } from '../platform.js';
|
|
@@ -176,6 +176,7 @@ function rowToOrg(row) {
|
|
|
176
176
|
issuerApps: row.hostingIssuerApps,
|
|
177
177
|
entries: row.hostingEntries.map((entry) => storedEntryToEntry(entry, row.hostingUsesDomain)),
|
|
178
178
|
gateLimit: row.hostingGateLimit,
|
|
179
|
+
repos: row.hostingRepos,
|
|
179
180
|
},
|
|
180
181
|
sso: { enabled: row.ssoEnabled },
|
|
181
182
|
guests: { enabled: row.guestsEnabled },
|
|
@@ -241,6 +242,7 @@ function orgToRow(org) {
|
|
|
241
242
|
hostingIssuerApps: org.hosting.issuerApps,
|
|
242
243
|
hostingEntries: org.hosting.entries,
|
|
243
244
|
hostingGateLimit: org.hosting.gateLimit,
|
|
245
|
+
hostingRepos: normalizeHostingRepos(org.hosting.repos),
|
|
244
246
|
ssoEnabled: org.sso.enabled,
|
|
245
247
|
guestsEnabled: org.guests.enabled,
|
|
246
248
|
soloEnabled: org.solo.enabled,
|
|
@@ -569,7 +571,16 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
569
571
|
// just be re-litigating a write this update isn't making.
|
|
570
572
|
if (patch.slug !== undefined && patch.slug !== current.slug)
|
|
571
573
|
requireValidSlug(patch.slug);
|
|
572
|
-
const
|
|
574
|
+
const merged = {
|
|
575
|
+
...current,
|
|
576
|
+
...patch,
|
|
577
|
+
id: current.id,
|
|
578
|
+
version: current.version,
|
|
579
|
+
};
|
|
580
|
+
const next = {
|
|
581
|
+
...merged,
|
|
582
|
+
hosting: { ...merged.hosting, repos: normalizeHostingRepos(merged.hosting.repos) },
|
|
583
|
+
};
|
|
573
584
|
requireValidDomain(next.domain);
|
|
574
585
|
requireValidLimits(next);
|
|
575
586
|
requireValidBuyer(next);
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -692,6 +692,25 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
692
692
|
identity: undefined;
|
|
693
693
|
generated: undefined;
|
|
694
694
|
}, {}, {}>;
|
|
695
|
+
hostingRepos: import("drizzle-orm/pg-core").PgColumn<{
|
|
696
|
+
name: "hosting_repos";
|
|
697
|
+
tableName: "foundry_declared_orgs";
|
|
698
|
+
dataType: "json";
|
|
699
|
+
columnType: "PgJsonb";
|
|
700
|
+
data: string[];
|
|
701
|
+
driverParam: unknown;
|
|
702
|
+
notNull: true;
|
|
703
|
+
hasDefault: true;
|
|
704
|
+
isPrimaryKey: false;
|
|
705
|
+
isAutoincrement: false;
|
|
706
|
+
hasRuntimeDefault: false;
|
|
707
|
+
enumValues: undefined;
|
|
708
|
+
baseColumn: never;
|
|
709
|
+
identity: undefined;
|
|
710
|
+
generated: undefined;
|
|
711
|
+
}, {}, {
|
|
712
|
+
$type: string[];
|
|
713
|
+
}>;
|
|
695
714
|
ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
696
715
|
name: "sso_enabled";
|
|
697
716
|
tableName: "foundry_declared_orgs";
|
package/dist/store/schema.js
CHANGED
|
@@ -74,6 +74,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
74
74
|
hostingEntries: jsonb('hosting_entries').notNull().default([]).$type(),
|
|
75
75
|
// HostingConfig.gateLimit (org.ts, #603); migrations/0048. Default 3 is DEFAULT_GATE_LIMIT.
|
|
76
76
|
hostingGateLimit: integer('hosting_gate_limit').notNull().default(3),
|
|
77
|
+
// HostingConfig.repos (org.ts); migrations/0060. Lowercase `wtfalch/<name>` strings.
|
|
78
|
+
hostingRepos: jsonb('hosting_repos').notNull().default([]).$type(),
|
|
77
79
|
ssoEnabled: boolean('sso_enabled').notNull().default(false),
|
|
78
80
|
guestsEnabled: boolean('guests_enabled').notNull().default(false),
|
|
79
81
|
soloEnabled: boolean('solo_enabled').notNull().default(false),
|