@wtfalch/foundry 0.47.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.
Files changed (51) hide show
  1. package/dist/bootstrap/declare.js +1 -0
  2. package/dist/index.d.ts +6 -4
  3. package/dist/index.js +3 -2
  4. package/dist/migrations/0059_org_builtin_blue_green.sql +9 -0
  5. package/dist/migrations/0060_hosting_repos.sql +7 -0
  6. package/dist/org.d.ts +25 -1
  7. package/dist/org.js +51 -3
  8. package/dist/plan/archon-blue-green.js +15 -1
  9. package/dist/plan/blue-green-apply.d.ts +35 -0
  10. package/dist/plan/blue-green-apply.js +167 -16
  11. package/dist/plan/blue-green.d.ts +97 -2
  12. package/dist/plan/blue-green.js +214 -13
  13. package/dist/plan/database-dump.d.ts +37 -0
  14. package/dist/plan/database-dump.js +101 -0
  15. package/dist/plan/diff.js +3 -0
  16. package/dist/plan/executor.d.ts +48 -0
  17. package/dist/plan/executor.js +79 -20
  18. package/dist/plan/github-release-hook.d.ts +81 -0
  19. package/dist/plan/github-release-hook.js +159 -0
  20. package/dist/plan/index.d.ts +4 -1
  21. package/dist/plan/index.js +5 -1
  22. package/dist/plan/live.d.ts +1 -1
  23. package/dist/plan/locked-hostnames.d.ts +1 -0
  24. package/dist/plan/locked-hostnames.js +1 -0
  25. package/dist/plan/plan-apply.js +16 -7
  26. package/dist/plan/platform-diff.d.ts +59 -0
  27. package/dist/plan/platform-diff.js +100 -2
  28. package/dist/plan/platform-providers.d.ts +10 -0
  29. package/dist/plan/platform-providers.js +73 -1
  30. package/dist/plan/proxy-labels.d.ts +13 -1
  31. package/dist/plan/proxy-labels.js +26 -3
  32. package/dist/plan/release-host.d.ts +1 -1
  33. package/dist/plan/release-host.js +36 -14
  34. package/dist/plan/stage-env.d.ts +38 -0
  35. package/dist/plan/stage-env.js +52 -0
  36. package/dist/plan/stage-guards.d.ts +104 -0
  37. package/dist/plan/stage-guards.js +149 -0
  38. package/dist/plan/stage-isolation.d.ts +57 -0
  39. package/dist/plan/stage-isolation.js +248 -0
  40. package/dist/refuse-org.d.ts +1 -1
  41. package/dist/refuse-org.js +5 -3
  42. package/dist/release/decide.d.ts +5 -0
  43. package/dist/release/decide.js +10 -6
  44. package/dist/release/health.d.ts +6 -1
  45. package/dist/release/health.js +10 -2
  46. package/dist/release/run.d.ts +2 -0
  47. package/dist/release/run.js +2 -1
  48. package/dist/store/postgres.js +32 -2
  49. package/dist/store/schema.d.ts +58 -1
  50. package/dist/store/schema.js +5 -0
  51. package/package.json +1 -1
@@ -121,6 +121,7 @@ export function declaredCompanyOrg(company, owner) {
121
121
  issuerApps: [],
122
122
  entries: [],
123
123
  gateLimit: DEFAULT_GATE_LIMIT,
124
+ repos: [],
124
125
  },
125
126
  sso: { enabled: false },
126
127
  guests: { enabled: false },
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, 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';
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, 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';
@@ -29,13 +29,15 @@ export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAudit
29
29
  export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, MailRoute, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, UnconfirmedUpgrade, } from './plan/index.js';
30
30
  export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
31
31
  export { AdoptRefusedError } from './plan/index.js';
32
+ export { DumpError, dumpNow } from './plan/index.js';
33
+ export type { DatabaseDump } from './plan/index.js';
32
34
  export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
33
35
  export type { OrgPostgresMigrationConfirmation } from './plan/index.js';
34
36
  export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
35
37
  export type { BackupGatePorts } from './plan/index.js';
36
38
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
37
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';
38
- 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';
39
41
  export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
40
42
  export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
41
43
  export type { PlatformOrgClientFactories } from './plan/index.js';
@@ -64,7 +66,7 @@ export { checkMailIdentityAudience } from './plan/index.js';
64
66
  export type { MailAllowlistApp } from './plan/index.js';
65
67
  export { checkMailAppsAllowlist } from './plan/index.js';
66
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';
67
- 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';
68
70
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
69
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';
70
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';
@@ -18,10 +18,11 @@ export { createPostgresReleaseStore } from './store/postgres.js';
18
18
  export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, foundryRequestedChanges, } from './store/schema.js';
19
19
  export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
