@wtfalch/foundry 0.70.1 → 0.72.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/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/modules/sign-in.js +81 -9
- package/dist/modules/types.d.ts +2 -0
- package/dist/org.d.ts +18 -1
- package/dist/org.js +40 -0
- package/dist/plan/boule.d.ts +15 -0
- package/dist/plan/boule.js +13 -0
- package/dist/plan/broker-registrar-token.d.ts +69 -0
- package/dist/plan/broker-registrar-token.js +215 -0
- package/dist/plan/broker-registrar.d.ts +2 -0
- package/dist/plan/broker-registrar.js +5 -1
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +26 -5
- package/dist/plan/executor.d.ts +82 -0
- package/dist/plan/executor.js +172 -10
- package/dist/plan/health-check-settings.d.ts +13 -0
- package/dist/plan/health-check-settings.js +17 -0
- package/dist/plan/own-diff.d.ts +32 -0
- package/dist/plan/own-diff.js +67 -10
- package/dist/plan/own-hostnames.d.ts +8 -0
- package/dist/plan/own-hostnames.js +14 -0
- package/dist/plan/own-services.d.ts +7 -0
- package/dist/plan/own-services.js +2 -0
- package/dist/plan/plan-apply.d.ts +36 -0
- package/dist/plan/plan-apply.js +91 -0
- package/dist/plan/platform-diff.d.ts +9 -5
- package/dist/plan/platform-diff.js +16 -10
- package/dist/plan/platform-providers.d.ts +24 -2
- package/dist/plan/platform-providers.js +155 -12
- package/dist/plan/signin-mail.d.ts +18 -5
- package/dist/plan/signin-mail.js +14 -6
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +72 -46
- package/dist/plan/zitadel-machine-user.d.ts +14 -1
- package/dist/plan/zitadel-machine-user.js +96 -13
- package/dist/platform.d.ts +3 -0
- package/dist/release/fanout.d.ts +77 -0
- package/dist/release/fanout.js +177 -0
- package/dist/release/run.js +2 -1
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +3 -0
- package/dist/release/types.d.ts +9 -1
- package/dist/store/schema.d.ts +4 -0
- package/package.json +3 -3
|
@@ -27,6 +27,19 @@
|
|
|
27
27
|
* no live measurement to pin it to instead"). A caller whose app is known to
|
|
28
28
|
* be slower or faster to boot passes its own.
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* #1054: the two fields that let Coolify run a rolling update for an app other containers reach by a fixed name.
|
|
32
|
+
* Coolify starts the new container beside the old one, waits for its check, then stops the old one, only when the
|
|
33
|
+
* app has no fixed container name (`custom_internal_name`) and the consistent-name flag is off. A network alias
|
|
34
|
+
* answers on the Docker network under the same name throughout the swap, so callers send this instead of
|
|
35
|
+
* `custom_internal_name`. The app also needs a health check to wait on (`healthCheckSettings` or
|
|
36
|
+
* {@link IMAGE_HEALTHCHECK_SETTINGS}).
|
|
37
|
+
*/
|
|
38
|
+
export declare function rollingUpdateSettings(alias: string): {
|
|
39
|
+
custom_network_aliases: string;
|
|
40
|
+
is_consistent_container_name_enabled: false;
|
|
41
|
+
custom_internal_name: null;
|
|
42
|
+
};
|
|
30
43
|
/** The four Coolify `updateApplication` fields {@link healthCheckSettings} returns. */
|
|
31
44
|
export interface HealthCheckPatch {
|
|
32
45
|
health_check_enabled: true;
|
|
@@ -27,6 +27,23 @@
|
|
|
27
27
|
* no live measurement to pin it to instead"). A caller whose app is known to
|
|
28
28
|
* be slower or faster to boot passes its own.
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* #1054: the two fields that let Coolify run a rolling update for an app other containers reach by a fixed name.
|
|
32
|
+
* Coolify starts the new container beside the old one, waits for its check, then stops the old one, only when the
|
|
33
|
+
* app has no fixed container name (`custom_internal_name`) and the consistent-name flag is off. A network alias
|
|
34
|
+
* answers on the Docker network under the same name throughout the swap, so callers send this instead of
|
|
35
|
+
* `custom_internal_name`. The app also needs a health check to wait on (`healthCheckSettings` or
|
|
36
|
+
* {@link IMAGE_HEALTHCHECK_SETTINGS}).
|
|
37
|
+
*/
|
|
38
|
+
export function rollingUpdateSettings(alias) {
|
|
39
|
+
// `custom_internal_name: null` clears the fixed name an app made by an earlier foundry still has: with it set
|
|
40
|
+
// Coolify refuses the rolling update whatever else is patched.
|
|
41
|
+
return {
|
|
42
|
+
custom_network_aliases: alias,
|
|
43
|
+
is_consistent_container_name_enabled: false,
|
|
44
|
+
custom_internal_name: null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
30
47
|
/** Archon's own number (see this module's doc comment) -- the default every other caller inherits unless it knows better. */
|
|
31
48
|
export const DEFAULT_HEALTH_CHECK_START_PERIOD_SECONDS = 30;
|
|
32
49
|
/**
|
package/dist/plan/own-diff.d.ts
CHANGED
|
@@ -52,7 +52,35 @@ export declare const OWN_AUTH_BLOCKED_KEY = "own-auth";
|
|
|
52
52
|
export { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY };
|
|
53
53
|
/** The auth git ref an own org's sign-in last deployed, so a changed declared ref plans the step again. */
|
|
54
54
|
export declare const OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY = "own_auth_applied_git_ref";
|
|
55
|
+
/** The Git repository the company's login app, in-app login app and broker are built from. */
|
|
56
|
+
export declare const OWN_AUTH_REPOSITORY = "wtfalch/auth";
|
|
57
|
+
/** What the host read of the declared auth branch: its head commit, or why it could not be read (fixed text). */
|
|
58
|
+
export type OwnAuthHead = {
|
|
59
|
+
commit: string;
|
|
60
|
+
} | {
|
|
61
|
+
failed: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* True for a declared auth ref whose head can move: not a commit and not a release tag (`v1.2.3`, `1.2`).
|
|
65
|
+
* A tag or a commit already names one build, so it is never read.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isBranchRef(ref: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The company's auth apps a moved branch head redeploys, with the name their `built_commit_<name>` is recorded
|
|
70
|
+
* under. The in-app login app is listed once it is recorded; the broker once it was ever deployed (before that it
|
|
71
|
+
* crash-loops on missing inputs and is not redeployed, so it never records a build).
|
|
72
|
+
*/
|
|
73
|
+
export declare function ownAuthBuildApps(org: Pick<DeclaredOrg, 'slug'>, recordedIds: Readonly<Record<string, string>>): Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}>;
|
|
55
77
|
export interface PlanOwnAuthOptions {
|
|
78
|
+
/**
|
|
79
|
+
* #886: the head of the declared auth branch, read by the executor (`FoundryStepExecutor.ownAuthHead`).
|
|
80
|
+
* Absent: not read (a tag, a commit, or a host without the read), so only the declared ref decides.
|
|
81
|
+
* A failed read plans nothing from the head and is reported.
|
|
82
|
+
*/
|
|
83
|
+
authHead?: OwnAuthHead;
|
|
56
84
|
org: DeclaredOrg;
|
|
57
85
|
recordedIds: Readonly<Record<string, string>>;
|
|
58
86
|
readiness: OrgPlatformReadiness;
|
|
@@ -76,6 +104,10 @@ export interface PlanOwnAuthOptions {
|
|
|
76
104
|
signinDbBackupConfigured?: boolean;
|
|
77
105
|
/** The host's ZITADEL port can write the sign-in mail text and look. Default true. False: the marker is not required. */
|
|
78
106
|
signinMailWritable?: boolean;
|
|
107
|
+
/** The host's ZITADEL port can make the broker's registrar token. True: it is asked for. False: reported blocked. Absent: neither. */
|
|
108
|
+
registrarTokenWritable?: boolean;
|
|
109
|
+
/** The registrar token's held-key ref holds a value, so a rerun after a crash delivers it. Default false. */
|
|
110
|
+
registrarTokenHeld?: boolean;
|
|
79
111
|
}
|
|
80
112
|
/**
|
|
81
113
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -3,18 +3,20 @@ import { sfuSlotPorts } from '../calls-sfu.js';
|
|
|
3
3
|
import { SFU_SLOT_MAX, ownsServices } from '../org.js';
|
|
4
4
|
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey } from './broker-entries.js';
|
|
6
|
+
import { REGISTRAR_TOKEN_SCOPE_KEY, REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY, registrarTokenDue, } from './broker-registrar-token.js';
|
|
7
|
+
import { builtCommitScopeKey } from './build-record.js';
|
|
6
8
|
import { ZITADEL_FILES_PROJECT_SCOPE_KEY } from './core-files.js';
|
|
7
9
|
import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
|
|
8
10
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_BASE, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, } from './mail-relay.js';
|
|
9
11
|
import { mailStoreIsLegacy, mailStoreSwitchDescription } from './mail-store.js';
|
|
10
|
-
import { ownHostnames } from './own-hostnames.js';
|
|
12
|
+
import { ownHostnames, passkeyRpIdFor } from './own-hostnames.js';
|
|
11
13
|
import { JOBS_PUBLIC_URL_SCOPE_KEY, OWN_JOBS_ROUTE_TUNNEL_SCOPE_KEY } from './own-jobs-route.js';
|
|
12
14
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } from './own-services.js';
|
|
13
15
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
14
|
-
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
16
|
+
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, authLoginEnvMarker, orgDeclaresMcp, platformMailMxHost, } from './platform-diff.js';
|
|
15
17
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
16
|
-
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
17
|
-
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
|
|
18
|
+
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
19
|
+
import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn, signInLoginNames } from './sign-in.js';
|
|
18
20
|
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
19
21
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, signinDbCertDue } from './signin-db-tls.js';
|
|
20
22
|
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db.js';
|
|
@@ -126,6 +128,39 @@ export const OWN_AUTH_BLOCKED_KEY = 'own-auth';
|
|
|
126
128
|
export { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY };
|
|
127
129
|
/** The auth git ref an own org's sign-in last deployed, so a changed declared ref plans the step again. */
|
|
128
130
|
export const OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY = 'own_auth_applied_git_ref';
|
|
131
|
+
/** The Git repository the company's login app, in-app login app and broker are built from. */
|
|
132
|
+
export const OWN_AUTH_REPOSITORY = 'wtfalch/auth';
|
|
133
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
134
|
+
/**
|
|
135
|
+
* True for a declared auth ref whose head can move: not a commit and not a release tag (`v1.2.3`, `1.2`).
|
|
136
|
+
* A tag or a commit already names one build, so it is never read.
|
|
137
|
+
*/
|
|
138
|
+
export function isBranchRef(ref) {
|
|
139
|
+
return !FULL_SHA.test(ref) && !/^v?\d+\.\d+/.test(ref);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The company's auth apps a moved branch head redeploys, with the name their `built_commit_<name>` is recorded
|
|
143
|
+
* under. The in-app login app is listed once it is recorded; the broker once it was ever deployed (before that it
|
|
144
|
+
* crash-loops on missing inputs and is not redeployed, so it never records a build).
|
|
145
|
+
*/
|
|
146
|
+
export function ownAuthBuildApps(org, recordedIds) {
|
|
147
|
+
const target = orgTarget(org);
|
|
148
|
+
const apps = [
|
|
149
|
+
{ id: recordedIds[PLATFORM_AUTH_LOGIN_SCOPE_KEY], name: target.names.authLogin },
|
|
150
|
+
{ id: recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY], name: signInLoginNames(target).name },
|
|
151
|
+
{
|
|
152
|
+
id: recordedIds[PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY]
|
|
153
|
+
? recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY]
|
|
154
|
+
: undefined,
|
|
155
|
+
name: target.names.authBroker,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
return apps.filter((app) => Boolean(app.id));
|
|
159
|
+
}
|
|
160
|
+
/** True when the read head differs from the commit any of the company's auth apps was last built from. */
|
|
161
|
+
function authHeadMoved(org, recordedIds, head) {
|
|
162
|
+
return ownAuthBuildApps(org, recordedIds).some((app) => recordedIds[builtCommitScopeKey(app.name)] !== head);
|
|
163
|
+
}
|
|
129
164
|
/**
|
|
130
165
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
|
131
166
|
* and the broker in the org's own Coolify project, at `auth.<domain>`.
|
|
@@ -197,7 +232,21 @@ export function planOwnAuth(options) {
|
|
|
197
232
|
return blocked('the host supplies no backup bucket for the sign-in database');
|
|
198
233
|
}
|
|
199
234
|
}
|
|
235
|
+
const registrarDue = options.registrarTokenWritable === true && registrarTokenDue(org, recordedIds);
|
|
236
|
+
// A token minted whose hold did not finish is never minted again: a person removes the marker.
|
|
237
|
+
if (registrarDue &&
|
|
238
|
+
recordedIds[REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY] &&
|
|
239
|
+
!recordedIds[REGISTRAR_TOKEN_SCOPE_KEY] &&
|
|
240
|
+
!options.registrarTokenHeld) {
|
|
241
|
+
return blocked(`the broker's registrar token of ZITADEL machine user ${recordedIds[REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY]} was minted but never confirmed held; remove the recorded key ${REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY} to make a new one`);
|
|
242
|
+
}
|
|
200
243
|
const hostname = names.auth;
|
|
244
|
+
// #886: a branch head that moved past what the auth apps were built from plans the step again. A failed read
|
|
245
|
+
// plans nothing from the head and is reported, so a broken read never loops.
|
|
246
|
+
const head = options.authHead;
|
|
247
|
+
const headCommit = head && 'commit' in head ? head.commit : undefined;
|
|
248
|
+
const headFailed = head && 'failed' in head ? head.failed : undefined;
|
|
249
|
+
const headMoved = headCommit !== undefined && authHeadMoved(org, recordedIds, headCommit);
|
|
201
250
|
// The broker starts only on a valid `AUTH_APPS`, and the login client is minted only once it does:
|
|
202
251
|
// the first broker entry is what lets this step finish, as the platform's first registration does.
|
|
203
252
|
// Archon's registration is an entry too; the platform records it, not this org, so it comes in
|
|
@@ -219,7 +268,7 @@ export function planOwnAuth(options) {
|
|
|
219
268
|
recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
|
|
220
269
|
// The login apps' mail env (`LOGIN_MAIL_FROM`, the Lettermint key); a stack from before it is written again.
|
|
221
270
|
recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
|
|
222
|
-
authLoginEnvMarker(`noreply@${names.mailDomain}
|
|
271
|
+
authLoginEnvMarker(`noreply@${names.mailDomain}`, passkeyRpIdFor(org)) &&
|
|
223
272
|
// The code mail carries the company's name and design; an older apply, or a renamed company, writes it again.
|
|
224
273
|
(options.signinMailWritable === false ||
|
|
225
274
|
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] === signinMailMarker(org.displayName)) &&
|
|
@@ -234,15 +283,23 @@ export function planOwnAuth(options) {
|
|
|
234
283
|
!signinDbCertDue(recordedIds, options.now ?? Date.now()) &&
|
|
235
284
|
!signinDbBackupMissing(recordedIds) &&
|
|
236
285
|
// ZITADEL was started on the TLS database and never seen healthy there: the step starts it again.
|
|
237
|
-
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY]
|
|
286
|
+
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY] &&
|
|
287
|
+
!headMoved &&
|
|
288
|
+
// The broker's registrar token (a company with customers or apps): made, or adopted, and covering the recorded organisations.
|
|
289
|
+
!registrarDue) {
|
|
238
290
|
// Applied and otherwise done: say why the mail stays ZITADEL's stock text, once.
|
|
239
291
|
const mailMissing = options.signinMailWritable === false &&
|
|
240
292
|
recordedIds[PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY] !== signinMailMarker(org.displayName);
|
|
293
|
+
const registrarMissing = options.registrarTokenWritable === false && registrarTokenDue(org, recordedIds);
|
|
241
294
|
return {
|
|
242
295
|
step: null,
|
|
243
|
-
blocked:
|
|
244
|
-
? `${org.slug}'s
|
|
245
|
-
:
|
|
296
|
+
blocked: headFailed
|
|
297
|
+
? `${org.slug}'s auth apps were not checked for a newer auth commit: ${headFailed}`
|
|
298
|
+
: registrarMissing
|
|
299
|
+
? `${org.slug}'s sign-in broker has no registrar token: the host cannot make it, so its apps cannot register sign-in`
|
|
300
|
+
: mailMissing
|
|
301
|
+
? `${org.slug}'s sign-in mail keeps ZITADEL's default text: the host cannot write message texts`
|
|
302
|
+
: null,
|
|
246
303
|
};
|
|
247
304
|
}
|
|
248
305
|
return {
|
|
@@ -252,7 +309,7 @@ export function planOwnAuth(options) {
|
|
|
252
309
|
system: 'coolify',
|
|
253
310
|
kind: 'own-auth',
|
|
254
311
|
key: org.id,
|
|
255
|
-
desired: { hostname, gitRef },
|
|
312
|
+
desired: { hostname, gitRef, ...(headCommit ? { head: headCommit } : {}) },
|
|
256
313
|
},
|
|
257
314
|
step: {
|
|
258
315
|
description: `provision ${org.slug}'s own sign-in at ${hostname}`,
|
|
@@ -27,3 +27,11 @@ export interface OwnHostnames {
|
|
|
27
27
|
* {@link loginHostnameFor}, so the two cannot disagree.
|
|
28
28
|
*/
|
|
29
29
|
export declare function ownHostnames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'issuer' | 'mail'>): OwnHostnames;
|
|
30
|
+
/**
|
|
31
|
+
* The WebAuthn RP ID for a company's login apps (`LOGIN_PASSKEY_RP_ID`): the company's own domain, which
|
|
32
|
+
* `auth.<domain>` and the in-app hosts (`agora.<domain>`, `boule.<domain>`) all sit under, so one passkey
|
|
33
|
+
* works on each. Undefined for a company without its own sign-in domain: its `<slug>-auth.oikos.host` shares
|
|
34
|
+
* `oikos.host` with every other company, so an RP ID there would span companies. The login app refuses a
|
|
35
|
+
* host outside the RP ID, so a company's hosts stay under its domain or the variable stays unset.
|
|
36
|
+
*/
|
|
37
|
+
export declare function passkeyRpIdFor(org: Pick<DeclaredOrg, 'domain' | 'issuer'>): string | undefined;
|
|
@@ -23,3 +23,17 @@ export function ownHostnames(org) {
|
|
|
23
23
|
mailDomain: mailOnDomain && domain ? domain : `${slug}.${NO_DOMAIN_ZONE}`,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
const BARE_DOMAIN = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
27
|
+
/**
|
|
28
|
+
* The WebAuthn RP ID for a company's login apps (`LOGIN_PASSKEY_RP_ID`): the company's own domain, which
|
|
29
|
+
* `auth.<domain>` and the in-app hosts (`agora.<domain>`, `boule.<domain>`) all sit under, so one passkey
|
|
30
|
+
* works on each. Undefined for a company without its own sign-in domain: its `<slug>-auth.oikos.host` shares
|
|
31
|
+
* `oikos.host` with every other company, so an RP ID there would span companies. The login app refuses a
|
|
32
|
+
* host outside the RP ID, so a company's hosts stay under its domain or the variable stays unset.
|
|
33
|
+
*/
|
|
34
|
+
export function passkeyRpIdFor(org) {
|
|
35
|
+
const domain = org.domain?.toLowerCase();
|
|
36
|
+
if (!domain || !org.issuer.usesDomain || !BARE_DOMAIN.test(domain))
|
|
37
|
+
return undefined;
|
|
38
|
+
return domain;
|
|
39
|
+
}
|
|
@@ -61,6 +61,8 @@ export interface ServiceTarget {
|
|
|
61
61
|
};
|
|
62
62
|
readonly refs: {
|
|
63
63
|
instanceBearer: string;
|
|
64
|
+
/** The sign-in broker's ZITADEL registrar token (`ZITADEL_REGISTRAR_TOKEN`), held after its one mint. */
|
|
65
|
+
registrarToken: string;
|
|
64
66
|
authAdmin: string;
|
|
65
67
|
brokerMailKey: string;
|
|
66
68
|
/** The connection string of the database for the broker's customer workspaces: foundry holds it after making the database, or the operator holds one first. */
|
|
@@ -209,4 +211,9 @@ export interface OwnServicesPort {
|
|
|
209
211
|
/** Builds the ZITADEL port for one company instance. */
|
|
210
212
|
zitadelFor(instance: OwnAuthInstance): ZitadelPort;
|
|
211
213
|
provision: OwnServicesProvision;
|
|
214
|
+
/**
|
|
215
|
+
* #886: the head commit (40 hex characters) of `ref` in the Git repository `repository` (`owner/name`).
|
|
216
|
+
* Absent: a moved auth branch is not noticed. The executor puts a timeout on every call.
|
|
217
|
+
*/
|
|
218
|
+
readBranchHead?(repository: string, ref: string): Promise<string>;
|
|
212
219
|
}
|
|
@@ -46,6 +46,7 @@ export function platformTarget() {
|
|
|
46
46
|
},
|
|
47
47
|
refs: {
|
|
48
48
|
instanceBearer: 'zitadel:platform:instanceBearer',
|
|
49
|
+
registrarToken: 'zitadel:platform:registrarToken',
|
|
49
50
|
authAdmin: 'zitadel:platform:admin',
|
|
50
51
|
brokerMailKey: 'lettermint:platform:apiToken',
|
|
51
52
|
brokerWorkspacesDatabase: 'auth:platform:workspacesDatabaseUrl',
|
|
@@ -103,6 +104,7 @@ export function orgTarget(org) {
|
|
|
103
104
|
},
|
|
104
105
|
refs: {
|
|
105
106
|
instanceBearer: `zitadel:org:${s}:instanceBearer`,
|
|
107
|
+
registrarToken: `zitadel:org:${s}:registrarToken`,
|
|
106
108
|
authAdmin: `zitadel:org:${s}:admin`,
|
|
107
109
|
brokerMailKey: `lettermint:org:${s}:apiToken`,
|
|
108
110
|
brokerWorkspacesDatabase: `auth:org:${s}:workspacesDatabaseUrl`,
|
|
@@ -18,6 +18,7 @@ import type { GithubDeployKeysPort } from './github-deploy-keys.js';
|
|
|
18
18
|
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
19
19
|
import { type LoginHostDiscoveryPort } from './login-host.js';
|
|
20
20
|
import { type OrgPostgresMigrationConfirmation } from './org-postgres-migrate.js';
|
|
21
|
+
import type { OwnAuthHead } from './own-diff.js';
|
|
21
22
|
import { type EdgePortName, type HeldKeyResolver, type PlatformOwnerInput } from './platform-org.js';
|
|
22
23
|
import type { CallsSfuConnectionPort } from './platform-sfu.js';
|
|
23
24
|
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
@@ -212,8 +213,12 @@ export interface StepExecutor {
|
|
|
212
213
|
}>;
|
|
213
214
|
/** Whether the executor can mint an own org's Lettermint key: a Lettermint port and a reader for the held ref. */
|
|
214
215
|
ownMailKeyMintable?(): boolean;
|
|
216
|
+
/** Whether the ZITADEL port can set the customers organisation's login policy. Absent: planned, as before. */
|
|
217
|
+
customersLoginPolicyAvailable?(): boolean;
|
|
215
218
|
/** C3: whether the ZITADEL port can mint and revoke a customers directory credential. */
|
|
216
219
|
customersDirectoryAvailable?(): boolean;
|
|
220
|
+
/** Whether the ZITADEL port can grant the directory user its customer removal role. */
|
|
221
|
+
customersDirectoryRolesAvailable?(): boolean;
|
|
217
222
|
/** C3: whether the host supplied `mintBouleArchonCredential`. */
|
|
218
223
|
bouleArchonCredentialAvailable?(): boolean;
|
|
219
224
|
/** Every Coolify server with whether it is usable and reachable; blue/green plans copies on each. Optional: an executor without it plans no blue/green. */
|
|
@@ -234,6 +239,21 @@ export interface StepExecutor {
|
|
|
234
239
|
* copy is not recorded. Optional: an executor without it runs no release.
|
|
235
240
|
*/
|
|
236
241
|
releaseHost?(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
242
|
+
/**
|
|
243
|
+
* The built-in fan-out's in-place deploy of one org's Boule or Agora at `sha` (`release/fanout.ts`). Optional:
|
|
244
|
+
* an executor without it runs no fan-out.
|
|
245
|
+
*/
|
|
246
|
+
deployBuiltinAt?(input: {
|
|
247
|
+
app: 'boule' | 'agora';
|
|
248
|
+
org: DeclaredOrg;
|
|
249
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
250
|
+
sha: string;
|
|
251
|
+
}): Promise<{
|
|
252
|
+
ok: true;
|
|
253
|
+
} | {
|
|
254
|
+
ok: false;
|
|
255
|
+
reason: string;
|
|
256
|
+
}>;
|
|
237
257
|
/**
|
|
238
258
|
* One release tick for one subject: the recorded stage gate's list follows the people holding `releases:preview`,
|
|
239
259
|
* owners staying. Never throws. Optional: an executor without it leaves every gate as it is.
|
|
@@ -249,6 +269,12 @@ export interface StepExecutor {
|
|
|
249
269
|
ownSigninDbBackupConfigured?(): boolean;
|
|
250
270
|
/** Whether the host's company ZITADEL port can write the sign-in mail text and look. Absent: true. */
|
|
251
271
|
ownSigninMailWritable?(): boolean;
|
|
272
|
+
/** #886: reads the head of the declared auth branch for an own org; `undefined` when there is nothing to read. */
|
|
273
|
+
ownAuthHead?(gitRef: string): Promise<OwnAuthHead | undefined>;
|
|
274
|
+
/** Whether the host's ZITADEL port can grant the shared sign-in's registrar user a company's organisations. Absent: false. */
|
|
275
|
+
registrarGrantsAvailable?(): boolean;
|
|
276
|
+
/** Whether the host's company ZITADEL port can make the broker's registrar token. Absent: the planner neither asks for it nor reports it. */
|
|
277
|
+
ownRegistrarTokenWritable?(): boolean;
|
|
252
278
|
/**
|
|
253
279
|
* Whether the host's scoped ports for this own org include a files-organisation
|
|
254
280
|
* port. The planner plans no `files:organisation` step for an own org while
|
|
@@ -935,6 +961,12 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
935
961
|
target: string;
|
|
936
962
|
error?: string;
|
|
937
963
|
}>>;
|
|
964
|
+
/**
|
|
965
|
+
* One fan-out pass for the owner org's built-in `app` (`release/fanout.ts`): the other orgs' copies are put on
|
|
966
|
+
* the commit the owner's copy runs, one after the other, in place, stopping at the first failure. Each state
|
|
967
|
+
* write takes the owner's lease on its own; no lease is held while a deploy waits.
|
|
968
|
+
*/
|
|
969
|
+
private runBuiltinFanOut;
|
|
938
970
|
/**
|
|
939
971
|
* Phase 2 for one org: takes the copy lock of each stage copy request under the org's lease, then runs the
|
|
940
972
|
* copies after the lease is released. While a copy runs, every other tick and apply for the org goes on, a
|
|
@@ -1001,8 +1033,12 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
1001
1033
|
* only the ref.
|
|
1002
1034
|
*/
|
|
1003
1035
|
private heldEnvReadiness;
|
|
1036
|
+
/** #886: the head of an own org's declared auth branch, or nothing when the org is not own or the executor has no read. */
|
|
1037
|
+
private ownAuthHeadFor;
|
|
1004
1038
|
/** An own org's Lettermint key is held. A resolver that throws counts as not held. */
|
|
1005
1039
|
private ownMailKeyHeld;
|
|
1040
|
+
/** An own org's registrar token is held (a rerun after a crash delivers it instead of blocking). */
|
|
1041
|
+
private ownRegistrarTokenHeld;
|
|
1006
1042
|
/** Which of an own org's undelivered-secret refs hold a value now, so a rerun resumes instead of blocking. */
|
|
1007
1043
|
private ownResumableRefs;
|
|
1008
1044
|
/**
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -4,6 +4,7 @@ import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
|
4
4
|
import { sfuSlotPorts } from '../calls-sfu.js';
|
|
5
5
|
import { ORG_SERVICE_SCHEMAS, heldEnvRef, ownsServices, validateHosting } from '../org.js';
|
|
6
6
|
import { PLATFORM_SERVICE_KEYS } from '../platform.js';
|
|
7
|
+
import { runBuiltinFanOut } from '../release/fanout.js';
|
|
7
8
|
import { recordedFrom, releaseKeys } from '../release/state.js';
|
|
8
9
|
import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
9
10
|
import { StaleLeaseError, } from '../store.js';
|
|
@@ -1338,6 +1339,16 @@ export class FoundryPlanApply {
|
|
|
1338
1339
|
settle('platform', e);
|
|
1339
1340
|
}
|
|
1340
1341
|
}
|
|
1342
|
+
// Between the phases: once the owner's Boule or Agora is fully live, copy it to every other org. Its errors
|
|
1343
|
+
// are reported by app, never as an org.
|
|
1344
|
+
for (const app of ['boule', 'agora']) {
|
|
1345
|
+
try {
|
|
1346
|
+
await this.runBuiltinFanOut(app);
|
|
1347
|
+
}
|
|
1348
|
+
catch (e) {
|
|
1349
|
+
settle(`fanout:${app}`, e);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1341
1352
|
// Phase 2: the copies, one subject at a time. A copy takes its lock under the org's lease and runs without it.
|
|
1342
1353
|
for (const { target, run } of copying) {
|
|
1343
1354
|
try {
|
|
@@ -1350,6 +1361,65 @@ export class FoundryPlanApply {
|
|
|
1350
1361
|
}
|
|
1351
1362
|
return results;
|
|
1352
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* One fan-out pass for the owner org's built-in `app` (`release/fanout.ts`): the other orgs' copies are put on
|
|
1366
|
+
* the commit the owner's copy runs, one after the other, in place, stopping at the first failure. Each state
|
|
1367
|
+
* write takes the owner's lease on its own; no lease is held while a deploy waits.
|
|
1368
|
+
*/
|
|
1369
|
+
async runBuiltinFanOut(app) {
|
|
1370
|
+
const store = this.releaseStore;
|
|
1371
|
+
const platform = await this.platformDeps?.store.get();
|
|
1372
|
+
if (!store || !platform || !this.executor.deployBuiltinAt)
|
|
1373
|
+
return;
|
|
1374
|
+
const ownerOrgId = platform.ownerOrgId;
|
|
1375
|
+
const owner = await this.store.get(ownerOrgId);
|
|
1376
|
+
if (!owner)
|
|
1377
|
+
return;
|
|
1378
|
+
const empty = { orgId: ownerOrgId, steps: [], unmanaged: [] };
|
|
1379
|
+
if ((await this.authz.canApplyPlatform?.(this.actor)) !== true) {
|
|
1380
|
+
await this.refusePlatform(empty, ownerOrgId, 'not authorized to run the release tick');
|
|
1381
|
+
throw new NotAuthorizedError(this.actor, ownerOrgId);
|
|
1382
|
+
}
|
|
1383
|
+
const deploy = this.executor.deployBuiltinAt.bind(this.executor);
|
|
1384
|
+
await runBuiltinFanOut({
|
|
1385
|
+
ownerOrgId,
|
|
1386
|
+
app,
|
|
1387
|
+
ownerDeclares: owner[app]?.blueGreen !== undefined,
|
|
1388
|
+
store,
|
|
1389
|
+
readIds: async () => ({
|
|
1390
|
+
...((await this.store.getAppliedState(ownerOrgId))?.serviceIds ?? {}),
|
|
1391
|
+
}),
|
|
1392
|
+
write: (writes) => this.withMutationLease(companyScope(ownerOrgId), (reason) => this.refuse(empty, reason), async (fence) => {
|
|
1393
|
+
const previous = await this.store.getAppliedState(ownerOrgId);
|
|
1394
|
+
if (!previous)
|
|
1395
|
+
throw new Error('the owner org has no applied state');
|
|
1396
|
+
const serviceIds = { ...previous.serviceIds };
|
|
1397
|
+
for (const [key, value] of Object.entries(writes)) {
|
|
1398
|
+
if (value === null)
|
|
1399
|
+
delete serviceIds[key];
|
|
1400
|
+
else
|
|
1401
|
+
serviceIds[key] = value;
|
|
1402
|
+
}
|
|
1403
|
+
await this.store.putAppliedState({ ...previous, serviceIds }, fence);
|
|
1404
|
+
}),
|
|
1405
|
+
targets: async () => {
|
|
1406
|
+
const others = (await this.store.list()).filter((o) => o.id !== ownerOrgId && o[app] !== undefined);
|
|
1407
|
+
return others.map((o) => ({ orgId: o.id, slug: o.slug }));
|
|
1408
|
+
},
|
|
1409
|
+
// Re-reads the org fresh right before its own deploy, never from a list taken when the pass
|
|
1410
|
+
// started: a deploy can wait many minutes with no lease held on the target org (CLAUDE.md,
|
|
1411
|
+
// "Built-in fan-out"), so an ordinary `apply()` to a later-in-order org can turn its own
|
|
1412
|
+
// blue/green on in that window, and `deployBuiltinAt`'s guard must see that, not a stale copy.
|
|
1413
|
+
deploy: async (target, sha) => {
|
|
1414
|
+
const org = await this.store.get(target.orgId);
|
|
1415
|
+
if (!org || org[app] === undefined)
|
|
1416
|
+
return { ok: false, reason: 'org not found' };
|
|
1417
|
+
const recordedIds = (await this.store.getAppliedState(org.id))?.serviceIds ?? {};
|
|
1418
|
+
return deploy({ app, org, recordedIds, sha });
|
|
1419
|
+
},
|
|
1420
|
+
now: this.now,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1353
1423
|
/**
|
|
1354
1424
|
* Phase 2 for one org: takes the copy lock of each stage copy request under the org's lease, then runs the
|
|
1355
1425
|
* copies after the lease is released. While a copy runs, every other tick and apply for the org goes on, a
|
|
@@ -1648,7 +1718,9 @@ export class FoundryPlanApply {
|
|
|
1648
1718
|
oikosZoneReady: platformReadiness.oikosIngressApplied === true,
|
|
1649
1719
|
ownMailKeyHeld: await this.ownMailKeyHeld(org),
|
|
1650
1720
|
ownMailKeyMintable: this.executor.ownMailKeyMintable?.() ?? false,
|
|
1721
|
+
customersLoginPolicy: this.executor.customersLoginPolicyAvailable?.() ?? true,
|
|
1651
1722
|
customersDirectory: this.executor.customersDirectoryAvailable?.() ?? false,
|
|
1723
|
+
customersDirectoryRoles: this.executor.customersDirectoryRolesAvailable?.() ?? false,
|
|
1652
1724
|
bouleArchonCredential: this.executor.bouleArchonCredentialAvailable?.() ?? false,
|
|
1653
1725
|
...(blueGreenServers ? { blueGreenServers } : {}),
|
|
1654
1726
|
...(stageSinkDown ? { stageSinkDown } : {}),
|
|
@@ -1671,6 +1743,10 @@ export class FoundryPlanApply {
|
|
|
1671
1743
|
ownSigninDbKeysPuttable: this.executor.ownSigninDbKeysPuttable?.() ?? true,
|
|
1672
1744
|
ownSigninDbBackupConfigured: this.executor.ownSigninDbBackupConfigured?.() ?? true,
|
|
1673
1745
|
ownSigninMailWritable: this.executor.ownSigninMailWritable?.() ?? true,
|
|
1746
|
+
...(await this.ownAuthHeadFor(org, platformReadiness)),
|
|
1747
|
+
registrarGrants: this.executor.registrarGrantsAvailable?.() ?? false,
|
|
1748
|
+
ownRegistrarTokenWritable: this.executor.ownRegistrarTokenWritable?.(),
|
|
1749
|
+
ownRegistrarTokenHeld: await this.ownRegistrarTokenHeld(org),
|
|
1674
1750
|
...(this.backupGate
|
|
1675
1751
|
? {
|
|
1676
1752
|
backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
|
|
@@ -1894,6 +1970,14 @@ export class FoundryPlanApply {
|
|
|
1894
1970
|
}
|
|
1895
1971
|
return { store: Boolean(heldKeys), refs, differs, unreadable, invalid };
|
|
1896
1972
|
}
|
|
1973
|
+
/** #886: the head of an own org's declared auth branch, or nothing when the org is not own or the executor has no read. */
|
|
1974
|
+
async ownAuthHeadFor(org, readiness) {
|
|
1975
|
+
const gitRef = readiness.serviceGitRefs?.auth;
|
|
1976
|
+
if (!ownsServices(org) || !gitRef || !this.executor.ownAuthHead)
|
|
1977
|
+
return {};
|
|
1978
|
+
const head = await this.executor.ownAuthHead(gitRef);
|
|
1979
|
+
return head ? { ownAuthHead: head } : {};
|
|
1980
|
+
}
|
|
1897
1981
|
/** An own org's Lettermint key is held. A resolver that throws counts as not held. */
|
|
1898
1982
|
async ownMailKeyHeld(org) {
|
|
1899
1983
|
const held = this.platformDeps?.heldKeys;
|
|
@@ -1901,6 +1985,13 @@ export class FoundryPlanApply {
|
|
|
1901
1985
|
return false;
|
|
1902
1986
|
return (await heldOwnKeyRefs(held, org)).has(orgTarget(org).refs.brokerMailKey);
|
|
1903
1987
|
}
|
|
1988
|
+
/** An own org's registrar token is held (a rerun after a crash delivers it instead of blocking). */
|
|
1989
|
+
async ownRegistrarTokenHeld(org) {
|
|
1990
|
+
const held = this.platformDeps?.heldKeys;
|
|
1991
|
+
if (!ownsServices(org) || !held)
|
|
1992
|
+
return false;
|
|
1993
|
+
return (await heldOwnKeyRefs(held, org)).has(orgTarget(org).refs.registrarToken);
|
|
1994
|
+
}
|
|
1904
1995
|
/** Which of an own org's undelivered-secret refs hold a value now, so a rerun resumes instead of blocking. */
|
|
1905
1996
|
async ownResumableRefs(org) {
|
|
1906
1997
|
const held = this.platformDeps?.heldKeys;
|
|
@@ -70,16 +70,18 @@ export declare const AUTH_TUNNEL_ROUTES_VERSION = "2";
|
|
|
70
70
|
export declare const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = "auth_tunnel_routes_applied";
|
|
71
71
|
/**
|
|
72
72
|
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
73
|
-
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
73
|
+
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`). Raise it when a key is added or its meaning changes, so an applied
|
|
74
74
|
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
75
75
|
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
76
|
+
* 2: a company's login apps get `LOGIN_PASSKEY_RP_ID` (foundry 0.70.2).
|
|
76
77
|
*/
|
|
77
|
-
export declare const AUTH_LOGIN_ENV_VERSION = "
|
|
78
|
+
export declare const AUTH_LOGIN_ENV_VERSION = "2";
|
|
78
79
|
/**
|
|
79
|
-
* What the login apps' env is written for: the version
|
|
80
|
-
*
|
|
80
|
+
* What the login apps' env is written for: the version, the sender address (`LOGIN_MAIL_FROM`) and the
|
|
81
|
+
* passkey RP ID (`LOGIN_PASSKEY_RP_ID`, none for the platform and a company without its own domain).
|
|
82
|
+
* Neither is a secret; a new mail domain or a changed RP ID gives a new marker, so the step is planned again.
|
|
81
83
|
*/
|
|
82
|
-
export declare function authLoginEnvMarker(smtpSender: string): string;
|
|
84
|
+
export declare function authLoginEnvMarker(smtpSender: string, passkeyRpId?: string): string;
|
|
83
85
|
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
84
86
|
export declare const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = "auth_login_env_applied";
|
|
85
87
|
/**
|
|
@@ -1327,6 +1329,8 @@ export interface PlanPlatformOptions {
|
|
|
1327
1329
|
authMailKeyHeld?: boolean;
|
|
1328
1330
|
/** C3: why the owner's invite cannot be made (no work address, no mail port). The rest of the step still plans. */
|
|
1329
1331
|
authOwnerInvite?: string;
|
|
1332
|
+
/** The broker's registrar token was minted and never held: reported, nothing planned for it. */
|
|
1333
|
+
authRegistrar?: string;
|
|
1330
1334
|
files?: string;
|
|
1331
1335
|
integrations?: string;
|
|
1332
1336
|
ai?: string;
|
|
@@ -77,17 +77,19 @@ export const AUTH_TUNNEL_ROUTES_VERSION = '2';
|
|
|
77
77
|
export const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = 'auth_tunnel_routes_applied';
|
|
78
78
|
/**
|
|
79
79
|
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
80
|
-
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
80
|
+
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`). Raise it when a key is added or its meaning changes, so an applied
|
|
81
81
|
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
82
82
|
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
83
|
+
* 2: a company's login apps get `LOGIN_PASSKEY_RP_ID` (foundry 0.70.2).
|
|
83
84
|
*/
|
|
84
|
-
export const AUTH_LOGIN_ENV_VERSION = '
|
|
85
|
+
export const AUTH_LOGIN_ENV_VERSION = '2';
|
|
85
86
|
/**
|
|
86
|
-
* What the login apps' env is written for: the version
|
|
87
|
-
*
|
|
87
|
+
* What the login apps' env is written for: the version, the sender address (`LOGIN_MAIL_FROM`) and the
|
|
88
|
+
* passkey RP ID (`LOGIN_PASSKEY_RP_ID`, none for the platform and a company without its own domain).
|
|
89
|
+
* Neither is a secret; a new mail domain or a changed RP ID gives a new marker, so the step is planned again.
|
|
88
90
|
*/
|
|
89
|
-
export function authLoginEnvMarker(smtpSender) {
|
|
90
|
-
return `${AUTH_LOGIN_ENV_VERSION}:${smtpSender}`;
|
|
91
|
+
export function authLoginEnvMarker(smtpSender, passkeyRpId) {
|
|
92
|
+
return `${AUTH_LOGIN_ENV_VERSION}:${smtpSender}${passkeyRpId ? `:${passkeyRpId}` : ''}`;
|
|
91
93
|
}
|
|
92
94
|
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
93
95
|
export const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = 'auth_login_env_applied';
|
|
@@ -1553,7 +1555,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1553
1555
|
// reconcile below can be planned in the *same* round hc first comes up,
|
|
1554
1556
|
// the same way files/mail/archon are planned in auth's own first round.
|
|
1555
1557
|
const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
|
|
1556
|
-
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false, blocked.authMailKeyHeld ?? true);
|
|
1558
|
+
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false, blocked.authMailKeyHeld ?? true, blocked.authRegistrar);
|
|
1557
1559
|
if (auth.step)
|
|
1558
1560
|
plannedSteps.push(auth.step);
|
|
1559
1561
|
// #174: the integrations host. Planned after auth; neither step reads the
|
|
@@ -2229,11 +2231,15 @@ function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
|
2229
2231
|
* promising "this apply re-mints" while one of those is still missing would
|
|
2230
2232
|
* be wrong the moment it is read.
|
|
2231
2233
|
*/
|
|
2232
|
-
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false, mailKeyHeld = true) {
|
|
2234
|
+
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false, mailKeyHeld = true, registrarBlocked) {
|
|
2233
2235
|
if (!platform.auth.enabled)
|
|
2234
2236
|
return { step: null, blocked: null };
|
|
2235
|
-
// C3/S2: standing notes. The invite and the domain wait on a person; they hold nothing else back.
|
|
2236
|
-
const standing = [
|
|
2237
|
+
// C3/S2: standing notes. The invite, the registrar token and the domain wait on a person; they hold nothing else back.
|
|
2238
|
+
const standing = [
|
|
2239
|
+
ownerInviteBlocked,
|
|
2240
|
+
registrarBlocked,
|
|
2241
|
+
ownerDomainBlockedText(recordedIds),
|
|
2242
|
+
].filter((t) => Boolean(t));
|
|
2237
2243
|
const standingNote = standing.length > 0 ? standing.join('; ') : null;
|
|
2238
2244
|
const key = resourceKey('coolify', 'app', PLATFORM_AUTH_KEY);
|
|
2239
2245
|
const connectorApplied = Boolean(recordedIds[PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY]);
|