@wtfalch/foundry 0.66.0 → 0.67.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/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/modules/analytics.d.ts +8 -0
- package/dist/modules/analytics.js +28 -0
- package/dist/modules/index.js +2 -0
- package/dist/org.d.ts +9 -1
- package/dist/org.js +4 -1
- package/dist/plan/analytics-boule.d.ts +17 -0
- package/dist/plan/analytics-boule.js +43 -0
- package/dist/plan/analytics.d.ts +58 -0
- package/dist/plan/analytics.js +89 -0
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +86 -0
- package/dist/plan/entry-analytics-key.d.ts +35 -0
- package/dist/plan/entry-analytics-key.js +134 -0
- package/dist/plan/executor.d.ts +36 -0
- package/dist/plan/executor.js +290 -1
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +3 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +3 -0
- package/dist/plan/plan-apply.js +9 -0
- package/dist/plan/platform-diff.d.ts +17 -1
- package/dist/plan/platform-diff.js +35 -2
- package/dist/plan/platform-providers.js +35 -2
- package/dist/plan/providers-readers.js +16 -1
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +2 -1
- package/dist/plan/uptime-worker.js +53 -3
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/worker-app-config.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { analyticsUrlFor } from './analytics.js';
|
|
2
|
+
import { appStepKey } from './platform-diff.js';
|
|
3
|
+
/**
|
|
4
|
+
* `uses.analytics` on a server entry: one site key of its own at the company's analytics backend, minted
|
|
5
|
+
* through `POST /v1/site-keys` with the operator key and written as `ANALYTICS_URL` and `ANALYTICS_SITE_KEY` to
|
|
6
|
+
* the entry's env. The backend shows the secret once, so this follows rule 1 of CLAUDE.md "Platform steps":
|
|
7
|
+
*
|
|
8
|
+
* 1. the key's id is recorded as `analytics_key_undelivered.<entry>` and persisted before the env is written;
|
|
9
|
+
* 2. the env write is tried 3 times with the same secret; the id is the main id `analytics:key:<orgId>:<entry>`
|
|
10
|
+
* and the applied marker only after it worked, and the undelivered marker is cleared then;
|
|
11
|
+
* 3. a marker left behind (every try failed) is reported blocked and never minted again.
|
|
12
|
+
*
|
|
13
|
+
* A Worker app entry is skipped: Boule installs its keys per copy. The entry's stage gets its own key through
|
|
14
|
+
* `plan/stage-services.ts` (service `analytics`, environment `stage`), so the two differ. Removing
|
|
15
|
+
* `uses.analytics` or the entry plans the retire form: revoke the key, take both env names away.
|
|
16
|
+
*/
|
|
17
|
+
export const entryAnalyticsUndeliveredScopeKey = (entryKey) => `analytics_key_undelivered.${entryKey}`;
|
|
18
|
+
/** The address the entry's env was last written with; a change rewrites the address only. */
|
|
19
|
+
export const entryAnalyticsAppliedScopeKey = (entryKey) => `analytics_key_applied.${entryKey}`;
|
|
20
|
+
/** The id of a key that was revoked, so a stale main id is not revoked again. */
|
|
21
|
+
export const entryAnalyticsRetiredScopeKey = (entryKey) => `analytics_key_retired.${entryKey}`;
|
|
22
|
+
export const ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS = 3;
|
|
23
|
+
/** The name the backend knows the key by; its site derives from it. */
|
|
24
|
+
export const entryAnalyticsKeyName = (org, entryKey) => `${org.slug}-${entryKey}-analytics`;
|
|
25
|
+
/** The stage copy's key name: the backend reads the `-stage` suffix as the stage environment. */
|
|
26
|
+
export const stageAnalyticsKeyName = (org, entryKey) => `${org.slug}-${entryKey}-stage-analytics`;
|
|
27
|
+
const servesKey = (entry) => entry.uses?.analytics === true && !entry.workerApp && entry.live !== false;
|
|
28
|
+
export function planEntryAnalyticsKeys(org, recordedIds, plannedSteps, analyticsPort) {
|
|
29
|
+
const steps = [];
|
|
30
|
+
const blocked = [];
|
|
31
|
+
if (!org.hosting.enabled)
|
|
32
|
+
return { steps, blocked };
|
|
33
|
+
const planned = (system, kind, key) => plannedSteps.some((p) => p.action.system === system && p.action.kind === kind && p.action.key === key);
|
|
34
|
+
const wanted = new Map(org.hosting.entries.filter(servesKey).map((e) => [e.key, e]));
|
|
35
|
+
const mainPrefix = `analytics:key:${org.id}:`;
|
|
36
|
+
// Keys whose entry stopped using analytics or is gone: a delivered key (main id) or one minted and never
|
|
37
|
+
// delivered (undelivered marker), so the retire clears the marker and the entry can mint again later.
|
|
38
|
+
const undeliveredPrefix = entryAnalyticsUndeliveredScopeKey('');
|
|
39
|
+
const retiring = new Set();
|
|
40
|
+
for (const [name, id] of Object.entries(recordedIds)) {
|
|
41
|
+
if (!id)
|
|
42
|
+
continue;
|
|
43
|
+
const prefix = name.startsWith(mainPrefix)
|
|
44
|
+
? mainPrefix
|
|
45
|
+
: name.startsWith(undeliveredPrefix)
|
|
46
|
+
? undeliveredPrefix
|
|
47
|
+
: null;
|
|
48
|
+
if (!prefix)
|
|
49
|
+
continue;
|
|
50
|
+
const entryKey = name.slice(prefix.length);
|
|
51
|
+
if (wanted.has(entryKey) ||
|
|
52
|
+
retiring.has(entryKey) ||
|
|
53
|
+
recordedIds[entryAnalyticsRetiredScopeKey(entryKey)] === id) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
retiring.add(entryKey);
|
|
57
|
+
const key = `${mainPrefix}${entryKey}`;
|
|
58
|
+
if (!analyticsPort) {
|
|
59
|
+
blocked.push({
|
|
60
|
+
description: `${org.slug}'s ${entryKey} analytics key is not revoked, not planned: the host supplies no analytics port`,
|
|
61
|
+
key,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else if (!recordedIds[`coolify:analytics:${org.id}`]) {
|
|
65
|
+
blocked.push({
|
|
66
|
+
description: `${org.slug}'s ${entryKey} analytics key is not revoked, not planned: the analytics backend is not recorded`,
|
|
67
|
+
key,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
steps.push({
|
|
72
|
+
action: {
|
|
73
|
+
system: 'analytics',
|
|
74
|
+
kind: 'key',
|
|
75
|
+
key: `${org.id}:${entryKey}`,
|
|
76
|
+
desired: { mode: 'retire' },
|
|
77
|
+
},
|
|
78
|
+
step: {
|
|
79
|
+
description: `revoke ${org.slug}'s ${entryKey} analytics key and take ANALYTICS_* out of its env: the entry no longer uses analytics`,
|
|
80
|
+
destroys: true,
|
|
81
|
+
migrates: false,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const entry of wanted.values()) {
|
|
87
|
+
const stepKey = appStepKey(org, entry.key);
|
|
88
|
+
const key = `analytics:key:${stepKey}`;
|
|
89
|
+
const at = `${org.slug}'s ${entry.key} analytics key`;
|
|
90
|
+
const block = (why) => blocked.push({ description: `${at} is blocked: ${why}`, key });
|
|
91
|
+
const url = analyticsUrlFor(org);
|
|
92
|
+
const undelivered = recordedIds[entryAnalyticsUndeliveredScopeKey(entry.key)];
|
|
93
|
+
if (undelivered) {
|
|
94
|
+
block(`key ${undelivered} was minted but never written to the app, and the backend shows a key once; remove the recorded id ${entryAnalyticsUndeliveredScopeKey(entry.key)} to mint a new key, which replaces that one`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const live = recordedIds[key] &&
|
|
98
|
+
recordedIds[entryAnalyticsRetiredScopeKey(entry.key)] !== recordedIds[key];
|
|
99
|
+
if (live && recordedIds[entryAnalyticsAppliedScopeKey(entry.key)] === url)
|
|
100
|
+
continue;
|
|
101
|
+
if (!analyticsPort) {
|
|
102
|
+
block('the host supplies no analytics port');
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!recordedIds[`coolify:analytics:${org.id}`] && !planned('coolify', 'analytics', org.id)) {
|
|
106
|
+
block("the company's analytics backend is not recorded or planned yet");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!recordedIds[`coolify:app:${stepKey}`] && !planned('coolify', 'app', stepKey)) {
|
|
110
|
+
block("the entry's app is not recorded yet");
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
steps.push({
|
|
114
|
+
action: {
|
|
115
|
+
system: 'analytics',
|
|
116
|
+
kind: 'key',
|
|
117
|
+
key: stepKey,
|
|
118
|
+
desired: {
|
|
119
|
+
mode: live ? 'url' : 'mint',
|
|
120
|
+
name: entryAnalyticsKeyName(org, entry.key),
|
|
121
|
+
analyticsUrl: url,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
step: {
|
|
125
|
+
description: live
|
|
126
|
+
? `write the analytics address to ${org.slug}'s ${entry.key} env`
|
|
127
|
+
: `mint ${at}, write ANALYTICS_URL and ANALYTICS_SITE_KEY to its env, restart the app`,
|
|
128
|
+
destroys: false,
|
|
129
|
+
migrates: false,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return { steps, blocked };
|
|
134
|
+
}
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/p
|
|
|
2
2
|
import { type CallsSfuUrls } from '../calls-sfu.js';
|
|
3
3
|
import type { DeclaredOrg, HostingBuildPack, OrgId } from '../org.js';
|
|
4
4
|
import type { DeclaredOrgStore } from '../store.js';
|
|
5
|
+
import { type AnalyticsPort } from './analytics.js';
|
|
5
6
|
import { type BlueGreenServer, type BlueGreenSubject } from './blue-green.js';
|
|
6
7
|
import { type BouleOwnerSeed } from './boule.js';
|
|
7
8
|
import { type BrokerPort } from './broker-entries.js';
|
|
@@ -1841,6 +1842,8 @@ export interface FoundryStepExecutorPorts {
|
|
|
1841
1842
|
workerAppKeys?: KeysCustodyPort;
|
|
1842
1843
|
/** F7: the keys service's per-company registrar. Without it the plan reports Boule's registrar key blocked (the host also sets `FoundryPlanApplyOptions.keysRegistrarUrl`). */
|
|
1843
1844
|
keysRegistrar?: KeysRegistrarPort;
|
|
1845
|
+
/** The company's analytics backend operator API: mints and revokes site keys. Without it the plan reports each `uses.analytics` key blocked (the host also sets `FoundryPlanApplyOptions.analyticsPort`). */
|
|
1846
|
+
analytics?: AnalyticsPort;
|
|
1844
1847
|
/**
|
|
1845
1848
|
* C3: mints the credential an org's Boule reaches Archon with (org-scoped; `orgs:request` and
|
|
1846
1849
|
* `orgs:read` for that org). Archon implements it. Every call rotates the credential (the old secret
|
|
@@ -2020,6 +2023,10 @@ export declare const BOULE_PORT = 3000;
|
|
|
2020
2023
|
export declare const KOSMOS_REPO = "wtfalch/kosmos";
|
|
2021
2024
|
export declare const KOSMOS_APP_DOCKERFILE = "/Dockerfile";
|
|
2022
2025
|
export declare const KOSMOS_PORT = 3000;
|
|
2026
|
+
/** The company's analytics backend (https://github.com/wtfalch/analytics): root Dockerfile, serves on 3000, migrates in its own start script. */
|
|
2027
|
+
export declare const ANALYTICS_REPO = "wtfalch/analytics";
|
|
2028
|
+
export declare const ANALYTICS_APP_DOCKERFILE = "/Dockerfile";
|
|
2029
|
+
export declare const ANALYTICS_PORT = 3000;
|
|
2023
2030
|
export declare const LOG_DIR = "/app/logs";
|
|
2024
2031
|
/** The company's Coolify destination (its Docker network `co-<slug>`), recorded when its first resource is created. */
|
|
2025
2032
|
export declare const COOLIFY_DESTINATION_SCOPE_KEY = "coolify_destination";
|
|
@@ -3425,6 +3432,35 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
3425
3432
|
* shape {@link coolifyAgora}'s own doc comment explains.
|
|
3426
3433
|
*/
|
|
3427
3434
|
private coolifyBoule;
|
|
3435
|
+
/**
|
|
3436
|
+
* `plan/diff.ts`'s `planAnalyticsBackend`: the company's analytics backend as one more Coolify application
|
|
3437
|
+
* from https://github.com/wtfalch/analytics's root Dockerfile ({@link ANALYTICS_REPO}), with a database of its
|
|
3438
|
+
* own, shaped like {@link coolifyKosmos}. Its start script migrates as the owner role and then serves as the
|
|
3439
|
+
* runtime role, so `pushDatabaseUrl` gives it both URLs. It needs no sign-in client: it reads only
|
|
3440
|
+
* `AUTH_ORGANIZATION_ID` (the tenant id derives from it), `APP_URL` and the operator key.
|
|
3441
|
+
*
|
|
3442
|
+
* `ANALYTICS_OPERATOR_KEY` is made once here (`mintIfAbsent`) and written only to this app's env. No outside
|
|
3443
|
+
* service shows it once, so no undelivered marker is needed: a failed write leaves nothing live to mint twice.
|
|
3444
|
+
* Steps that call the backend's operator API read it back from this env.
|
|
3445
|
+
*
|
|
3446
|
+
* A brand new app is deployed once ({@link ANALYTICS_DEPLOYED_SCOPE_KEY}), as for Boule (#504).
|
|
3447
|
+
*/
|
|
3448
|
+
private coolifyAnalytics;
|
|
3449
|
+
/** The backend's operator key, read from its own env. Fixed text when the backend or the key is not there. */
|
|
3450
|
+
private analyticsOperatorKey;
|
|
3451
|
+
/**
|
|
3452
|
+
* C2 `coolify:analytics-boule` (`plan/analytics-boule.ts`): the backend's address and operator key into
|
|
3453
|
+
* Boule's env, then Boule restarts. `analytics_boule_applied` is recorded last. Retire takes both names away.
|
|
3454
|
+
*/
|
|
3455
|
+
private analyticsBoule;
|
|
3456
|
+
/**
|
|
3457
|
+
* `plan/entry-analytics-key.ts`: one server entry's site key (CLAUDE.md "Platform steps" rule 1). Mint: the
|
|
3458
|
+
* key's id is recorded as undelivered and persisted before anything is written; the env write is tried 3
|
|
3459
|
+
* times with the same secret; the applied address and the cleared marker come last; every try failing leaves
|
|
3460
|
+
* the marker, and the error names the id only. `url` rewrites the address. Retire revokes the key and the
|
|
3461
|
+
* env names. The operator key is read from the backend's env and never recorded.
|
|
3462
|
+
*/
|
|
3463
|
+
private analyticsEntryKey;
|
|
3428
3464
|
/**
|
|
3429
3465
|
* `plan/diff.ts`'s `planKosmos`: provisions the org's own kosmos
|
|
3430
3466
|
* (https://github.com/wtfalch/kosmos) as one more Coolify application, from
|
package/dist/plan/executor.js
CHANGED
|
@@ -9,6 +9,8 @@ import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
|
9
9
|
import { AI_ENTRY_PERMISSIONS, APPS_DOMAIN, APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, FULL_COMMIT_SHA, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, filesCollectionEnvName, heldEnvRef, isLiveEntry, isMainEntry, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownsServices, sharedSecretRef, } from '../org.js';
|
|
10
10
|
import { adoptedKeysUnreadable, parseAdoptSnapshot } from './adopt.js';
|
|
11
11
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, DESKTOP_AUTH_PORT, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatCallsApplied, chatHasUpstreams, chatIntrospectionAppName, desiredAgoraEnv, desktopAuthInternalName, entryClientIdScopeKey, } from './agora.js';
|
|
12
|
+
import { ANALYTICS_BOULE_APPLIED_SCOPE_KEY, ANALYTICS_BOULE_ENV_NAMES, analyticsBouleMarker, } from './analytics-boule.js';
|
|
13
|
+
import { ANALYTICS_DATABASE_SCOPE_KEY, ANALYTICS_DEPLOYED_SCOPE_KEY, ANALYTICS_OPERATOR_KEY_ENV, analyticsEnvDrift, analyticsHostnameFor, analyticsUrlFor, desiredAnalyticsEnv, } from './analytics.js';
|
|
12
14
|
import { APPS_DATABASE_ADMIN_URL_ENV, applyAppsDbRoute, retireAppsDbRoute, } from './apps-db-route.js';
|
|
13
15
|
import { APPS_ZONES_APPLIED_SCOPE_KEY, applyAppsZones, retireAppsZones } from './apps-zones.js';
|
|
14
16
|
import { applyBlueGreenConnectors, applyBlueGreenServer, applyBlueGreenWeights, applyWorkspaceServer, } from './blue-green-apply.js';
|
|
@@ -38,6 +40,7 @@ import { createEdgeRateLimits, } from './edge-ratelimits.js';
|
|
|
38
40
|
import { createEdgeTunnels } from './edge-tunnels.js';
|
|
39
41
|
import { createEdgeZones } from './edge-zones.js';
|
|
40
42
|
import { AI_ENTRY_KEY_DAYS, AI_ENTRY_RENEW_WINDOW_DAYS, entryAiAppliedScopeKey, entryAiClampedScopeKey, entryAiExpiresScopeKey, entryAiPendingExpiresScopeKey, entryAiRetiredScopeKey, entryAiUndeliveredScopeKey, entryAiUnrevokedScopeKey, entryAiWebhookPendingScopeKey, entryAiWrittenScopeKey, parseIdList, } from './entry-ai-key.js';
|
|
43
|
+
import { ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS, entryAnalyticsAppliedScopeKey, entryAnalyticsRetiredScopeKey, entryAnalyticsUndeliveredScopeKey, stageAnalyticsKeyName, } from './entry-analytics-key.js';
|
|
41
44
|
import { applyEntryAudit } from './entry-audit.js';
|
|
42
45
|
import { CUSTOMER_GRANTS_CREDENTIAL_ENV, CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsJson, customerGrantsMintedScopeKey, customerGrantsRef, customerGrantsToDrop, ensureCustomerGrantsCredential, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
43
46
|
import { BOULE_EDGE_ENTRY_KEY, EDGE_ENTRY_KEY_DAYS, EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, EDGE_SLUG_APPLIED_SCOPE_KEY, ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS, bouleEdgeKeyWanted, edgeKeyAppRecordKey, edgeKeyDomain, entryEdgeMintedAtScopeKey, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
@@ -404,6 +407,10 @@ export const BOULE_PORT = 3000;
|
|
|
404
407
|
export const KOSMOS_REPO = 'wtfalch/kosmos';
|
|
405
408
|
export const KOSMOS_APP_DOCKERFILE = '/Dockerfile';
|
|
406
409
|
export const KOSMOS_PORT = 3000;
|
|
410
|
+
/** The company's analytics backend (https://github.com/wtfalch/analytics): root Dockerfile, serves on 3000, migrates in its own start script. */
|
|
411
|
+
export const ANALYTICS_REPO = 'wtfalch/analytics';
|
|
412
|
+
export const ANALYTICS_APP_DOCKERFILE = '/Dockerfile';
|
|
413
|
+
export const ANALYTICS_PORT = 3000;
|
|
407
414
|
/**
|
|
408
415
|
* Every app foundry deploys keeps its own logs across a redeploy, so a
|
|
409
416
|
* database outage does not also erase the only other copy of the evidence
|
|
@@ -969,6 +976,8 @@ function limitProfileOf(action) {
|
|
|
969
976
|
return LIMITS.boule;
|
|
970
977
|
case 'coolify:kosmos':
|
|
971
978
|
return LIMITS.kosmos;
|
|
979
|
+
case 'coolify:analytics':
|
|
980
|
+
return LIMITS.analytics;
|
|
972
981
|
case 'coolify:broker-registrar':
|
|
973
982
|
return LIMITS.brokerEntry;
|
|
974
983
|
case 'coolify:keys-registrar':
|
|
@@ -1404,6 +1413,12 @@ export class FoundryStepExecutor {
|
|
|
1404
1413
|
return this.bouleBrokerEntry(org, context);
|
|
1405
1414
|
case 'coolify:kosmos':
|
|
1406
1415
|
return this.coolifyKosmos(action, org, context);
|
|
1416
|
+
case 'coolify:analytics':
|
|
1417
|
+
return this.coolifyAnalytics(action, org, context);
|
|
1418
|
+
case 'coolify:analytics-boule':
|
|
1419
|
+
return this.analyticsBoule(action, org, context);
|
|
1420
|
+
case 'analytics:key':
|
|
1421
|
+
return this.analyticsEntryKey(action, org, context);
|
|
1407
1422
|
case 'coolify:broker-registrar':
|
|
1408
1423
|
return this.brokerRegistrar(org, context);
|
|
1409
1424
|
case 'coolify:keys-registrar':
|
|
@@ -3831,8 +3846,34 @@ export class FoundryStepExecutor {
|
|
|
3831
3846
|
: { id: registered.id, name: projectName };
|
|
3832
3847
|
},
|
|
3833
3848
|
};
|
|
3849
|
+
const analytics = {
|
|
3850
|
+
held: held('analytics'),
|
|
3851
|
+
available: async () => canHold &&
|
|
3852
|
+
Boolean(this.ports.analytics) &&
|
|
3853
|
+
Boolean(context.recordedIds()[`coolify:analytics:${org.id}`]) &&
|
|
3854
|
+
entry?.uses?.analytics === true,
|
|
3855
|
+
create: async () => {
|
|
3856
|
+
const port = this.ports.analytics;
|
|
3857
|
+
if (!port)
|
|
3858
|
+
throw new Error('no analytics port');
|
|
3859
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
3860
|
+
const name = stageAnalyticsKeyName(org, subject.id);
|
|
3861
|
+
const minted = await port.mintSiteKey({
|
|
3862
|
+
baseUrl: analyticsUrlFor(org),
|
|
3863
|
+
operatorKey,
|
|
3864
|
+
name,
|
|
3865
|
+
environment: 'stage',
|
|
3866
|
+
});
|
|
3867
|
+
return {
|
|
3868
|
+
id: minted.id,
|
|
3869
|
+
name,
|
|
3870
|
+
keyIds: [minted.id],
|
|
3871
|
+
secrets: [{ env: 'ANALYTICS_SITE_KEY', value: minted.secret }],
|
|
3872
|
+
};
|
|
3873
|
+
},
|
|
3874
|
+
};
|
|
3834
3875
|
const prodJobsProjectId = context.recordedIds()[`jobs:registration:${org.id}`];
|
|
3835
|
-
return { files, ai, jobs, ...(prodJobsProjectId ? { prodJobsProjectId } : {}) };
|
|
3876
|
+
return { files, ai, jobs, analytics, ...(prodJobsProjectId ? { prodJobsProjectId } : {}) };
|
|
3836
3877
|
}
|
|
3837
3878
|
/**
|
|
3838
3879
|
* Stage's own sign-in (`plan/stage-signin.ts`): a client named `<production client>-stage` on production's
|
|
@@ -7859,6 +7900,254 @@ export class FoundryStepExecutor {
|
|
|
7859
7900
|
this.recordSignInEnvLive(context, 'boule', bouleHostnameFor(org), env);
|
|
7860
7901
|
return { resourceId: app.appId };
|
|
7861
7902
|
}
|
|
7903
|
+
// ---- analytics backend ----------------------------------------------------
|
|
7904
|
+
/**
|
|
7905
|
+
* `plan/diff.ts`'s `planAnalyticsBackend`: the company's analytics backend as one more Coolify application
|
|
7906
|
+
* from https://github.com/wtfalch/analytics's root Dockerfile ({@link ANALYTICS_REPO}), with a database of its
|
|
7907
|
+
* own, shaped like {@link coolifyKosmos}. Its start script migrates as the owner role and then serves as the
|
|
7908
|
+
* runtime role, so `pushDatabaseUrl` gives it both URLs. It needs no sign-in client: it reads only
|
|
7909
|
+
* `AUTH_ORGANIZATION_ID` (the tenant id derives from it), `APP_URL` and the operator key.
|
|
7910
|
+
*
|
|
7911
|
+
* `ANALYTICS_OPERATOR_KEY` is made once here (`mintIfAbsent`) and written only to this app's env. No outside
|
|
7912
|
+
* service shows it once, so no undelivered marker is needed: a failed write leaves nothing live to mint twice.
|
|
7913
|
+
* Steps that call the backend's operator API read it back from this env.
|
|
7914
|
+
*
|
|
7915
|
+
* A brand new app is deployed once ({@link ANALYTICS_DEPLOYED_SCOPE_KEY}), as for Boule (#504).
|
|
7916
|
+
*/
|
|
7917
|
+
async coolifyAnalytics(_action, org, context) {
|
|
7918
|
+
const { env, missing } = desiredAnalyticsEnv(org, context.recordedIds());
|
|
7919
|
+
if (missing.length > 0) {
|
|
7920
|
+
throw new Error(`the analytics backend for ${org.slug} cannot be configured: missing ${missing.join(', ')}. Nothing was written.`);
|
|
7921
|
+
}
|
|
7922
|
+
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
7923
|
+
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
7924
|
+
const name = `${org.slug}-analytics`;
|
|
7925
|
+
const dbName = `${org.slug}-analytics-db`.replace(/-/g, '_');
|
|
7926
|
+
const dbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, `${org.slug}-analytics-db`, dbName, ANALYTICS_DATABASE_SCOPE_KEY);
|
|
7927
|
+
context.recordScopeId(ANALYTICS_DATABASE_SCOPE_KEY, dbId);
|
|
7928
|
+
const server = await this.resolveServer(org, context);
|
|
7929
|
+
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
7930
|
+
const app = await this.findOrCreateCallsApp(name, environment.id, () => this.ports.coolify.createGithubAppApplication({
|
|
7931
|
+
is_preview_deployments_enabled: false,
|
|
7932
|
+
project_uuid: projectUuid,
|
|
7933
|
+
server_uuid: server.uuid,
|
|
7934
|
+
environment_name: environment.name,
|
|
7935
|
+
environment_uuid: environment.uuid,
|
|
7936
|
+
github_app_uuid: githubApp.uuid,
|
|
7937
|
+
git_repository: ANALYTICS_REPO,
|
|
7938
|
+
git_branch: 'main',
|
|
7939
|
+
build_pack: 'dockerfile',
|
|
7940
|
+
name,
|
|
7941
|
+
domains: coolifyTunnelDomains([analyticsHostnameFor(org)]),
|
|
7942
|
+
dockerfile_location: ANALYTICS_APP_DOCKERFILE,
|
|
7943
|
+
}));
|
|
7944
|
+
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
7945
|
+
await this.ports.coolify.updateApplication(app.appId, {
|
|
7946
|
+
...healthCheckSettings('/api/health', ANALYTICS_PORT),
|
|
7947
|
+
...(await this.routedLabels(name, analyticsHostnameFor(org), ANALYTICS_PORT)),
|
|
7948
|
+
});
|
|
7949
|
+
const liveEnv = Object.fromEntries((await this.ports.coolify.listEnvs(app.appId))
|
|
7950
|
+
.filter((e) => !e.is_preview)
|
|
7951
|
+
.map((e) => [e.key, e.value]));
|
|
7952
|
+
const envChanged = analyticsEnvDrift(env, liveEnv).length > 0;
|
|
7953
|
+
const envs = Object.entries(env).map(([key, value]) => ({ key, value }));
|
|
7954
|
+
const operatorKey = await this.mintIfAbsent(app.appId, ANALYTICS_OPERATOR_KEY_ENV, 32);
|
|
7955
|
+
if (operatorKey) {
|
|
7956
|
+
this.handledSecrets.add(operatorKey);
|
|
7957
|
+
envs.push({ key: ANALYTICS_OPERATOR_KEY_ENV, value: operatorKey });
|
|
7958
|
+
}
|
|
7959
|
+
try {
|
|
7960
|
+
await this.ports.coolify.bulkUpsertEnvs(app.appId, envs);
|
|
7961
|
+
}
|
|
7962
|
+
catch (error) {
|
|
7963
|
+
if (!operatorKey)
|
|
7964
|
+
throw error;
|
|
7965
|
+
throw new Error(redactSecrets(error instanceof Error ? error.message : String(error), [operatorKey]));
|
|
7966
|
+
}
|
|
7967
|
+
const dbPush = await this.pushDatabaseUrl(app.appId, dbId, dbName);
|
|
7968
|
+
if (!dbPush.pushed) {
|
|
7969
|
+
throw new Error(`the analytics backend for ${org.slug}: its database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
7970
|
+
}
|
|
7971
|
+
if (!context.recordedIds()[ANALYTICS_DEPLOYED_SCOPE_KEY]) {
|
|
7972
|
+
const runtime = this.ports.runtime ?? {};
|
|
7973
|
+
const built = await waitForNewDeployment(this.ports.coolify, app.appId, publicHealthCheck(org, context.recordedIds(), analyticsHostnameFor(org), '/api/health'), runtime, runtime.sleep ?? defaultSleep, 'coolify:analytics', `${org.slug}'s analytics backend stays at its previous deploy`);
|
|
7974
|
+
await recordBuiltCommit(this.ports.coolify, { appId: app.appId, deploymentUuid: built.deploymentUuid, name: 'analytics' }, (key, value) => context.recordScopeId(key, value));
|
|
7975
|
+
context.recordScopeId(ANALYTICS_DEPLOYED_SCOPE_KEY, '1');
|
|
7976
|
+
}
|
|
7977
|
+
else if (app.existed && (envChanged || operatorKey !== null || dbPush.changed)) {
|
|
7978
|
+
await this.ports.coolify.restart(app.appId);
|
|
7979
|
+
}
|
|
7980
|
+
return { resourceId: app.appId };
|
|
7981
|
+
}
|
|
7982
|
+
/** The backend's operator key, read from its own env. Fixed text when the backend or the key is not there. */
|
|
7983
|
+
async analyticsOperatorKey(org, context) {
|
|
7984
|
+
const appId = context.recordedIds()[`coolify:analytics:${org.id}`];
|
|
7985
|
+
if (!appId)
|
|
7986
|
+
throw new Error(`the analytics backend for ${org.slug} is not recorded yet`);
|
|
7987
|
+
const rows = await this.ports.coolify.listEnvs(appId);
|
|
7988
|
+
const key = rows.find((e) => e.key === ANALYTICS_OPERATOR_KEY_ENV && !e.is_preview)?.value;
|
|
7989
|
+
if (!key)
|
|
7990
|
+
throw new Error(`the analytics backend for ${org.slug} has no operator key yet`);
|
|
7991
|
+
this.handledSecrets.add(key);
|
|
7992
|
+
return key;
|
|
7993
|
+
}
|
|
7994
|
+
/**
|
|
7995
|
+
* C2 `coolify:analytics-boule` (`plan/analytics-boule.ts`): the backend's address and operator key into
|
|
7996
|
+
* Boule's env, then Boule restarts. `analytics_boule_applied` is recorded last. Retire takes both names away.
|
|
7997
|
+
*/
|
|
7998
|
+
async analyticsBoule(action, org, context) {
|
|
7999
|
+
const at = `coolify:analytics-boule for ${org.slug}`;
|
|
8000
|
+
const recorded = context.recordedIds();
|
|
8001
|
+
const bouleId = recorded[`coolify:boule:${org.id}`];
|
|
8002
|
+
if (!bouleId)
|
|
8003
|
+
throw new Error(`${at}: Boule is not recorded yet`);
|
|
8004
|
+
if (action.desired.mode === 'retire') {
|
|
8005
|
+
const rows = await this.ports.coolify.listEnvs(bouleId);
|
|
8006
|
+
for (const name of ANALYTICS_BOULE_ENV_NAMES) {
|
|
8007
|
+
const row = rows.find((e) => e.key === name && !e.is_preview);
|
|
8008
|
+
if (row?.uuid) {
|
|
8009
|
+
try {
|
|
8010
|
+
await this.ports.coolify.deleteEnv(bouleId, row.uuid);
|
|
8011
|
+
}
|
|
8012
|
+
catch (error) {
|
|
8013
|
+
if (!isNotFoundError(error))
|
|
8014
|
+
throw error;
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
}
|
|
8018
|
+
await this.ports.coolify.restart(bouleId);
|
|
8019
|
+
context.recordScopeId(ANALYTICS_BOULE_APPLIED_SCOPE_KEY, '');
|
|
8020
|
+
return {};
|
|
8021
|
+
}
|
|
8022
|
+
if (!appsOn(org))
|
|
8023
|
+
throw new Error(`${at}: the company's Boule runs no apps (org.boule.apps)`);
|
|
8024
|
+
const backendId = recorded[`coolify:analytics:${org.id}`];
|
|
8025
|
+
if (!backendId)
|
|
8026
|
+
throw new Error(`${at}: the analytics backend is not recorded yet`);
|
|
8027
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
8028
|
+
try {
|
|
8029
|
+
await this.ports.coolify.bulkUpsertEnvs(bouleId, [
|
|
8030
|
+
{ key: 'ANALYTICS_URL', value: analyticsUrlFor(org), literal: true },
|
|
8031
|
+
{ key: ANALYTICS_OPERATOR_KEY_ENV, value: operatorKey, literal: true },
|
|
8032
|
+
]);
|
|
8033
|
+
await this.ports.coolify.restart(bouleId);
|
|
8034
|
+
}
|
|
8035
|
+
catch (error) {
|
|
8036
|
+
throw new Error(redactSecrets(error instanceof Error ? error.message : String(error), [operatorKey]));
|
|
8037
|
+
}
|
|
8038
|
+
context.recordScopeId(ANALYTICS_BOULE_APPLIED_SCOPE_KEY, analyticsBouleMarker(org, backendId));
|
|
8039
|
+
return {};
|
|
8040
|
+
}
|
|
8041
|
+
/**
|
|
8042
|
+
* `plan/entry-analytics-key.ts`: one server entry's site key (CLAUDE.md "Platform steps" rule 1). Mint: the
|
|
8043
|
+
* key's id is recorded as undelivered and persisted before anything is written; the env write is tried 3
|
|
8044
|
+
* times with the same secret; the applied address and the cleared marker come last; every try failing leaves
|
|
8045
|
+
* the marker, and the error names the id only. `url` rewrites the address. Retire revokes the key and the
|
|
8046
|
+
* env names. The operator key is read from the backend's env and never recorded.
|
|
8047
|
+
*/
|
|
8048
|
+
async analyticsEntryKey(action, org, context) {
|
|
8049
|
+
const entryKey = action.key.slice(org.id.length + 1);
|
|
8050
|
+
const at = `analytics:key for ${org.slug} entry '${entryKey}'`;
|
|
8051
|
+
const analytics = this.ports.analytics;
|
|
8052
|
+
if (!analytics)
|
|
8053
|
+
throw new Error(`${at}: this executor was built without an analytics port`);
|
|
8054
|
+
const desired = action.desired;
|
|
8055
|
+
const recorded = context.recordedIds();
|
|
8056
|
+
const appId = recorded[`coolify:app:${action.key}`];
|
|
8057
|
+
const baseUrl = analyticsUrlFor(org);
|
|
8058
|
+
const tolerate = async (call, gone) => {
|
|
8059
|
+
try {
|
|
8060
|
+
return await call();
|
|
8061
|
+
}
|
|
8062
|
+
catch (error) {
|
|
8063
|
+
if (!isNotFoundError(error))
|
|
8064
|
+
throw error;
|
|
8065
|
+
return gone;
|
|
8066
|
+
}
|
|
8067
|
+
};
|
|
8068
|
+
if (desired.mode === 'retire') {
|
|
8069
|
+
const mainId = recorded[`analytics:key:${action.key}`];
|
|
8070
|
+
const undelivered = recorded[entryAnalyticsUndeliveredScopeKey(entryKey)];
|
|
8071
|
+
const ids = [...new Set([mainId, undelivered].filter((id) => Boolean(id)))];
|
|
8072
|
+
if (ids.length > 0) {
|
|
8073
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
8074
|
+
for (const keyId of ids) {
|
|
8075
|
+
await tolerate(() => analytics.revokeSiteKey({ baseUrl, operatorKey, keyId }), undefined);
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
if (appId) {
|
|
8079
|
+
const rows = await tolerate(() => this.ports.coolify.listEnvs(appId), []);
|
|
8080
|
+
let removed = false;
|
|
8081
|
+
for (const name of ['ANALYTICS_URL', 'ANALYTICS_SITE_KEY']) {
|
|
8082
|
+
const row = rows.find((e) => e.key === name && !e.is_preview);
|
|
8083
|
+
if (row?.uuid) {
|
|
8084
|
+
const uuid = row.uuid;
|
|
8085
|
+
await tolerate(() => this.ports.coolify.deleteEnv(appId, uuid), undefined);
|
|
8086
|
+
removed = true;
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
if (removed)
|
|
8090
|
+
await tolerate(() => this.ports.coolify.restart(appId), undefined);
|
|
8091
|
+
}
|
|
8092
|
+
context.recordScopeId(entryAnalyticsUndeliveredScopeKey(entryKey), '');
|
|
8093
|
+
context.recordScopeId(entryAnalyticsAppliedScopeKey(entryKey), '');
|
|
8094
|
+
if (mainId ?? undelivered) {
|
|
8095
|
+
context.recordScopeId(entryAnalyticsRetiredScopeKey(entryKey), (mainId ?? undelivered));
|
|
8096
|
+
}
|
|
8097
|
+
return {};
|
|
8098
|
+
}
|
|
8099
|
+
if (!org.hosting.entries.some((e) => e.key === entryKey && e.uses?.analytics)) {
|
|
8100
|
+
throw new Error(`${at}: the entry no longer declares uses.analytics`);
|
|
8101
|
+
}
|
|
8102
|
+
if (!appId)
|
|
8103
|
+
throw new Error(`${at}: the entry's app is not recorded yet`);
|
|
8104
|
+
if (desired.mode === 'url') {
|
|
8105
|
+
await this.ports.coolify.bulkUpsertEnvs(appId, [
|
|
8106
|
+
{ key: 'ANALYTICS_URL', value: baseUrl, literal: true },
|
|
8107
|
+
]);
|
|
8108
|
+
await this.ports.coolify.restart(appId);
|
|
8109
|
+
context.recordScopeId(entryAnalyticsAppliedScopeKey(entryKey), baseUrl);
|
|
8110
|
+
return {};
|
|
8111
|
+
}
|
|
8112
|
+
if (recorded[entryAnalyticsUndeliveredScopeKey(entryKey)]) {
|
|
8113
|
+
throw new Error(`${at}: a key was minted and never delivered; it is not minted again`);
|
|
8114
|
+
}
|
|
8115
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
8116
|
+
let minted;
|
|
8117
|
+
try {
|
|
8118
|
+
minted = await analytics.mintSiteKey({
|
|
8119
|
+
baseUrl,
|
|
8120
|
+
operatorKey,
|
|
8121
|
+
name: desired.name,
|
|
8122
|
+
environment: 'production',
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
catch (error) {
|
|
8126
|
+
const status = error?.status;
|
|
8127
|
+
throw new Error(`${at}: the backend did not mint a key${typeof status === 'number' ? ` (status ${status})` : ''}`);
|
|
8128
|
+
}
|
|
8129
|
+
this.handledSecrets.add(minted.secret);
|
|
8130
|
+
context.recordScopeId(entryAnalyticsUndeliveredScopeKey(entryKey), minted.id);
|
|
8131
|
+
await context.persistScopeIds();
|
|
8132
|
+
const rows = [
|
|
8133
|
+
{ key: 'ANALYTICS_URL', value: baseUrl, literal: true },
|
|
8134
|
+
{ key: 'ANALYTICS_SITE_KEY', value: minted.secret, literal: true },
|
|
8135
|
+
];
|
|
8136
|
+
let lastError;
|
|
8137
|
+
for (let attempt = 1; attempt <= ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS; attempt++) {
|
|
8138
|
+
try {
|
|
8139
|
+
await this.ports.coolify.bulkUpsertEnvs(appId, rows);
|
|
8140
|
+
await this.ports.coolify.restart(appId);
|
|
8141
|
+
context.recordScopeId(entryAnalyticsAppliedScopeKey(entryKey), baseUrl);
|
|
8142
|
+
context.recordScopeId(entryAnalyticsUndeliveredScopeKey(entryKey), '');
|
|
8143
|
+
return { resourceId: minted.id };
|
|
8144
|
+
}
|
|
8145
|
+
catch (error) {
|
|
8146
|
+
lastError = error;
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
throw new Error(`${at}: key ${minted.id} was minted but could not be written to the app's env after ${ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS} attempts (${lastError instanceof Error ? lastError.name : 'error'})`);
|
|
8150
|
+
}
|
|
7862
8151
|
// ---- kosmos (foundry#949) --------------------------------------------------
|
|
7863
8152
|
/**
|
|
7864
8153
|
* `plan/diff.ts`'s `planKosmos`: provisions the org's own kosmos
|
package/dist/plan/hostnames.js
CHANGED
|
@@ -2,6 +2,7 @@ import { INBOX_HOST_PREFIX, WORKSPACE_HOST_PREFIX } from '../hosted-label.js';
|
|
|
2
2
|
import { APPS_DOMAIN, isMainEntry, mainEntry } from '../org.js';
|
|
3
3
|
import { workerAppHostnameOf } from '../worker-app-config.js';
|
|
4
4
|
import { agoraHostnameFor } from './agora.js';
|
|
5
|
+
import { analyticsHostnameFor, analyticsWanted } from './analytics.js';
|
|
5
6
|
import { bouleHostnameFor } from './boule.js';
|
|
6
7
|
import { workspaceWorkerParent } from './workspace-worker.js';
|
|
7
8
|
/**
|
|
@@ -154,6 +155,9 @@ export function tunnelIngressHostnames(org, loginHost = null) {
|
|
|
154
155
|
hostnames.push(agoraHostnameFor(org));
|
|
155
156
|
// #161: and Boule moves onto `boule.<domain>` the same way.
|
|
156
157
|
hostnames.push(bouleHostnameFor(org));
|
|
158
|
+
// The analytics backend's route, only while something uses it.
|
|
159
|
+
if (analyticsWanted(org))
|
|
160
|
+
hostnames.push(analyticsHostnameFor(org));
|
|
157
161
|
}
|
|
158
162
|
const share = filesShareHostname(org);
|
|
159
163
|
if (share)
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { KeysRegistrarPort } from './keys-registrar.js';
|
|
2
|
+
export type { AnalyticsPort } from './analytics.js';
|
|
3
|
+
export { httpAnalytics } from './analytics.js';
|
|
2
4
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, } from './live.js';
|
|
3
5
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
4
6
|
export type { StepAction, PlannedStep, PlanDetail, OrgPostgresPlan } from './diff.js';
|
package/dist/plan/index.js
CHANGED
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' | 'github' | 'audit';
|
|
45
|
+
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'edge' | 'heartbeats' | 'github' | 'audit' | 'analytics';
|
|
46
46
|
kind: string;
|
|
47
47
|
key: string;
|
|
48
48
|
config: Record<string, unknown>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { kosmosOn, ownsServices } from '../org.js';
|
|
2
2
|
import { OWN_SERVICE_LABELS } from '../service-labels.js';
|
|
3
3
|
import { agoraHostnameFor } from './agora.js';
|
|
4
|
+
import { analyticsHostnameFor } from './analytics.js';
|
|
4
5
|
import { bouleHostnameFor } from './boule.js';
|
|
5
6
|
import { cmsStudioHostnameFor, siteHostnameFor } from './hostnames.js';
|
|
6
7
|
import { kosmosHostnameFor } from './kosmos.js';
|
|
@@ -32,6 +33,8 @@ export function lockedHostnames(org, platformHostnames) {
|
|
|
32
33
|
}
|
|
33
34
|
locked.add(agoraHostnameFor(org));
|
|
34
35
|
locked.add(bouleHostnameFor(org));
|
|
36
|
+
// Reserved always, planned only while something uses analytics (`analyticsWanted`).
|
|
37
|
+
locked.add(analyticsHostnameFor(org));
|
|
35
38
|
if (kosmosOn(org))
|
|
36
39
|
locked.add(kosmosHostnameFor(org));
|
|
37
40
|
if (org.site.enabled)
|
package/dist/plan/org-names.d.ts
CHANGED
|
@@ -6,13 +6,13 @@ export type OrgNameSource = 'own' | 'hosting';
|
|
|
6
6
|
* hosting entries: Agora, Boule, the tunnel and its connector, and chat,
|
|
7
7
|
* calls and the cms site while they are on.
|
|
8
8
|
*/
|
|
9
|
-
export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos'>): string[];
|
|
9
|
+
export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule' | 'hosting'>): string[];
|
|
10
10
|
/**
|
|
11
11
|
* Every container name, network alias, database, tunnel and bucket name that
|
|
12
12
|
* foundry derives for an org: its own-service names when it owns services,
|
|
13
13
|
* and its hosting-entry and database names.
|
|
14
14
|
*/
|
|
15
|
-
export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>): {
|
|
15
|
+
export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule'>): {
|
|
16
16
|
name: string;
|
|
17
17
|
source: OrgNameSource;
|
|
18
18
|
}[];
|
|
@@ -26,5 +26,5 @@ export interface NameClash {
|
|
|
26
26
|
source: OrgNameSource;
|
|
27
27
|
}
|
|
28
28
|
/** Every name `org` derives that another org or the platform also derives. */
|
|
29
|
-
export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>[]): NameClash[];
|
|
29
|
+
export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule'>[]): NameClash[];
|
|
30
30
|
export declare function nameClashMessage(slug: string, clash: NameClash): string;
|
package/dist/plan/org-names.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { kosmosOn, ownsServices } from '../org.js';
|
|
2
2
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
3
|
+
import { analyticsWanted } from './analytics.js';
|
|
3
4
|
import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
|
|
4
5
|
import { workerAppHostname, workspaceHost, workspaceInboxHost } from './hostnames.js';
|
|
5
6
|
import { orgTarget, platformTarget } from './own-services.js';
|
|
@@ -20,6 +21,8 @@ export function companyServiceNames(org) {
|
|
|
20
21
|
`${s}-tunnel`,
|
|
21
22
|
`${s}-cloudflared`,
|
|
22
23
|
];
|
|
24
|
+
if (analyticsWanted(org))
|
|
25
|
+
names.push(`${s}-analytics`, `${s}-analytics-db`);
|
|
23
26
|
if (kosmosOn(org))
|
|
24
27
|
names.push(`${s}-kosmos`, `${s}-kosmos-db`);
|
|
25
28
|
if (org.chat?.enabled) {
|
|
@@ -113,6 +113,12 @@ export declare const LIMITS: {
|
|
|
113
113
|
autoDeploy: false | "declared";
|
|
114
114
|
capabilities: CapabilityTier;
|
|
115
115
|
};
|
|
116
|
+
readonly analytics: {
|
|
117
|
+
memory: string;
|
|
118
|
+
dbMemory: string;
|
|
119
|
+
autoDeploy: false | "declared";
|
|
120
|
+
capabilities: CapabilityTier;
|
|
121
|
+
};
|
|
116
122
|
readonly companyOther: {
|
|
117
123
|
memory: string;
|
|
118
124
|
dbMemory: string;
|