20
20
  export { AdoptRefusedError } from './plan/index.js';
21
+ export { DumpError, dumpNow } from './plan/index.js';
21
22
  export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
22
23
  export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
23
24
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
24
- 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';
25
26
  export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
26
27
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
27
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,9 @@
1
+ -- @wtfalch/foundry: `boule_blue_green` and `agora_blue_green` are `org.boule.blueGreen` and
2
+ -- `org.agora.blueGreen` as JSON ({ steps?, stepHoldMinutes? }); null (the default) keeps the company's
3
+ -- Boule or Agora on one app. Copies 0056_platform_archon_blue_green.sql: nullable columns added with
4
+ -- `if not exists`, so it is safe to run at container start on live rows.
5
+ -- Mirrors src/store/schema.ts's `declaredOrgs`.
6
+
7
+ alter table foundry_declared_orgs
8
+ add column if not exists boule_blue_green jsonb,
9
+ add column if not exists agora_blue_green jsonb;
@@ -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_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' | '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.
@@ -385,6 +391,14 @@ export interface HostingEntry {
385
391
  */
386
392
  blueGreen?: BlueGreenConfig;
387
393
  }
394
+ /**
395
+ * `boule` and `agora` on a company: the built-in apps foundry runs for every company (`<slug>-boule`,
396
+ * `<slug>-agora`). Only blue/green is declared here; absent means off. Optional so every existing literal and
397
+ * fixture compiles unchanged.
398
+ */
399
+ export interface OrgBuiltinApp {
400
+ blueGreen?: BlueGreenConfig;
401
+ }
388
402
  /** The traffic steps (percent to the new color) and the hold at each step. Both default in `plan/blue-green.ts`. */
