@wtfalch/foundry 0.41.2 → 0.43.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/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/migrations/0054_org_signin_in_app.sql +6 -0
- package/dist/migrations/0055_release_requests.sql +20 -0
- package/dist/migrations/0056_platform_archon_blue_green.sql +7 -0
- package/dist/org.d.ts +30 -0
- package/dist/org.js +52 -1
- package/dist/plan/agora.d.ts +4 -1
- package/dist/plan/agora.js +16 -2
- package/dist/plan/archon-blue-green.d.ts +54 -0
- package/dist/plan/archon-blue-green.js +72 -0
- package/dist/plan/blue-green-apply.d.ts +82 -0
- package/dist/plan/blue-green-apply.js +360 -0
- package/dist/plan/blue-green.d.ts +223 -0
- package/dist/plan/blue-green.js +444 -0
- package/dist/plan/boule.d.ts +5 -2
- package/dist/plan/boule.js +14 -2
- package/dist/plan/cloudflared-connector.d.ts +26 -0
- package/dist/plan/cloudflared-connector.js +37 -0
- package/dist/plan/coolify-previews.d.ts +10 -0
- package/dist/plan/coolify-previews.js +14 -0
- package/dist/plan/diff.d.ts +6 -0
- package/dist/plan/diff.js +123 -11
- package/dist/plan/edge-ratelimits.d.ts +5 -0
- package/dist/plan/edge-ratelimits.js +16 -6
- package/dist/plan/executor.d.ts +79 -0
- package/dist/plan/executor.js +527 -31
- package/dist/plan/login-host.d.ts +23 -2
- package/dist/plan/login-host.js +30 -5
- package/dist/plan/own-diff.js +6 -0
- package/dist/plan/plan-apply.d.ts +51 -0
- package/dist/plan/plan-apply.js +222 -0
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +28 -1
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-org.js +5 -0
- package/dist/plan/platform-providers.d.ts +31 -2
- package/dist/plan/platform-providers.js +345 -13
- package/dist/plan/providers-readers.js +6 -2
- package/dist/plan/proxy-labels.d.ts +2 -2
- package/dist/plan/proxy-labels.js +12 -4
- package/dist/plan/release-host.d.ts +59 -0
- package/dist/plan/release-host.js +239 -0
- package/dist/plan/remote-database.d.ts +124 -0
- package/dist/plan/remote-database.js +373 -0
- package/dist/plan/sign-in-archon.d.ts +46 -0
- package/dist/plan/sign-in-archon.js +71 -0
- package/dist/plan/sign-in-bindings.d.ts +16 -0
- package/dist/plan/sign-in-bindings.js +44 -0
- package/dist/plan/sign-in.d.ts +135 -0
- package/dist/plan/sign-in.js +172 -0
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +4 -1
- package/dist/release/decide.d.ts +95 -0
- package/dist/release/decide.js +242 -0
- package/dist/release/health.d.ts +48 -0
- package/dist/release/health.js +61 -0
- package/dist/release/run.d.ts +40 -0
- package/dist/release/run.js +56 -0
- package/dist/release/state.d.ts +64 -0
- package/dist/release/state.js +126 -0
- package/dist/release/types.d.ts +54 -0
- package/dist/release/types.js +4 -0
- package/dist/store/postgres.d.ts +8 -0
- package/dist/store/postgres.js +62 -3
- package/dist/store/schema.d.ts +171 -0
- package/dist/store/schema.js +14 -0
- package/package.json +1 -1
- package/versions.digests.json +2 -1
- package/versions.json +2 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-app sign-in (`signIn.inApp`, campaign 2026-10-15): a person signs in to Agora or Boule on that
|
|
4
|
+
* app's own address, under `/sign-in`, served by the login through the app's own proxy. This file holds
|
|
5
|
+
* the names the planner, the apps' env builders and the executor agree on. It imports `org.ts` only,
|
|
6
|
+
* so `agora.ts`, `boule.ts` and `login-host.ts` can all import it.
|
|
7
|
+
*/
|
|
8
|
+
/** The path the apps proxy to the login, and the login binding's `basePath`. */
|
|
9
|
+
export declare const SIGN_IN_BASE_PATH = "/sign-in";
|
|
10
|
+
/** The app env that turns the proxy on: the login's internal URL. Unset leaves the app unchanged. */
|
|
11
|
+
export declare const SIGN_IN_UPSTREAM_ENV = "SIGNIN_UPSTREAM";
|
|
12
|
+
/**
|
|
13
|
+
* The login image built with base path `/sign-in` (login build arg `NEXT_PUBLIC_BASE_PATH`). A Next.js
|
|
14
|
+
* base path is fixed at build, so the company login on `/` cannot also serve `/sign-in`: the in-app
|
|
15
|
+
* hosts are served by a second login app, `platform-auth-login-sign-in`, on the same Coolify network,
|
|
16
|
+
* with image tag suffix `-sign-in`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SIGN_IN_LOGIN_INTERNAL_NAME = "platform-auth-login-sign-in";
|
|
19
|
+
export declare const SIGN_IN_LOGIN_PORT = "3000";
|
|
20
|
+
export declare const SIGN_IN_IMAGE_TAG_SUFFIX = "-sign-in";
|
|
21
|
+
/**
|
|
22
|
+
* Platform scope id the platform's sign-in step (`platform-providers.ts`) records once the second login app
|
|
23
|
+
* is created, deployed and holds the login token. Until it exists the login-host step plans no in-app host
|
|
24
|
+
* (a blocked line says so).
|
|
25
|
+
*/
|
|
26
|
+
export declare const SIGN_IN_LOGIN_APP_SCOPE_KEY = "coolify_platform_auth_login_sign_in_app";
|
|
27
|
+
/**
|
|
28
|
+
* The order that switches a company on, and off again in reverse. Each step leaves people able to sign in on
|
|
29
|
+
* the old path until the next one is done, except where noted.
|
|
30
|
+
*
|
|
31
|
+
* 1. on: the login-host step trusts each app host ({@link SIGN_IN_TRUSTED_SCOPE_KEY}).
|
|
32
|
+
* 2. on: the app step writes and deploys `SIGNIN_UPSTREAM` and records it live ({@link SIGN_IN_ENV_LIVE_SCOPE_PREFIX}).
|
|
33
|
+
* 3. on: the login-host step binds each app host and points the base URI at `https://<app host>/sign-in/`
|
|
34
|
+
* ({@link SIGN_IN_IN_APP_SCOPE_KEY}). The login refuses a client id held by two bindings, so the bind moves
|
|
35
|
+
* the app's client id off the company's binding at the same write: that write and the base URI are the only
|
|
36
|
+
* moment the old path can fail, and they are the last two calls.
|
|
37
|
+
* Off: the login-host step moves the client id back and points the base URI back at the login host; the app
|
|
38
|
+
* step then drops `SIGNIN_UPSTREAM`; the login-host step then gives up the trusted domain.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* The app hosts the login-host step has trusted: `<host>=<clientId>` pairs, sorted and comma-joined
|
|
42
|
+
* ({@link inAppState}). Absent or empty means none. Recorded last, after every write it covers.
|
|
43
|
+
* A host stays here while its app still holds `SIGNIN_UPSTREAM`, even after the switch is off.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SIGN_IN_TRUSTED_SCOPE_KEY = "login_host_in_app_trusted";
|
|
46
|
+
/**
|
|
47
|
+
* What the login-host step last bound and pointed at the app's own address (base URI `https://<host>/sign-in/`):
|
|
48
|
+
* the same pairs, a subset of {@link SIGN_IN_TRUSTED_SCOPE_KEY}. Absent or empty means none. Recorded last,
|
|
49
|
+
* after every write it covers.
|
|
50
|
+
*/
|
|
51
|
+
export declare const SIGN_IN_IN_APP_SCOPE_KEY = "login_host_in_app";
|
|
52
|
+
/**
|
|
53
|
+
* Prefix of the scope ids the Agora and Boule steps record after the app has been written, restarted or deployed:
|
|
54
|
+
* `<prefix>agora` holds the app host while its running env has `SIGNIN_UPSTREAM`, `''` while it has not.
|
|
55
|
+
*/
|
|
56
|
+
export declare const SIGN_IN_ENV_LIVE_SCOPE_PREFIX = "signin_env_live_";
|
|
57
|
+
/** The app hosts whose running env holds `SIGNIN_UPSTREAM`, as the app steps recorded them. */
|
|
58
|
+
export declare function signInEnvLiveHosts(recorded: Readonly<Record<string, string>>): Set<string>;
|
|
59
|
+
/** Recorded `'1'` once the zone's one rate limit rule is the in-app one, `''` once it is the base one again. */
|
|
60
|
+
export declare const SIGN_IN_RATE_LIMIT_SCOPE_KEY = "ratelimit_in_app";
|
|
61
|
+
/**
|
|
62
|
+
* The `rate limit` rule description for a company with the switch: it also limits `/sign-in/` paths,
|
|
63
|
+
* except the static assets under `/sign-in/_next/`. Cloudflare Free allows the Path field only (no Method).
|
|
64
|
+
*/
|
|
65
|
+
export declare const SIGN_IN_RATE_LIMIT_DESCRIPTION = "foundry: rate limit sign-in paths (in-app)";
|
|
66
|
+
export declare const SIGN_IN_RATE_LIMIT_EXPRESSION = "(starts_with(http.request.uri.path, \"/auth/\")) or (starts_with(http.request.uri.path, \"/sign-in/\") and not starts_with(http.request.uri.path, \"/sign-in/_next/\"))";
|
|
67
|
+
/**
|
|
68
|
+
* True when the company has the switch on and it applies: a company domain whose login host is
|
|
69
|
+
* `auth.<domain>` (`issuer.usesDomain`) and the shared sign-in. A tier 1 company on `oikos.host` keeps
|
|
70
|
+
* today's flow even when the switch is set (the declaration is refused: `org.ts`'s `validateSignIn`).
|
|
71
|
+
*/
|
|
72
|
+
export declare function inAppSignIn(org: Pick<DeclaredOrg, 'signIn' | 'domain' | 'issuer' | 'services'>): boolean;
|
|
73
|
+
/** True when an app's live env holds `SIGNIN_UPSTREAM` and the desired env does not: it is to be removed. */
|
|
74
|
+
export declare function staleSignInUpstream(desired: Readonly<Record<string, string>>, live: Readonly<Record<string, string>>): boolean;
|
|
75
|
+
/** The build argument (a build-time env in Coolify) that fixes the login's base path. */
|
|
76
|
+
export declare const SIGN_IN_BUILD_ARG_ENV = "NEXT_PUBLIC_BASE_PATH";
|
|
77
|
+
/** The Coolify name and internal address of the in-app login app next to a target's login app. */
|
|
78
|
+
export declare function signInLoginNames(target: {
|
|
79
|
+
names: {
|
|
80
|
+
authLogin: string;
|
|
81
|
+
};
|
|
82
|
+
internal: {
|
|
83
|
+
authLogin: string;
|
|
84
|
+
};
|
|
85
|
+
}): {
|
|
86
|
+
name: string;
|
|
87
|
+
internal: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* True when the company's in-app sign-in goes through the platform's shared login (its sign-in is the
|
|
91
|
+
* platform's ZITADEL), so the platform needs the in-app login app. A company running its own services
|
|
92
|
+
* has its own login stack instead.
|
|
93
|
+
*/
|
|
94
|
+
export declare function inAppOnSharedLogin(org: Pick<DeclaredOrg, 'signIn' | 'domain' | 'issuer' | 'services'>): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* The internal URL every in-app app proxies `/sign-in/*` to: the platform's in-app login, or, for a
|
|
97
|
+
* company that runs its own services, the one in its own sign-in stack (`<slug>-auth-login-sign-in`).
|
|
98
|
+
*/
|
|
99
|
+
export declare function signInUpstream(org?: Pick<DeclaredOrg, 'slug' | 'services'>): string;
|
|
100
|
+
/** One app host that signs in on its own address. */
|
|
101
|
+
export interface InAppApp {
|
|
102
|
+
app: 'agora' | 'boule' | 'archon';
|
|
103
|
+
host: string;
|
|
104
|
+
clientId: string;
|
|
105
|
+
/** The ZITADEL organisation the host signs people in to, when it is not the company's own (Archon's). */
|
|
106
|
+
organisationId?: string;
|
|
107
|
+
}
|
|
108
|
+
/** {@link SIGN_IN_IN_APP_SCOPE_KEY}'s value for these apps. */
|
|
109
|
+
export declare function inAppState(apps: readonly Pick<InAppApp, 'host' | 'clientId'>[]): string;
|
|
110
|
+
/** The `{host, clientId}` pairs a recorded {@link inAppState} names. */
|
|
111
|
+
export declare function inAppStatePairs(state: string | undefined): Array<Pick<InAppApp, 'host' | 'clientId'>>;
|
|
112
|
+
/** The hosts a recorded {@link inAppState} names. */
|
|
113
|
+
export declare function inAppStateHosts(state: string | undefined): string[];
|
|
114
|
+
/** The login binding, ZITADEL org id aside, for one in-app host. */
|
|
115
|
+
export declare function inAppBindingFor(app: InAppApp): {
|
|
116
|
+
host: string;
|
|
117
|
+
clientIds: string[];
|
|
118
|
+
apps: Record<string, 'agora' | 'boule' | 'archon'>;
|
|
119
|
+
basePath: typeof SIGN_IN_BASE_PATH;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* What the login-host step records for these wanted apps, given what the apps' steps have recorded:
|
|
123
|
+
* `trusted` is every wanted host plus any host given up whose app still holds `SIGNIN_UPSTREAM` (its trusted
|
|
124
|
+
* domain stays until the env is gone); `active` is the wanted hosts whose env is live: bound, with the app's own base URI.
|
|
125
|
+
*/
|
|
126
|
+
export declare function inAppTargets(apps: readonly InAppApp[], recorded: Readonly<Record<string, string>>): {
|
|
127
|
+
trusted: string;
|
|
128
|
+
active: string;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* True when an app should proxy `/sign-in/*` to the login: its host is trusted there, and the
|
|
132
|
+
* company still has the switch on or the base URI still points at the app (it is switched off in reverse
|
|
133
|
+
* order: the base URI goes back before the app stops proxying).
|
|
134
|
+
*/
|
|
135
|
+
export declare function inAppHostApplied(org: Pick<DeclaredOrg, 'signIn' | 'domain' | 'issuer' | 'services'>, recorded: Readonly<Record<string, string>>, host: string): boolean;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ownsServices } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-app sign-in (`signIn.inApp`, campaign 2026-10-15): a person signs in to Agora or Boule on that
|
|
4
|
+
* app's own address, under `/sign-in`, served by the login through the app's own proxy. This file holds
|
|
5
|
+
* the names the planner, the apps' env builders and the executor agree on. It imports `org.ts` only,
|
|
6
|
+
* so `agora.ts`, `boule.ts` and `login-host.ts` can all import it.
|
|
7
|
+
*/
|
|
8
|
+
/** The path the apps proxy to the login, and the login binding's `basePath`. */
|
|
9
|
+
export const SIGN_IN_BASE_PATH = '/sign-in';
|
|
10
|
+
/** The app env that turns the proxy on: the login's internal URL. Unset leaves the app unchanged. */
|
|
11
|
+
export const SIGN_IN_UPSTREAM_ENV = 'SIGNIN_UPSTREAM';
|
|
12
|
+
/**
|
|
13
|
+
* The login image built with base path `/sign-in` (login build arg `NEXT_PUBLIC_BASE_PATH`). A Next.js
|
|
14
|
+
* base path is fixed at build, so the company login on `/` cannot also serve `/sign-in`: the in-app
|
|
15
|
+
* hosts are served by a second login app, `platform-auth-login-sign-in`, on the same Coolify network,
|
|
16
|
+
* with image tag suffix `-sign-in`.
|
|
17
|
+
*/
|
|
18
|
+
export const SIGN_IN_LOGIN_INTERNAL_NAME = 'platform-auth-login-sign-in';
|
|
19
|
+
export const SIGN_IN_LOGIN_PORT = '3000';
|
|
20
|
+
export const SIGN_IN_IMAGE_TAG_SUFFIX = '-sign-in';
|
|
21
|
+
/**
|
|
22
|
+
* Platform scope id the platform's sign-in step (`platform-providers.ts`) records once the second login app
|
|
23
|
+
* is created, deployed and holds the login token. Until it exists the login-host step plans no in-app host
|
|
24
|
+
* (a blocked line says so).
|
|
25
|
+
*/
|
|
26
|
+
export const SIGN_IN_LOGIN_APP_SCOPE_KEY = 'coolify_platform_auth_login_sign_in_app';
|
|
27
|
+
/**
|
|
28
|
+
* The order that switches a company on, and off again in reverse. Each step leaves people able to sign in on
|
|
29
|
+
* the old path until the next one is done, except where noted.
|
|
30
|
+
*
|
|
31
|
+
* 1. on: the login-host step trusts each app host ({@link SIGN_IN_TRUSTED_SCOPE_KEY}).
|
|
32
|
+
* 2. on: the app step writes and deploys `SIGNIN_UPSTREAM` and records it live ({@link SIGN_IN_ENV_LIVE_SCOPE_PREFIX}).
|
|
33
|
+
* 3. on: the login-host step binds each app host and points the base URI at `https://<app host>/sign-in/`
|
|
34
|
+
* ({@link SIGN_IN_IN_APP_SCOPE_KEY}). The login refuses a client id held by two bindings, so the bind moves
|
|
35
|
+
* the app's client id off the company's binding at the same write: that write and the base URI are the only
|
|
36
|
+
* moment the old path can fail, and they are the last two calls.
|
|
37
|
+
* Off: the login-host step moves the client id back and points the base URI back at the login host; the app
|
|
38
|
+
* step then drops `SIGNIN_UPSTREAM`; the login-host step then gives up the trusted domain.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* The app hosts the login-host step has trusted: `<host>=<clientId>` pairs, sorted and comma-joined
|
|
42
|
+
* ({@link inAppState}). Absent or empty means none. Recorded last, after every write it covers.
|
|
43
|
+
* A host stays here while its app still holds `SIGNIN_UPSTREAM`, even after the switch is off.
|
|
44
|
+
*/
|
|
45
|
+
export const SIGN_IN_TRUSTED_SCOPE_KEY = 'login_host_in_app_trusted';
|
|
46
|
+
/**
|
|
47
|
+
* What the login-host step last bound and pointed at the app's own address (base URI `https://<host>/sign-in/`):
|
|
48
|
+
* the same pairs, a subset of {@link SIGN_IN_TRUSTED_SCOPE_KEY}. Absent or empty means none. Recorded last,
|
|
49
|
+
* after every write it covers.
|
|
50
|
+
*/
|
|
51
|
+
export const SIGN_IN_IN_APP_SCOPE_KEY = 'login_host_in_app';
|
|
52
|
+
/**
|
|
53
|
+
* Prefix of the scope ids the Agora and Boule steps record after the app has been written, restarted or deployed:
|
|
54
|
+
* `<prefix>agora` holds the app host while its running env has `SIGNIN_UPSTREAM`, `''` while it has not.
|
|
55
|
+
*/
|
|
56
|
+
export const SIGN_IN_ENV_LIVE_SCOPE_PREFIX = 'signin_env_live_';
|
|
57
|
+
/** The app hosts whose running env holds `SIGNIN_UPSTREAM`, as the app steps recorded them. */
|
|
58
|
+
export function signInEnvLiveHosts(recorded) {
|
|
59
|
+
return new Set(Object.entries(recorded)
|
|
60
|
+
.filter(([key, host]) => key.startsWith(SIGN_IN_ENV_LIVE_SCOPE_PREFIX) && host !== '')
|
|
61
|
+
.map(([, host]) => host));
|
|
62
|
+
}
|
|
63
|
+
/** Recorded `'1'` once the zone's one rate limit rule is the in-app one, `''` once it is the base one again. */
|
|
64
|
+
export const SIGN_IN_RATE_LIMIT_SCOPE_KEY = 'ratelimit_in_app';
|
|
65
|
+
/**
|
|
66
|
+
* The `rate limit` rule description for a company with the switch: it also limits `/sign-in/` paths,
|
|
67
|
+
* except the static assets under `/sign-in/_next/`. Cloudflare Free allows the Path field only (no Method).
|
|
68
|
+
*/
|
|
69
|
+
export const SIGN_IN_RATE_LIMIT_DESCRIPTION = 'foundry: rate limit sign-in paths (in-app)';
|
|
70
|
+
export const SIGN_IN_RATE_LIMIT_EXPRESSION = '(starts_with(http.request.uri.path, "/auth/")) or (starts_with(http.request.uri.path, "/sign-in/") and not starts_with(http.request.uri.path, "/sign-in/_next/"))';
|
|
71
|
+
/**
|
|
72
|
+
* True when the company has the switch on and it applies: a company domain whose login host is
|
|
73
|
+
* `auth.<domain>` (`issuer.usesDomain`) and the shared sign-in. A tier 1 company on `oikos.host` keeps
|
|
74
|
+
* today's flow even when the switch is set (the declaration is refused: `org.ts`'s `validateSignIn`).
|
|
75
|
+
*/
|
|
76
|
+
export function inAppSignIn(org) {
|
|
77
|
+
return org.signIn?.inApp === true && Boolean(org.domain) && org.issuer.usesDomain;
|
|
78
|
+
}
|
|
79
|
+
/** True when an app's live env holds `SIGNIN_UPSTREAM` and the desired env does not: it is to be removed. */
|
|
80
|
+
export function staleSignInUpstream(desired, live) {
|
|
81
|
+
const held = live[SIGN_IN_UPSTREAM_ENV];
|
|
82
|
+
return desired[SIGN_IN_UPSTREAM_ENV] === undefined && held !== undefined && held !== '';
|
|
83
|
+
}
|
|
84
|
+
/** The build argument (a build-time env in Coolify) that fixes the login's base path. */
|
|
85
|
+
export const SIGN_IN_BUILD_ARG_ENV = 'NEXT_PUBLIC_BASE_PATH';
|
|
86
|
+
/** The Coolify name and internal address of the in-app login app next to a target's login app. */
|
|
87
|
+
export function signInLoginNames(target) {
|
|
88
|
+
return {
|
|
89
|
+
name: `${target.names.authLogin}${SIGN_IN_IMAGE_TAG_SUFFIX}`,
|
|
90
|
+
internal: `${target.internal.authLogin}${SIGN_IN_IMAGE_TAG_SUFFIX}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True when the company's in-app sign-in goes through the platform's shared login (its sign-in is the
|
|
95
|
+
* platform's ZITADEL), so the platform needs the in-app login app. A company running its own services
|
|
96
|
+
* has its own login stack instead.
|
|
97
|
+
*/
|
|
98
|
+
export function inAppOnSharedLogin(org) {
|
|
99
|
+
return inAppSignIn(org) && !ownsServices(org);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The internal URL every in-app app proxies `/sign-in/*` to: the platform's in-app login, or, for a
|
|
103
|
+
* company that runs its own services, the one in its own sign-in stack (`<slug>-auth-login-sign-in`).
|
|
104
|
+
*/
|
|
105
|
+
export function signInUpstream(org) {
|
|
106
|
+
const host = org && ownsServices(org)
|
|
107
|
+
? signInLoginNames({
|
|
108
|
+
names: { authLogin: `${org.slug}-auth-login` },
|
|
109
|
+
internal: { authLogin: `${org.slug}-auth-login` },
|
|
110
|
+
}).internal
|
|
111
|
+
: SIGN_IN_LOGIN_INTERNAL_NAME;
|
|
112
|
+
return `http://${host}:${SIGN_IN_LOGIN_PORT}`;
|
|
113
|
+
}
|
|
114
|
+
/** {@link SIGN_IN_IN_APP_SCOPE_KEY}'s value for these apps. */
|
|
115
|
+
export function inAppState(apps) {
|
|
116
|
+
return apps
|
|
117
|
+
.map((a) => `${a.host}=${a.clientId}`)
|
|
118
|
+
.sort()
|
|
119
|
+
.join(',');
|
|
120
|
+
}
|
|
121
|
+
/** The `{host, clientId}` pairs a recorded {@link inAppState} names. */
|
|
122
|
+
export function inAppStatePairs(state) {
|
|
123
|
+
return (state ?? '')
|
|
124
|
+
.split(',')
|
|
125
|
+
.map((pair) => {
|
|
126
|
+
const at = pair.indexOf('=');
|
|
127
|
+
return at < 0
|
|
128
|
+
? { host: pair, clientId: '' }
|
|
129
|
+
: { host: pair.slice(0, at), clientId: pair.slice(at + 1) };
|
|
130
|
+
})
|
|
131
|
+
.filter((pair) => pair.host.length > 0);
|
|
132
|
+
}
|
|
133
|
+
/** The hosts a recorded {@link inAppState} names. */
|
|
134
|
+
export function inAppStateHosts(state) {
|
|
135
|
+
return (state ?? '')
|
|
136
|
+
.split(',')
|
|
137
|
+
.map((pair) => pair.split('=')[0] ?? '')
|
|
138
|
+
.filter((host) => host.length > 0);
|
|
139
|
+
}
|
|
140
|
+
/** The login binding, ZITADEL org id aside, for one in-app host. */
|
|
141
|
+
export function inAppBindingFor(app) {
|
|
142
|
+
return {
|
|
143
|
+
host: app.host,
|
|
144
|
+
clientIds: [app.clientId],
|
|
145
|
+
apps: { [app.clientId]: app.app },
|
|
146
|
+
basePath: SIGN_IN_BASE_PATH,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* What the login-host step records for these wanted apps, given what the apps' steps have recorded:
|
|
151
|
+
* `trusted` is every wanted host plus any host given up whose app still holds `SIGNIN_UPSTREAM` (its trusted
|
|
152
|
+
* domain stays until the env is gone); `active` is the wanted hosts whose env is live: bound, with the app's own base URI.
|
|
153
|
+
*/
|
|
154
|
+
export function inAppTargets(apps, recorded) {
|
|
155
|
+
const live = signInEnvLiveHosts(recorded);
|
|
156
|
+
const wanted = new Set(apps.map((a) => a.host));
|
|
157
|
+
const kept = inAppStatePairs(recorded[SIGN_IN_TRUSTED_SCOPE_KEY]).filter((pair) => !wanted.has(pair.host) && live.has(pair.host));
|
|
158
|
+
return {
|
|
159
|
+
trusted: inAppState([...apps, ...kept]),
|
|
160
|
+
active: inAppState(apps.filter((a) => live.has(a.host))),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* True when an app should proxy `/sign-in/*` to the login: its host is trusted there, and the
|
|
165
|
+
* company still has the switch on or the base URI still points at the app (it is switched off in reverse
|
|
166
|
+
* order: the base URI goes back before the app stops proxying).
|
|
167
|
+
*/
|
|
168
|
+
export function inAppHostApplied(org, recorded, host) {
|
|
169
|
+
if (!inAppStateHosts(recorded[SIGN_IN_TRUSTED_SCOPE_KEY]).includes(host))
|
|
170
|
+
return false;
|
|
171
|
+
return inAppSignIn(org) || inAppStateHosts(recorded[SIGN_IN_IN_APP_SCOPE_KEY]).includes(host);
|
|
172
|
+
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -12,6 +12,16 @@ export interface PlatformService {
|
|
|
12
12
|
hostname: string;
|
|
13
13
|
gitRef: string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* `archon`'s own extra field: presence turns blue/green on for platform-archon, through the same code as a hosting
|
|
17
|
+
* entry (`plan/blue-green.ts`). The tunnel moves to the balancer only once a balancer is recorded.
|
|
18
|
+
*/
|
|
19
|
+
export interface PlatformArchonService extends PlatformService {
|
|
20
|
+
blueGreen?: {
|
|
21
|
+
steps?: number[];
|
|
22
|
+
stepHoldMinutes?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* `sfu`'s own extra field. A host that changes what the SFU VM is reached
|
|
17
27
|
* with (its SSH key, host key or tunnel token) sets a new `connectionRevision`
|
|
@@ -97,7 +107,7 @@ export interface DeclaredPlatform {
|
|
|
97
107
|
ai: PlatformService;
|
|
98
108
|
jobs: PlatformService;
|
|
99
109
|
integrations: PlatformService;
|
|
100
|
-
archon:
|
|
110
|
+
archon: PlatformArchonService;
|
|
101
111
|
/** #317 (F7). No deploy step reads this service's own `gitRef` yet -- `N1` is deployed by hand until it exists. */
|
|
102
112
|
edge: PlatformService;
|
|
103
113
|
/**
|
package/dist/platform.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isIPv4 } from 'node:net';
|
|
2
|
-
import { validateDomain } from './org.js';
|
|
2
|
+
import { validateBlueGreen, validateDomain } from './org.js';
|
|
3
3
|
/** Where Archon publishes the Worker script for `tag` (a private repo; the host's `fetchArtifact` fetches it). */
|
|
4
4
|
export function uptimeArtifactUrl(tag) {
|
|
5
5
|
return `https://github.com/wtfalch/archon/releases/download/${tag}/archon-uptime-worker.js`;
|
|
@@ -69,6 +69,9 @@ export function validatePlatform(platform) {
|
|
|
69
69
|
return `${key}.gitRef must not be empty once ${key}.enabled is true`;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
const blueGreenReason = validateBlueGreen(platform.archon.blueGreen, 'archon');
|
|
73
|
+
if (blueGreenReason)
|
|
74
|
+
return blueGreenReason;
|
|
72
75
|
const revision = platform.sfu.connectionRevision;
|
|
73
76
|
if (revision != null && !/^[A-Za-z0-9:.-]{0,64}$/.test(revision)) {
|
|
74
77
|
return 'sfu.connectionRevision must be at most 64 characters of A-Z, a-z, 0-9, ":", "." and "-"';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type Color, type ReleaseAction, type ReleaseSignal, type WindowState } from './types.js';
|
|
2
|
+
/** ADR 0001 rule 5: the default steps and the least time on each. */
|
|
3
|
+
export declare const DEFAULT_STEPS: readonly number[];
|
|
4
|
+
export declare const DEFAULT_STEP_HOLD_MINUTES = 60;
|
|
5
|
+
/** Rule 7: errors this share of requests, with at least HARD_FAIL_MIN_REQUESTS, roll back at any level. */
|
|
6
|
+
export declare const HARD_FAIL_ERROR_RATE = 0.9;
|
|
7
|
+
export declare const HARD_FAIL_MIN_REQUESTS = 2;
|
|
8
|
+
/** Rule 8: a relative fail needs this many requests and this many failed checks in a row. */
|
|
9
|
+
export declare const RELATIVE_MIN_REQUESTS = 20;
|
|
10
|
+
export declare const RELATIVE_FAILED_CHECKS = 3;
|
|
11
|
+
/** A relative check counts only this long after the last counted one, so runs close together (applies also run releases) are one check. */
|
|
12
|
+
export declare const CHECK_INTERVAL_MINUTES = 10;
|
|
13
|
+
/** The new color matches the baseline while its error rate is at most baseline plus this (absolute). */
|
|
14
|
+
export declare const ERROR_RATE_MARGIN = 0.02;
|
|
15
|
+
/** Rules 12 and 13: the window at 100% closes after this long, and at most this long. */
|
|
16
|
+
export declare const WINDOW_MIN_HOURS = 24;
|
|
17
|
+
export declare const WINDOW_CAP_DAYS = 7;
|
|
18
|
+
/** The new color must be running and answer on its own address within this time of the deploy. */
|
|
19
|
+
export declare const DEPLOY_TIMEOUT_MINUTES = 30;
|
|
20
|
+
export interface ActiveRelease {
|
|
21
|
+
from: Color;
|
|
22
|
+
to: Color;
|
|
23
|
+
digest: string;
|
|
24
|
+
/** Relative-fail checks in a row. */
|
|
25
|
+
fails: number;
|
|
26
|
+
/** When the last counted relative check ran (ISO). Absent: none yet in this step. */
|
|
27
|
+
checkedAt?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface Recorded {
|
|
30
|
+
live: Color;
|
|
31
|
+
digests: Record<Color, string | null>;
|
|
32
|
+
stageDigest: string | null;
|
|
33
|
+
weights: Record<Color, number>;
|
|
34
|
+
window: WindowState;
|
|
35
|
+
/** Set while window.kind is step, inconclusive, window or cap. */
|
|
36
|
+
release: ActiveRelease | null;
|
|
37
|
+
held: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface ColorCounts {
|
|
40
|
+
requests: number;
|
|
41
|
+
errors: number;
|
|
42
|
+
}
|
|
43
|
+
/** The reporting health read (C1), once validated. */
|
|
44
|
+
export interface HealthRead {
|
|
45
|
+
colors: Record<Color, ColorCounts>;
|
|
46
|
+
daily: {
|
|
47
|
+
days: number;
|
|
48
|
+
requestsPerDay: number;
|
|
49
|
+
errorsPerDay: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface Observation {
|
|
53
|
+
/** null when the health read failed: counted as inconclusive, never as a pass. */
|
|
54
|
+
health: HealthRead | null;
|
|
55
|
+
/** The new color's Coolify status. */
|
|
56
|
+
status: 'running' | 'crash_loop' | 'other';
|
|
57
|
+
/** The new color's own address, checked over HTTP. */
|
|
58
|
+
address: 'ok' | 'failed' | 'unchecked';
|
|
59
|
+
}
|
|
60
|
+
export type ReleaseEffect = {
|
|
61
|
+
kind: 'deploy';
|
|
62
|
+
color: Color;
|
|
63
|
+
digest: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'stop';
|
|
66
|
+
color: Color;
|
|
67
|
+
};
|
|
68
|
+
export interface Decision {
|
|
69
|
+
recorded: Recorded;
|
|
70
|
+
signal: ReleaseSignal | null;
|
|
71
|
+
effects: ReleaseEffect[];
|
|
72
|
+
/** Blocked items for the operator, fixed text. */
|
|
73
|
+
blocked: string[];
|
|
74
|
+
/** Actions that changed nothing, with a fixed reason. */
|
|
75
|
+
refused: string[];
|
|
76
|
+
}
|
|
77
|
+
export declare function normalizeSteps(steps: readonly number[] | undefined): number[];
|
|
78
|
+
/** The time a health read should cover: from the step or window start to now. */
|
|
79
|
+
export declare function healthRange(r: Recorded, now: Date): {
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
} | null;
|
|
83
|
+
/**
|
|
84
|
+
* The whole release decision for one run: from the recorded state, the
|
|
85
|
+
* actions requested, one observation and the time, the next recorded state.
|
|
86
|
+
* Pure: it reads nothing and writes nothing (ADR 0001 rules 5, 7-13, 16).
|
|
87
|
+
*/
|
|
88
|
+
export declare function decide(input: {
|
|
89
|
+
recorded: Recorded;
|
|
90
|
+
actions: readonly ReleaseAction[];
|
|
91
|
+
steps?: readonly number[] | undefined;
|
|
92
|
+
stepHoldMinutes?: number | undefined;
|
|
93
|
+
now: Date;
|
|
94
|
+
observation: Observation;
|
|
95
|
+
}): Decision;
|