@wtfalch/foundry 0.66.0 → 0.68.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/build-json.d.ts +16 -1
- package/dist/build-json.js +21 -5
- 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/assistants.js +2 -2
- package/dist/modules/index.js +2 -0
- package/dist/org.d.ts +9 -1
- package/dist/org.js +6 -3
- 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/blue-green.d.ts +6 -0
- package/dist/plan/blue-green.js +7 -0
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +87 -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 +15 -1
- package/dist/plan/platform-diff.d.ts +17 -1
- package/dist/plan/platform-diff.js +35 -2
- package/dist/plan/platform-providers.js +42 -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
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;
|
package/dist/plan/own-limits.js
CHANGED
|
@@ -583,6 +583,8 @@ export interface FoundryPlanApplyOptions {
|
|
|
583
583
|
workerAppKeysUrl?: string;
|
|
584
584
|
/** F7: the keys service's registrar address, when the executor has a `KeysRegistrarPort`. */
|
|
585
585
|
keysRegistrarUrl?: string;
|
|
586
|
+
/** True when the host's executor was built with an `analytics` port. A `uses.analytics` key is blocked without it. */
|
|
587
|
+
analyticsPort?: boolean;
|
|
586
588
|
/** C2: true when the host's executor was built with a `WorkerAppDatabasePort`. */
|
|
587
589
|
workerAppDatabase?: boolean;
|
|
588
590
|
/** True when that port has `deletableTables`. A `workerApp.database.noDelete` is blocked without it. */
|
|
@@ -747,6 +749,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
747
749
|
private readonly workerApps;
|
|
748
750
|
private readonly workerAppKeysUrl;
|
|
749
751
|
private readonly keysRegistrarUrl;
|
|
752
|
+
private readonly analyticsPort;
|
|
750
753
|
private readonly workerAppDatabase;
|
|
751
754
|
private readonly workerAppNoDeleteCheck;
|
|
752
755
|
private readonly workerAppRegistryAccountId;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -188,6 +188,7 @@ export class FoundryPlanApply {
|
|
|
188
188
|
workerApps;
|
|
189
189
|
workerAppKeysUrl;
|
|
190
190
|
keysRegistrarUrl;
|
|
191
|
+
analyticsPort;
|
|
191
192
|
workerAppDatabase;
|
|
192
193
|
workerAppNoDeleteCheck;
|
|
193
194
|
workerAppRegistryAccountId;
|
|
@@ -233,6 +234,7 @@ export class FoundryPlanApply {
|
|
|
233
234
|
this.workerApps = options.workerApps ?? false;
|
|
234
235
|
this.workerAppKeysUrl = options.workerAppKeysUrl;
|
|
235
236
|
this.keysRegistrarUrl = options.keysRegistrarUrl;
|
|
237
|
+
this.analyticsPort = options.analyticsPort ?? false;
|
|
236
238
|
this.workerAppDatabase = options.workerAppDatabase ?? false;
|
|
237
239
|
this.workerAppNoDeleteCheck = options.workerAppNoDeleteCheck ?? false;
|
|
238
240
|
this.workerAppRegistryAccountId = options.workerAppRegistryAccountId;
|
|
@@ -1177,7 +1179,12 @@ export class FoundryPlanApply {
|
|
|
1177
1179
|
const primaryUuid = context.recordedIds()[PRIMARY_SERVER_SCOPE_KEY];
|
|
1178
1180
|
if (!store || !this.executor.releaseHost || !primaryUuid)
|
|
1179
1181
|
return out;
|
|
1180
|
-
|
|
1182
|
+
// #1019: Boule and Agora are our own products, so their blue/green release only runs for the
|
|
1183
|
+
// platform's own company; any other company's declaration was never planned as a structure, and a
|
|
1184
|
+
// leftover one is not run here either.
|
|
1185
|
+
const declaredPlatform = await this.platformDeps?.store.get();
|
|
1186
|
+
const ownerOrgId = declaredPlatform?.ownerOrgId ?? null;
|
|
1187
|
+
const subjects = blueGreenSubjects(org, context.recordedIds()).filter((s) => (s.builtin !== 'boule' && s.builtin !== 'agora') || ownerOrgId === org.id);
|
|
1181
1188
|
// A copy waits while any subject of the org has a release running: the runner must stay free to roll back.
|
|
1182
1189
|
if (copies === 'claim' && releaseRunning(subjects, context.recordedIds()))
|
|
1183
1190
|
return out;
|
|
@@ -1627,6 +1634,7 @@ export class FoundryPlanApply {
|
|
|
1627
1634
|
workerApps: this.workerApps,
|
|
1628
1635
|
workerAppKeysUrl: this.workerAppKeysUrl,
|
|
1629
1636
|
keysRegistrarUrl: this.keysRegistrarUrl,
|
|
1637
|
+
analyticsPort: this.analyticsPort,
|
|
1630
1638
|
workerAppDatabase: this.workerAppDatabase,
|
|
1631
1639
|
workerAppNoDeleteCheck: this.workerAppNoDeleteCheck,
|
|
1632
1640
|
workerAppRegistryAccountId: this.workerAppRegistryAccountId,
|
|
@@ -2665,6 +2673,8 @@ const STEP_FIELDS = {
|
|
|
2665
2673
|
'cloudflare:worker': 'hosting.entries.worker',
|
|
2666
2674
|
'cloudflare:worker-app': 'hosting.entries.workerApp',
|
|
2667
2675
|
// F4 (#925): both hang off the company's Apps switch.
|
|
2676
|
+
'coolify:analytics': 'hosting.entries.uses.analytics',
|
|
2677
|
+
'coolify:analytics-boule': 'boule.apps',
|
|
2668
2678
|
'coolify:org-apps-admin': 'boule.apps',
|
|
2669
2679
|
'cloudflare:apps-db-route': 'boule.apps',
|
|
2670
2680
|
'cloudflare:apps-zones': 'boule.apps',
|
|
@@ -2688,6 +2698,10 @@ function stepField(action) {
|
|
|
2688
2698
|
// An entry's jobs project: always `<orgId>:<entry>`.
|
|
2689
2699
|
if (action.system === 'jobs' && action.kind === 'project')
|
|
2690
2700
|
return 'hosting.entries.uses.jobs';
|
|
2701
|
+
// An entry's analytics site key: always `<orgId>:<entry>`.
|
|
2702
|
+
if (action.system === 'analytics' && action.kind === 'key') {
|
|
2703
|
+
return 'hosting.entries.uses.analytics';
|
|
2704
|
+
}
|
|
2691
2705
|
// An entry's audit ledger registration: always `<orgId>:<entry>`.
|
|
2692
2706
|
if (action.system === 'audit' && action.kind === 'ledger')
|
|
2693
2707
|
return 'hosting.entries.uses.audit';
|
|
@@ -1117,6 +1117,13 @@ export declare const PLATFORM_UPTIME_KV_TITLE = "platform-uptime";
|
|
|
1117
1117
|
* a blocked item, never a throw.
|
|
1118
1118
|
*/
|
|
1119
1119
|
export declare const PLATFORM_UPTIME_TOKEN_REF = "uptime:worker:token";
|
|
1120
|
+
/**
|
|
1121
|
+
* The release tick and audit seal tick tokens: minted by the step like `UPTIME_TOKEN`, each its own
|
|
1122
|
+
* value, set on the Worker and on Archon's env under the same names. The Worker skips a tick while
|
|
1123
|
+
* its token is unset, and Archon's route refuses every call while its own is unset.
|
|
1124
|
+
*/
|
|
1125
|
+
export declare const PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF = "uptime:worker:release-tick-token";
|
|
1126
|
+
export declare const PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF = "uptime:worker:audit-seal-tick-token";
|
|
1120
1127
|
export declare const PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF = "uptime:worker:access-client-id";
|
|
1121
1128
|
export declare const PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF = "uptime:worker:access-client-secret";
|
|
1122
1129
|
export declare const PLATFORM_UPTIME_CHAT_TOKEN_REF = "uptime:worker:chat-token";
|
|
@@ -1138,6 +1145,15 @@ export declare const PLATFORM_UPTIME_OPTIONAL_SECRETS: Readonly<Record<string, s
|
|
|
1138
1145
|
*/
|
|
1139
1146
|
export declare const PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY = "uptime_token_undelivered";
|
|
1140
1147
|
export declare const PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY = "uptime_token_id";
|
|
1148
|
+
/** The same pair of scope keys for each tick token: `<undelivered>` before the value is held, `<id>` once it is. */
|
|
1149
|
+
export declare const PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY = "uptime_release_tick_undelivered";
|
|
1150
|
+
export declare const PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY = "uptime_release_tick_id";
|
|
1151
|
+
export declare const PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY = "uptime_audit_seal_tick_undelivered";
|
|
1152
|
+
export declare const PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY = "uptime_audit_seal_tick_id";
|
|
1153
|
+
/** The tick token ids (`<release>:<seal>`) Archon's env was last written with; like {@link PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY}. */
|
|
1154
|
+
export declare const PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY = "archon_uptime_ticks_synced";
|
|
1155
|
+
/** What {@link PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY} holds once Archon's env carries the tick tokens; empty while neither is minted. */
|
|
1156
|
+
export declare function uptimeTicksSyncValue(recorded: Readonly<Record<string, string>>): string;
|
|
1141
1157
|
/** The token id Archon's env was last written with; `planArchon` plans again when it differs from {@link PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY}. */
|
|
1142
1158
|
export declare const PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY = "archon_uptime_token_synced";
|
|
1143
1159
|
/** The tag, sha256 and Archon hostname the last successful run applied; a change plans the step again and mints nothing. */
|
|
@@ -1171,7 +1187,7 @@ export declare function uptimeEffectiveVars(hostVars: Readonly<Record<string, st
|
|
|
1171
1187
|
} | null): Record<string, string | undefined>;
|
|
1172
1188
|
/** The digest of the Worker's plain text variables that are set; empty when none is. */
|
|
1173
1189
|
export declare function uptimeVarsDigest(vars?: Readonly<Record<string, string | undefined>>): string;
|
|
1174
|
-
/** The Worker secret names the step sets: the
|
|
1190
|
+
/** The Worker secret names the step sets: the five it mints, and each held one. */
|
|
1175
1191
|
export declare function uptimeSecretNames(held: readonly string[]): string;
|
|
1176
1192
|
/** The Access service token's name at Cloudflare; edge answers `conflict` when a token of this name exists. */
|
|
1177
1193
|
export declare const PLATFORM_UPTIME_ACCESS_TOKEN_NAME = "platform-uptime";
|