@wtfalch/foundry 0.62.0 → 0.64.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/apps/index.d.ts +1 -0
- package/dist/apps/index.js +1 -0
- package/dist/apps/server-entry.d.ts +43 -0
- package/dist/apps/server-entry.js +99 -0
- package/dist/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/build-json.d.ts +25 -0
- package/dist/build-json.js +61 -0
- package/dist/catalogue/copy-en.js +10 -0
- package/dist/catalogue/integrations.js +10 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/customer-grants.d.ts +3 -0
- package/dist/modules/customer-grants.js +28 -0
- package/dist/modules/index.js +49 -1
- package/dist/modules/repository.d.ts +6 -0
- package/dist/modules/repository.js +40 -0
- package/dist/modules/types.d.ts +8 -0
- package/dist/native-build.d.ts +7 -27
- package/dist/native-build.js +7 -27
- package/dist/org.d.ts +19 -1
- package/dist/org.js +45 -3
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +61 -2
- package/dist/plan/blue-green.js +80 -6
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +21 -2
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +7 -0
- package/dist/plan/executor.d.ts +71 -1
- package/dist/plan/executor.js +491 -56
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -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 +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +23 -1
- package/dist/plan/platform-diff.js +96 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +156 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
package/dist/native-build.d.ts
CHANGED
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
* workflow, and the key Boule presents when it asks the host (Archon) to
|
|
8
8
|
* dispatch it.
|
|
9
9
|
*
|
|
10
|
-
* Neither function here is wired into `plan/diff.ts` or `plan/executor.ts`:
|
|
11
10
|
* `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
|
|
12
|
-
* route, not foundry's apply loop
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* route, not foundry's apply loop. The key itself is minted by
|
|
12
|
+
* `plan/executor.ts`'s `coolifyBoule` (`ensureNativeBuildKey`), reusing the
|
|
13
|
+
* same `mintIfAbsent` path every other per-company Boule secret goes
|
|
14
|
+
* through (foundry#970) -- this file keeps only the env name and the byte
|
|
15
|
+
* length, not a second mint function (foundry#957 review note 2 and
|
|
16
|
+
* thermo note 1: the earlier `mintNativeBuildKey` was a third, buggy copy
|
|
17
|
+
* of that same check).
|
|
19
18
|
*/
|
|
20
19
|
export declare const NATIVE_BUILD_REPO = "wtfalch/shell";
|
|
21
20
|
export declare const NATIVE_BUILD_WORKFLOW_FILE = "build-app.yml";
|
|
@@ -49,22 +48,3 @@ export declare function dispatchNativeBuild(deps: NativeBuildDispatchDeps, run:
|
|
|
49
48
|
/** The env name Boule presents this key under, to Archon's native-build route. */
|
|
50
49
|
export declare const NATIVE_BUILD_KEY_ENV = "NATIVE_BUILD_KEY";
|
|
51
50
|
export declare const NATIVE_BUILD_KEY_BYTES = 32;
|
|
52
|
-
/** The one thing {@link mintNativeBuildKey} needs from Coolify: a live app's env, to check whether it already has a key. */
|
|
53
|
-
export interface NativeBuildKeyEnvPort {
|
|
54
|
-
listEnvs(appId: string): Promise<ReadonlyArray<{
|
|
55
|
-
key: string;
|
|
56
|
-
value: string;
|
|
57
|
-
is_preview: boolean;
|
|
58
|
-
}>>;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Mints the per-company key Boule presents to Archon's native-build route
|
|
62
|
-
* (estate#262), the same way every other per-company key in this codebase is
|
|
63
|
-
* minted (estate#207): a random value, written to Boule's env once and never
|
|
64
|
-
* rotated, kept unchanged on every later apply once it is there. Returns
|
|
65
|
-
* `null` when Boule's env already holds one -- the caller then writes
|
|
66
|
-
* nothing, so the live value stands. `onSecret`, when given, is told the
|
|
67
|
-
* minted value so the caller can keep it out of its own logs and error
|
|
68
|
-
* messages.
|
|
69
|
-
*/
|
|
70
|
-
export declare function mintNativeBuildKey(coolify: NativeBuildKeyEnvPort, appId: string, onSecret?: (value: string) => void): Promise<string | null>;
|
package/dist/native-build.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
/**
|
|
3
2
|
* `wtfalch/shell`'s `build-app.yml`: the one GitHub Actions workflow that
|
|
4
3
|
* builds and signs a company's native app shell (estate
|
|
@@ -8,15 +7,14 @@ import { randomBytes } from 'node:crypto';
|
|
|
8
7
|
* workflow, and the key Boule presents when it asks the host (Archon) to
|
|
9
8
|
* dispatch it.
|
|
10
9
|
*
|
|
11
|
-
* Neither function here is wired into `plan/diff.ts` or `plan/executor.ts`:
|
|
12
10
|
* `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
|
|
13
|
-
* route, not foundry's apply loop
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
11
|
+
* route, not foundry's apply loop. The key itself is minted by
|
|
12
|
+
* `plan/executor.ts`'s `coolifyBoule` (`ensureNativeBuildKey`), reusing the
|
|
13
|
+
* same `mintIfAbsent` path every other per-company Boule secret goes
|
|
14
|
+
* through (foundry#970) -- this file keeps only the env name and the byte
|
|
15
|
+
* length, not a second mint function (foundry#957 review note 2 and
|
|
16
|
+
* thermo note 1: the earlier `mintNativeBuildKey` was a third, buggy copy
|
|
17
|
+
* of that same check).
|
|
20
18
|
*/
|
|
21
19
|
export const NATIVE_BUILD_REPO = 'wtfalch/shell';
|
|
22
20
|
export const NATIVE_BUILD_WORKFLOW_FILE = 'build-app.yml';
|
|
@@ -72,21 +70,3 @@ export async function dispatchNativeBuild(deps, run) {
|
|
|
72
70
|
/** The env name Boule presents this key under, to Archon's native-build route. */
|
|
73
71
|
export const NATIVE_BUILD_KEY_ENV = 'NATIVE_BUILD_KEY';
|
|
74
72
|
export const NATIVE_BUILD_KEY_BYTES = 32;
|
|
75
|
-
/**
|
|
76
|
-
* Mints the per-company key Boule presents to Archon's native-build route
|
|
77
|
-
* (estate#262), the same way every other per-company key in this codebase is
|
|
78
|
-
* minted (estate#207): a random value, written to Boule's env once and never
|
|
79
|
-
* rotated, kept unchanged on every later apply once it is there. Returns
|
|
80
|
-
* `null` when Boule's env already holds one -- the caller then writes
|
|
81
|
-
* nothing, so the live value stands. `onSecret`, when given, is told the
|
|
82
|
-
* minted value so the caller can keep it out of its own logs and error
|
|
83
|
-
* messages.
|
|
84
|
-
*/
|
|
85
|
-
export async function mintNativeBuildKey(coolify, appId, onSecret) {
|
|
86
|
-
const envs = await coolify.listEnvs(appId);
|
|
87
|
-
if (envs.some((e) => e.key === NATIVE_BUILD_KEY_ENV && !e.is_preview))
|
|
88
|
-
return null;
|
|
89
|
-
const value = randomBytes(NATIVE_BUILD_KEY_BYTES).toString('base64url');
|
|
90
|
-
onSecret?.(value);
|
|
91
|
-
return value;
|
|
92
|
-
}
|
package/dist/org.d.ts
CHANGED
|
@@ -301,7 +301,7 @@ export type AccessRule = {
|
|
|
301
301
|
/** The key of the entry that is today's one app: the Coolify app named `<slug>`. */
|
|
302
302
|
export declare const APP_ENTRY_KEY = "app";
|
|
303
303
|
/** Why a validator refused. `code` is stable and never reworded; `message` is for people. */
|
|
304
|
-
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'issuer_app_name_reserved_kosmos' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'hosting_github_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'hosted_name_invalid' | 'hosted_name_not_unique' | 'hosted_name_worker_app' | 'entry_workspace_invalid' | 'entry_workspace_unsupported' | 'entry_live_needs_workspace' | 'entry_live_commit_invalid' | 'gate_key_taken_by_workspace' | 'gate_key_taken_by_private_page' | 'private_page_invalid' | 'private_page_path_invalid' | 'private_page_duplicate' | 'private_page_limit' | 'private_page_needs_hosting' | 'private_page_roles_invalid' | 'private_page_people_invalid' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'blue_green_advance_invalid' | 'blue_green_min_requests_invalid' | 'blue_green_error_margin_invalid' | 'blue_green_staff_first_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'workspaces_needs_customers' | 'workspaces_needs_server' | 'workspaces_roles_invalid' | 'workspaces_role_reserved' | 'workspaces_invite_path_invalid' | 'workspaces_env_clash' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_needs_own_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'mail_not_true' | 'mail_needs_hosting' | 'mail_env_clash' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'customer_grants_not_true' | 'customer_grants_needs_customers' | 'customer_grants_env_clash' | 'repo_use_invalid' | 'repo_use_needs_repo' | 'repo_use_env_clash' | 'audit_not_true' | 'audit_needs_hosting' | 'audit_needs_worker_database' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
304
|
+
export type OrgRefusalCode = 'hosting_tunnel_needs_hosting' | 'issuer_app_name_missing' | 'issuer_app_name_not_unique' | 'issuer_app_kind_unknown' | 'issuer_app_name_reserved_agora' | 'issuer_app_name_reserved_boule' | 'issuer_app_name_reserved_kosmos' | 'hosting_needs_entry' | 'hosting_main_count' | 'entry_key_not_unique' | 'entry_image_invalid' | 'ai_webhook_path_not_unique' | 'worker_route_not_unique' | 'database_not_unique' | 'access_hostname_not_unique' | 'access_hostname_invalid' | 'access_needs_hosting' | 'access_kind_unknown' | 'access_service_token_name_missing' | 'access_hostname_locked' | 'access_hostname_not_served' | 'gate_limit_invalid' | 'hosting_repo_invalid' | 'hosting_github_invalid' | 'gate_needs_domain' | 'gate_key_invalid' | 'gate_key_not_unique' | 'gate_needs_hostname' | 'gate_hostname_not_entry_hostname' | 'gate_hostname_held_twice' | 'gate_hostname_locked' | 'gate_limit_exceeded' | 'wildcard_needs_domain' | 'wildcard_held_twice' | 'hostnames_need_domain' | 'hostname_needed' | 'hostname_invalid' | 'hostname_not_unique' | 'hostname_locked' | 'hosted_name_invalid' | 'hosted_name_not_unique' | 'hosted_name_worker_app' | 'entry_workspace_invalid' | 'entry_workspace_unsupported' | 'entry_live_needs_workspace' | 'entry_live_commit_invalid' | 'gate_key_taken_by_workspace' | 'gate_key_taken_by_private_page' | 'private_page_invalid' | 'private_page_path_invalid' | 'private_page_duplicate' | 'private_page_limit' | 'private_page_needs_hosting' | 'private_page_roles_invalid' | 'private_page_people_invalid' | 'entry_key_invalid' | 'entry_key_reserved' | 'entry_repo_invalid' | 'entry_folder_invalid' | 'entry_branch_empty' | 'entry_build_pack_unknown' | 'entry_build_pack_unavailable' | 'entry_port_invalid' | 'entry_health_path_invalid' | 'entry_capabilities_invalid' | 'entry_database_needs_hosting' | 'blue_green_needs_hostname' | 'env_name_empty' | 'env_name_reserved' | 'env_source_platform_mail_key' | 'env_source_unknown' | 'env_shared_secret_name_invalid' | 'env_held_secret_name_mismatch' | 'env_held_secret_name_invalid' | 'env_looks_like_secret' | 'builtin_app_invalid' | 'blue_green_not_object' | 'blue_green_steps_length' | 'blue_green_steps_range' | 'blue_green_steps_order' | 'blue_green_steps_end' | 'blue_green_hold_invalid' | 'blue_green_advance_invalid' | 'blue_green_min_requests_invalid' | 'blue_green_error_margin_invalid' | 'blue_green_staff_first_invalid' | 'volumes_not_list' | 'volumes_too_many' | 'volume_name_invalid' | 'volume_name_logs' | 'volume_name_not_unique' | 'volume_mount_path_invalid' | 'volume_mount_path_too_long' | 'volume_mount_path_log_dir' | 'volume_mount_path_forbidden' | 'volume_mount_path_not_unique' | 'auth_needs_hosting' | 'auth_users_invalid' | 'auth_registration_invalid' | 'registration_open_needs_customers' | 'auth_replaces_broker_entry_invalid' | 'auth_replaces_broker_entry_owned' | 'auth_needs_exact_hostname' | 'mcp_needs_open_customers' | 'mcp_path_invalid' | 'mcp_onboarding_path_invalid' | 'auth_sign_in_invalid' | 'email_code_needs_customers' | 'auth_room_invalid' | 'auth_room_needs_customers' | 'auth_room_not_unique' | 'client_name_taken' | 'workspaces_needs_customers' | 'workspaces_needs_server' | 'workspaces_roles_invalid' | 'workspaces_role_reserved' | 'workspaces_invite_path_invalid' | 'workspaces_env_clash' | 'ai_needs_hosting' | 'ai_offerings_invalid' | 'ai_permissions_invalid' | 'ai_webhook_path_invalid' | 'ai_webhook_needs_exact_hostname' | 'ai_env_clash' | 'files_needs_hosting' | 'files_collections_invalid' | 'files_permissions_invalid' | 'files_share_host_invalid' | 'files_share_host_needs_domain' | 'files_env_clash' | 'worker_needs_hosting' | 'worker_needs_repo' | 'worker_needs_own_repo' | 'worker_key_too_long' | 'worker_artifact_invalid' | 'worker_route_invalid' | 'worker_route_off_domain' | 'worker_rate_limit_name_missing' | 'worker_rate_limit_namespace_missing' | 'worker_rate_limit_invalid' | 'worker_rate_limit_period_invalid' | 'worker_app_invalid' | 'edge_not_true' | 'edge_needs_hosting' | 'edge_needs_domain' | 'edge_env_clash' | 'mail_not_true' | 'mail_needs_hosting' | 'mail_env_clash' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'customer_grants_not_true' | 'customer_grants_needs_customers' | 'customer_grants_env_clash' | 'repo_use_invalid' | 'repo_use_needs_repo' | 'repo_use_env_clash' | 'audit_not_true' | 'audit_needs_hosting' | 'audit_needs_worker_database' | 'customers_invalid' | 'customers_sign_up_needs_enabled' | 'registration_open_needs_sign_up';
|
|
305
305
|
/**
|
|
306
306
|
* One refusal: a stable `code`, the dotted `path` into {@link DeclaredOrg} it is about
|
|
307
307
|
* (`hosting.entries.terminal.uses.auth.registration`) and the sentence for people.
|
|
@@ -311,6 +311,19 @@ export interface OrgRefusal {
|
|
|
311
311
|
path: string;
|
|
312
312
|
message: string;
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* A built image a server entry runs instead of building from a repository: `repository` is
|
|
316
|
+
* `<host>[:port]/<owner>/<name>`, `digest` is `sha256:` and 64 lowercase hex digits. Same shapes as a server
|
|
317
|
+
* `build.json` (`build-json.ts`).
|
|
318
|
+
*/
|
|
319
|
+
export interface EntryImage {
|
|
320
|
+
repository: string;
|
|
321
|
+
digest: `sha256:${string}`;
|
|
322
|
+
}
|
|
323
|
+
/** The registry path of an {@link EntryImage}: `<host>[:port]/<owner>/<name>`, any host. */
|
|
324
|
+
export declare const ENTRY_IMAGE_REPOSITORY: RegExp;
|
|
325
|
+
/** The digest of an {@link EntryImage}. */
|
|
326
|
+
export declare const ENTRY_IMAGE_DIGEST: RegExp;
|
|
314
327
|
/**
|
|
315
328
|
* One app the org runs. `database` and `gates` are meaningless without
|
|
316
329
|
* `HostingConfig.enabled`, so `validateHosting` requires it before either is
|
|
@@ -334,6 +347,11 @@ export interface HostingEntry {
|
|
|
334
347
|
buildPack: HostingBuildPack;
|
|
335
348
|
/** The port the app listens on. `null` is Coolify's default. */
|
|
336
349
|
port: number | null;
|
|
350
|
+
/**
|
|
351
|
+
* A built image the app runs, pulled as `repository@digest`, instead of a build from `repo`. Then `folder` is `null`, `branch` and `buildPack` stay at their defaults;
|
|
352
|
+
* `validateEntryImage` states the rest. A new digest redeploys the app.
|
|
353
|
+
*/
|
|
354
|
+
image?: EntryImage;
|
|
337
355
|
/**
|
|
338
356
|
* The app's live address on the platform's apps domain: `<hostedName>.companyincluded.app`. A
|
|
339
357
|
* label {@link hostedLabelRefusal} accepts; unique within the org (the host checks it across orgs).
|
package/dist/org.js
CHANGED
|
@@ -16,6 +16,10 @@ export const APP_ENTRY_KEY = 'app';
|
|
|
16
16
|
function refusal(code, path, message) {
|
|
17
17
|
return { code, path, message };
|
|
18
18
|
}
|
|
19
|
+
/** The registry path of an {@link EntryImage}: `<host>[:port]/<owner>/<name>`, any host. */
|
|
20
|
+
export const ENTRY_IMAGE_REPOSITORY = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[0-9]{1,5})?\/[a-z0-9]([a-z0-9-]*[a-z0-9])?\/[a-z0-9][a-z0-9._-]*$/;
|
|
21
|
+
/** The digest of an {@link EntryImage}. */
|
|
22
|
+
export const ENTRY_IMAGE_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
19
23
|
export const MAX_PRIVATE_PAGES = 20;
|
|
20
24
|
export const MAX_PRIVATE_PAGE_ROLES = 50;
|
|
21
25
|
export const MAX_PRIVATE_PAGE_PEOPLE = 200;
|
|
@@ -211,6 +215,9 @@ export function refuseHosting(hosting, names) {
|
|
|
211
215
|
return refusal('entry_key_not_unique', `hosting.entries.${entry.key}.key`, `hosting.entries key '${entry.key}' is not unique`);
|
|
212
216
|
}
|
|
213
217
|
seenKeys.add(entry.key);
|
|
218
|
+
const invalidImage = validateEntryImage(entry);
|
|
219
|
+
if (invalidImage)
|
|
220
|
+
return invalidImage;
|
|
214
221
|
const invalid = validateHostingEntry(entry, hosting.enabled, reserveKeys, hosting.github);
|
|
215
222
|
if (invalid)
|
|
216
223
|
return invalid;
|
|
@@ -513,8 +520,8 @@ export function companyGithubFor(hosting, repo) {
|
|
|
513
520
|
const github = hosting.github;
|
|
514
521
|
return github && repo !== null && repoUnderAccount(repo, github.account) ? github : undefined;
|
|
515
522
|
}
|
|
516
|
-
// The env names `pushAppEnv` and `pushDatabaseUrl` set themselves.
|
|
517
|
-
const FOUNDRY_ENV_NAME = /^(
|
|
523
|
+
// The env names `pushAppEnv` and `pushDatabaseUrl` set themselves. `APP_URL` is also set, but only on the main entry.
|
|
524
|
+
const FOUNDRY_ENV_NAME = /^(LOG_DIR|OWNER_USER_ID|AUTH_.*|DATABASE_URL.*)$/;
|
|
518
525
|
// Set only on the colors of a blueGreen entry; any other entry may declare them.
|
|
519
526
|
const BLUE_GREEN_ENV_NAME = /^(REPORTING_(COLOR|RELEASE|HEALTH_TOKEN)|NEXT_SERVER_ACTIONS_ENCRYPTION_KEY)$/;
|
|
520
527
|
const SECRET_ENV_NAME = /_(KEY|SECRET|TOKEN|PASSWORD)$/;
|
|
@@ -626,6 +633,39 @@ function validateEntryPrivatePages(entry, at, hostingEnabled) {
|
|
|
626
633
|
}
|
|
627
634
|
return null;
|
|
628
635
|
}
|
|
636
|
+
/** Why an entry's `image` is refused, or `null`. Read wherever hosting is read, so a stored entry is checked too. */
|
|
637
|
+
function validateEntryImage(entry) {
|
|
638
|
+
const value = entry.image;
|
|
639
|
+
if (value === undefined)
|
|
640
|
+
return null;
|
|
641
|
+
const path = `hosting.entries.${entry.key}.image`;
|
|
642
|
+
const at = `hosting.entries '${entry.key}'`;
|
|
643
|
+
const invalid = (message) => refusal('entry_image_invalid', path, `${at} ${message}`);
|
|
644
|
+
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
645
|
+
const { repository, digest } = (isObject ? value : {});
|
|
646
|
+
if (!isObject ||
|
|
647
|
+
Object.keys(value).some((k) => k !== 'repository' && k !== 'digest') ||
|
|
648
|
+
typeof repository !== 'string' ||
|
|
649
|
+
!ENTRY_IMAGE_REPOSITORY.test(repository) ||
|
|
650
|
+
repository.split('/').some((part) => /^\.+$/.test(part)) ||
|
|
651
|
+
typeof digest !== 'string' ||
|
|
652
|
+
!ENTRY_IMAGE_DIGEST.test(digest)) {
|
|
653
|
+
return invalid('image must be { repository, digest }: a registry path like registry.example.com/owner/name, and a digest of sha256: and 64 lowercase letters a-f or digits');
|
|
654
|
+
}
|
|
655
|
+
if (entry.folder !== null || entry.branch !== 'main' || entry.buildPack !== 'nixpacks') {
|
|
656
|
+
return invalid("runs a built image, so it cannot also build from a repository: remove the image, or set folder to null and keep the default branch and build pack (repo may stay, as the app's source)");
|
|
657
|
+
}
|
|
658
|
+
if (entry.worker !== undefined || entry.workerApp !== undefined) {
|
|
659
|
+
return invalid('runs a built image on a server, so it cannot also be a Worker or a Worker app');
|
|
660
|
+
}
|
|
661
|
+
if (entry.blueGreen !== undefined ||
|
|
662
|
+
entry.workspace !== undefined ||
|
|
663
|
+
entry.live === false ||
|
|
664
|
+
entry.liveCommit !== undefined) {
|
|
665
|
+
return invalid('runs a built image, which does not support a workspace, blue/green releases or a promote yet: remove them, or build from a repository');
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
629
669
|
function validateHostingEntry(entry, enabled, reserveKeys, github) {
|
|
630
670
|
const at = `hosting.entries '${entry.key}'`;
|
|
631
671
|
if (!ENTRY_KEY.test(entry.key)) {
|
|
@@ -689,7 +729,9 @@ function validateHostingEntry(entry, enabled, reserveKeys, github) {
|
|
|
689
729
|
if (!name) {
|
|
690
730
|
return refusal('env_name_empty', `hosting.entries.${entry.key}.env`, `${at} env needs a non-empty env var name`);
|
|
691
731
|
}
|
|
692
|
-
if (FOUNDRY_ENV_NAME.test(name) ||
|
|
732
|
+
if (FOUNDRY_ENV_NAME.test(name) ||
|
|
733
|
+
(name === 'APP_URL' && isMainEntry(entry)) ||
|
|
734
|
+
(entry.blueGreen && BLUE_GREEN_ENV_NAME.test(name))) {
|
|
693
735
|
return refusal('env_name_reserved', `hosting.entries.${entry.key}.env.${name}`, `${at} env '${name}' is set by foundry and cannot be declared`);
|
|
694
736
|
}
|
|
695
737
|
if (typeof value !== 'string') {
|
|
@@ -2,6 +2,7 @@ import { type BlueGreenApplyDeps } from './blue-green-apply.js';
|
|
|
2
2
|
import { type BlueGreenSubject } from './blue-green.js';
|
|
3
3
|
import type { DeployHealthRuntime } from './deploy-health.js';
|
|
4
4
|
import type { PlatformTunnelPort } from './platform-providers.js';
|
|
5
|
+
import type { StageGuardDeps } from './stage-guards.js';
|
|
5
6
|
export interface ArchonBlueGreenRun {
|
|
6
7
|
subject: BlueGreenSubject;
|
|
7
8
|
/** The owner company's id: the health token's held ref is `sharedSecretRef(ownerOrgId, ...)` like an entry's. */
|
|
@@ -42,6 +43,10 @@ export interface ArchonBlueGreenRun {
|
|
|
42
43
|
sleep: (ms: number) => Promise<void>;
|
|
43
44
|
logDir: string;
|
|
44
45
|
isRunning: (status: string | undefined) => boolean;
|
|
46
|
+
/** Stage's probe port and Access gate, the same guards an entry's stage gets. Absent: the guards read "not run". */
|
|
47
|
+
stageGuards?: StageGuardDeps;
|
|
48
|
+
/** The guard names the plan counted (`desired.guards`); they go into the applied hash. */
|
|
49
|
+
guardInputs?: readonly string[];
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
|
|
@@ -70,6 +70,8 @@ export async function applyArchonBlueGreen(run) {
|
|
|
70
70
|
generate: () => randomBytes(32).toString('hex'),
|
|
71
71
|
secret: run.secret,
|
|
72
72
|
}),
|
|
73
|
+
...(run.stageGuards ? { stageGuards: run.stageGuards } : {}),
|
|
74
|
+
...(run.guardInputs ? { guardInputs: run.guardInputs } : {}),
|
|
73
75
|
actionsKey: async () => {
|
|
74
76
|
const key = await mintActionsKey({
|
|
75
77
|
id,
|
|
@@ -54,6 +54,11 @@ export interface BlueGreenApplyDeps {
|
|
|
54
54
|
* guard unrecorded ("not run", never passed), so `stageGuardsReady` stays false and the copy does not start.
|
|
55
55
|
*/
|
|
56
56
|
stageGuards?: StageGuardDeps;
|
|
57
|
+
/**
|
|
58
|
+
* The guard names the plan counted ({@link stageGuardInputs}, from `desired.guards`). They go into the
|
|
59
|
+
* applied structure hash, so the next plan sees the same hash until a guard becomes possible.
|
|
60
|
+
*/
|
|
61
|
+
guardInputs?: readonly string[];
|
|
57
62
|
/**
|
|
58
63
|
* The company the stage mail sink belongs to (its name is `<slug>-stage-sink`). Absent: no sink is made and
|
|
59
64
|
* stage holds no mail env at all, so the copy stays refused.
|
|
@@ -10,6 +10,8 @@ import { remoteDatabaseAddress, remoteDatabaseHost } from './remote-database.js'
|
|
|
10
10
|
import { STAGE_SIGN_IN_NAMES, buildStageEnv } from './stage-env.js';
|
|
11
11
|
import { buildStageProbes, runStageProbes, stageProbesRecord, } from './stage-guards.js';
|
|
12
12
|
import { assertStageIsolated, envClass, stageIsolationHash, } from './stage-isolation.js';
|
|
13
|
+
import { StageServiceError, ensureStageServices } from './stage-services.js';
|
|
14
|
+
import { STAGE_SIGNIN_NONE, StageSignInError, ensureStagePublicClient, ensureStageSignIn, } from './stage-signin.js';
|
|
13
15
|
import { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_APP_SCOPE_KEY, STAGE_SINK_HTTP_PORT, STAGE_SINK_IMAGE_TAG, STAGE_SINK_REPOSITORY, stageMailEnv, stageSinkAppName, } from './stage-sink.js';
|
|
14
16
|
import { readWorkspaceCommit, wsKey } from './workspace.js';
|
|
15
17
|
/** How long one Coolify call of the sink may take. */
|
|
@@ -95,7 +97,10 @@ export async function applyBlueGreenServer(deps, subject, server) {
|
|
|
95
97
|
await deps.persist();
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
|
|
100
|
+
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
|
|
101
|
+
primary: isPrimary,
|
|
102
|
+
...(deps.guardInputs ? { guards: deps.guardInputs } : {}),
|
|
103
|
+
}));
|
|
99
104
|
await deps.persist();
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
@@ -481,30 +486,92 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
481
486
|
const stageHas = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
482
487
|
const mailChanged = [...mail, ...signInEnv].some((e) => stageHas.get(e.key) !== e.value);
|
|
483
488
|
stageEnv.push(...mail);
|
|
484
|
-
const
|
|
489
|
+
const stageSurfaceOf = () => ({
|
|
485
490
|
env: stageEnv.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
486
491
|
resources: stageResources(subject),
|
|
492
|
+
});
|
|
493
|
+
const prodSurface = {
|
|
494
|
+
env: [...prodEnv, ...greenEnv]
|
|
495
|
+
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
496
|
+
.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
497
|
+
resources: prodResources(subject),
|
|
498
|
+
internalHosts: Object.values(prodResources(subject)),
|
|
487
499
|
};
|
|
488
|
-
const
|
|
500
|
+
const sinkMail = Object.fromEntries(mail.map((e) => [e.key, e.value]));
|
|
501
|
+
const signInIsolation = deps.signIn
|
|
502
|
+
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
503
|
+
: {};
|
|
504
|
+
const isolationViolations = (surface) => assertStageIsolated({
|
|
489
505
|
kind,
|
|
490
|
-
prod:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
resources: prodResources(subject),
|
|
495
|
-
internalHosts: Object.values(prodResources(subject)),
|
|
496
|
-
},
|
|
497
|
-
stage: stageSurface,
|
|
498
|
-
sinkMail: Object.fromEntries(mail.map((e) => [e.key, e.value])),
|
|
499
|
-
...(deps.signIn
|
|
500
|
-
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
501
|
-
: {}),
|
|
506
|
+
prod: prodSurface,
|
|
507
|
+
stage: surface,
|
|
508
|
+
sinkMail,
|
|
509
|
+
...signInIsolation,
|
|
502
510
|
});
|
|
511
|
+
// The env written this run, and the env stage already holds (put there by an earlier apply, an older
|
|
512
|
+
// builder or a hand): a forbidden name or a production value already on the live stage app is refused too.
|
|
513
|
+
// A name this run writes again is replaced, so only the other live names are checked here.
|
|
514
|
+
const written = new Set(stageEnv.map((e) => e.key));
|
|
515
|
+
const liveStage = {
|
|
516
|
+
env: [...stageHas]
|
|
517
|
+
.filter(([name, value]) => value !== undefined && !written.has(name))
|
|
518
|
+
.map(([name, value]) => ({ name, class: envClass(name), value })),
|
|
519
|
+
resources: {},
|
|
520
|
+
};
|
|
521
|
+
let stageSurface = stageSurfaceOf();
|
|
522
|
+
const violations = [
|
|
523
|
+
...new Set([
|
|
524
|
+
...isolationViolations(stageSurface),
|
|
525
|
+
...assertStageIsolated({
|
|
526
|
+
kind,
|
|
527
|
+
prod: prodSurface,
|
|
528
|
+
stage: liveStage,
|
|
529
|
+
sinkMail,
|
|
530
|
+
...signInIsolation,
|
|
531
|
+
}),
|
|
532
|
+
]),
|
|
533
|
+
].sort();
|
|
503
534
|
if (violations.length > 0) {
|
|
504
535
|
// Stage fails closed for itself: nothing is written to it, production's structure goes on.
|
|
505
536
|
await stageFailed(deps, subject, `blocked:${violations.join(',')}`);
|
|
506
537
|
return;
|
|
507
538
|
}
|
|
539
|
+
// Stage's own files, ai and jobs organisations and keys, for the services production's app uses. They are made
|
|
540
|
+
// before the probes, which need the keys; nothing is written to the stage app until the probes were rejected.
|
|
541
|
+
const prodValues = new Map(prodEnv
|
|
542
|
+
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
543
|
+
.map((e) => [e.key, e.value]));
|
|
544
|
+
let servicesRecord;
|
|
545
|
+
try {
|
|
546
|
+
const made = await ensureStageServices({
|
|
547
|
+
id: subject.id,
|
|
548
|
+
kind,
|
|
549
|
+
prod: prodValues,
|
|
550
|
+
port: deps.stageGuards?.services,
|
|
551
|
+
recorded: deps.recorded,
|
|
552
|
+
record: deps.record,
|
|
553
|
+
persist: deps.persist,
|
|
554
|
+
secret: deps.secret,
|
|
555
|
+
log: (line) => console.info(`${deps.label}: ${line}`),
|
|
556
|
+
});
|
|
557
|
+
stageEnv.push(...made.env);
|
|
558
|
+
servicesRecord = made.record;
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
// A StageServiceError holds ids and refs only; any other cause can echo a credential.
|
|
562
|
+
if (error instanceof StageServiceError)
|
|
563
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
564
|
+
await stageFailed(deps, subject, 'blocked:services', error);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (servicesRecord !== 'none') {
|
|
568
|
+
stageSurface = stageSurfaceOf();
|
|
569
|
+
const withServices = isolationViolations(stageSurface);
|
|
570
|
+
if (withServices.length > 0) {
|
|
571
|
+
await stageFailed(deps, subject, `blocked:${withServices.join(',')}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
508
575
|
// Stage's credentials are tried against production before they are written; one that production accepts
|
|
509
576
|
// blocks stage (nothing is written to it), and what was tried is recorded by name so the copy slice sees it.
|
|
510
577
|
const stageValues = new Map((await coolify.listEnvs(appId))
|
|
@@ -512,7 +579,7 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
512
579
|
.map((e) => [e.key, e.value]));
|
|
513
580
|
for (const e of stageEnv)
|
|
514
581
|
stageValues.set(e.key, e.value);
|
|
515
|
-
|
|
582
|
+
let isolationHash = stageIsolationHash(stageSurface);
|
|
516
583
|
let probeResult;
|
|
517
584
|
try {
|
|
518
585
|
probeResult = await runStageProbes(buildStageProbes({
|
|
@@ -524,9 +591,12 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
524
591
|
? { liveCustomersOrgId: deps.signIn.live.customersOrgId }
|
|
525
592
|
: {}),
|
|
526
593
|
...(deps.signIn ? { signInRedirectUri: deps.signIn.redirectUri } : {}),
|
|
527
|
-
prod: new Map(
|
|
528
|
-
|
|
529
|
-
|
|
594
|
+
prod: new Map([
|
|
595
|
+
...prodValues,
|
|
596
|
+
...(deps.stageGuards?.services?.prodJobsProjectId
|
|
597
|
+
? [['JOBS_PROJECT_ID', deps.stageGuards.services.prodJobsProjectId]]
|
|
598
|
+
: []),
|
|
599
|
+
]),
|
|
530
600
|
}));
|
|
531
601
|
}
|
|
532
602
|
catch (error) {
|
|
@@ -534,7 +604,7 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
534
604
|
await stageFailed(deps, subject, 'blocked:database', error);
|
|
535
605
|
return;
|
|
536
606
|
}
|
|
537
|
-
|
|
607
|
+
let probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
538
608
|
if (probeResult.failed.length > 0) {
|
|
539
609
|
deps.record(bgKey.stageProbes(subject.id), probes.value);
|
|
540
610
|
await stageFailed(deps, subject, `blocked:${probeResult.failed.join(',')}`);
|
|
@@ -544,9 +614,108 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
544
614
|
await stageFailed(deps, subject, 'blocked:gate');
|
|
545
615
|
return;
|
|
546
616
|
}
|
|
617
|
+
// Stage's own sign-in: made only now, after the isolation check, the probes and the gate. Its credentials are
|
|
618
|
+
// added to the env and the isolation check runs once more over the whole of it.
|
|
619
|
+
const signIn = deps.stageGuards?.signIn;
|
|
620
|
+
let signInGuard;
|
|
621
|
+
if (signIn?.required === true && signIn.public === true) {
|
|
622
|
+
// A static SPA (Agora): a client and nothing secret. Its config is built from production's with the stage
|
|
623
|
+
// client id, then the isolation check runs once more over the whole env.
|
|
624
|
+
try {
|
|
625
|
+
const made = await ensureStagePublicClient({
|
|
626
|
+
id: subject.id,
|
|
627
|
+
port: signIn,
|
|
628
|
+
recorded: deps.recorded,
|
|
629
|
+
record: deps.record,
|
|
630
|
+
persist: deps.persist,
|
|
631
|
+
});
|
|
632
|
+
const rebuilt = buildStageEnv({
|
|
633
|
+
kind,
|
|
634
|
+
appUrl: stageAppUrl(subject),
|
|
635
|
+
logDir: deps.logDir,
|
|
636
|
+
database,
|
|
637
|
+
prod: prodEnv,
|
|
638
|
+
existing: [],
|
|
639
|
+
mint: () => '',
|
|
640
|
+
stageClientId: made.clientId,
|
|
641
|
+
});
|
|
642
|
+
const config = rebuilt.env.find((e) => e.key === 'AGORA_CONFIG');
|
|
643
|
+
if (config)
|
|
644
|
+
stageEnv.push(config);
|
|
645
|
+
signInGuard = made.guard;
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (error instanceof StageSignInError)
|
|
649
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
650
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
651
|
+
await stageFailed(deps, subject, 'blocked:signin', error);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
for (const e of stageEnv)
|
|
655
|
+
stageValues.set(e.key, e.value);
|
|
656
|
+
stageSurface = stageSurfaceOf();
|
|
657
|
+
const again = isolationViolations(stageSurface);
|
|
658
|
+
if (again.length > 0) {
|
|
659
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
660
|
+
await stageFailed(deps, subject, `blocked:${again.join(',')}`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
isolationHash = stageIsolationHash(stageSurface);
|
|
664
|
+
probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
665
|
+
}
|
|
666
|
+
else if (signIn?.required === true) {
|
|
667
|
+
try {
|
|
668
|
+
const made = await ensureStageSignIn({
|
|
669
|
+
id: subject.id,
|
|
670
|
+
port: signIn,
|
|
671
|
+
recorded: deps.recorded,
|
|
672
|
+
record: deps.record,
|
|
673
|
+
persist: deps.persist,
|
|
674
|
+
secret: deps.secret,
|
|
675
|
+
generate: () => randomBytes(32).toString('base64url'),
|
|
676
|
+
sleep: deps.sleep,
|
|
677
|
+
});
|
|
678
|
+
stageEnv.push(...made.env);
|
|
679
|
+
signInGuard = made.guard;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
// A StageSignInError holds ids and refs only; any other cause can echo a credential.
|
|
683
|
+
if (error instanceof StageSignInError)
|
|
684
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
685
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
686
|
+
await stageFailed(deps, subject, 'blocked:signin', error);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (!stageEnv.some((e) => e.key === 'AUTH_COOKIE_SECRET')) {
|
|
690
|
+
// A cookie secret of stage's own, kept across applies; never a copy of production's.
|
|
691
|
+
const have = stageHas.get('AUTH_COOKIE_SECRET') || randomBytes(32).toString('base64url');
|
|
692
|
+
deps.secret(have);
|
|
693
|
+
stageEnv.push({ key: 'AUTH_COOKIE_SECRET', value: have });
|
|
694
|
+
}
|
|
695
|
+
for (const e of stageEnv)
|
|
696
|
+
stageValues.set(e.key, e.value);
|
|
697
|
+
stageSurface = stageSurfaceOf();
|
|
698
|
+
const again = isolationViolations(stageSurface);
|
|
699
|
+
if (again.length > 0) {
|
|
700
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
701
|
+
await stageFailed(deps, subject, `blocked:${again.join(',')}`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
isolationHash = stageIsolationHash(stageSurface);
|
|
705
|
+
probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
706
|
+
}
|
|
707
|
+
else if (signIn?.required === false) {
|
|
708
|
+
signInGuard = STAGE_SIGNIN_NONE;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
console.info(`${deps.label}: stage sign-in was not set up: the host gave no sign-in for this subject, so ${bgKey.stageSignIn(subject.id)} stays unrecorded`);
|
|
712
|
+
}
|
|
547
713
|
await upsertEnv(deps, appId, stageEnv);
|
|
714
|
+
if (signInGuard)
|
|
715
|
+
deps.record(bgKey.stageSignIn(subject.id), signInGuard);
|
|
548
716
|
if (mail.length > 0)
|
|
549
717
|
deps.record(bgKey.stageSink(subject.id), 'applied');
|
|
718
|
+
deps.record(bgKey.stageServices(subject.id), servicesRecord);
|
|
550
719
|
deps.record(bgKey.stageIsolation(subject.id), isolationHash);
|
|
551
720
|
deps.record(bgKey.stageProbes(subject.id), probes.value);
|
|
552
721
|
await deps.persist();
|
|
@@ -589,8 +758,11 @@ async function ensureStageGate(deps, subject) {
|
|
|
589
758
|
if (!gate || deps.recorded()[bgKey.stageGate(subject.id)])
|
|
590
759
|
return true;
|
|
591
760
|
let id;
|
|
761
|
+
let membersHash;
|
|
592
762
|
try {
|
|
593
|
-
|
|
763
|
+
const created = await gate.create();
|
|
764
|
+
id = created.id;
|
|
765
|
+
membersHash = created.membersHash;
|
|
594
766
|
}
|
|
595
767
|
catch (error) {
|
|
596
768
|
// An Access error can echo the request, which names the people allowed in: only its name is logged.
|
|
@@ -598,6 +770,8 @@ async function ensureStageGate(deps, subject) {
|
|
|
598
770
|
return false;
|
|
599
771
|
}
|
|
600
772
|
deps.record(bgKey.stageGate(subject.id), id);
|
|
773
|
+
if (membersHash)
|
|
774
|
+
deps.record(bgKey.stageGateMembers(subject.id), membersHash);
|
|
601
775
|
await deps.persist();
|
|
602
776
|
return true;
|
|
603
777
|
}
|
|
@@ -607,7 +781,8 @@ async function stageAppId(deps, subject, name, serverUuid) {
|
|
|
607
781
|
name,
|
|
608
782
|
server: serverUuid,
|
|
609
783
|
branch: subject.branch,
|
|
610
|
-
|
|
784
|
+
// Stage deploys only when a person promotes a commit to it (`deploy_stage`); a merge never reaches it.
|
|
785
|
+
autoDeploy: false,
|
|
611
786
|
});
|
|
612
787
|
}
|
|
613
788
|
function isNotFound(error) {
|