@wtfalch/foundry 0.17.1 → 0.19.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.d.ts +11 -2
- package/dist/bootstrap/declare.js +20 -11
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/migrations/0045_names_to_edge.sql +107 -0
- package/dist/org.d.ts +17 -17
- package/dist/org.js +48 -58
- package/dist/plan/agora.d.ts +25 -0
- package/dist/plan/agora.js +25 -0
- package/dist/plan/boule.d.ts +8 -0
- package/dist/plan/boule.js +8 -0
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +115 -52
- package/dist/plan/entry-edge-key.d.ts +79 -0
- package/dist/plan/{entry-names-key.js → entry-edge-key.js} +90 -66
- package/dist/plan/executor.d.ts +87 -42
- package/dist/plan/executor.js +277 -120
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/login-host.d.ts +15 -11
- package/dist/plan/login-host.js +16 -16
- package/dist/plan/plan-apply.d.ts +6 -5
- package/dist/plan/plan-apply.js +40 -38
- package/dist/plan/platform-diff.d.ts +22 -22
- package/dist/plan/platform-diff.js +45 -45
- package/dist/plan/platform-org-ports.d.ts +1 -0
- package/dist/plan/platform-org-ports.js +3 -0
- package/dist/plan/platform-org.d.ts +8 -8
- package/dist/plan/platform-org.js +7 -7
- package/dist/plan/platform-providers.d.ts +17 -16
- package/dist/plan/platform-providers.js +136 -118
- package/dist/plan/providers-readers.d.ts +34 -11
- package/dist/plan/providers-readers.js +72 -15
- package/dist/plan/step-system.js +1 -1
- package/dist/plan/{edge-worker.d.ts → worker.d.ts} +53 -33
- package/dist/plan/{edge-worker.js → worker.js} +92 -54
- package/dist/platform.d.ts +3 -3
- package/dist/platform.js +1 -1
- package/dist/store/postgres.js +7 -7
- package/dist/store/schema.d.ts +6 -6
- package/dist/store/schema.js +4 -4
- package/package.json +1 -1
- package/dist/plan/entry-names-key.d.ts +0 -73
|
@@ -37,8 +37,17 @@ export interface BootstrapOwner {
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function ownerIdentity(owner: BootstrapOwner): OwnerIdentity;
|
|
39
39
|
/**
|
|
40
|
-
* Company Included's declared org: the platform's owner,
|
|
41
|
-
*
|
|
40
|
+
* Company Included's declared org: the platform's owner, tier 3 on the
|
|
41
|
+
* platform's own apex from the start (#512) -- it runs on that domain with
|
|
42
|
+
* hosted mail, not a level-1 org whose domain gets set by hand afterward.
|
|
43
|
+
* `issuer`/`calls`/`site` go on the domain too, the same tier-3 defaults
|
|
44
|
+
* archon#388 gives every company declared there. `calls` and `site` stay
|
|
45
|
+
* disabled (calls is not in v1; no site is declared), so `usesDomain` is
|
|
46
|
+
* inert on both until one is switched on. Chat stays off the domain: its
|
|
47
|
+
* re-key onto a per-org homeserver is not built yet (#463). The org's own
|
|
48
|
+
* sign-in still never collides with the shared ZITADEL instance's host --
|
|
49
|
+
* `loginHostnameFor` (#488) falls back to `<slug>-auth.oikos.host` whenever
|
|
50
|
+
* `auth.<domain>` would equal the platform's own auth hostname.
|
|
42
51
|
*/
|
|
43
52
|
export declare function declaredCompanyOrg(company: BootstrapCompany, owner: BootstrapOwner): DeclaredOrg;
|
|
44
53
|
/**
|
|
@@ -23,11 +23,11 @@ export const DEFAULT_GIT_REF = 'main';
|
|
|
23
23
|
*/
|
|
24
24
|
export const DEFAULT_MAIL_IMAGE_TAG = VERSIONS.stalwart;
|
|
25
25
|
/**
|
|
26
|
-
* Every platform service except `sfu` and `
|
|
26
|
+
* Every platform service except `sfu` and `edge` is enabled, like the other
|
|
27
27
|
* steps: a service whose dependencies the host did not supply is reported
|
|
28
28
|
* blocked, not planned. `sfu` stays off because it needs a held SSH key and
|
|
29
|
-
* host fingerprint, a separate human step (issue 170). `
|
|
30
|
-
* because the
|
|
29
|
+
* host fingerprint, a separate human step (issue 170). `edge` stays off
|
|
30
|
+
* because the edge service (N1) does not exist yet -- a human deploys it
|
|
31
31
|
* and sets its hostname before enabling it (#317's own "Human steps").
|
|
32
32
|
*/
|
|
33
33
|
const SERVICES_ENABLED_BY_DEFAULT = new Set([
|
|
@@ -55,25 +55,34 @@ export function ownerIdentity(owner) {
|
|
|
55
55
|
return identity;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Company Included's declared org: the platform's owner,
|
|
59
|
-
*
|
|
58
|
+
* Company Included's declared org: the platform's owner, tier 3 on the
|
|
59
|
+
* platform's own apex from the start (#512) -- it runs on that domain with
|
|
60
|
+
* hosted mail, not a level-1 org whose domain gets set by hand afterward.
|
|
61
|
+
* `issuer`/`calls`/`site` go on the domain too, the same tier-3 defaults
|
|
62
|
+
* archon#388 gives every company declared there. `calls` and `site` stay
|
|
63
|
+
* disabled (calls is not in v1; no site is declared), so `usesDomain` is
|
|
64
|
+
* inert on both until one is switched on. Chat stays off the domain: its
|
|
65
|
+
* re-key onto a per-org homeserver is not built yet (#463). The org's own
|
|
66
|
+
* sign-in still never collides with the shared ZITADEL instance's host --
|
|
67
|
+
* `loginHostnameFor` (#488) falls back to `<slug>-auth.oikos.host` whenever
|
|
68
|
+
* `auth.<domain>` would equal the platform's own auth hostname.
|
|
60
69
|
*/
|
|
61
70
|
export function declaredCompanyOrg(company, owner) {
|
|
62
71
|
return {
|
|
63
72
|
id: COMPANY_INCLUDED_ORG_ID,
|
|
64
73
|
slug: company.slug,
|
|
65
74
|
displayName: company.name,
|
|
66
|
-
domain:
|
|
75
|
+
domain: company.apex,
|
|
67
76
|
mail: {
|
|
68
|
-
mode: '
|
|
69
|
-
usesDomain:
|
|
77
|
+
mode: 'hosted',
|
|
78
|
+
usesDomain: true,
|
|
70
79
|
ownerMailboxAddress: null,
|
|
71
80
|
noReplyLocalPart: 'no-reply',
|
|
72
81
|
},
|
|
73
82
|
chat: { enabled: true, usesDomain: false },
|
|
74
|
-
calls: { enabled: false, usesDomain:
|
|
75
|
-
issuer: { usesDomain:
|
|
76
|
-
site: { enabled: false, usesDomain:
|
|
83
|
+
calls: { enabled: false, usesDomain: true },
|
|
84
|
+
issuer: { usesDomain: true },
|
|
85
|
+
site: { enabled: false, usesDomain: true },
|
|
77
86
|
hosting: {
|
|
78
87
|
enabled: false,
|
|
79
88
|
usesDomain: false,
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { FoundryStoreDb } from './store/postgres.js';
|
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
21
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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,
|
|
23
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
24
24
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
25
25
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
26
26
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
|
@@ -28,6 +28,8 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
|
28
28
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, } from './plan/index.js';
|
|
29
29
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
30
30
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
31
|
+
export type { ScopeResolver, RecordedIds } from './plan/index.js';
|
|
32
|
+
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
31
33
|
export type { ZitadelListAppsRawApp } from './plan/index.js';
|
|
32
34
|
export { adaptZitadelListApps } from './plan/index.js';
|
|
33
35
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js';
|
|
@@ -35,7 +37,7 @@ export { checkMailIdentityAudience } from './plan/index.js';
|
|
|
35
37
|
export type { MailAllowlistApp } from './plan/index.js';
|
|
36
38
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
37
39
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
38
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime,
|
|
40
|
+
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformEdgeRuntime, } from './plan/index.js';
|
|
39
41
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
40
42
|
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';
|
|
41
43
|
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
|
@@ -12,11 +12,12 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
|
|
|
12
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
14
|
export { AdoptRefusedError } from './plan/index.js';
|
|
15
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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,
|
|
15
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, 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_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
16
16
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
17
17
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
18
18
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
19
19
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
20
|
+
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
20
21
|
export { adaptZitadelListApps } from './plan/index.js';
|
|
21
22
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
22
23
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the names service is renamed edge, and the hosting entry
|
|
2
|
+
-- field `edge` (a Cloudflare Worker) is renamed `worker`. No behaviour change.
|
|
3
|
+
-- Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` columns.
|
|
4
|
+
--
|
|
5
|
+
-- 1. foundry_declared_platform: names_enabled / names_hostname /
|
|
6
|
+
-- names_git_ref become edge_*. Each rename runs only while the old column
|
|
7
|
+
-- exists, so a second run is a no-op.
|
|
8
|
+
-- 2. The hostname moves from `names.<apex>` to `edge.<apex>` only where it
|
|
9
|
+
-- still equals the old default (0043); an operator's own value stays.
|
|
10
|
+
-- 3. Recorded ids are rewritten in place: `names:key:`, `names:reserved-labels:`
|
|
11
|
+
-- and `platform:names` (ids and step ids), and the colon-free scope keys
|
|
12
|
+
-- `names_*` and `*_platform_names*`. The applied-hostname value is left
|
|
13
|
+
-- alone, so the planner sees a changed hostname and applies the step again.
|
|
14
|
+
-- Every WHERE clause matches only text that still holds an old key, so a
|
|
15
|
+
-- second run changes nothing. `foundry_audit_events` is a signed chain and
|
|
16
|
+
-- is not touched.
|
|
17
|
+
-- 4. foundry_declared_orgs.hosting_entries: each entry's `uses.names` becomes
|
|
18
|
+
-- `uses.edge`, and its `edge` becomes `worker`.
|
|
19
|
+
|
|
20
|
+
do $$
|
|
21
|
+
begin
|
|
22
|
+
if exists (select 1 from information_schema.columns
|
|
23
|
+
where table_schema = current_schema()
|
|
24
|
+
and table_name = 'foundry_declared_platform' and column_name = 'names_enabled') then
|
|
25
|
+
alter table foundry_declared_platform rename column names_enabled to edge_enabled;
|
|
26
|
+
end if;
|
|
27
|
+
if exists (select 1 from information_schema.columns
|
|
28
|
+
where table_schema = current_schema()
|
|
29
|
+
and table_name = 'foundry_declared_platform' and column_name = 'names_hostname') then
|
|
30
|
+
alter table foundry_declared_platform rename column names_hostname to edge_hostname;
|
|
31
|
+
end if;
|
|
32
|
+
if exists (select 1 from information_schema.columns
|
|
33
|
+
where table_schema = current_schema()
|
|
34
|
+
and table_name = 'foundry_declared_platform' and column_name = 'names_git_ref') then
|
|
35
|
+
alter table foundry_declared_platform rename column names_git_ref to edge_git_ref;
|
|
36
|
+
end if;
|
|
37
|
+
end
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
update foundry_declared_platform
|
|
41
|
+
set edge_hostname = 'edge.' || apex
|
|
42
|
+
where edge_hostname = 'names.' || apex;
|
|
43
|
+
|
|
44
|
+
-- The old-to-new text rewrite for recorded ids, applied to the jsonb as text.
|
|
45
|
+
create or replace function pg_temp.names_to_edge(value jsonb) returns jsonb
|
|
46
|
+
language sql immutable as $fn$
|
|
47
|
+
select replace(
|
|
48
|
+
replace(
|
|
49
|
+
replace(
|
|
50
|
+
replace(
|
|
51
|
+
replace(value::text, '"names:key:', '"edge:key:'),
|
|
52
|
+
'"names:reserved-labels:', '"edge:reserved-labels:'),
|
|
53
|
+
'"names_', '"edge_'),
|
|
54
|
+
'_platform_names', '_platform_edge'),
|
|
55
|
+
'platform:names', 'platform:edge')::jsonb
|
|
56
|
+
$fn$;
|
|
57
|
+
|
|
58
|
+
update foundry_declared_platform
|
|
59
|
+
set service_ids = pg_temp.names_to_edge(service_ids)
|
|
60
|
+
where service_ids::text ~ '"names[:_]|_platform_names|platform:names';
|
|
61
|
+
|
|
62
|
+
update foundry_applied_state
|
|
63
|
+
set service_ids = pg_temp.names_to_edge(service_ids)
|
|
64
|
+
where service_ids::text ~ '"names[:_]|_platform_names|platform:names';
|
|
65
|
+
|
|
66
|
+
update foundry_applied_state
|
|
67
|
+
set checkpoints = pg_temp.names_to_edge(checkpoints)
|
|
68
|
+
where checkpoints::text ~ '"names[:_]|_platform_names|platform:names';
|
|
69
|
+
|
|
70
|
+
update foundry_applied_state
|
|
71
|
+
set destroy = pg_temp.names_to_edge(destroy)
|
|
72
|
+
where destroy is not null
|
|
73
|
+
and destroy::text ~ '"names[:_]|_platform_names|platform:names';
|
|
74
|
+
|
|
75
|
+
update foundry_runs
|
|
76
|
+
set step_ids = pg_temp.names_to_edge(step_ids)
|
|
77
|
+
where step_ids::text ~ '"names[:_]|_platform_names|platform:names';
|
|
78
|
+
|
|
79
|
+
update foundry_runs
|
|
80
|
+
set checkpoints = pg_temp.names_to_edge(checkpoints)
|
|
81
|
+
where checkpoints::text ~ '"names[:_]|_platform_names|platform:names';
|
|
82
|
+
|
|
83
|
+
update foundry_runs
|
|
84
|
+
set failure = pg_temp.names_to_edge(failure)
|
|
85
|
+
where failure is not null
|
|
86
|
+
and failure::text ~ '"names[:_]|_platform_names|platform:names';
|
|
87
|
+
|
|
88
|
+
update foundry_declared_orgs o
|
|
89
|
+
set hosting_entries = (
|
|
90
|
+
select coalesce(jsonb_agg(
|
|
91
|
+
case
|
|
92
|
+
when jsonb_typeof(s -> 'uses') = 'object' and (s -> 'uses') ? 'names'
|
|
93
|
+
then jsonb_set(s, '{uses}',
|
|
94
|
+
((s -> 'uses') - 'names') || jsonb_build_object('edge', s -> 'uses' -> 'names'))
|
|
95
|
+
else s
|
|
96
|
+
end order by t.ord), '[]'::jsonb)
|
|
97
|
+
from jsonb_array_elements(o.hosting_entries) with ordinality as t(e, ord),
|
|
98
|
+
lateral (select case
|
|
99
|
+
when jsonb_typeof(t.e) = 'object' and t.e ? 'edge'
|
|
100
|
+
then (t.e - 'edge') || jsonb_build_object('worker', t.e -> 'edge')
|
|
101
|
+
else t.e
|
|
102
|
+
end as s) as r(s))
|
|
103
|
+
where exists (
|
|
104
|
+
select 1 from jsonb_array_elements(o.hosting_entries) e
|
|
105
|
+
where jsonb_typeof(e) = 'object'
|
|
106
|
+
and (e ? 'edge' or (jsonb_typeof(e -> 'uses') = 'object' and (e -> 'uses') ? 'names'))
|
|
107
|
+
);
|
package/dist/org.d.ts
CHANGED
|
@@ -335,26 +335,26 @@ export interface HostingEntry {
|
|
|
335
335
|
* F8: a Cloudflare Worker foundry deploys from this entry's own repo,
|
|
336
336
|
* beside the app, on the org's own zone. Only our own repos may use this
|
|
337
337
|
* (#318: outside customers need Workers for Platforms, not built yet), so
|
|
338
|
-
* `
|
|
338
|
+
* `validateEntryWorker` refuses it unless `repo` is set (every non-null
|
|
339
339
|
* `repo` already matches `wtfalch/...`, {@link REPO}) and the route's zone
|
|
340
340
|
* is the org's own domain.
|
|
341
341
|
*/
|
|
342
|
-
|
|
342
|
+
worker?: WorkerConfig;
|
|
343
343
|
}
|
|
344
344
|
/**
|
|
345
345
|
* One Cloudflare rate limiting binding on a Worker (#318) -- Cloudflare's own
|
|
346
346
|
* "simple" shape: a fixed number of requests per period, where `period` is
|
|
347
347
|
* the only two values Cloudflare's rate limiting binding accepts.
|
|
348
348
|
*/
|
|
349
|
-
export interface
|
|
349
|
+
export interface WorkerRateLimitBinding {
|
|
350
350
|
name: string;
|
|
351
351
|
namespaceId: string;
|
|
352
352
|
limit: number;
|
|
353
353
|
period: 10 | 60;
|
|
354
354
|
}
|
|
355
|
-
/** {@link HostingEntry.
|
|
356
|
-
export interface
|
|
357
|
-
rateLimits:
|
|
355
|
+
/** {@link HostingEntry.worker}'s bindings, one list per binding kind -- only rate limits today. */
|
|
356
|
+
export interface WorkerBindings {
|
|
357
|
+
rateLimits: WorkerRateLimitBinding[];
|
|
358
358
|
}
|
|
359
359
|
/**
|
|
360
360
|
* A plain Worker deployed beside a hosting entry (#318), named by one route
|
|
@@ -365,14 +365,14 @@ export interface EdgeBindings {
|
|
|
365
365
|
* Worker setup": "The app's automated build process builds the bundle
|
|
366
366
|
* outside Foundry; Foundry consumes and deploys the artifact through the
|
|
367
367
|
* provider boundary... No manual build step and no source checkout/bundling
|
|
368
|
-
* in Foundry"). `
|
|
368
|
+
* in Foundry"). `validateEntryWorker` refuses anything that is not an
|
|
369
369
|
* `https://` URL -- a bare path or relative file name is exactly the shape
|
|
370
370
|
* this decision rules out.
|
|
371
371
|
*/
|
|
372
|
-
export interface
|
|
372
|
+
export interface WorkerConfig {
|
|
373
373
|
/** Where foundry fetches the built module from, behind `WorkersPort` -- never checked out or bundled here. */
|
|
374
374
|
artifactUrl: string;
|
|
375
|
-
/** The built module's sha256, lowercase hex. Verified against the fetched bytes before upload, and the sole signal `
|
|
375
|
+
/** The built module's sha256, lowercase hex. Verified against the fetched bytes before upload, and the sole signal `planWorkers` diffs on: an org that rebuilds under an unchanged URL redeploys only when this changes. */
|
|
376
376
|
artifactSha256: string;
|
|
377
377
|
/**
|
|
378
378
|
* One route per entry today. A wildcard route (`*.<domain>/*`) matches
|
|
@@ -386,7 +386,7 @@ export interface EdgeConfig {
|
|
|
386
386
|
* separate entry with that exact route; there is no exclusion list.
|
|
387
387
|
*/
|
|
388
388
|
route: string;
|
|
389
|
-
bindings?:
|
|
389
|
+
bindings?: WorkerBindings;
|
|
390
390
|
}
|
|
391
391
|
/**
|
|
392
392
|
* Sign-in for one entry: one ZITADEL client and one broker entry, signing in
|
|
@@ -441,16 +441,16 @@ export interface HostingEntryUses {
|
|
|
441
441
|
ai?: EntryAi;
|
|
442
442
|
files?: EntryFiles;
|
|
443
443
|
/**
|
|
444
|
-
* F7 (#317): the entry gets its own
|
|
445
|
-
* org with the
|
|
446
|
-
* Serves only the org's own domain (
|
|
447
|
-
* so `
|
|
444
|
+
* F7 (#317): the entry gets its own edge key, minted directly below the
|
|
445
|
+
* org with the edge service, and `EDGE_URL`/`EDGE_API_KEY` in its env.
|
|
446
|
+
* Serves only the org's own domain (edge claims `<label>.<org's domain>`),
|
|
447
|
+
* so `validateEntryEdge` refuses it without `hosting.usesDomain` and an
|
|
448
448
|
* org `domain`.
|
|
449
449
|
*/
|
|
450
|
-
|
|
450
|
+
edge?: true;
|
|
451
451
|
}
|
|
452
|
-
/** The env names `uses.
|
|
453
|
-
export declare const
|
|
452
|
+
/** The env names `uses.edge` writes; `env` may not name them (#317). */
|
|
453
|
+
export declare const EDGE_ENTRY_ENV_NAMES: readonly ["EDGE_URL", "EDGE_API_KEY"];
|
|
454
454
|
/**
|
|
455
455
|
* A plain string is pushed as it is, so `validateHosting` refuses one that
|
|
456
456
|
* looks like a secret. `{ from }` names a value the plan already mints.
|
package/dist/org.js
CHANGED
|
@@ -36,8 +36,8 @@ export const FILES_ENTRY_ENV_NAMES = [
|
|
|
36
36
|
'FILES_API_KEY',
|
|
37
37
|
];
|
|
38
38
|
export const filesCollectionEnvName = (name) => `FILES_COLLECTION_${name}_ID`;
|
|
39
|
-
/** The env names `uses.
|
|
40
|
-
export const
|
|
39
|
+
/** The env names `uses.edge` writes; `env` may not name them (#317). */
|
|
40
|
+
export const EDGE_ENTRY_ENV_NAMES = ['EDGE_URL', 'EDGE_API_KEY'];
|
|
41
41
|
/** The org's `app` entry: today's one app. */
|
|
42
42
|
export function appEntry(hosting) {
|
|
43
43
|
return hosting.entries.find((entry) => entry.key === APP_ENTRY_KEY);
|
|
@@ -132,29 +132,29 @@ export function validateHosting(hosting, names) {
|
|
|
132
132
|
const invalidFiles = validateEntryFiles(entry, hosting, names?.domain);
|
|
133
133
|
if (invalidFiles)
|
|
134
134
|
return invalidFiles;
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
135
|
+
const invalidWorker = validateEntryWorker(entry, hosting, names?.domain);
|
|
136
|
+
if (invalidWorker)
|
|
137
|
+
return invalidWorker;
|
|
138
|
+
const invalidEdgeUse = validateEntryEdge(entry, hosting, names?.domain);
|
|
139
|
+
if (invalidEdgeUse)
|
|
140
|
+
return invalidEdgeUse;
|
|
141
141
|
}
|
|
142
142
|
if (hosting.entries.filter((entry) => entry.uses?.ai?.webhookPath !== undefined).length > 1) {
|
|
143
143
|
return 'at most one hosting entry may set uses.ai webhookPath: ai keeps one webhook per organisation';
|
|
144
144
|
}
|
|
145
|
-
// #318 re-review: two entries declaring the same
|
|
145
|
+
// #318 re-review: two entries declaring the same worker route both throw at
|
|
146
146
|
// apply (`cloudflareWorker` refuses a route already bound to a Worker
|
|
147
147
|
// that is not its own), one of which stops Agora and everything planned
|
|
148
148
|
// after it provisioning at all -- refused here instead, before an apply
|
|
149
149
|
// ever reaches it.
|
|
150
|
-
const
|
|
150
|
+
const seenWorkerRoutes = new Set();
|
|
151
151
|
for (const entry of hosting.entries) {
|
|
152
|
-
if (!entry.
|
|
152
|
+
if (!entry.worker)
|
|
153
153
|
continue;
|
|
154
|
-
if (
|
|
155
|
-
return `hosting.entries '${entry.key}'
|
|
154
|
+
if (seenWorkerRoutes.has(entry.worker.route)) {
|
|
155
|
+
return `hosting.entries '${entry.key}' worker route '${entry.worker.route}' is already declared by another entry`;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
seenWorkerRoutes.add(entry.worker.route);
|
|
158
158
|
}
|
|
159
159
|
const invalidNames = validateEntryHostnames(hosting, names);
|
|
160
160
|
if (invalidNames)
|
|
@@ -438,27 +438,27 @@ function validateEntryFiles(entry, hosting, domain) {
|
|
|
438
438
|
return null;
|
|
439
439
|
}
|
|
440
440
|
/** A route pattern's zone apex: the host part before the first `/`, with one leading `*.` label stripped. */
|
|
441
|
-
function
|
|
441
|
+
function workerRouteZone(route) {
|
|
442
442
|
const slash = route.indexOf('/');
|
|
443
443
|
if (slash < 1)
|
|
444
444
|
return null;
|
|
445
445
|
const host = route.slice(0, slash);
|
|
446
446
|
return host.startsWith('*.') ? host.slice(2) : host;
|
|
447
447
|
}
|
|
448
|
-
const
|
|
449
|
-
const
|
|
448
|
+
const WORKER_ROUTE = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+\/.*$/;
|
|
449
|
+
const WORKER_SHA256 = /^[0-9a-f]{64}$/;
|
|
450
450
|
/**
|
|
451
|
-
* Restated, not shared, from `plan/
|
|
452
|
-
* `
|
|
451
|
+
* Restated, not shared, from `plan/worker.ts`'s `WORKER_ORG_HASH_LENGTH`/
|
|
452
|
+
* `WORKER_NAME_MAX_LENGTH` -- importing them here would import that
|
|
453
453
|
* module's own `DeclaredOrg`/`HostingEntry` imports right back into this
|
|
454
|
-
* one. `
|
|
454
|
+
* one. `workerName`'s shape is `${8-hex-char hash}-${entry.key}-edge`;
|
|
455
455
|
* the hash and the `-edge` suffix are fixed length, so the entry key's own
|
|
456
456
|
* budget against Cloudflare's 63-character Worker name limit is fixed too.
|
|
457
457
|
*/
|
|
458
|
-
const
|
|
459
|
-
const
|
|
458
|
+
const WORKER_NAME_FIXED_LENGTH = 8 + 1 + '-edge'.length;
|
|
459
|
+
const WORKER_NAME_MAX_LENGTH = 63;
|
|
460
460
|
/**
|
|
461
|
-
* `
|
|
461
|
+
* `worker`: the entry's own repo (never the shared standard app -- entries
|
|
462
462
|
* with `repo: null` have no Worker source of their own), an `https://`
|
|
463
463
|
* artifact URL with a lowercase-hex sha256 (never a bare path or relative
|
|
464
464
|
* file -- estate DECISIONS.md, 2026-10-04, rules out any checkout or
|
|
@@ -467,22 +467,22 @@ const EDGE_WORKER_NAME_MAX_LENGTH = 63;
|
|
|
467
467
|
* binding's `period` restricted to the two values Cloudflare's simple rate
|
|
468
468
|
* limiting binding accepts.
|
|
469
469
|
*/
|
|
470
|
-
function
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
470
|
+
function validateEntryWorker(entry, hosting, domain) {
|
|
471
|
+
const worker = entry.worker;
|
|
472
|
+
if (!worker)
|
|
473
473
|
return null;
|
|
474
|
-
const at = `hosting.entries '${entry.key}'
|
|
474
|
+
const at = `hosting.entries '${entry.key}' worker`;
|
|
475
475
|
if (!hosting.enabled)
|
|
476
476
|
return `${at} requires hosting.enabled`;
|
|
477
477
|
if (entry.repo === null) {
|
|
478
478
|
return `${at} needs its own repo (repo: null is the shared standard app, not a Worker source)`;
|
|
479
479
|
}
|
|
480
|
-
if (entry.key.length >
|
|
481
|
-
return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "-edge") must fit Cloudflare's ${
|
|
480
|
+
if (entry.key.length > WORKER_NAME_MAX_LENGTH - WORKER_NAME_FIXED_LENGTH) {
|
|
481
|
+
return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "-edge") must fit Cloudflare's ${WORKER_NAME_MAX_LENGTH}-character limit`;
|
|
482
482
|
}
|
|
483
483
|
let artifactUrl;
|
|
484
484
|
try {
|
|
485
|
-
artifactUrl = new URL(
|
|
485
|
+
artifactUrl = new URL(worker.artifactUrl);
|
|
486
486
|
}
|
|
487
487
|
catch {
|
|
488
488
|
return `${at} artifactUrl must be an absolute URL, not a bare path or relative file`;
|
|
@@ -497,32 +497,22 @@ function validateEntryEdge(entry, hosting, domain, locked) {
|
|
|
497
497
|
return `${at} artifactUrl must not carry a query string (a signed or credentialed URL ends up stored verbatim in recorded scope ids)`;
|
|
498
498
|
}
|
|
499
499
|
// `URL.hash` is '' for a bare trailing `#`, so the raw string is checked.
|
|
500
|
-
if (
|
|
500
|
+
if (worker.artifactUrl.includes('#')) {
|
|
501
501
|
return `${at} artifactUrl must not carry a fragment (refused for the same reason as a query string)`;
|
|
502
502
|
}
|
|
503
|
-
if (!
|
|
503
|
+
if (!WORKER_SHA256.test(worker.artifactSha256)) {
|
|
504
504
|
return `${at} artifactSha256 must be 64 lowercase hex characters`;
|
|
505
505
|
}
|
|
506
|
-
if (!
|
|
506
|
+
if (!WORKER_ROUTE.test(worker.route)) {
|
|
507
507
|
return `${at} route must be a hostname (an optional leading '*.' label, then at least two more labels) and a path, joined by '/'`;
|
|
508
508
|
}
|
|
509
|
-
const zone =
|
|
509
|
+
const zone = workerRouteZone(worker.route);
|
|
510
510
|
if (!domain || zone !== domain) {
|
|
511
|
-
return `${at} route '${
|
|
512
|
-
}
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
// `locked-hostnames.ts`'s set -- not only the org's other entries. (A
|
|
517
|
-
// non-wildcard route can only ever be the bare apex here -- the check
|
|
518
|
-
// just above requires `edgeRouteZone(edge.route) === domain`, and that
|
|
519
|
-
// function returns a non-wildcard route's host unchanged -- and the apex
|
|
520
|
-
// alone is never itself a locked name, so there is no direct-match case
|
|
521
|
-
// to refuse alongside this one.)
|
|
522
|
-
if (locked && locked.size > 0 && edge.route.startsWith('*.')) {
|
|
523
|
-
return `${at} route '${edge.route}' is a wildcard: it would also capture ${[...locked].sort().join(', ')}, every hostname foundry already owns under this domain`;
|
|
524
|
-
}
|
|
525
|
-
for (const limit of edge.bindings?.rateLimits ?? []) {
|
|
511
|
+
return `${at} route '${worker.route}' must be on the org's own domain${domain ? ` (${domain})` : ''}: worker is only for our own projects`;
|
|
512
|
+
}
|
|
513
|
+
// A wildcard route (`*.<domain>/*`) is allowed beside hostnames foundry owns:
|
|
514
|
+
// `planWorkers` plans a no-Worker route for each of them, so they stay out of it.
|
|
515
|
+
for (const limit of worker.bindings?.rateLimits ?? []) {
|
|
526
516
|
if (!limit.name.trim())
|
|
527
517
|
return `${at} bindings.rateLimits needs a non-empty name`;
|
|
528
518
|
if (!limit.namespaceId.trim())
|
|
@@ -537,23 +527,23 @@ function validateEntryEdge(entry, hosting, domain, locked) {
|
|
|
537
527
|
return null;
|
|
538
528
|
}
|
|
539
529
|
/**
|
|
540
|
-
* `uses.
|
|
530
|
+
* `uses.edge`: serves only the org's own domain (edge claims
|
|
541
531
|
* `<label>.<domain>`), so it needs `hosting.usesDomain` and a `domain`, and
|
|
542
|
-
* no `env` entry that names a value `uses.
|
|
532
|
+
* no `env` entry that names a value `uses.edge` writes.
|
|
543
533
|
*/
|
|
544
|
-
function
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
534
|
+
function validateEntryEdge(entry, hosting, domain) {
|
|
535
|
+
const edge = entry.uses?.edge;
|
|
536
|
+
if (edge === undefined)
|
|
547
537
|
return null;
|
|
548
|
-
const at = `hosting.entries '${entry.key}' uses.
|
|
549
|
-
if (
|
|
538
|
+
const at = `hosting.entries '${entry.key}' uses.edge`;
|
|
539
|
+
if (edge !== true)
|
|
550
540
|
return `${at} must be true`;
|
|
551
541
|
if (!hosting.enabled)
|
|
552
542
|
return `${at} requires hosting.enabled`;
|
|
553
543
|
if (!hosting.usesDomain || !domain) {
|
|
554
|
-
return `${at} needs hosting.usesDomain and an org domain:
|
|
544
|
+
return `${at} needs hosting.usesDomain and an org domain: edge serves only the org's own domain`;
|
|
555
545
|
}
|
|
556
|
-
const named = Object.keys(entry.env ?? {}).find((name) =>
|
|
546
|
+
const named = Object.keys(entry.env ?? {}).find((name) => EDGE_ENTRY_ENV_NAMES.includes(name));
|
|
557
547
|
if (named)
|
|
558
548
|
return `${at} writes ${named}; remove it from env`;
|
|
559
549
|
return null;
|
package/dist/plan/agora.d.ts
CHANGED
|
@@ -110,6 +110,18 @@ export declare function agoraHostMissing(host: AgoraHostValues): string[];
|
|
|
110
110
|
export declare function agoraHostnameFor(org: DeclaredOrg): string;
|
|
111
111
|
/** The port `calls/packages/service/Dockerfile` documents (`EXPOSE 8080`); `config.mjs`'s `PORT` has no default, so the executor pushes it. */
|
|
112
112
|
export declare const CALLS_SERVICE_PORT = "8080";
|
|
113
|
+
/**
|
|
114
|
+
* The port `wtfalch/agora`'s `apps/agora/Dockerfile` serves Caddy on
|
|
115
|
+
* (`EXPOSE 8080`). Coolify's github-app build never reads a Dockerfile's
|
|
116
|
+
* `EXPOSE` on its own, so without this passed as `ports_exposes` Traefik is
|
|
117
|
+
* left routing to a port nothing listens on and Agora answers 502 behind the
|
|
118
|
+
* tunnel (#506, seen live on Company Included, 2026-10-05). `executor.ts`'s
|
|
119
|
+
* `coolifyAgora` sets it at create time and fixes it on a found app whose
|
|
120
|
+
* live value differs; `diff.ts`'s `planAgora` plans that fix the same way it
|
|
121
|
+
* plans an env difference, by comparing this constant against the live
|
|
122
|
+
* app's own `ports_exposes` (`providers-readers.ts`'s `withAgoraPortConfig`).
|
|
123
|
+
*/
|
|
124
|
+
export declare const AGORA_APP_PORTS_EXPOSES = "8080";
|
|
113
125
|
/**
|
|
114
126
|
* Recorded by the executor once the calls service app carries its fixed
|
|
115
127
|
* internal name (`callsServiceInternalName`). `desiredAgoraEnv` waits for it,
|
|
@@ -196,6 +208,19 @@ export declare function agoraEnvDrift(desired: Readonly<Record<string, string>>,
|
|
|
196
208
|
* against (#442's own probe: "one env write, zero restarts, zero deploys").
|
|
197
209
|
*/
|
|
198
210
|
export declare const AGORA_ACTIVATED_ENV_SCOPE_KEY = "coolify_agora_activated_env";
|
|
211
|
+
/**
|
|
212
|
+
* #504: recorded once `coolifyAgora` has actually called `deploy` on this
|
|
213
|
+
* org's Agora app -- `createGithubAppApplication` never sets
|
|
214
|
+
* `instant_deploy`, and Coolify defaults it to false, so a freshly created
|
|
215
|
+
* app (or one found by name that a previous apply created but crashed
|
|
216
|
+
* before deploying) sat at `exited:unhealthy` with zero deployments
|
|
217
|
+
* forever: nothing ever deployed it, and `restart` cannot start a container
|
|
218
|
+
* that was never built. Absent means "never deployed", regardless of
|
|
219
|
+
* whether the app is found live this run or created fresh -- unlike
|
|
220
|
+
* {@link AGORA_ACTIVATED_ENV_SCOPE_KEY}, this never changes value once set,
|
|
221
|
+
* so a later env change restarts rather than deploys again.
|
|
222
|
+
*/
|
|
223
|
+
export declare const AGORA_DEPLOYED_SCOPE_KEY = "coolify_agora_deployed";
|
|
199
224
|
/**
|
|
200
225
|
* A stable digest of `env`, for comparison against what was last recorded
|
|
201
226
|
* under {@link AGORA_ACTIVATED_ENV_SCOPE_KEY}. `AGORA_CONFIG` is compared by
|
package/dist/plan/agora.js
CHANGED
|
@@ -118,6 +118,18 @@ export function agoraHostnameFor(org) {
|
|
|
118
118
|
}
|
|
119
119
|
/** The port `calls/packages/service/Dockerfile` documents (`EXPOSE 8080`); `config.mjs`'s `PORT` has no default, so the executor pushes it. */
|
|
120
120
|
export const CALLS_SERVICE_PORT = '8080';
|
|
121
|
+
/**
|
|
122
|
+
* The port `wtfalch/agora`'s `apps/agora/Dockerfile` serves Caddy on
|
|
123
|
+
* (`EXPOSE 8080`). Coolify's github-app build never reads a Dockerfile's
|
|
124
|
+
* `EXPOSE` on its own, so without this passed as `ports_exposes` Traefik is
|
|
125
|
+
* left routing to a port nothing listens on and Agora answers 502 behind the
|
|
126
|
+
* tunnel (#506, seen live on Company Included, 2026-10-05). `executor.ts`'s
|
|
127
|
+
* `coolifyAgora` sets it at create time and fixes it on a found app whose
|
|
128
|
+
* live value differs; `diff.ts`'s `planAgora` plans that fix the same way it
|
|
129
|
+
* plans an env difference, by comparing this constant against the live
|
|
130
|
+
* app's own `ports_exposes` (`providers-readers.ts`'s `withAgoraPortConfig`).
|
|
131
|
+
*/
|
|
132
|
+
export const AGORA_APP_PORTS_EXPOSES = '8080';
|
|
121
133
|
/**
|
|
122
134
|
* Recorded by the executor once the calls service app carries its fixed
|
|
123
135
|
* internal name (`callsServiceInternalName`). `desiredAgoraEnv` waits for it,
|
|
@@ -305,6 +317,19 @@ export function agoraEnvDrift(desired, live) {
|
|
|
305
317
|
* against (#442's own probe: "one env write, zero restarts, zero deploys").
|
|
306
318
|
*/
|
|
307
319
|
export const AGORA_ACTIVATED_ENV_SCOPE_KEY = 'coolify_agora_activated_env';
|
|
320
|
+
/**
|
|
321
|
+
* #504: recorded once `coolifyAgora` has actually called `deploy` on this
|
|
322
|
+
* org's Agora app -- `createGithubAppApplication` never sets
|
|
323
|
+
* `instant_deploy`, and Coolify defaults it to false, so a freshly created
|
|
324
|
+
* app (or one found by name that a previous apply created but crashed
|
|
325
|
+
* before deploying) sat at `exited:unhealthy` with zero deployments
|
|
326
|
+
* forever: nothing ever deployed it, and `restart` cannot start a container
|
|
327
|
+
* that was never built. Absent means "never deployed", regardless of
|
|
328
|
+
* whether the app is found live this run or created fresh -- unlike
|
|
329
|
+
* {@link AGORA_ACTIVATED_ENV_SCOPE_KEY}, this never changes value once set,
|
|
330
|
+
* so a later env change restarts rather than deploys again.
|
|
331
|
+
*/
|
|
332
|
+
export const AGORA_DEPLOYED_SCOPE_KEY = 'coolify_agora_deployed';
|
|
308
333
|
/**
|
|
309
334
|
* A stable digest of `env`, for comparison against what was last recorded
|
|
310
335
|
* under {@link AGORA_ACTIVATED_ENV_SCOPE_KEY}. `AGORA_CONFIG` is compared by
|
package/dist/plan/boule.d.ts
CHANGED
|
@@ -20,6 +20,14 @@ export declare const BOULE_CLIENT_ID_SCOPE_KEY = "boule_client_id";
|
|
|
20
20
|
* scope id, like the cms and calls databases.
|
|
21
21
|
*/
|
|
22
22
|
export declare const BOULE_DATABASE_SCOPE_KEY = "coolify_boule_database";
|
|
23
|
+
/**
|
|
24
|
+
* #504: the same marker `agora.ts`'s `AGORA_DEPLOYED_SCOPE_KEY` is, for
|
|
25
|
+
* Boule -- recorded once `coolifyBoule` has actually called `deploy` on this
|
|
26
|
+
* org's Boule app. See that constant's own doc comment for why: Coolify
|
|
27
|
+
* defaults `instant_deploy` to false, so without this a freshly created app
|
|
28
|
+
* sat built but never started, and `restart` cannot fix that.
|
|
29
|
+
*/
|
|
30
|
+
export declare const BOULE_DEPLOYED_SCOPE_KEY = "coolify_boule_deployed";
|
|
23
31
|
/**
|
|
24
32
|
* `boule.<domain>` when the org has a domain, `<slug>-boule.oikos.host`
|
|
25
33
|
* otherwise (the tier rule of boule#128 and foundry#147; `../slug.ts`'s
|
package/dist/plan/boule.js
CHANGED
|
@@ -19,6 +19,14 @@ export const BOULE_CLIENT_ID_SCOPE_KEY = 'boule_client_id';
|
|
|
19
19
|
* scope id, like the cms and calls databases.
|
|
20
20
|
*/
|
|
21
21
|
export const BOULE_DATABASE_SCOPE_KEY = 'coolify_boule_database';
|
|
22
|
+
/**
|
|
23
|
+
* #504: the same marker `agora.ts`'s `AGORA_DEPLOYED_SCOPE_KEY` is, for
|
|
24
|
+
* Boule -- recorded once `coolifyBoule` has actually called `deploy` on this
|
|
25
|
+
* org's Boule app. See that constant's own doc comment for why: Coolify
|
|
26
|
+
* defaults `instant_deploy` to false, so without this a freshly created app
|
|
27
|
+
* sat built but never started, and `restart` cannot fix that.
|
|
28
|
+
*/
|
|
29
|
+
export const BOULE_DEPLOYED_SCOPE_KEY = 'coolify_boule_deployed';
|
|
22
30
|
/**
|
|
23
31
|
* `boule.<domain>` when the org has a domain, `<slug>-boule.oikos.host`
|
|
24
32
|
* otherwise (the tier rule of boule#128 and foundry#147; `../slug.ts`'s
|