@wtfalch/foundry 0.76.1 → 0.77.1
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 +24 -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
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' | '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' | 'mcp_events_path_invalid' | 'mcp_manage_path_invalid' | 'auth_sign_in_invalid' | 'auth_sign_ins_invalid' | 'auth_second_factor_invalid' | 'sign_in_staff_password_only' | 'passkey_needs_another_method' | 'email_code_needs_customers' | 'email_code_needs_sign_in' | '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' | 'analytics_not_true' | 'analytics_needs_hosting' | 'analytics_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_both_needs_customers' | 'auth_both_worker_app' | 'auth_both_unsupported' | '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' | 'mcp_events_path_invalid' | 'mcp_manage_path_invalid' | 'auth_sign_in_invalid' | 'auth_sign_ins_invalid' | 'auth_second_factor_invalid' | 'sign_in_staff_password_only' | 'passkey_needs_another_method' | 'email_code_needs_customers' | 'email_code_needs_sign_in' | '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' | 'files_settings_invalid' | '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' | 'mail_settings_invalid' | 'jobs_not_true' | 'jobs_needs_hosting' | 'jobs_env_clash' | 'jobs_settings_invalid' | 'analytics_not_true' | 'analytics_needs_hosting' | 'analytics_env_clash' | 'reporting_not_true' | 'reporting_needs_hosting' | 'reporting_env_clash' | 'reporting_site_invalid' | '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.
|
|
@@ -604,15 +604,22 @@ export interface WorkerConfig {
|
|
|
604
604
|
route: string;
|
|
605
605
|
bindings?: WorkerBindings;
|
|
606
606
|
}
|
|
607
|
-
/**
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
*
|
|
612
|
-
*
|
|
607
|
+
/** Who signs in to an app: `staff` is the company's own team, `customers` the people of its customers organisation. */
|
|
608
|
+
export type EntryAudience = 'staff' | 'customers';
|
|
609
|
+
export declare const ENTRY_AUDIENCES: readonly EntryAudience[];
|
|
610
|
+
/**
|
|
611
|
+
* Sign-in for one entry. With one audience (`users: 'staff'` or `'customers'`, or a list of that one): one
|
|
612
|
+
* ZITADEL client foundry makes and one broker `AUTH_APPS` entry, signing in ONE organisation. `staff` is the
|
|
613
|
+
* org's own ZITADEL org, `customers` the `<slug> customers` org. With both (`users: ['customers', 'staff']`,
|
|
614
|
+
* ADR 0003): the app is registered at the sign-in broker through the company's registrar
|
|
615
|
+
* (`plan/registered-app.ts`, auth ADR 0008), in the customers organisation, and the broker makes its two
|
|
616
|
+
* clients: the customers' and the team's, in the company's own organisation. `registration` is the broker
|
|
617
|
+
* entry's sign-up switch (for customers). `replacesBrokerEntry` is the `name` of the hand-written broker
|
|
618
|
+
* `AUTH_APPS` entry this entry's own entry replaces; the first write removes it. Read who signs in only
|
|
619
|
+
* through {@link entryAudiences}, {@link signsInCustomers} and {@link signsInBoth}.
|
|
613
620
|
*/
|
|
614
621
|
export interface EntryAuth {
|
|
615
|
-
users:
|
|
622
|
+
users: EntryAudience | EntryAudience[];
|
|
616
623
|
registration: 'open' | 'closed';
|
|
617
624
|
replacesBrokerEntry?: string;
|
|
618
625
|
/**
|
|
@@ -672,6 +679,16 @@ export interface EntryAuth {
|
|
|
672
679
|
*/
|
|
673
680
|
room?: EntryAuthRoom;
|
|
674
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Who signs in, as a sorted list without repeats (`['customers']`, `['staff']` or `['customers', 'staff']`).
|
|
684
|
+
* A one-value list is the same as the value. An invalid `users` answers what it holds that is known, so call
|
|
685
|
+
* this on a validated org.
|
|
686
|
+
*/
|
|
687
|
+
export declare function entryAudiences(auth: Pick<EntryAuth, 'users'>): EntryAudience[];
|
|
688
|
+
/** True when the entry's customers sign in (alone, or beside the team): the app lives in the customers organisation. */
|
|
689
|
+
export declare function signsInCustomers(auth: Pick<EntryAuth, 'users'> | undefined): boolean;
|
|
690
|
+
/** True when both the customers and the company's team sign in: the app is registered through the registrar. */
|
|
691
|
+
export declare function signsInBoth(auth: Pick<EntryAuth, 'users'> | undefined): boolean;
|
|
675
692
|
export type SignInMethod = 'email-code' | 'password' | 'passkey';
|
|
676
693
|
export declare const SIGN_IN_METHODS: readonly SignInMethod[];
|
|
677
694
|
export type SecondFactorMode = 'off' | 'allowed' | 'required';
|
|
@@ -714,10 +731,66 @@ export interface EntryFiles {
|
|
|
714
731
|
collections: string[];
|
|
715
732
|
permissions?: string[];
|
|
716
733
|
shareHost?: boolean;
|
|
734
|
+
/**
|
|
735
|
+
* #1116: how many days a new share link works, one of {@link FILES_SHARE_LINK_DAYS}, or `null` for links
|
|
736
|
+
* that never stop. Absent is {@link FILES_SHARE_LINK_DAYS_DEFAULT}. Delivered to the app as
|
|
737
|
+
* `FILES_SHARE_LINK_DAYS` (`never` for null) while it is not the default, for the app to pass to files as
|
|
738
|
+
* the link's `expiresAt`.
|
|
739
|
+
*/
|
|
740
|
+
shareLinkDays?: number | null;
|
|
741
|
+
/**
|
|
742
|
+
* #1116: people may pick another time when they share a file. Absent is true. Delivered as
|
|
743
|
+
* `FILES_SHARE_LINK_PICK=false` while it is false.
|
|
744
|
+
*/
|
|
745
|
+
shareLinkPick?: boolean;
|
|
746
|
+
/**
|
|
747
|
+
* #1116: the share of the space used, in percent, at which the company's owners are emailed. Absent is
|
|
748
|
+
* {@link FILES_SPACE_ALERT_DEFAULT}. For the files service, which does not read it yet; foundry delivers it
|
|
749
|
+
* to nothing.
|
|
750
|
+
*/
|
|
751
|
+
spaceAlertPercent?: number;
|
|
717
752
|
}
|
|
718
753
|
/** The env names `uses.files` always writes, besides one `FILES_COLLECTION_<NAME>_ID` per collection. */
|
|
719
754
|
export declare const FILES_ENTRY_ENV_NAMES: readonly ["FILES_URL", "FILES_ORGANISATION_ID", "FILES_API_KEY"];
|
|
720
755
|
export declare const filesCollectionEnvName: (name: string) => string;
|
|
756
|
+
/**
|
|
757
|
+
* #1116: Email's settings. `fromName` and `replyTo` go to the app's env (`MAIL_FROM_NAME`, `MAIL_REPLY_TO`,
|
|
758
|
+
* only when set), for `@wtfalch/mailer`'s `fromName` and `replyTo`. The bounce settings are for the
|
|
759
|
+
* company's mail service, which does not read them yet; foundry delivers them to nothing. `uses.mail: true`
|
|
760
|
+
* is every setting at its default.
|
|
761
|
+
*/
|
|
762
|
+
export interface EntryMail {
|
|
763
|
+
/** The name people see as the sender. Absent: the company's name, as today. */
|
|
764
|
+
fromName?: string;
|
|
765
|
+
/** Where replies go. Absent: to the sending address. */
|
|
766
|
+
replyTo?: string;
|
|
767
|
+
/** Stop sending to an address after this many bounces. Absent is {@link MAIL_BOUNCES_DEFAULT}. */
|
|
768
|
+
stopAfterBounces?: number;
|
|
769
|
+
/** Email the owners when more than this share of a week's mail bounces, in percent. Absent is {@link MAIL_BOUNCE_ALERT_DEFAULT}. */
|
|
770
|
+
bounceAlertPercent?: number;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* #1116: Jobs' settings, for the jobs scheduler's project, which does not read them yet (jobs#79 has no
|
|
774
|
+
* project settings); foundry delivers them to nothing. `uses.jobs: true` is every setting at its default.
|
|
775
|
+
*/
|
|
776
|
+
export interface EntryJobs {
|
|
777
|
+
/** How many times a failed job is tried again. Absent is {@link JOBS_RETRIES_DEFAULT}. */
|
|
778
|
+
retries?: number;
|
|
779
|
+
/** Email the owners when a job failed all its tries. Absent is true. */
|
|
780
|
+
alertOwners?: boolean;
|
|
781
|
+
/** Pause a job after this many failed runs in a row. Absent or null: never paused for failing. */
|
|
782
|
+
pauseAfterFailures?: number | null;
|
|
783
|
+
}
|
|
784
|
+
/** #1116: the env names the module settings write; `env` may not name one while its setting is set. */
|
|
785
|
+
export declare const FILES_SETTINGS_ENV_NAMES: readonly ["FILES_SHARE_LINK_DAYS", "FILES_SHARE_LINK_PICK"];
|
|
786
|
+
export declare const MAIL_SETTINGS_ENV_NAMES: readonly ["MAIL_FROM_NAME", "MAIL_REPLY_TO"];
|
|
787
|
+
/**
|
|
788
|
+
* #1116: the env an entry's module settings give the app. A name is written only while its setting differs
|
|
789
|
+
* from the default, so an entry saved before these settings plans as it did; the app reads an absent name as
|
|
790
|
+
* the default (`FILES_SHARE_LINK_DAYS` 7, `FILES_SHARE_LINK_PICK` true, no `MAIL_FROM_NAME` or
|
|
791
|
+
* `MAIL_REPLY_TO`). A name a setting stopped writing is taken out of the app's env by the next apply.
|
|
792
|
+
*/
|
|
793
|
+
export declare function entrySettingsEnv(entry: HostingEntry): Record<string, string>;
|
|
721
794
|
export interface HostingEntryUses {
|
|
722
795
|
auth?: EntryAuth;
|
|
723
796
|
ai?: EntryAi;
|
|
@@ -732,19 +805,22 @@ export interface HostingEntryUses {
|
|
|
732
805
|
edge?: true;
|
|
733
806
|
/**
|
|
734
807
|
* The entry sends mail from the company's mail domain: it gets the company's
|
|
735
|
-
* Lettermint token and `MAIL_FROM`, as the main app does
|
|
736
|
-
* stage gets the mail sink's
|
|
808
|
+
* Lettermint token and `MAIL_FROM`, as the main app does, and `MAIL_APP_KEY`
|
|
809
|
+
* ({@link MAIL_APP_KEY_ENV}). In a workspace the stage gets the mail sink's
|
|
810
|
+
* env only (`stageMailEnv`) and `MAIL_APP_KEY` = `<key>-work`. An object carries the Email module's settings
|
|
811
|
+
* ({@link EntryMail}); `true` is all of them at their defaults.
|
|
737
812
|
*/
|
|
738
|
-
mail?: true;
|
|
813
|
+
mail?: true | EntryMail;
|
|
739
814
|
/**
|
|
740
815
|
* The entry is a project of its own in the jobs scheduler and gets
|
|
741
816
|
* `JOBS_SCHEDULER_URL` (the scheduler's public origin), `JOBS_SIGNING_SECRET`
|
|
742
817
|
* and `JOBS_SYNC_KEY` (`plan/entry-jobs.ts`). Blocked, not planned, until the
|
|
743
818
|
* scheduler's public route (`plan/own-jobs-route.ts`) is recorded: the owner
|
|
744
819
|
* company's for a company on shared services, the company's own otherwise.
|
|
745
|
-
* A workspace gets none of the three.
|
|
820
|
+
* A workspace gets none of the three. An object carries the Jobs module's settings ({@link EntryJobs});
|
|
821
|
+
* `true` is all of them at their defaults.
|
|
746
822
|
*/
|
|
747
|
-
jobs?: true;
|
|
823
|
+
jobs?: true | EntryJobs;
|
|
748
824
|
/**
|
|
749
825
|
* The app keeps a tamper-evident audit log (`@wtfalch/audit`). A Worker app with a database: the append-only
|
|
750
826
|
* tables and the two chain functions are derived (`audit/ledger-set.ts`), never listed by hand, and the
|
|
@@ -772,13 +848,38 @@ export interface HostingEntryUses {
|
|
|
772
848
|
* app gets its keys from Boule, which installs them per copy, so foundry only plans the backend for it.
|
|
773
849
|
*/
|
|
774
850
|
analytics?: true;
|
|
851
|
+
/**
|
|
852
|
+
* The app sends its errors to the company's own reporting backend, and Boule shows them. A server entry gets
|
|
853
|
+
* its own site key (`REPORTING_URL`, `REPORTING_SITE_KEY`; `plan/entry-reporting-key.ts`) with site = the
|
|
854
|
+
* entry key, and its stage another (environment `stage`). A Worker app gets its keys from Boule, which installs
|
|
855
|
+
* them per copy, so foundry only plans the backend for it. The entry key must be a site the backend accepts
|
|
856
|
+
* (`reporting_site_invalid`).
|
|
857
|
+
*/
|
|
858
|
+
reporting?: true;
|
|
775
859
|
}
|
|
776
860
|
/** The env names `uses.edge` writes; `env` may not name them (#317). */
|
|
777
861
|
export declare const EDGE_ENTRY_ENV_NAMES: readonly ["EDGE_URL", "EDGE_API_KEY"];
|
|
778
862
|
/** The env names `uses.analytics` writes on a server entry; `env` may not name them. */
|
|
779
863
|
export declare const ANALYTICS_ENTRY_ENV_NAMES: readonly ["ANALYTICS_URL", "ANALYTICS_SITE_KEY"];
|
|
864
|
+
/**
|
|
865
|
+
* What the reporting backend accepts as a site (`SITE_PATTERN` in wtfalch/reporting's core): a lower-case
|
|
866
|
+
* letter, then letters, digits and hyphens, at most 64 in all. An entry's site is its key.
|
|
867
|
+
*/
|
|
868
|
+
export declare const REPORTING_SITE_PATTERN: RegExp;
|
|
869
|
+
/** The env names `uses.reporting` writes on a server entry; `env` may not name them. */
|
|
870
|
+
export declare const REPORTING_ENTRY_ENV_NAMES: readonly ["REPORTING_URL", "REPORTING_SITE_KEY"];
|
|
780
871
|
/** The env names `uses.mail` writes; `env` may not name them. */
|
|
781
872
|
export declare const MAIL_ENTRY_ENV_NAMES: readonly ["LETTERMINT_API_TOKEN", "MAIL_FROM"];
|
|
873
|
+
/**
|
|
874
|
+
* The env name every entry that sends mail ({@link sendsMail}) gets, holding its entry key, and its
|
|
875
|
+
* stage or workspace copy `<key>-work` ({@link mailAppKeyOf}). `@wtfalch/mailer` 0.10.0 tags each send
|
|
876
|
+
* with it, so Archon's mail reads can split mail per app. Not a secret. No entry's `env` may name it.
|
|
877
|
+
*/
|
|
878
|
+
export declare const MAIL_APP_KEY_ENV = "MAIL_APP_KEY";
|
|
879
|
+
/** Whether the entry sends mail: the main entry and each entry with `uses.mail`. */
|
|
880
|
+
export declare function sendsMail(entry: Pick<HostingEntry, 'key' | 'main' | 'uses'>): boolean;
|
|
881
|
+
/** The `MAIL_APP_KEY` of an entry's copy: its key live, `<key>-work` for its stage or workspace. */
|
|
882
|
+
export declare function mailAppKeyOf(entryKey: string, copy: 'live' | 'work'): string;
|
|
782
883
|
/** The env names `uses.jobs` writes; `env` may not name them. */
|
|
783
884
|
export declare const JOBS_ENTRY_ENV_NAMES: readonly ["JOBS_SCHEDULER_URL", "JOBS_SIGNING_SECRET", "JOBS_SYNC_KEY"];
|
|
784
885
|
/**
|
package/dist/org.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_RULES, checkName, explain } from '@wtfalch/auth/names';
|
|
2
2
|
import { floatingRefBlocked, floatingTagBlocked } from './bootstrap/versions.js';
|
|
3
|
-
import { AI_ENTRY_PERMISSIONS, AI_WEBHOOK_PATH, FILES_COLLECTION_NAME, FILES_ENTRY_PERMISSIONS, MCP_PATH, ONBOARDING_PATH_MAX_LENGTH, ROOM_STEPS_MAX, ROOM_STEPS_MIN, ROOM_TEXT_MAX, ROOM_UNSAFE_TEXT, WORKSPACE_ROLE, WORKSPACE_ROLES_MAX, isEdgePath, isPagePath, } from './entry-rules.js';
|
|
3
|
+
import { AI_ENTRY_PERMISSIONS, AI_WEBHOOK_PATH, FILES_COLLECTION_NAME, FILES_ENTRY_PERMISSIONS, FILES_SHARE_LINK_DAYS, FILES_SHARE_LINK_DAYS_DEFAULT, JOBS_PAUSE_AFTER_MAX, JOBS_RETRIES_MAX, MAIL_BOUNCES_MAX, MAIL_FROM_NAME, MAIL_FROM_NAME_MAX, MAIL_REPLY_TO, MAIL_REPLY_TO_MAX, MCP_PATH, ONBOARDING_PATH_MAX_LENGTH, PERCENT_MAX, PERCENT_MIN, ROOM_STEPS_MAX, ROOM_STEPS_MIN, ROOM_TEXT_MAX, ROOM_UNSAFE_TEXT, WORKSPACE_ROLE, WORKSPACE_ROLES_MAX, isEdgePath, isPagePath, wholeIn, } from './entry-rules.js';
|
|
4
4
|
import { hostedLabelRefusal } from './hosted-label.js';
|
|
5
5
|
import { entryHasDatabase, validateArtifact, validateEntryWorkerApp, } from './worker-app-config.js';
|
|
6
6
|
export { AI_ENTRY_PERMISSIONS, FILES_ENTRY_PERMISSIONS, MCP_PATH, ONBOARDING_PATH_MAX_LENGTH, ROOM_STEPS_MAX, ROOM_STEPS_MIN, ROOM_TEXT_MAX, WORKSPACE_ROLE, WORKSPACE_ROLES_MAX, } from './entry-rules.js';
|
|
@@ -55,6 +55,24 @@ export function withNewOrgAppsDefault(org) {
|
|
|
55
55
|
export const kosmosOn = (org) => org.kosmos?.enabled === true;
|
|
56
56
|
/** The most volumes one entry may declare. */
|
|
57
57
|
export const MAX_ENTRY_VOLUMES = 8;
|
|
58
|
+
export const ENTRY_AUDIENCES = ['customers', 'staff'];
|
|
59
|
+
/**
|
|
60
|
+
* Who signs in, as a sorted list without repeats (`['customers']`, `['staff']` or `['customers', 'staff']`).
|
|
61
|
+
* A one-value list is the same as the value. An invalid `users` answers what it holds that is known, so call
|
|
62
|
+
* this on a validated org.
|
|
63
|
+
*/
|
|
64
|
+
export function entryAudiences(auth) {
|
|
65
|
+
const raw = Array.isArray(auth.users) ? auth.users : [auth.users];
|
|
66
|
+
return ENTRY_AUDIENCES.filter((audience) => raw.includes(audience));
|
|
67
|
+
}
|
|
68
|
+
/** True when the entry's customers sign in (alone, or beside the team): the app lives in the customers organisation. */
|
|
69
|
+
export function signsInCustomers(auth) {
|
|
70
|
+
return auth !== undefined && entryAudiences(auth).includes('customers');
|
|
71
|
+
}
|
|
72
|
+
/** True when both the customers and the company's team sign in: the app is registered through the registrar. */
|
|
73
|
+
export function signsInBoth(auth) {
|
|
74
|
+
return auth !== undefined && entryAudiences(auth).length === 2;
|
|
75
|
+
}
|
|
58
76
|
export const SIGN_IN_METHODS = ['email-code', 'password', 'passkey'];
|
|
59
77
|
export const SECOND_FACTOR_MODES = ['off', 'allowed', 'required'];
|
|
60
78
|
/** The env names `uses.auth.workspaces` writes; `env` may not name them. */
|
|
@@ -74,12 +92,63 @@ export const FILES_ENTRY_ENV_NAMES = [
|
|
|
74
92
|
'FILES_API_KEY',
|
|
75
93
|
];
|
|
76
94
|
export const filesCollectionEnvName = (name) => `FILES_COLLECTION_${name}_ID`;
|
|
95
|
+
/** #1116: the env names the module settings write; `env` may not name one while its setting is set. */
|
|
96
|
+
export const FILES_SETTINGS_ENV_NAMES = ['FILES_SHARE_LINK_DAYS', 'FILES_SHARE_LINK_PICK'];
|
|
97
|
+
export const MAIL_SETTINGS_ENV_NAMES = ['MAIL_FROM_NAME', 'MAIL_REPLY_TO'];
|
|
98
|
+
/**
|
|
99
|
+
* #1116: the env an entry's module settings give the app. A name is written only while its setting differs
|
|
100
|
+
* from the default, so an entry saved before these settings plans as it did; the app reads an absent name as
|
|
101
|
+
* the default (`FILES_SHARE_LINK_DAYS` 7, `FILES_SHARE_LINK_PICK` true, no `MAIL_FROM_NAME` or
|
|
102
|
+
* `MAIL_REPLY_TO`). A name a setting stopped writing is taken out of the app's env by the next apply.
|
|
103
|
+
*/
|
|
104
|
+
export function entrySettingsEnv(entry) {
|
|
105
|
+
const env = {};
|
|
106
|
+
const files = entry.uses?.files;
|
|
107
|
+
if (files) {
|
|
108
|
+
if (files.shareLinkDays !== undefined &&
|
|
109
|
+
files.shareLinkDays !== FILES_SHARE_LINK_DAYS_DEFAULT) {
|
|
110
|
+
env.FILES_SHARE_LINK_DAYS =
|
|
111
|
+
files.shareLinkDays === null ? 'never' : String(files.shareLinkDays);
|
|
112
|
+
}
|
|
113
|
+
if (files.shareLinkPick === false)
|
|
114
|
+
env.FILES_SHARE_LINK_PICK = 'false';
|
|
115
|
+
}
|
|
116
|
+
const mail = entry.uses?.mail;
|
|
117
|
+
if (mail && mail !== true) {
|
|
118
|
+
if (mail.fromName !== undefined)
|
|
119
|
+
env.MAIL_FROM_NAME = mail.fromName;
|
|
120
|
+
if (mail.replyTo !== undefined)
|
|
121
|
+
env.MAIL_REPLY_TO = mail.replyTo;
|
|
122
|
+
}
|
|
123
|
+
return env;
|
|
124
|
+
}
|
|
77
125
|
/** The env names `uses.edge` writes; `env` may not name them (#317). */
|
|
78
126
|
export const EDGE_ENTRY_ENV_NAMES = ['EDGE_URL', 'EDGE_API_KEY'];
|
|
79
127
|
/** The env names `uses.analytics` writes on a server entry; `env` may not name them. */
|
|
80
128
|
export const ANALYTICS_ENTRY_ENV_NAMES = ['ANALYTICS_URL', 'ANALYTICS_SITE_KEY'];
|
|
129
|
+
/**
|
|
130
|
+
* What the reporting backend accepts as a site (`SITE_PATTERN` in wtfalch/reporting's core): a lower-case
|
|
131
|
+
* letter, then letters, digits and hyphens, at most 64 in all. An entry's site is its key.
|
|
132
|
+
*/
|
|
133
|
+
export const REPORTING_SITE_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
134
|
+
/** The env names `uses.reporting` writes on a server entry; `env` may not name them. */
|
|
135
|
+
export const REPORTING_ENTRY_ENV_NAMES = ['REPORTING_URL', 'REPORTING_SITE_KEY'];
|
|
81
136
|
/** The env names `uses.mail` writes; `env` may not name them. */
|
|
82
137
|
export const MAIL_ENTRY_ENV_NAMES = ['LETTERMINT_API_TOKEN', 'MAIL_FROM'];
|
|
138
|
+
/**
|
|
139
|
+
* The env name every entry that sends mail ({@link sendsMail}) gets, holding its entry key, and its
|
|
140
|
+
* stage or workspace copy `<key>-work` ({@link mailAppKeyOf}). `@wtfalch/mailer` 0.10.0 tags each send
|
|
141
|
+
* with it, so Archon's mail reads can split mail per app. Not a secret. No entry's `env` may name it.
|
|
142
|
+
*/
|
|
143
|
+
export const MAIL_APP_KEY_ENV = 'MAIL_APP_KEY';
|
|
144
|
+
/** Whether the entry sends mail: the main entry and each entry with `uses.mail`. */
|
|
145
|
+
export function sendsMail(entry) {
|
|
146
|
+
return isMainEntry(entry) || entry.uses?.mail === true;
|
|
147
|
+
}
|
|
148
|
+
/** The `MAIL_APP_KEY` of an entry's copy: its key live, `<key>-work` for its stage or workspace. */
|
|
149
|
+
export function mailAppKeyOf(entryKey, copy) {
|
|
150
|
+
return copy === 'live' ? entryKey : `${entryKey}-work`;
|
|
151
|
+
}
|
|
83
152
|
/** The env names `uses.jobs` writes; `env` may not name them. */
|
|
84
153
|
export const JOBS_ENTRY_ENV_NAMES = [
|
|
85
154
|
'JOBS_SCHEDULER_URL',
|
|
@@ -749,6 +818,7 @@ function validateHostingEntry(entry, enabled, reserveKeys, github) {
|
|
|
749
818
|
}
|
|
750
819
|
if (FOUNDRY_ENV_NAME.test(name) ||
|
|
751
820
|
(name === 'APP_URL' && isMainEntry(entry)) ||
|
|
821
|
+
name === MAIL_APP_KEY_ENV ||
|
|
752
822
|
(entry.blueGreen && BLUE_GREEN_ENV_NAME.test(name))) {
|
|
753
823
|
return refusal('env_name_reserved', `hosting.entries.${entry.key}.env.${name}`, `${at} env '${name}' is set by foundry and cannot be declared`);
|
|
754
824
|
}
|
|
@@ -916,14 +986,36 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
916
986
|
const at = `hosting.entries '${entry.key}' uses.auth`;
|
|
917
987
|
if (!hosting.enabled)
|
|
918
988
|
return refusal('auth_needs_hosting', `hosting.entries.${entry.key}.uses.auth`, `${at} requires hosting.enabled`);
|
|
919
|
-
|
|
920
|
-
|
|
989
|
+
const users = auth.users;
|
|
990
|
+
const usersList = Array.isArray(users) ? users : [users];
|
|
991
|
+
if (usersList.length < 1 ||
|
|
992
|
+
usersList.length > ENTRY_AUDIENCES.length ||
|
|
993
|
+
!usersList.every((u) => ENTRY_AUDIENCES.includes(u)) ||
|
|
994
|
+
new Set(usersList).size !== usersList.length) {
|
|
995
|
+
return refusal('auth_users_invalid', `hosting.entries.${entry.key}.uses.auth.users`, `${at} users must be 'staff', 'customers' or a list of one or both of them, each once`);
|
|
996
|
+
}
|
|
997
|
+
const both = signsInBoth(auth);
|
|
998
|
+
// Both audiences are registered through the company's registrar (ADR 0003, auth ADR 0008).
|
|
999
|
+
if (both && entry.workerApp !== undefined) {
|
|
1000
|
+
return refusal('auth_both_worker_app', `hosting.entries.${entry.key}.uses.auth.users`, `${at} users 'customers' and 'staff' is for an app that runs as a server: a Worker app's sign-in is registered by Boule`);
|
|
1001
|
+
}
|
|
1002
|
+
if (both) {
|
|
1003
|
+
const unsupported = [
|
|
1004
|
+
[auth.mcp, 'uses.auth.mcp', 'mcp'],
|
|
1005
|
+
[auth.workspaces, 'uses.auth.workspaces', 'workspaces'],
|
|
1006
|
+
[auth.replacesBrokerEntry, 'uses.auth.replacesBrokerEntry', 'replacesBrokerEntry'],
|
|
1007
|
+
[entry.blueGreen, 'blueGreen', 'blueGreen'],
|
|
1008
|
+
];
|
|
1009
|
+
const found = unsupported.find(([value]) => value !== undefined);
|
|
1010
|
+
if (found) {
|
|
1011
|
+
return refusal('auth_both_unsupported', `hosting.entries.${entry.key}.${found[1]}`, `${at} users 'customers' and 'staff' cannot have ${found[2]} yet: an app both sign in to is registered at the sign-in broker, which foundry does not do with ${found[2]}`);
|
|
1012
|
+
}
|
|
921
1013
|
}
|
|
922
1014
|
if (auth.registration !== 'open' && auth.registration !== 'closed') {
|
|
923
1015
|
return refusal('auth_registration_invalid', `hosting.entries.${entry.key}.uses.auth.registration`, `${at} registration must be 'open' or 'closed'`);
|
|
924
1016
|
}
|
|
925
1017
|
// The staff organisation holds the org's own people and its project.
|
|
926
|
-
if (auth
|
|
1018
|
+
if (!signsInCustomers(auth) && auth.registration === 'open') {
|
|
927
1019
|
return refusal('registration_open_needs_customers', `hosting.entries.${entry.key}.uses.auth.registration`, `${at} registration 'open' needs users 'customers': nobody signs themselves up as staff`);
|
|
928
1020
|
}
|
|
929
1021
|
if (auth.replacesBrokerEntry !== undefined &&
|
|
@@ -938,7 +1030,7 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
938
1030
|
return refusal('auth_needs_exact_hostname', `hosting.entries.${entry.key}.hostnames`, `${at} needs an exact hostname to sign in at`);
|
|
939
1031
|
}
|
|
940
1032
|
if (auth.mcp !== undefined) {
|
|
941
|
-
if (auth
|
|
1033
|
+
if (!signsInCustomers(auth)) {
|
|
942
1034
|
return refusal('mcp_needs_open_customers', `hosting.entries.${entry.key}.uses.auth.mcp`, `${at} mcp needs users 'customers': an MCP client signs the customer in, and registration may be open or closed`);
|
|
943
1035
|
}
|
|
944
1036
|
if (typeof auth.mcp?.path !== 'string' ||
|
|
@@ -987,13 +1079,13 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
987
1079
|
if (signIn !== undefined && !methods.includes(signIn)) {
|
|
988
1080
|
return refusal('auth_sign_ins_invalid', signInsAt, `${at} signIns must include signIn '${String(signIn)}'`);
|
|
989
1081
|
}
|
|
990
|
-
if (auth
|
|
1082
|
+
if (!signsInCustomers(auth) && (methods.length !== 1 || methods[0] !== 'password')) {
|
|
991
1083
|
return refusal('sign_in_staff_password_only', signInsAt, `${at} signIns for users 'staff' must be ['password']: your team signs in with a password`);
|
|
992
1084
|
}
|
|
993
1085
|
if (methods.length === 1 && methods[0] === 'passkey') {
|
|
994
1086
|
return refusal('passkey_needs_another_method', signInsAt, `${at} signIns 'passkey' needs 'email-code' or 'password' beside it: a first passkey and its recovery both use one of them`);
|
|
995
1087
|
}
|
|
996
|
-
if (methods.includes('email-code') && auth
|
|
1088
|
+
if (methods.includes('email-code') && !signsInCustomers(auth)) {
|
|
997
1089
|
return refusal('email_code_needs_customers', signInsAt, `${at} signIns 'email-code' needs users 'customers': only customers sign in with an emailed code`);
|
|
998
1090
|
}
|
|
999
1091
|
// Every reader of the emailed code (the sign-in metadata, the broker entry, the workspace sign-in) reads
|
|
@@ -1007,15 +1099,15 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
1007
1099
|
if (!SECOND_FACTOR_MODES.includes(secondFactor)) {
|
|
1008
1100
|
return refusal('auth_second_factor_invalid', `hosting.entries.${entry.key}.uses.auth.secondFactor`, `${at} secondFactor must be 'off', 'allowed' or 'required'`);
|
|
1009
1101
|
}
|
|
1010
|
-
if (secondFactor !== 'off' && auth
|
|
1102
|
+
if (secondFactor !== 'off' && !signsInCustomers(auth)) {
|
|
1011
1103
|
return refusal('sign_in_staff_password_only', `hosting.entries.${entry.key}.uses.auth.secondFactor`, `${at} secondFactor for users 'staff' must be 'off': staff two-step sign-in is set for the whole company`);
|
|
1012
1104
|
}
|
|
1013
1105
|
}
|
|
1014
|
-
if (signIn === 'email-code' && auth
|
|
1106
|
+
if (signIn === 'email-code' && !signsInCustomers(auth)) {
|
|
1015
1107
|
return refusal('email_code_needs_customers', `hosting.entries.${entry.key}.uses.auth.signIn`, `${at} signIn 'email-code' needs users 'customers': only customers sign in with an emailed code`);
|
|
1016
1108
|
}
|
|
1017
1109
|
if (auth.room !== undefined) {
|
|
1018
|
-
if (auth
|
|
1110
|
+
if (!signsInCustomers(auth)) {
|
|
1019
1111
|
return refusal('auth_room_needs_customers', `hosting.entries.${entry.key}.uses.auth.room`, `${at} room needs users 'customers': only the customers' sign-in window shows it`);
|
|
1020
1112
|
}
|
|
1021
1113
|
const problem = roomProblem(auth.room);
|
|
@@ -1047,7 +1139,7 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
1047
1139
|
*/
|
|
1048
1140
|
function validateEntryWorkspaces(entry, workspaces, at) {
|
|
1049
1141
|
const path = `hosting.entries.${entry.key}.uses.auth.workspaces`;
|
|
1050
|
-
if (entry.uses?.auth
|
|
1142
|
+
if (!signsInCustomers(entry.uses?.auth)) {
|
|
1051
1143
|
return refusal('workspaces_needs_customers', path, `${at} workspaces needs users 'customers': workspaces belong to an app's own customers`);
|
|
1052
1144
|
}
|
|
1053
1145
|
if (entry.workerApp !== undefined) {
|
|
@@ -1191,7 +1283,15 @@ function validateEntryFiles(entry, hosting, domain) {
|
|
|
1191
1283
|
if (files.shareHost && !(hosting.usesDomain && domain)) {
|
|
1192
1284
|
return refusal('files_share_host_needs_domain', `hosting.entries.${entry.key}.uses.files.shareHost`, `${at} shareHost needs hosting.usesDomain and a domain: it is served at share.<domain>`);
|
|
1193
1285
|
}
|
|
1194
|
-
const
|
|
1286
|
+
const settings = filesSettingsProblem(files);
|
|
1287
|
+
if (settings) {
|
|
1288
|
+
return refusal('files_settings_invalid', `hosting.entries.${entry.key}.uses.files.${settings[0]}`, `${at} ${settings[1]}`);
|
|
1289
|
+
}
|
|
1290
|
+
const written = [
|
|
1291
|
+
...FILES_ENTRY_ENV_NAMES,
|
|
1292
|
+
...FILES_SETTINGS_ENV_NAMES.filter((name) => name in entrySettingsEnv(entry)),
|
|
1293
|
+
...files.collections.map(filesCollectionEnvName),
|
|
1294
|
+
];
|
|
1195
1295
|
const named = Object.keys(entry.env ?? {}).find((name) => written.includes(name));
|
|
1196
1296
|
if (named)
|
|
1197
1297
|
return refusal('files_env_clash', `hosting.entries.${entry.key}.env.${named}`, `${at} writes ${named}; remove it from env`);
|
|
@@ -1335,7 +1435,7 @@ function validateEntryCustomerGrants(entry) {
|
|
|
1335
1435
|
const at = `hosting.entries '${entry.key}' uses.customerGrants`;
|
|
1336
1436
|
if (value !== true)
|
|
1337
1437
|
return refusal('customer_grants_not_true', path, `${at} must be true`);
|
|
1338
|
-
if (entry.uses?.auth
|
|
1438
|
+
if (!signsInCustomers(entry.uses?.auth)) {
|
|
1339
1439
|
return refusal('customer_grants_needs_customers', path, `${at} is for an app whose customers sign in: declare uses.auth with users 'customers', or remove it`);
|
|
1340
1440
|
}
|
|
1341
1441
|
const named = Object.keys(entry.env ?? {}).find((name) => CUSTOMER_GRANTS_ENTRY_ENV_NAMES.includes(name));
|
|
@@ -1344,20 +1444,125 @@ function validateEntryCustomerGrants(entry) {
|
|
|
1344
1444
|
}
|
|
1345
1445
|
return null;
|
|
1346
1446
|
}
|
|
1347
|
-
/**
|
|
1447
|
+
/** #1116: the field and the words when `uses.files`' settings are not ones the module can write, else null. */
|
|
1448
|
+
function filesSettingsProblem(files) {
|
|
1449
|
+
const days = files.shareLinkDays;
|
|
1450
|
+
if (days !== undefined &&
|
|
1451
|
+
days !== null &&
|
|
1452
|
+
!FILES_SHARE_LINK_DAYS.includes(days)) {
|
|
1453
|
+
return [
|
|
1454
|
+
'shareLinkDays',
|
|
1455
|
+
`shareLinkDays must be null or one of ${FILES_SHARE_LINK_DAYS.join(', ')}`,
|
|
1456
|
+
];
|
|
1457
|
+
}
|
|
1458
|
+
if (files.shareLinkPick !== undefined && typeof files.shareLinkPick !== 'boolean') {
|
|
1459
|
+
return ['shareLinkPick', 'shareLinkPick must be true or false'];
|
|
1460
|
+
}
|
|
1461
|
+
if (files.spaceAlertPercent !== undefined &&
|
|
1462
|
+
!wholeIn(files.spaceAlertPercent, PERCENT_MIN, PERCENT_MAX)) {
|
|
1463
|
+
return [
|
|
1464
|
+
'spaceAlertPercent',
|
|
1465
|
+
`spaceAlertPercent must be a whole number from ${PERCENT_MIN} to ${PERCENT_MAX}`,
|
|
1466
|
+
];
|
|
1467
|
+
}
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
const MAIL_SETTING_KEYS = ['fromName', 'replyTo', 'stopAfterBounces', 'bounceAlertPercent'];
|
|
1471
|
+
const JOBS_SETTING_KEYS = ['retries', 'alertOwners', 'pauseAfterFailures'];
|
|
1472
|
+
/** #1116: the field and the words when `uses.mail`'s settings object is not one the Email module can write, else null. */
|
|
1473
|
+
function mailSettingsProblem(mail) {
|
|
1474
|
+
const stray = Object.keys(mail).find((key) => !MAIL_SETTING_KEYS.includes(key));
|
|
1475
|
+
if (stray !== undefined)
|
|
1476
|
+
return [stray, `has no setting ${stray}`];
|
|
1477
|
+
const { fromName, replyTo, stopAfterBounces, bounceAlertPercent } = mail;
|
|
1478
|
+
if (fromName !== undefined &&
|
|
1479
|
+
(typeof fromName !== 'string' ||
|
|
1480
|
+
fromName.length > MAIL_FROM_NAME_MAX ||
|
|
1481
|
+
fromName.trim() !== fromName ||
|
|
1482
|
+
!MAIL_FROM_NAME.test(fromName))) {
|
|
1483
|
+
return [
|
|
1484
|
+
'fromName',
|
|
1485
|
+
`fromName must be 1 to ${MAIL_FROM_NAME_MAX} characters, with no space at either end and none of < > " , ; \\`,
|
|
1486
|
+
];
|
|
1487
|
+
}
|
|
1488
|
+
if (replyTo !== undefined &&
|
|
1489
|
+
(typeof replyTo !== 'string' ||
|
|
1490
|
+
replyTo.length > MAIL_REPLY_TO_MAX ||
|
|
1491
|
+
!MAIL_REPLY_TO.test(replyTo))) {
|
|
1492
|
+
return ['replyTo', 'replyTo must be one email address, such as help@example.com'];
|
|
1493
|
+
}
|
|
1494
|
+
if (stopAfterBounces !== undefined && !wholeIn(stopAfterBounces, 1, MAIL_BOUNCES_MAX)) {
|
|
1495
|
+
return [
|
|
1496
|
+
'stopAfterBounces',
|
|
1497
|
+
`stopAfterBounces must be a whole number from 1 to ${MAIL_BOUNCES_MAX}`,
|
|
1498
|
+
];
|
|
1499
|
+
}
|
|
1500
|
+
if (bounceAlertPercent !== undefined && !wholeIn(bounceAlertPercent, PERCENT_MIN, PERCENT_MAX)) {
|
|
1501
|
+
return [
|
|
1502
|
+
'bounceAlertPercent',
|
|
1503
|
+
`bounceAlertPercent must be a whole number from ${PERCENT_MIN} to ${PERCENT_MAX}`,
|
|
1504
|
+
];
|
|
1505
|
+
}
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
/** #1116: the field and the words when `uses.jobs`' settings object is not one the Jobs module can write, else null. */
|
|
1509
|
+
function jobsSettingsProblem(jobs) {
|
|
1510
|
+
const stray = Object.keys(jobs).find((key) => !JOBS_SETTING_KEYS.includes(key));
|
|
1511
|
+
if (stray !== undefined)
|
|
1512
|
+
return [stray, `has no setting ${stray}`];
|
|
1513
|
+
const { retries, alertOwners, pauseAfterFailures } = jobs;
|
|
1514
|
+
if (retries !== undefined && !wholeIn(retries, 0, JOBS_RETRIES_MAX)) {
|
|
1515
|
+
return ['retries', `retries must be a whole number from 0 to ${JOBS_RETRIES_MAX}`];
|
|
1516
|
+
}
|
|
1517
|
+
if (alertOwners !== undefined && typeof alertOwners !== 'boolean') {
|
|
1518
|
+
return ['alertOwners', 'alertOwners must be true or false'];
|
|
1519
|
+
}
|
|
1520
|
+
if (pauseAfterFailures !== undefined &&
|
|
1521
|
+
pauseAfterFailures !== null &&
|
|
1522
|
+
!wholeIn(pauseAfterFailures, 1, JOBS_PAUSE_AFTER_MAX)) {
|
|
1523
|
+
return [
|
|
1524
|
+
'pauseAfterFailures',
|
|
1525
|
+
`pauseAfterFailures must be null or a whole number from 1 to ${JOBS_PAUSE_AFTER_MAX}`,
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* `uses.mail`, `uses.jobs`, `uses.analytics` and `uses.reporting`: `true` (mail and jobs: or their settings
|
|
1532
|
+
* object, #1116), `hosting.enabled`, and no `env` entry that names a value they write. `uses.reporting` also
|
|
1533
|
+
* needs an entry key the reporting backend takes as a site (a letter first): the site of the entry's key is the
|
|
1534
|
+
* entry key, and Boule reads an app's errors by it.
|
|
1535
|
+
*/
|
|
1348
1536
|
function validateEntryMailJobs(entry, hosting) {
|
|
1349
|
-
for (const [use, written, code] of [
|
|
1350
|
-
|
|
1351
|
-
[
|
|
1352
|
-
|
|
1537
|
+
for (const [use, written, code, settings] of [
|
|
1538
|
+
// A mail setting's env name clashes only while that setting is set: absent, the app's own env value stays.
|
|
1539
|
+
[
|
|
1540
|
+
'mail',
|
|
1541
|
+
[
|
|
1542
|
+
...MAIL_ENTRY_ENV_NAMES,
|
|
1543
|
+
...MAIL_SETTINGS_ENV_NAMES.filter((name) => name in entrySettingsEnv(entry)),
|
|
1544
|
+
],
|
|
1545
|
+
'mail',
|
|
1546
|
+
mailSettingsProblem,
|
|
1547
|
+
],
|
|
1548
|
+
['jobs', JOBS_ENTRY_ENV_NAMES, 'jobs', jobsSettingsProblem],
|
|
1549
|
+
['analytics', ANALYTICS_ENTRY_ENV_NAMES, 'analytics', null],
|
|
1550
|
+
['reporting', REPORTING_ENTRY_ENV_NAMES, 'reporting', null],
|
|
1353
1551
|
]) {
|
|
1354
1552
|
const value = entry.uses?.[use];
|
|
1355
1553
|
if (value === undefined)
|
|
1356
1554
|
continue;
|
|
1357
1555
|
const path = `hosting.entries.${entry.key}.uses.${use}`;
|
|
1358
1556
|
const at = `hosting.entries '${entry.key}' uses.${use}`;
|
|
1359
|
-
|
|
1360
|
-
|
|
1557
|
+
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1558
|
+
if (value !== true && !(settings && isObject)) {
|
|
1559
|
+
return refusal(`${code}_not_true`, path, settings ? `${at} must be true or an object of settings` : `${at} must be true`);
|
|
1560
|
+
}
|
|
1561
|
+
if (settings && isObject) {
|
|
1562
|
+
const bad = settings(value);
|
|
1563
|
+
if (bad)
|
|
1564
|
+
return refusal(`${code}_settings_invalid`, `${path}.${bad[0]}`, `${at} ${bad[1]}`);
|
|
1565
|
+
}
|
|
1361
1566
|
if (!hosting.enabled) {
|
|
1362
1567
|
return refusal(`${code}_needs_hosting`, path, `${at} requires hosting.enabled`);
|
|
1363
1568
|
}
|
|
@@ -1365,6 +1570,9 @@ function validateEntryMailJobs(entry, hosting) {
|
|
|
1365
1570
|
if (named) {
|
|
1366
1571
|
return refusal(`${code}_env_clash`, `hosting.entries.${entry.key}.env.${named}`, `${at} writes ${named}; remove it from env`);
|
|
1367
1572
|
}
|
|
1573
|
+
if (use === 'reporting' && !REPORTING_SITE_PATTERN.test(entry.key)) {
|
|
1574
|
+
return refusal('reporting_site_invalid', path, `${at} needs an entry key that starts with a letter: the key is the app's site in the reporting backend`);
|
|
1575
|
+
}
|
|
1368
1576
|
}
|
|
1369
1577
|
return null;
|
|
1370
1578
|
}
|
|
@@ -1630,6 +1838,11 @@ export function refuseCustomers(org) {
|
|
|
1630
1838
|
return refusal('customers_sign_up_needs_enabled', 'customers.signUp', 'customers.signUp needs customers.enabled');
|
|
1631
1839
|
}
|
|
1632
1840
|
}
|
|
1841
|
+
// Both audiences register in the customers organisation, which the registrar's record covers only with customers on.
|
|
1842
|
+
const both = org.hosting.entries.find((entry) => signsInBoth(entry.uses?.auth));
|
|
1843
|
+
if (both && !org.customers?.enabled) {
|
|
1844
|
+
return refusal('auth_both_needs_customers', `hosting.entries.${both.key}.uses.auth.users`, `hosting.entries '${both.key}' uses.auth users 'customers' and 'staff' needs customers.enabled`);
|
|
1845
|
+
}
|
|
1633
1846
|
const open = org.hosting.entries.find((entry) => entry.uses?.auth?.registration === 'open');
|
|
1634
1847
|
if (open && !(org.customers?.enabled && org.customers.signUp)) {
|
|
1635
1848
|
return refusal('registration_open_needs_sign_up', `hosting.entries.${open.key}.uses.auth.registration`, `hosting.entries '${open.key}' uses.auth registration 'open' needs customers.enabled and customers.signUp`);
|