389
403
  export interface BlueGreenConfig {
390
404
  /** Ascending, each 1..100, ending at 100. */
@@ -615,11 +629,17 @@ export interface HostingNames {
615
629
  locked: ReadonlySet<string>;
616
630
  slug?: string;
617
631
  siteOwnsApex?: boolean;
632
+ /** Boule or Agora declares `blueGreen`: the blue/green key names are reserved for the entries too. */
633
+ builtinBlueGreen?: boolean;
618
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[];
619
637
  /** {@link validateHosting} with the code and path of the first refusal. */
620
638
  export declare function refuseHosting(hosting: HostingConfig, names?: HostingNames): OrgRefusal | null;
621
639
  /** `blueGreen`: steps ascending, each a whole number 1..100, the last 100; a hold of 1..1440 minutes. */
622
640
  export declare function validateBlueGreen(value: unknown, at: string): string | null;
641
+ /** {@link refuseBlueGreen} for the `blueGreen` of the company's Boule and Agora; the same codes. */
642
+ export declare function refuseBuiltinBlueGreen(org: Pick<DeclaredOrg, 'boule' | 'agora'>): OrgRefusal | null;
623
643
  export declare function validateDomain(domain: string): string | null;
624
644
  /**
625
645
  * A broker entry name foundry writes itself: Archon's, and any company's Boule.
@@ -1035,6 +1055,10 @@ export interface DeclaredOrg {
1035
1055
  services?: ServicesConfig;
1036
1056
  /** Absent means `{ inApp: false }`. Optional so every existing literal and fixture compiles unchanged. */
1037
1057
  signIn?: SignInConfig;
1058
+ /** The company's Boule. Absent means no blue/green. */
1059
+ boule?: OrgBuiltinApp;
1060
+ /** The company's Agora, a static app: its release moves on hard fails and time only. Absent means no blue/green. */
1061
+ agora?: OrgBuiltinApp;
1038
1062
  /** Absent means `{ enabled: false, signUp: false }`. Optional so every existing literal and fixture compiles unchanged. */
1039
1063
  customers?: CustomersConfig;
1040
1064
  /** Parts of this company held on their own version. Absent or null follows the platform. Ignored for the owner company. */
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
  }
@@ -156,13 +182,14 @@ export function refuseHosting(hosting, names) {
156
182
  if (hosting.enabled && hosting.entries.length === 0) {
157
183
  return refusal('hosting_needs_entry', 'hosting.entries', 'hosting.enabled requires at least one entry');
158
184
  }
185
+ const reserveKeys = names?.builtinBlueGreen === true || hosting.entries.some((e) => e.blueGreen !== undefined);
159
186
  const seenKeys = new Set();
160
187
  for (const entry of hosting.entries) {
161
188
  if (seenKeys.has(entry.key)) {
162
189
  return refusal('entry_key_not_unique', `hosting.entries.${entry.key}.key`, `hosting.entries key '${entry.key}' is not unique`);
163
190
  }
164
191
  seenKeys.add(entry.key);
165
- const invalid = validateHostingEntry(entry, hosting.enabled);
192
+ const invalid = validateHostingEntry(entry, hosting.enabled, reserveKeys);
166
193
  if (invalid)
167
194
  return invalid;
168
195
  const invalidAuth = validateEntryAuth(entry, hosting, names?.domain);
@@ -358,6 +385,8 @@ function validateEntryHostnames(hosting, names) {
358
385
  return null;
359
386
  }
360
387
  const ENTRY_KEY = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
388
+ /** Names of the parts blue/green adds to an app (`<app>-stage`, `-blue`, `-green`, `-lb`): an entry of that name would adopt one. */
389
+ const RESERVED_ENTRY_KEY = /^(stage|blue|green|lb)$|-(stage|blue|green|lb)$/;
361
390
  const VOLUME_MOUNT_PATH = /^(\/(?!\.\.?(\/|$))[A-Za-z0-9._-]+)+$/;
362
391
  /** The log volume's mount path; `executor.ts`'s `LOG_DIR` is the same value. */
363
392
  const LOG_DIR_PATH = '/app/logs';
@@ -378,18 +407,22 @@ const VOLUME_FORBIDDEN_ROOTS = [
378
407
  ];
379
408
  const VOLUME_MOUNT_PATH_MAX = 255;
380
409
  const REPO = /^wtfalch\/[A-Za-z0-9._-]+$/;
410
+ const HOSTING_REPO = /^wtfalch\/[A-Za-z0-9._-]{1,100}$/;
381
411
  // The env names `pushAppEnv` and `pushDatabaseUrl` set themselves.
382
412
  const FOUNDRY_ENV_NAME = /^(APP_URL|LOG_DIR|OWNER_USER_ID|AUTH_.*|DATABASE_URL.*)$/;
383
413
  // Set only on the colors of a blueGreen entry; any other entry may declare them.
384
- const BLUE_GREEN_ENV_NAME = /^REPORTING_(COLOR|RELEASE|HEALTH_TOKEN)$/;
414
+ const BLUE_GREEN_ENV_NAME = /^(REPORTING_(COLOR|RELEASE|HEALTH_TOKEN)|NEXT_SERVER_ACTIONS_ENCRYPTION_KEY)$/;
385
415
  const SECRET_ENV_NAME = /_(KEY|SECRET|TOKEN|PASSWORD)$/;
386
416
  const HELD_SECRETS = "declare it as { from: 'heldSecret' }, not a plain value";
387
417
  /** The rules for one {@link HostingEntry}; `null` when it is valid. */
388
- function validateHostingEntry(entry, enabled) {
418
+ function validateHostingEntry(entry, enabled, reserveKeys) {
389
419
  const at = `hosting.entries '${entry.key}'`;
390
420
  if (!ENTRY_KEY.test(entry.key)) {
391
421
  return refusal('entry_key_invalid', `hosting.entries.${entry.key}.key`, `hosting.entries key '${entry.key}' must be a DNS label (lowercase letters, digits and hyphens, 1-63 characters)`);
392
422
  }
423
+ if (reserveKeys && RESERVED_ENTRY_KEY.test(entry.key)) {
424
+ return refusal('entry_key_reserved', `hosting.entries.${entry.key}.key`, `hosting.entries key '${entry.key}' is reserved: stage, blue, green and lb, and any key ending in -stage, -blue, -green or -lb, name the parts blue/green adds to an app`);
425
+ }
393
426
  if (entry.repo !== null && !REPO.test(entry.repo)) {
394
427
  return refusal('entry_repo_invalid', `hosting.entries.${entry.key}.repo`, `${at} repo must be a repo under wtfalch, or null`);
395
428
  }
@@ -494,6 +527,21 @@ function refuseBlueGreen(value, at, path) {
494
527
  }
495
528
  return null;
496
529
  }
530
+ /** {@link refuseBlueGreen} for the `blueGreen` of the company's Boule and Agora; the same codes. */
531
+ export function refuseBuiltinBlueGreen(org) {
532
+ for (const app of ['boule', 'agora']) {
533
+ const value = org[app];
534
+ if (value === undefined)
535
+ continue;
536
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
537
+ return refusal('builtin_app_invalid', app, `${app} must be an object with an optional blueGreen`);
538
+ }
539
+ const refused = refuseBlueGreen(value.blueGreen, app, `${app}.blueGreen`);
540
+ if (refused)
541
+ return refused;
542
+ }
543
+ return null;
544
+ }
497
545
  /** `volumes`: a bounded list of named volumes at distinct absolute paths, none on the log volume's path or name,
498
546
  * on `/app` or under a system path. */
499
547
  function validateEntryVolumes(entry, at) {
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { sharedSecretRef } from '../org.js';
3
3
  import { applyBlueGreenServer } from './blue-green-apply.js';
4
- import { ARCHON_BG_ORIGIN_SCOPE_KEY, DEFAULT_WEIGHTS, PRIMARY_SERVER_SCOPE_KEY, bgKey, healthTokenName, lbOrigin, splitServers, } from './blue-green.js';
4
+ import { ARCHON_BG_ORIGIN_SCOPE_KEY, DEFAULT_WEIGHTS, PRIMARY_SERVER_SCOPE_KEY, actionsKeyName, bgKey, healthTokenName, lbOrigin, mintActionsKey, splitServers, } from './blue-green.js';
5
5
  /**
6
6
  * platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
7
7
  * blue; green, the balancer and stage are new apps beside it on the primary server. The tunnel moves to the
@@ -58,6 +58,20 @@ export async function applyArchonBlueGreen(run) {
58
58
  primaryBlueAppId: run.blueAppId,
59
59
  logDir: run.logDir,
60
60
  isRunning: run.isRunning,
61
+ adoptRunningBlue: true,
62
+ actionsKey: async () => {
63
+ const key = await mintActionsKey({
64
+ id,
65
+ ref: sharedSecretRef(run.ownerOrgId, actionsKeyName(id)),
66
+ recorded: run.recorded,
67
+ record: run.record,
68
+ persist: run.persist,
69
+ heldKeys: run.heldKeys,
70
+ generate: () => randomBytes(32).toString('base64'),
71
+ secret: run.secret,
72
+ });
73
+ return key;
74
+ },
61
75
  };
62
76
  await applyBlueGreenServer(deps, subject, { uuid: primary, primaryUuid: primary });
63
77
  // The structure is applied and every part ran: now, and only now, the tunnel moves.
@@ -3,6 +3,7 @@ import { ensureConnectorsOnServers } from './cloudflared-connector.js';
3
3
  import { type DeployHealthRuntime } from './deploy-health.js';
4
4
  import type { TunnelPort } from './edge-tunnels.js';
5
5
  import type { CoolifyPort } from './executor.js';
6
+ import { type StageGuardDeps } from './stage-guards.js';
6
7
  /** The Coolify calls this module makes. */
7
8
  type Coolify = Pick<CoolifyPort, 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'deployAndWait' | 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'updateFileStorage' | 'requireGithubApp' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw'> & {
8
9
  listApplicationDeployments?: CoolifyPort['listApplicationDeployments'];
@@ -31,6 +32,21 @@ export interface BlueGreenApplyDeps {
31
32
  primaryBlueAppId: string;
32
33
  logDir: string;
33
34
  isRunning: (status: string | undefined) => boolean;
35
+ /**
36
+ * The primary blue is the process running this apply (platform-archon). Its first apply writes the reporting
37
+ * env but does not redeploy it: a restart would cut the apply, and the env is read at its next start.
38
+ */
39
+ adoptRunningBlue?: boolean;
40
+ /**
41
+ * The subject's server actions key (held, minted once). Blue and green are built with it; stage gets a key of
42
+ * its own. Absent: no key is set.
43
+ */
44
+ actionsKey?: () => Promise<string>;
45
+ /**
46
+ * What stage's guards need from the host: the probe port and the Access gate. A missing part leaves that
47
+ * guard unrecorded ("not run", never passed), so `stageGuardsReady` stays false and the copy does not start.
48
+ */
49
+ stageGuards?: StageGuardDeps;
34
50
  }
35
51
  /** What writing a balancer's config needs; {@link BlueGreenApplyDeps} and the release runner both have it. */
36
52
  export interface LbConfigDeps {
@@ -60,6 +76,25 @@ export declare function applyBlueGreenWeights(deps: LbConfigDeps, subject: BlueG
60
76
  uuid: string;
61
77
  primaryUuid: string;
62
78
  }): Promise<void>;
79
+ /**
80
+ * The primary blue's env for another color: the database host swapped when the server is not the primary, and
81
+ * never the reporting names or the actions key, which differ by design. The structure apply and a deploy both
82
+ * copy through this, so a color is never deployed with an older copy than the structure apply wrote.
83
+ */
84
+ export declare function copiedEnv(deps: {
85
+ label: string;
86
+ secret: (value: string) => void;
87
+ }, subject: Pick<BlueGreenSubject, 'base'>, source: Array<{
88
+ key: string;
89
+ value?: string;
90
+ is_preview?: boolean;
91
+ }>, server: {
92
+ uuid: string;
93
+ primaryUuid: string;
94
+ }): Array<{
95
+ key: string;
96
+ value: string;
97
+ }>;
63
98
  /** A connector for the org's tunnel on each given server; the primary's is the existing one. */
64
99
  export declare function applyBlueGreenConnectors(input: {
65
100
  coolify: Parameters<typeof ensureConnectorsOnServers>[0]['coolify'];