@wtfalch/foundry 0.76.1 → 0.77.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/app-canary-worker.d.ts +35 -0
- package/dist/app-canary-worker.js +56 -0
- package/dist/audit/connector.d.ts +6 -2
- package/dist/audit/connector.js +3 -2
- package/dist/audit/evidence.d.ts +4 -0
- package/dist/audit/evidence.js +14 -2
- package/dist/audit/host-store.d.ts +58 -0
- package/dist/audit/host-store.js +147 -0
- package/dist/audit/host.d.ts +53 -0
- package/dist/audit/host.js +294 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.js +2 -0
- package/dist/audit/registry.d.ts +14 -0
- package/dist/audit/sealer-role.d.ts +12 -0
- package/dist/audit/sealer-role.js +26 -2
- package/dist/audit/tick.d.ts +10 -2
- package/dist/audit/tick.js +15 -2
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/entry-rules.d.ts +35 -0
- package/dist/entry-rules.js +35 -0
- package/dist/hosted-label.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/migrations/0072_audit_host.sql +46 -0
- package/dist/migrations/0074_platform_app_canary.sql +11 -0
- package/dist/modules/ai.js +15 -1
- package/dist/modules/assistants.js +3 -0
- package/dist/modules/email.d.ts +6 -1
- package/dist/modules/email.js +88 -5
- package/dist/modules/files.js +79 -2
- package/dist/modules/index.js +7 -0
- package/dist/modules/jobs.d.ts +5 -1
- package/dist/modules/jobs.js +74 -4
- package/dist/modules/reporting.d.ts +9 -0
- package/dist/modules/reporting.js +29 -0
- package/dist/modules/sign-in.js +20 -2
- package/dist/modules/types.d.ts +2 -0
- package/dist/modules/workspaces.js +3 -0
- package/dist/org.d.ts +114 -13
- package/dist/org.js +233 -20
- package/dist/plan/ai-boule-key-apply.d.ts +65 -0
- package/dist/plan/ai-boule-key-apply.js +169 -0
- package/dist/plan/ai-boule-key.d.ts +89 -0
- package/dist/plan/ai-boule-key.js +195 -0
- package/dist/plan/analytics.d.ts +8 -1
- package/dist/plan/analytics.js +8 -1
- package/dist/plan/app-canary-apply.d.ts +49 -0
- package/dist/plan/app-canary-apply.js +232 -0
- package/dist/plan/app-canary.d.ts +44 -0
- package/dist/plan/app-canary.js +110 -0
- package/dist/plan/blue-green-apply.d.ts +6 -2
- package/dist/plan/blue-green-apply.js +14 -3
- package/dist/plan/broker-entries.js +20 -5
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +22 -0
- package/dist/plan/diff.js +231 -18
- package/dist/plan/entry-files-key.d.ts +39 -3
- package/dist/plan/entry-files-key.js +103 -6
- package/dist/plan/entry-jobs-boule-key-apply.d.ts +65 -0
- package/dist/plan/entry-jobs-boule-key-apply.js +192 -0
- package/dist/plan/entry-jobs-boule-key.d.ts +59 -0
- package/dist/plan/entry-jobs-boule-key.js +141 -0
- package/dist/plan/entry-jobs.d.ts +2 -0
- package/dist/plan/entry-reporting-key.d.ts +36 -0
- package/dist/plan/entry-reporting-key.js +136 -0
- package/dist/plan/executor.d.ts +108 -1
- package/dist/plan/executor.js +669 -23
- package/dist/plan/files-boule-key-apply.d.ts +73 -0
- package/dist/plan/files-boule-key-apply.js +223 -0
- package/dist/plan/files-boule-key.d.ts +92 -0
- package/dist/plan/files-boule-key.js +175 -0
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/jobs-scheduler-http.d.ts +13 -0
- package/dist/plan/jobs-scheduler-http.js +39 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +4 -1
- package/dist/plan/login-host.js +2 -2
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-diff.js +3 -2
- package/dist/plan/own-jobs-route.d.ts +5 -2
- package/dist/plan/own-jobs-route.js +6 -2
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +24 -0
- package/dist/plan/plan-apply.js +39 -4
- package/dist/plan/platform-diff.d.ts +2 -0
- package/dist/plan/platform-diff.js +15 -1
- package/dist/plan/platform-providers.d.ts +9 -0
- package/dist/plan/platform-providers.js +21 -0
- package/dist/plan/providers-readers.js +25 -1
- package/dist/plan/registered-app-env.d.ts +31 -0
- package/dist/plan/registered-app-env.js +63 -0
- package/dist/plan/registered-app.d.ts +121 -0
- package/dist/plan/registered-app.js +262 -0
- package/dist/plan/release-host.js +2 -1
- package/dist/plan/reporting-boule.d.ts +18 -0
- package/dist/plan/reporting-boule.js +44 -0
- package/dist/plan/reporting.d.ts +90 -0
- package/dist/plan/reporting.js +135 -0
- package/dist/plan/stage-env.js +1 -0
- package/dist/plan/stage-isolation.d.ts +3 -1
- package/dist/plan/stage-isolation.js +20 -2
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +8 -1
- package/dist/plan/stage-sink.d.ts +2 -2
- package/dist/plan/stage-sink.js +2 -2
- package/dist/plan/workspace-client.d.ts +1 -1
- package/dist/plan/workspace-client.js +2 -1
- package/dist/plan/workspace-sign-in.d.ts +1 -1
- package/dist/plan/workspace-sign-in.js +39 -7
- package/dist/platform.d.ts +25 -0
- package/dist/platform.js +20 -0
- package/dist/service-labels.d.ts +8 -0
- package/dist/service-labels.js +8 -0
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/store/postgres.js +13 -0
- package/dist/store/schema.d.ts +485 -2
- package/dist/store/schema.js +38 -1
- package/dist/worker-app-config.js +2 -2
- package/package.json +1 -1
|
@@ -50,6 +50,45 @@ export class HttpJobsSchedulerClient {
|
|
|
50
50
|
syncKey: typeof fields.syncKey === 'string' ? fields.syncKey : undefined,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* foundry#1119: `POST /admin/projects/<name>/keys` (jobs#79). Answers `201 { id, key, ... }`, the key shown
|
|
55
|
+
* once. A refusal throws with the status only: no response text reaches the error.
|
|
56
|
+
*/
|
|
57
|
+
async mintProjectKey(project, grant) {
|
|
58
|
+
const response = await this.fetchImpl(`${this.baseUrl}/admin/projects/${encodeURIComponent(project)}/keys`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
authorization: `Bearer ${this.adminToken}`,
|
|
62
|
+
'content-type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ principal: grant.principal, scopes: [...grant.scopes] }),
|
|
65
|
+
signal: AbortSignal.timeout(ADMIN_CALL_TIMEOUT_MS),
|
|
66
|
+
});
|
|
67
|
+
const body = await response.json().catch(() => undefined);
|
|
68
|
+
if (response.status !== 201) {
|
|
69
|
+
throw Object.assign(new Error(`jobs scheduler: POST /admin/projects/<name>/keys answered ${response.status}`), { status: response.status });
|
|
70
|
+
}
|
|
71
|
+
const fields = typeof body === 'object' && body !== null ? body : {};
|
|
72
|
+
if (typeof fields.id !== 'string' || fields.id.length === 0) {
|
|
73
|
+
throw new Error('jobs scheduler: POST /admin/projects/<name>/keys answered 201 with no key id');
|
|
74
|
+
}
|
|
75
|
+
if (typeof fields.key !== 'string' || fields.key.length === 0) {
|
|
76
|
+
// The id is all the caller can act on: the key is shown once.
|
|
77
|
+
return { id: fields.id, key: '' };
|
|
78
|
+
}
|
|
79
|
+
return { id: fields.id, key: fields.key };
|
|
80
|
+
}
|
|
81
|
+
/** `DELETE /admin/projects/<name>/keys/<id>`. A refusal throws with the status only (404: no such key). */
|
|
82
|
+
async revokeProjectKey(project, keyId) {
|
|
83
|
+
const response = await this.fetchImpl(`${this.baseUrl}/admin/projects/${encodeURIComponent(project)}/keys/${encodeURIComponent(keyId)}`, {
|
|
84
|
+
method: 'DELETE',
|
|
85
|
+
headers: { authorization: `Bearer ${this.adminToken}` },
|
|
86
|
+
signal: AbortSignal.timeout(ADMIN_CALL_TIMEOUT_MS),
|
|
87
|
+
});
|
|
88
|
+
if (response.status === 200 || response.status === 204)
|
|
89
|
+
return;
|
|
90
|
+
throw Object.assign(new Error(`jobs scheduler: DELETE /admin/projects/<name>/keys/<id> answered ${response.status}`), { status: response.status });
|
|
91
|
+
}
|
|
53
92
|
/** `DELETE /admin/projects/<name>`. A refusal throws with the status only. */
|
|
54
93
|
async deleteProject(name) {
|
|
55
94
|
const response = await this.fetchImpl(`${this.baseUrl}/admin/projects/${encodeURIComponent(name)}`, {
|
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' | 'analytics';
|
|
45
|
+
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'edge' | 'heartbeats' | 'github' | 'audit' | 'analytics' | 'reporting';
|
|
46
46
|
kind: string;
|
|
47
47
|
key: string;
|
|
48
48
|
config: Record<string, unknown>;
|
|
@@ -3,7 +3,7 @@ import { OWN_SERVICE_LABELS } from '../service-labels.js';
|
|
|
3
3
|
export { OWN_SERVICE_LABELS };
|
|
4
4
|
/**
|
|
5
5
|
* The names under the org's domain that foundry owns, so no hosting entry may
|
|
6
|
-
* take one: Agora, Boule, the cms studio when the site is on, calls when it uses the
|
|
6
|
+
* take one: Agora, Boule, the analytics and reporting backends, the cms studio when the site is on, calls when it uses the
|
|
7
7
|
* domain, the sign-in host when the issuer does, `auth.`, `mail.`, `files.` and
|
|
8
8
|
* `sfu.` for an org that runs its own services, and every platform service
|
|
9
9
|
* hostname that falls under the domain. An own org with no domain holds its
|
|
@@ -7,10 +7,11 @@ import { cmsStudioHostnameFor, siteHostnameFor } from './hostnames.js';
|
|
|
7
7
|
import { kosmosHostnameFor } from './kosmos.js';
|
|
8
8
|
import { loginHostnameFor } from './login-host.js';
|
|
9
9
|
import { ownHostnames } from './own-hostnames.js';
|
|
10
|
+
import { reportingHostnameFor } from './reporting.js';
|
|
10
11
|
export { OWN_SERVICE_LABELS };
|
|
11
12
|
/**
|
|
12
13
|
* The names under the org's domain that foundry owns, so no hosting entry may
|
|
13
|
-
* take one: Agora, Boule, the cms studio when the site is on, calls when it uses the
|
|
14
|
+
* take one: Agora, Boule, the analytics and reporting backends, the cms studio when the site is on, calls when it uses the
|
|
14
15
|
* domain, the sign-in host when the issuer does, `auth.`, `mail.`, `files.` and
|
|
15
16
|
* `sfu.` for an org that runs its own services, and every platform service
|
|
16
17
|
* hostname that falls under the domain. An own org with no domain holds its
|
|
@@ -35,6 +36,8 @@ export function lockedHostnames(org, platformHostnames) {
|
|
|
35
36
|
locked.add(bouleHostnameFor(org));
|
|
36
37
|
// Reserved always, planned only while something uses analytics (`analyticsWanted`).
|
|
37
38
|
locked.add(analyticsHostnameFor(org));
|
|
39
|
+
// And the reporting backend's, the same way (`reportingWanted`).
|
|
40
|
+
locked.add(reportingHostnameFor(org));
|
|
38
41
|
if (kosmosOn(org))
|
|
39
42
|
locked.add(kosmosHostnameFor(org));
|
|
40
43
|
if (org.site.enabled)
|
package/dist/plan/login-host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ownsServices } from '../org.js';
|
|
1
|
+
import { ownsServices, signsInCustomers } from '../org.js';
|
|
2
2
|
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, agoraHostnameFor, } from './agora.js';
|
|
3
3
|
import { BOULE_CLIENT_ID_SCOPE_KEY, bouleHostnameFor } from './boule.js';
|
|
4
4
|
import { SIGN_IN_IN_APP_SCOPE_KEY, inAppSignIn, inAppStatePairs, } from './sign-in.js';
|
|
@@ -170,7 +170,7 @@ export const LOGIN_BINDING_SCOPE_KEY = 'login_host_binding';
|
|
|
170
170
|
*/
|
|
171
171
|
export function customersEntryKeys(org) {
|
|
172
172
|
return new Set([
|
|
173
|
-
...org.hosting.entries.filter((e) => e.uses?.auth
|
|
173
|
+
...org.hosting.entries.filter((e) => signsInCustomers(e.uses?.auth)).map((e) => e.key),
|
|
174
174
|
// A workspace's customers client signs in the test customers organisation, on the shared host as well.
|
|
175
175
|
...testCustomersEntries(org).map((e) => workspaceClientKey(e.key)),
|
|
176
176
|
]);
|
package/dist/plan/org-names.js
CHANGED
|
@@ -5,6 +5,7 @@ import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
|
|
|
5
5
|
import { workerAppHostname, workspaceHost, workspaceInboxHost } from './hostnames.js';
|
|
6
6
|
import { orgTarget, platformTarget } from './own-services.js';
|
|
7
7
|
import { appNameFor } from './platform-diff.js';
|
|
8
|
+
import { reportingWanted } from './reporting.js';
|
|
8
9
|
/**
|
|
9
10
|
* The names foundry gives the services every company can run beside its own
|
|
10
11
|
* hosting entries: Agora, Boule, the tunnel and its connector, and chat,
|
|
@@ -23,6 +24,8 @@ export function companyServiceNames(org) {
|
|
|
23
24
|
];
|
|
24
25
|
if (analyticsWanted(org))
|
|
25
26
|
names.push(`${s}-analytics`, `${s}-analytics-db`);
|
|
27
|
+
if (reportingWanted(org))
|
|
28
|
+
names.push(`${s}-reporting`, `${s}-reporting-db`);
|
|
26
29
|
if (kosmosOn(org))
|
|
27
30
|
names.push(`${s}-kosmos`, `${s}-kosmos-db`);
|
|
28
31
|
if (org.chat?.enabled) {
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -10,7 +10,7 @@ import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-hos
|
|
|
10
10
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, } from './mail-relay.js';
|
|
11
11
|
import { mailStoreIsLegacy, mailStoreSwitchDescription } from './mail-store.js';
|
|
12
12
|
import { ownHostnames, passkeyRpIdFor } from './own-hostnames.js';
|
|
13
|
-
import { JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY } from './own-jobs-route.js';
|
|
13
|
+
import { JOBS_ENTRY_PATH_PATTERN, JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_PATHS_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY, } from './own-jobs-route.js';
|
|
14
14
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
15
15
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
16
16
|
import { AUTH_ISSUER_SOURCE_ENV_VERSION, AUTH_MCP_ENV_VERSION, AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ISSUER_SOURCE_ENV_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
@@ -92,7 +92,8 @@ export function planOwnJobsRoute(options) {
|
|
|
92
92
|
if (!schedulerApplied && !options.schedulerPlanned)
|
|
93
93
|
return { step: null, blocked: null };
|
|
94
94
|
if (recordedIds[JOBS_PUBLIC_URL_SCOPE_KEY] === `https://${hostname}` &&
|
|
95
|
-
recordedIds[OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY]
|
|
95
|
+
recordedIds[OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY] &&
|
|
96
|
+
recordedIds[OWN_JOBS_ROUTE_PATHS_SCOPE_KEY] === JOBS_ENTRY_PATH_PATTERN) {
|
|
96
97
|
return { step: null, blocked: null };
|
|
97
98
|
}
|
|
98
99
|
const waiting = (reason) => ({
|
|
@@ -17,9 +17,12 @@ export declare const OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY = "cloudflare_own_jobs_tunn
|
|
|
17
17
|
export declare const OWN_JOBS_ROUTE_CONNECTOR_APP_SCOPE_KEY = "coolify_own_jobs_connector_app";
|
|
18
18
|
/**
|
|
19
19
|
* The only paths the public route serves: what a project's app calls (`/sync`, `/timers`, `/timers/<name>`,
|
|
20
|
-
* `/deliveries`)
|
|
20
|
+
* `/deliveries`) and what Boule reads and presses for one project with that project's key (`/project/*`,
|
|
21
|
+
* jobs#79, foundry#1119). `/admin/*` is not on the internet; foundry reaches it on the Coolify network.
|
|
21
22
|
*/
|
|
22
|
-
export declare const JOBS_ENTRY_PATH_PATTERN = "^/(sync$|timers(/|$)|deliveries
|
|
23
|
+
export declare const JOBS_ENTRY_PATH_PATTERN = "^/(sync$|timers(/|$)|deliveries$|project/)";
|
|
24
|
+
/** The path pattern the route's ingress was last written with; a different pattern plans the route again. */
|
|
25
|
+
export declare const OWN_JOBS_ROUTE_PATHS_SCOPE_KEY = "own_jobs_route_paths";
|
|
23
26
|
export declare const JOBS_ROUTE_CALL_TIMEOUT_MS = 60000;
|
|
24
27
|
/** The tunnel's name and the connector app's, one pair per company. */
|
|
25
28
|
export declare const jobsRouteNames: (target: Pick<ServiceTarget, "id">) => {
|
|
@@ -18,9 +18,12 @@ export const OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY = 'cloudflare_own_jobs_tunnel';
|
|
|
18
18
|
export const OWN_JOBS_ROUTE_CONNECTOR_APP_SCOPE_KEY = 'coolify_own_jobs_connector_app';
|
|
19
19
|
/**
|
|
20
20
|
* The only paths the public route serves: what a project's app calls (`/sync`, `/timers`, `/timers/<name>`,
|
|
21
|
-
* `/deliveries`)
|
|
21
|
+
* `/deliveries`) and what Boule reads and presses for one project with that project's key (`/project/*`,
|
|
22
|
+
* jobs#79, foundry#1119). `/admin/*` is not on the internet; foundry reaches it on the Coolify network.
|
|
22
23
|
*/
|
|
23
|
-
export const JOBS_ENTRY_PATH_PATTERN = '^/(sync$|timers(/|$)|deliveries
|
|
24
|
+
export const JOBS_ENTRY_PATH_PATTERN = '^/(sync$|timers(/|$)|deliveries$|project/)';
|
|
25
|
+
/** The path pattern the route's ingress was last written with; a different pattern plans the route again. */
|
|
26
|
+
export const OWN_JOBS_ROUTE_PATHS_SCOPE_KEY = 'own_jobs_route_paths';
|
|
24
27
|
export const JOBS_ROUTE_CALL_TIMEOUT_MS = 60_000;
|
|
25
28
|
/** The tunnel's name and the connector app's, one pair per company. */
|
|
26
29
|
export const jobsRouteNames = (target) => ({
|
|
@@ -55,6 +58,7 @@ export async function provisionJobsRoute(scope, ports, target, input) {
|
|
|
55
58
|
{ service: 'http_status:404' },
|
|
56
59
|
],
|
|
57
60
|
}));
|
|
61
|
+
await record(OWN_JOBS_ROUTE_PATHS_SCOPE_KEY, JOBS_ENTRY_PATH_PATTERN);
|
|
58
62
|
await withTimeout(`${at}: connector app`, ms * 5, () => ensureCloudflaredConnector({
|
|
59
63
|
coolify: ports.coolify,
|
|
60
64
|
tunnels,
|
|
@@ -119,6 +119,12 @@ export declare const LIMITS: {
|
|
|
119
119
|
autoDeploy: false | "declared";
|
|
120
120
|
capabilities: CapabilityTier;
|
|
121
121
|
};
|
|
122
|
+
readonly reporting: {
|
|
123
|
+
memory: string;
|
|
124
|
+
dbMemory: string;
|
|
125
|
+
autoDeploy: false | "declared";
|
|
126
|
+
capabilities: CapabilityTier;
|
|
127
|
+
};
|
|
122
128
|
readonly companyOther: {
|
|
123
129
|
memory: string;
|
|
124
130
|
dbMemory: string;
|
package/dist/plan/own-limits.js
CHANGED
|
@@ -596,6 +596,11 @@ export interface FoundryPlanApplyOptions {
|
|
|
596
596
|
* reported blocked instead of planned.
|
|
597
597
|
*/
|
|
598
598
|
broker?: boolean;
|
|
599
|
+
/**
|
|
600
|
+
* ADR 0003: true when the host's executor was built with a `registrarApi` port. Optional, and false without
|
|
601
|
+
* it: an entry whose customers and team both sign in is reported blocked instead of registered.
|
|
602
|
+
*/
|
|
603
|
+
registrarApi?: boolean;
|
|
599
604
|
/**
|
|
600
605
|
* #318: true when the host's executor was built with a `WorkersPort`.
|
|
601
606
|
* Optional, and false without it: each entry's `worker` Worker is then
|
|
@@ -613,6 +618,21 @@ export interface FoundryPlanApplyOptions {
|
|
|
613
618
|
keysRegistrarUrl?: string;
|
|
614
619
|
/** True when the host's executor was built with an `analytics` port. A `uses.analytics` key is blocked without it. */
|
|
615
620
|
analyticsPort?: boolean;
|
|
621
|
+
/** True when the host's executor was built with a `reporting` port. A `uses.reporting` key is blocked without it. */
|
|
622
|
+
reportingPort?: boolean;
|
|
623
|
+
/**
|
|
624
|
+
* foundry#1119: true when the host's jobs scheduler port (`FoundryStepExecutorPorts.jobsScheduler`, or the
|
|
625
|
+
* platform's `HttpJobsSchedulerClient`) has `mintProjectKey` and `revokeProjectKey`. Without it each
|
|
626
|
+
* `uses.jobs` entry's Boule key is reported blocked.
|
|
627
|
+
*/
|
|
628
|
+
jobsProjectKeys?: boolean;
|
|
629
|
+
/**
|
|
630
|
+
* foundry#1122: true once ai offers a company Boule's five read permissions (`ai.usage:read`,
|
|
631
|
+
* `ai.runs:read`, `ai.budgets:read`, `ai.budgets:update`, `ai.offerings:read`; all `offered: false` today).
|
|
632
|
+
* With it each organisation is offered them, its own ai key is minted again with them, and Boule gets a key
|
|
633
|
+
* below it. Without it Boule's ai key is reported blocked.
|
|
634
|
+
*/
|
|
635
|
+
aiBouleReads?: boolean;
|
|
616
636
|
/** C2: true when the host's executor was built with a `WorkerAppDatabasePort`. */
|
|
617
637
|
workerAppDatabase?: boolean;
|
|
618
638
|
/** True when that port has `deletableTables`. A `workerApp.database.noDelete` is blocked without it. */
|
|
@@ -773,11 +793,15 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
773
793
|
private readonly zitadelMcp;
|
|
774
794
|
private readonly zitadelOrgMetadata;
|
|
775
795
|
private readonly broker;
|
|
796
|
+
private readonly registrarApi;
|
|
776
797
|
private readonly edgeWorkers;
|
|
777
798
|
private readonly workerApps;
|
|
778
799
|
private readonly workerAppKeysUrl;
|
|
779
800
|
private readonly keysRegistrarUrl;
|
|
780
801
|
private readonly analyticsPort;
|
|
802
|
+
private readonly reportingPort;
|
|
803
|
+
private readonly jobsProjectKeys;
|
|
804
|
+
private readonly aiBouleReads;
|
|
781
805
|
private readonly workerAppDatabase;
|
|
782
806
|
private readonly workerAppNoDeleteCheck;
|
|
783
807
|
private readonly workerAppRegistryAccountId;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -11,6 +11,7 @@ import { StaleLeaseError, } from '../store.js';
|
|
|
11
11
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
12
12
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
13
13
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
14
|
+
import { AI_BOULE_KEY_SCOPE_KEY, AI_BOULE_UNDELIVERED_SCOPE_KEY } from './ai-boule-key.js';
|
|
14
15
|
import { APPS_DB_ENTRY_KEY } from './apps-db-route.js';
|
|
15
16
|
import { NetworkMoveBackupMissingError, OrgPostgresMigrationBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForOrgPostgresMigration, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
16
17
|
import { ARCHON_REPO, PRIMARY_SERVER_SCOPE_KEY, archonSubject, blueGreenSubjects, declaresBlueGreen, lbOrigin, } from './blue-green.js';
|
|
@@ -20,7 +21,7 @@ import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
|
20
21
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
21
22
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
22
23
|
import { BOULE_EDGE_ENTRY_KEY, bouleEdgeKeyWanted, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
23
|
-
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
24
|
+
import { FILES_BOULE_KEY_SCOPE_KEY, FILES_BOULE_UNDELIVERED_SCOPE_KEY, FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
24
25
|
import { probeRepoKeys, repoKeyProbeTargets, repoKeysInPlay, } from './entry-repo-key.js';
|
|
25
26
|
import { PLATFORM_GITHUB_APP_REFS } from './held-key-refs.js';
|
|
26
27
|
import { KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY, KEYS_REGISTRAR_SCOPE_KEY, KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY, } from './keys-registrar.js';
|
|
@@ -185,11 +186,15 @@ export class FoundryPlanApply {
|
|
|
185
186
|
zitadelMcp;
|
|
186
187
|
zitadelOrgMetadata;
|
|
187
188
|
broker;
|
|
189
|
+
registrarApi;
|
|
188
190
|
edgeWorkers;
|
|
189
191
|
workerApps;
|
|
190
192
|
workerAppKeysUrl;
|
|
191
193
|
keysRegistrarUrl;
|
|
192
194
|
analyticsPort;
|
|
195
|
+
reportingPort;
|
|
196
|
+
jobsProjectKeys;
|
|
197
|
+
aiBouleReads;
|
|
193
198
|
workerAppDatabase;
|
|
194
199
|
workerAppNoDeleteCheck;
|
|
195
200
|
workerAppRegistryAccountId;
|
|
@@ -231,11 +236,15 @@ export class FoundryPlanApply {
|
|
|
231
236
|
this.zitadelMcp = options.zitadelMcp ?? false;
|
|
232
237
|
this.zitadelOrgMetadata = options.zitadelOrgMetadata ?? false;
|
|
233
238
|
this.broker = options.broker ?? false;
|
|
239
|
+
this.registrarApi = options.registrarApi ?? false;
|
|
234
240
|
this.edgeWorkers = options.edgeWorkers ?? false;
|
|
235
241
|
this.workerApps = options.workerApps ?? false;
|
|
236
242
|
this.workerAppKeysUrl = options.workerAppKeysUrl;
|
|
237
243
|
this.keysRegistrarUrl = options.keysRegistrarUrl;
|
|
238
244
|
this.analyticsPort = options.analyticsPort ?? false;
|
|
245
|
+
this.reportingPort = options.reportingPort ?? false;
|
|
246
|
+
this.jobsProjectKeys = options.jobsProjectKeys ?? false;
|
|
247
|
+
this.aiBouleReads = options.aiBouleReads ?? false;
|
|
239
248
|
this.workerAppDatabase = options.workerAppDatabase ?? false;
|
|
240
249
|
this.workerAppNoDeleteCheck = options.workerAppNoDeleteCheck ?? false;
|
|
241
250
|
this.workerAppRegistryAccountId = options.workerAppRegistryAccountId;
|
|
@@ -1703,11 +1712,15 @@ export class FoundryPlanApply {
|
|
|
1703
1712
|
broker: ownsServices(org) && this.hostOwnServices && this.executor.ownBrokerPort
|
|
1704
1713
|
? await this.executor.ownBrokerPort(org, recorded)
|
|
1705
1714
|
: this.broker,
|
|
1715
|
+
registrarApi: this.registrarApi,
|
|
1706
1716
|
edgeWorkers: this.edgeWorkers,
|
|
1707
1717
|
workerApps: this.workerApps,
|
|
1708
1718
|
workerAppKeysUrl: this.workerAppKeysUrl,
|
|
1709
1719
|
keysRegistrarUrl: this.keysRegistrarUrl,
|
|
1710
1720
|
analyticsPort: this.analyticsPort,
|
|
1721
|
+
reportingPort: this.reportingPort,
|
|
1722
|
+
jobsProjectKeys: this.jobsProjectKeys,
|
|
1723
|
+
aiBouleReads: this.aiBouleReads,
|
|
1711
1724
|
workerAppDatabase: this.workerAppDatabase,
|
|
1712
1725
|
workerAppNoDeleteCheck: this.workerAppNoDeleteCheck,
|
|
1713
1726
|
workerAppRegistryAccountId: this.workerAppRegistryAccountId,
|
|
@@ -2358,7 +2371,7 @@ export function stepEntryKey(step, orgId, entries = []) {
|
|
|
2358
2371
|
}
|
|
2359
2372
|
const entryOwned = (system === 'coolify' && kind === 'app') ||
|
|
2360
2373
|
(system === 'cloudflare' && (kind === 'worker' || kind === 'worker-app' || kind === 'gate')) ||
|
|
2361
|
-
(system === 'jobs' && kind === 'project') ||
|
|
2374
|
+
(system === 'jobs' && (kind === 'project' || kind === 'boule-key')) ||
|
|
2362
2375
|
(system === 'github' && kind === 'deploy-key') ||
|
|
2363
2376
|
(system === 'audit' && kind === 'ledger') ||
|
|
2364
2377
|
(kind === 'key' && (system === 'ai' || system === 'edge' || system === 'files'));
|
|
@@ -2770,6 +2783,8 @@ const STEP_FIELDS = {
|
|
|
2770
2783
|
// F4 (#925): both hang off the company's Apps switch.
|
|
2771
2784
|
'coolify:analytics': 'hosting.entries.uses.analytics',
|
|
2772
2785
|
'coolify:analytics-boule': 'boule.apps',
|
|
2786
|
+
'coolify:reporting': 'hosting.entries.uses.reporting',
|
|
2787
|
+
'coolify:reporting-boule': 'boule.apps',
|
|
2773
2788
|
'coolify:org-apps-admin': 'boule.apps',
|
|
2774
2789
|
'cloudflare:apps-db-route': 'boule.apps',
|
|
2775
2790
|
'cloudflare:apps-zones': 'boule.apps',
|
|
@@ -2780,6 +2795,9 @@ function stepField(action) {
|
|
|
2780
2795
|
(action.desired.entry === 'boule' || action.desired.entry === 'agora')) {
|
|
2781
2796
|
return `${action.desired.entry}.blueGreen`;
|
|
2782
2797
|
}
|
|
2798
|
+
// Boule's ai key (foundry#1122) belongs to the org's ai, like the org's own key.
|
|
2799
|
+
if (action.system === 'ai' && action.kind === 'boule-key')
|
|
2800
|
+
return 'ai';
|
|
2783
2801
|
// An entry's ai key (F6a) is keyed `<orgId>:<entry>`; the org's own is `<orgId>`.
|
|
2784
2802
|
if (action.system === 'ai' && action.kind === 'key' && action.key.includes(':')) {
|
|
2785
2803
|
return 'hosting.entries.uses.ai';
|
|
@@ -2787,16 +2805,23 @@ function stepField(action) {
|
|
|
2787
2805
|
// The org's own files key (F6b) is keyed `<orgId>`, an entry's `<orgId>:<entry>`.
|
|
2788
2806
|
if (action.system === 'files' && action.kind === 'key')
|
|
2789
2807
|
return 'hosting.entries.uses.files';
|
|
2808
|
+
// Boule's files key (foundry#1121) is keyed `<orgId>` and follows the entries' `uses.files`.
|
|
2809
|
+
if (action.system === 'files' && action.kind === 'boule-key')
|
|
2810
|
+
return 'hosting.entries.uses.files';
|
|
2790
2811
|
// An entry's edge key (F7): always `<orgId>:<entry>`, edge has no org-root key.
|
|
2791
2812
|
if (action.system === 'edge' && action.kind === 'key')
|
|
2792
2813
|
return 'hosting.entries.uses.edge';
|
|
2793
2814
|
// An entry's jobs project: always `<orgId>:<entry>`.
|
|
2794
|
-
if (action.system === 'jobs' && action.kind === 'project')
|
|
2815
|
+
if (action.system === 'jobs' && (action.kind === 'project' || action.kind === 'boule-key'))
|
|
2795
2816
|
return 'hosting.entries.uses.jobs';
|
|
2796
2817
|
// An entry's analytics site key: always `<orgId>:<entry>`.
|
|
2797
2818
|
if (action.system === 'analytics' && action.kind === 'key') {
|
|
2798
2819
|
return 'hosting.entries.uses.analytics';
|
|
2799
2820
|
}
|
|
2821
|
+
// An entry's reporting site key: always `<orgId>:<entry>`.
|
|
2822
|
+
if (action.system === 'reporting' && action.kind === 'key') {
|
|
2823
|
+
return 'hosting.entries.uses.reporting';
|
|
2824
|
+
}
|
|
2800
2825
|
// An entry's audit ledger registration: always `<orgId>:<entry>`.
|
|
2801
2826
|
if (action.system === 'audit' && action.kind === 'ledger')
|
|
2802
2827
|
return 'hosting.entries.uses.audit';
|
|
@@ -2839,7 +2864,12 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
2839
2864
|
// `planAiKey` plans no step for a key minted but never delivered, so no
|
|
2840
2865
|
// step is left to fail. Without this, the next apply would record the org
|
|
2841
2866
|
// as converged with no working key.
|
|
2842
|
-
if (serviceIds[AI_KEY_UNDELIVERED_SCOPE_KEY] &&
|
|
2867
|
+
if (serviceIds[AI_KEY_UNDELIVERED_SCOPE_KEY] &&
|
|
2868
|
+
serviceIds[AI_KEY_UNDELIVERED_SCOPE_KEY] !== serviceIds[`ai:key:${org.id}`])
|
|
2869
|
+
failed.add('ai');
|
|
2870
|
+
// Boule's ai key (foundry#1122): minted, never held, so no step is planned.
|
|
2871
|
+
if (serviceIds[AI_BOULE_UNDELIVERED_SCOPE_KEY] &&
|
|
2872
|
+
serviceIds[AI_BOULE_UNDELIVERED_SCOPE_KEY] !== serviceIds[AI_BOULE_KEY_SCOPE_KEY])
|
|
2843
2873
|
failed.add('ai');
|
|
2844
2874
|
// The same for an entry's ai key (F6a): minted, never delivered, so no step is planned.
|
|
2845
2875
|
for (const entry of org.hosting.entries) {
|
|
@@ -2858,6 +2888,11 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
2858
2888
|
failed.add('hosting.entries.uses.files');
|
|
2859
2889
|
}
|
|
2860
2890
|
}
|
|
2891
|
+
// Boule's files key (foundry#1121): minted, never held, so no step is planned.
|
|
2892
|
+
if (serviceIds[FILES_BOULE_UNDELIVERED_SCOPE_KEY] &&
|
|
2893
|
+
serviceIds[FILES_BOULE_UNDELIVERED_SCOPE_KEY] !== serviceIds[FILES_BOULE_KEY_SCOPE_KEY]) {
|
|
2894
|
+
failed.add('hosting.entries.uses.files');
|
|
2895
|
+
}
|
|
2861
2896
|
}
|
|
2862
2897
|
// The same for an entry's edge key (F7): minted, never delivered is no
|
|
2863
2898
|
// longer blocked forever on its own (the next apply revokes it and
|
|
@@ -1424,6 +1424,8 @@ export interface PlanPlatformOptions {
|
|
|
1424
1424
|
};
|
|
1425
1425
|
/** foundry#1024: why the usage tail Worker is blocked, or absent when it is not. */
|
|
1426
1426
|
usageTail?: string;
|
|
1427
|
+
/** foundry#1133: why the app-layer canary is blocked (`appCanaryBlockedReason`), or absent when it is not. */
|
|
1428
|
+
appCanary?: string;
|
|
1427
1429
|
/** Why the organisation's release webhook step cannot run (the token is not held), or absent. */
|
|
1428
1430
|
githubReleaseHook?: string;
|
|
1429
1431
|
/**
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { VERSIONS } from '../bootstrap/versions.js';
|
|
3
3
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
4
|
-
import { heldEnvRef } from '../org.js';
|
|
4
|
+
import { entrySettingsEnv, heldEnvRef } from '../org.js';
|
|
5
5
|
import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
6
|
+
import { planAppCanary } from './app-canary.js';
|
|
6
7
|
import { ARCHON_REPO, planArchonBlueGreen } from './blue-green.js';
|
|
7
8
|
import { buildDriftNotes } from './build-record.js';
|
|
8
9
|
import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
|
|
@@ -958,6 +959,12 @@ export function resolveAppEnv(org, entry, recorded, available = { sharedSecret:
|
|
|
958
959
|
else if (typeof declared !== 'string')
|
|
959
960
|
unresolved.push([name, declared.from]);
|
|
960
961
|
}
|
|
962
|
+
// #1116: the module settings that go to the app (Files' share links, Email's sender name and reply-to).
|
|
963
|
+
// A name the entry's own `env` declares is refused by the org validator, so these never overwrite one.
|
|
964
|
+
for (const [name, value] of Object.entries(entrySettingsEnv(entry))) {
|
|
965
|
+
values[name] = value;
|
|
966
|
+
marker[name] = value;
|
|
967
|
+
}
|
|
961
968
|
// F13: the entry's customer-grants credential is held and written by the app step. The marker names it,
|
|
962
969
|
// never its value, so adding or removing the flag replans the step.
|
|
963
970
|
if (entry.uses?.customerGrants && available.sharedSecret) {
|
|
@@ -1541,6 +1548,10 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1541
1548
|
const usageTail = planUsageTail(platform, recordedIds, blocked.usageTail, edgeApplied);
|
|
1542
1549
|
if (usageTail.step)
|
|
1543
1550
|
plannedSteps.push(usageTail.step);
|
|
1551
|
+
// foundry#1133: after edge too; its Worker, route and record all go through it.
|
|
1552
|
+
const appCanary = planAppCanary(platform, recordedIds, blocked.appCanary, edgeApplied);
|
|
1553
|
+
if (appCanary.step)
|
|
1554
|
+
plannedSteps.push(appCanary.step);
|
|
1544
1555
|
const dnsBlocked = edgeApplied || edge.step
|
|
1545
1556
|
? undefined
|
|
1546
1557
|
: (service) => `the platform's ${service} is blocked, not planned: every DNS record is written through edge, and edge is neither applied nor planned (${platform.edge.enabled
|
|
@@ -1813,6 +1824,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1813
1824
|
for (const description of usageTail.blocked) {
|
|
1814
1825
|
unmanaged.push({ description, key: 'usage-tail' });
|
|
1815
1826
|
}
|
|
1827
|
+
for (const description of appCanary.blocked) {
|
|
1828
|
+
unmanaged.push({ description, key: 'app-canary' });
|
|
1829
|
+
}
|
|
1816
1830
|
if (releaseHook.blocked) {
|
|
1817
1831
|
unmanaged.push({ description: releaseHook.blocked, key: 'github-release-hook' });
|
|
1818
1832
|
}
|
|
@@ -25,6 +25,7 @@ import { type PlatformRegistryDeps } from './platform-registry.js';
|
|
|
25
25
|
import type { SigninDbBackupPort } from './signin-db-backup.js';
|
|
26
26
|
import { type PlatformZitadelMailBranding } from './signin-mail.js';
|
|
27
27
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
28
|
+
import { type PlatformAppCanaryDeps } from './app-canary-apply.js';
|
|
28
29
|
import { type PlatformGithubReleaseHookDeps } from './github-release-hook.js';
|
|
29
30
|
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
30
31
|
import { type PlatformUptimeDeps } from './uptime-worker.js';
|
|
@@ -343,6 +344,12 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
343
344
|
* for the same reason as `auth`; without it the step is reported blocked.
|
|
344
345
|
*/
|
|
345
346
|
usageTail?: PlatformUsageTailDeps;
|
|
347
|
+
/**
|
|
348
|
+
* foundry#1133: dependencies for the app-layer canary step
|
|
349
|
+
* (`cloudflare:worker` keyed {@link PLATFORM_APP_CANARY_KEY}) -- optional
|
|
350
|
+
* for the same reason as `auth`; without it the step is reported blocked.
|
|
351
|
+
*/
|
|
352
|
+
appCanary?: PlatformAppCanaryDeps;
|
|
346
353
|
/**
|
|
347
354
|
* #604: dependencies for the `cloudflare:access-provider` step -- optional
|
|
348
355
|
* for the same reason as `auth`; edge and sign-in applied with this unset
|
|
@@ -380,6 +387,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
380
387
|
private readonly heartbeats?;
|
|
381
388
|
private readonly uptime?;
|
|
382
389
|
private readonly usageTail?;
|
|
390
|
+
private readonly appCanary?;
|
|
383
391
|
private readonly accessProvider?;
|
|
384
392
|
private readonly oikosIngress?;
|
|
385
393
|
private readonly githubReleaseHook?;
|
|
@@ -448,6 +456,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
448
456
|
blocked?: string;
|
|
449
457
|
};
|
|
450
458
|
usageTail?: string;
|
|
459
|
+
appCanary?: string;
|
|
451
460
|
githubReleaseHook?: string;
|
|
452
461
|
githubReleaseHookReady?: boolean;
|
|
453
462
|
githubReleaseHookHash?: string;
|
|
@@ -45,6 +45,8 @@ import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
|
45
45
|
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, signinDbDsn, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
46
46
|
import { PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY, applySigninMail, signinMailMarker, signinMailName, } from './signin-mail.js';
|
|
47
47
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
48
|
+
import { appCanaryBlockedReason, provisionPlatformAppCanary, } from './app-canary-apply.js';
|
|
49
|
+
import { PLATFORM_APP_CANARY_KEY } from './app-canary.js';
|
|
48
50
|
import { githubReleaseHookPlanInputs, provisionGithubReleaseHook, } from './github-release-hook.js';
|
|
49
51
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
50
52
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
@@ -155,6 +157,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
155
157
|
heartbeats;
|
|
156
158
|
uptime;
|
|
157
159
|
usageTail;
|
|
160
|
+
appCanary;
|
|
158
161
|
accessProvider;
|
|
159
162
|
oikosIngress;
|
|
160
163
|
githubReleaseHook;
|
|
@@ -178,6 +181,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
178
181
|
this.heartbeats = options.heartbeats;
|
|
179
182
|
this.uptime = options.uptime;
|
|
180
183
|
this.usageTail = options.usageTail;
|
|
184
|
+
this.appCanary = options.appCanary;
|
|
181
185
|
this.accessProvider = options.accessProvider;
|
|
182
186
|
this.oikosIngress = options.oikosIngress;
|
|
183
187
|
this.githubReleaseHook = options.githubReleaseHook;
|
|
@@ -623,6 +627,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
623
627
|
}
|
|
624
628
|
await this.uptimeBlocked(platform, recordedIds, blocked, owner);
|
|
625
629
|
await this.usageTailBlocked(platform, recordedIds, blocked);
|
|
630
|
+
const appCanaryBlocked = await appCanaryBlockedReason(this.appCanary, platform, recordedIds);
|
|
631
|
+
if (appCanaryBlocked)
|
|
632
|
+
blocked.appCanary = appCanaryBlocked;
|
|
626
633
|
if (platform.archon.enabled && !this.githubReleaseHook) {
|
|
627
634
|
blocked.githubReleaseHook = GITHUB_RELEASE_HOOK_DEPS_MISSING;
|
|
628
635
|
}
|
|
@@ -1217,6 +1224,14 @@ export class FoundryPlatformStepExecutor {
|
|
|
1217
1224
|
}
|
|
1218
1225
|
return provisionPlatformUsageTail(context, this.usageTail);
|
|
1219
1226
|
}
|
|
1227
|
+
if (action.system === 'cloudflare' &&
|
|
1228
|
+
action.kind === 'worker' &&
|
|
1229
|
+
action.key === PLATFORM_APP_CANARY_KEY) {
|
|
1230
|
+
if (!this.appCanary) {
|
|
1231
|
+
throw new Error('platform:app-canary needs FoundryPlatformStepExecutor to be constructed with its app-canary dependencies (foundry#1133)');
|
|
1232
|
+
}
|
|
1233
|
+
return provisionPlatformAppCanary(context, this.appCanary);
|
|
1234
|
+
}
|
|
1220
1235
|
if (action.system === 'coolify' &&
|
|
1221
1236
|
action.kind === 'app' &&
|
|
1222
1237
|
action.key === PLATFORM_OIKOS_INGRESS_KEY) {
|
|
@@ -4414,6 +4429,12 @@ export const PLATFORM_ARCHON_PUBLIC_PATHS = [
|
|
|
4414
4429
|
{ name: 'github-releases', path: '/api/github/releases' },
|
|
4415
4430
|
{ name: 'github-install-callback', path: '/api/github/install/callback' },
|
|
4416
4431
|
{ name: 'org-github', path: '/api/orgs/*/github' },
|
|
4432
|
+
// archon#808: the company's mail, read by Boule's Email page; Archon checks `orgs:read` itself.
|
|
4433
|
+
{ name: 'org-mail', path: '/api/orgs/*/mail' },
|
|
4434
|
+
{ name: 'org-mail-sub', path: '/api/orgs/*/mail/*' },
|
|
4435
|
+
// The audit host (foundry#1110): Archon mounts `createAuditHostHandler` here; every route below it checks a
|
|
4436
|
+
// company's bearer key itself. Boule's `AUDIT_HOST_URL` is `https://<archon host>/api/audit`.
|
|
4437
|
+
{ name: 'audit-host', path: '/api/audit' },
|
|
4417
4438
|
];
|
|
4418
4439
|
const PLATFORM_ARCHON_PUBLIC_POLICY_NAME = 'foundry: public path';
|
|
4419
4440
|
const archonPublicAppName = (name) => `${PLATFORM_ARCHON_ACCESS_APP_NAME}:public:${name}`;
|
|
@@ -5,6 +5,7 @@ import { BOULE_MAIL_TOKEN_ENV, BOULE_MANAGED_ENV_KEYS } from './boule.js';
|
|
|
5
5
|
import { previewsOn } from './coolify-previews.js';
|
|
6
6
|
import { KOSMOS_MANAGED_ENV_KEYS } from './kosmos.js';
|
|
7
7
|
import { APP_GONE_KIND, } from './live.js';
|
|
8
|
+
import { REPORTING_MAIL_TOKEN_ENV, REPORTING_MANAGED_ENV_KEYS } from './reporting.js';
|
|
8
9
|
import { SIGN_IN_UPSTREAM_ENV } from './sign-in.js';
|
|
9
10
|
/**
|
|
10
11
|
* A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
|
|
@@ -334,6 +335,28 @@ async function withAnalyticsEnvConfig(client, resource, recorded) {
|
|
|
334
335
|
}
|
|
335
336
|
return { ...resource, config: { ...resource.config, env } };
|
|
336
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* The recorded reporting backend's live env (`APP_URL`, `AUTH_ORGANIZATION_ID`, `MAIL_FROM`) as `config.env`,
|
|
340
|
+
* read like {@link withAnalyticsEnvConfig}, and `config.mailTokenHeld`: whether it holds a Lettermint token.
|
|
341
|
+
*/
|
|
342
|
+
async function withReportingEnvConfig(client, resource, recorded) {
|
|
343
|
+
if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('reporting:')) {
|
|
344
|
+
return resource;
|
|
345
|
+
}
|
|
346
|
+
const managed = REPORTING_MANAGED_ENV_KEYS;
|
|
347
|
+
const env = {};
|
|
348
|
+
let mailTokenHeld = false;
|
|
349
|
+
for (const e of await client.listEnvs(resource.key)) {
|
|
350
|
+
if (e.is_preview)
|
|
351
|
+
continue;
|
|
352
|
+
if (managed.includes(e.key))
|
|
353
|
+
env[e.key] = e.value;
|
|
354
|
+
// The token is a secret: only whether it is there leaves this reader.
|
|
355
|
+
if (e.key === REPORTING_MAIL_TOKEN_ENV && e.value)
|
|
356
|
+
mailTokenHeld = true;
|
|
357
|
+
}
|
|
358
|
+
return { ...resource, config: { ...resource.config, env, mailTokenHeld } };
|
|
359
|
+
}
|
|
337
360
|
/** `@wtfalch/providers`' own `LiveResource` (`resource.ts`) has no `system` field — foundry's adds it at this boundary. */
|
|
338
361
|
function toFoundryResources(system, resources) {
|
|
339
362
|
return resources.map((r) => ({ system, kind: r.kind, key: r.key, config: r.config }));
|
|
@@ -369,7 +392,8 @@ export class ProvidersCoolifyReader {
|
|
|
369
392
|
const withBouleConfig = await Promise.all(withAgoraConfig.map((r) => withBouleEnvConfig(this.client, r, recorded, liveKeys)));
|
|
370
393
|
const withKosmosConfig = await Promise.all(withBouleConfig.map((r) => withKosmosEnvConfig(this.client, r, recorded)));
|
|
371
394
|
const withAnalyticsConfig = await Promise.all(withKosmosConfig.map((r) => withAnalyticsEnvConfig(this.client, r, recorded)));
|
|
372
|
-
|
|
395
|
+
const withReportingConfig = await Promise.all(withAnalyticsConfig.map((r) => withReportingEnvConfig(this.client, r, recorded)));
|
|
396
|
+
return applyRecordedIds(withReportingConfig, recorded);
|
|
373
397
|
}
|
|
374
398
|
async liveEnvEquals(orgId, appKey, name, value) {
|
|
375
399
|
const recorded = await this.resolveRecordedIds(orgId);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0003 (auth ADR 0008): what an entry whose customers and team both sign in was answered by the sign-in
|
|
3
|
+
* broker's registrar, as recorded. A leaf module: the readers of the app's sign-in env (`broker-entries.ts`,
|
|
4
|
+
* `workspace-sign-in.ts`) and the step itself (`registered-app.ts`) all read it here, so they agree.
|
|
5
|
+
*/
|
|
6
|
+
import { type DeclaredOrg } from '../org.js';
|
|
7
|
+
/** The ids the broker answered for one copy of a registered app. Ids only: never the key. */
|
|
8
|
+
export interface RegisteredAppEnv {
|
|
9
|
+
AUTH_ORGANIZATION_ID: string;
|
|
10
|
+
AUTH_CLIENT_ID: string;
|
|
11
|
+
AUTH_TEAM_CLIENT_ID: string;
|
|
12
|
+
AUTH_TEAM_ORGANIZATION_ID: string;
|
|
13
|
+
}
|
|
14
|
+
/** The env names the broker's answer gives the app beside `AUTH_ISSUER` and `AUTH_APP_KEY`. */
|
|
15
|
+
export declare const REGISTERED_APP_ENV_NAMES: readonly ["AUTH_ORGANIZATION_ID", "AUTH_CLIENT_ID", "AUTH_TEAM_CLIENT_ID", "AUTH_TEAM_ORGANIZATION_ID"];
|
|
16
|
+
/** The team's two env names, which only an app both sign in to holds. */
|
|
17
|
+
export declare const TEAM_ENV_NAMES: readonly ["AUTH_TEAM_CLIENT_ID", "AUTH_TEAM_ORGANIZATION_ID"];
|
|
18
|
+
/**
|
|
19
|
+
* Scope id holding {@link RegisteredAppEnv} as JSON for one copy (`<entry>` live, `<entry>-work` the
|
|
20
|
+
* workspace), recorded once the app's env holds it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const registeredAppEnvScopeKey: (clientKey: string) => string;
|
|
23
|
+
/** Scope id holding the registration last sent for one copy (JSON, no key); a different want plans the step again. */
|
|
24
|
+
export declare const registeredAppAppliedScopeKey: (clientKey: string) => string;
|
|
25
|
+
/** The recorded answer of one copy, or `null` when none is recorded or it does not read as one. */
|
|
26
|
+
export declare function registeredAppEnv(recorded: Readonly<Record<string, string>>, clientKey: string): RegisteredAppEnv | null;
|
|
27
|
+
/** The client ids (the customers' and the team's) of the live copy of every entry both sign in to, as recorded. */
|
|
28
|
+
export declare function liveRegisteredClientIds(org: Pick<DeclaredOrg, 'hosting'>, recorded: Readonly<Record<string, string>>): {
|
|
29
|
+
clientIds: string[];
|
|
30
|
+
teamClientIds: string[];
|
|
31
|
+
};
|