@wtfalch/foundry 0.41.1 → 0.42.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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0054_org_signin_in_app.sql +6 -0
- package/dist/org.d.ts +17 -0
- package/dist/org.js +14 -0
- package/dist/plan/agora.d.ts +4 -1
- package/dist/plan/agora.js +16 -2
- package/dist/plan/boule.d.ts +5 -2
- package/dist/plan/boule.js +14 -2
- package/dist/plan/coolify-previews.d.ts +10 -0
- package/dist/plan/coolify-previews.js +14 -0
- package/dist/plan/diff.js +92 -9
- package/dist/plan/edge-ratelimits.d.ts +5 -0
- package/dist/plan/edge-ratelimits.js +16 -6
- package/dist/plan/executor.d.ts +45 -0
- package/dist/plan/executor.js +283 -22
- package/dist/plan/login-host.d.ts +23 -2
- package/dist/plan/login-host.js +30 -5
- package/dist/plan/own-diff.d.ts +1 -1
- package/dist/plan/own-diff.js +9 -2
- package/dist/plan/own-sfu.d.ts +2 -0
- package/dist/plan/own-sfu.js +3 -0
- package/dist/plan/platform-diff.js +4 -0
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-org.js +5 -0
- package/dist/plan/platform-providers.d.ts +16 -1
- package/dist/plan/platform-providers.js +255 -8
- package/dist/plan/providers-readers.js +6 -2
- 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/store/postgres.js +7 -1
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/schema.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The login's host table env, and the document it holds (`auth/docs/company-login-hosts.md`). */
|
|
2
|
+
export const LOGIN_HOST_BINDINGS_ENV = 'LOGIN_HOST_BINDINGS';
|
|
3
|
+
/**
|
|
4
|
+
* The host table on a company's in-app login app. The company's own login stack has none of its own:
|
|
5
|
+
* its login host is the instance host, every in-app host is a binding. Parsed with a fixed error that
|
|
6
|
+
* never echoes the live value.
|
|
7
|
+
*/
|
|
8
|
+
function readDoc(raw, instanceHost, appId) {
|
|
9
|
+
if (!raw)
|
|
10
|
+
return { version: 1, instanceHost, sharedHosts: [instanceHost], bindings: [] };
|
|
11
|
+
let doc;
|
|
12
|
+
try {
|
|
13
|
+
doc = JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`${LOGIN_HOST_BINDINGS_ENV} on app ${appId} is not JSON; left as it is`);
|
|
17
|
+
}
|
|
18
|
+
const d = doc;
|
|
19
|
+
if (!d || d.version !== 1 || !Array.isArray(d.bindings) || !Array.isArray(d.sharedHosts)) {
|
|
20
|
+
throw new Error(`${LOGIN_HOST_BINDINGS_ENV} on app ${appId} has an unknown shape; left as it is`);
|
|
21
|
+
}
|
|
22
|
+
return d;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Adds, replaces and removes hosts in the host table of one in-app login app, then redeploys it when the
|
|
26
|
+
* document changed. Hosts not named stay as they are, so two steps that each own some hosts (the
|
|
27
|
+
* company's login-host step, Archon's step) never undo each other. Entries are sorted by host and the
|
|
28
|
+
* document is written compact, so a repeat writes the same text and deploys nothing.
|
|
29
|
+
*/
|
|
30
|
+
export async function editSignInBindings(coolify, input) {
|
|
31
|
+
const { appId, instanceHost } = input;
|
|
32
|
+
const live = (await coolify.listEnvs(appId)).find((e) => e.key === LOGIN_HOST_BINDINGS_ENV && !e.is_preview);
|
|
33
|
+
const doc = readDoc(live?.value || undefined, instanceHost, appId);
|
|
34
|
+
const touched = new Set([...(input.upsert ?? []).map((b) => b.host), ...(input.remove ?? [])]);
|
|
35
|
+
doc.bindings = [
|
|
36
|
+
...doc.bindings.filter((b) => !touched.has(b.host)),
|
|
37
|
+
...(input.upsert ?? []).map((b) => ({ ...b, clientIds: [...b.clientIds].sort() })),
|
|
38
|
+
].sort((a, b) => (a.host < b.host ? -1 : 1));
|
|
39
|
+
const next = JSON.stringify(doc);
|
|
40
|
+
if (live?.value === next)
|
|
41
|
+
return;
|
|
42
|
+
await coolify.bulkUpsertEnvs(appId, [{ key: LOGIN_HOST_BINDINGS_ENV, value: next }]);
|
|
43
|
+
await coolify.deploy(appId);
|
|
44
|
+
}
|
|
@@ -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/store/postgres.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, asc, desc, eq, ne, sql } from 'drizzle-orm';
|
|
2
|
-
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateHosting, validateMail, validateOwners, validateServices, validateSite, validateVersionPin, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateHosting, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
|
|
3
3
|
import { lockedHostnames } from '../plan/locked-hostnames.js';
|
|
4
4
|
import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
|
|
5
5
|
import { validatePlatform } from '../platform.js';
|
|
@@ -146,6 +146,8 @@ function rowToOrg(row) {
|
|
|
146
146
|
},
|
|
147
147
|
}
|
|
148
148
|
: {}),
|
|
149
|
+
// Omitted while off.
|
|
150
|
+
...(row.signInInApp ? { signIn: { inApp: true } } : {}),
|
|
149
151
|
// Omitted while unset. Validated on read: a stored value is not trusted.
|
|
150
152
|
...(row.versionPin ? { versionPin: readVersionPin(row) } : {}),
|
|
151
153
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
@@ -198,6 +200,7 @@ function orgToRow(org) {
|
|
|
198
200
|
callsVersionPinSynapse: org.calls.versionPin?.synapse ?? null,
|
|
199
201
|
callsVersionPinMas: org.calls.versionPin?.mas ?? null,
|
|
200
202
|
issuerUsesDomain: org.issuer.usesDomain,
|
|
203
|
+
signInInApp: org.signIn?.inApp ?? false,
|
|
201
204
|
servicesOwn: org.services?.own ?? false,
|
|
202
205
|
servicesSfuSlot: org.services?.sfuSlot ?? null,
|
|
203
206
|
versionPin: org.versionPin && Object.keys(org.versionPin).length > 0 ? org.versionPin : null,
|
|
@@ -401,6 +404,9 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
401
404
|
const reason = validateServices(org);
|
|
402
405
|
if (reason)
|
|
403
406
|
throw new DeclaredOrgError(`services invalid: ${reason}`);
|
|
407
|
+
const signInReason = validateSignIn(org);
|
|
408
|
+
if (signInReason)
|
|
409
|
+
throw new DeclaredOrgError(`signIn invalid: ${signInReason}`);
|
|
404
410
|
const slot = org.services?.sfuSlot ?? null;
|
|
405
411
|
if (slot === null)
|
|
406
412
|
return;
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -355,6 +355,23 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
355
355
|
identity: undefined;
|
|
356
356
|
generated: undefined;
|
|
357
357
|
}, {}, {}>;
|
|
358
|
+
signInInApp: import("drizzle-orm/pg-core").PgColumn<{
|
|
359
|
+
name: "signin_in_app";
|
|
360
|
+
tableName: "foundry_declared_orgs";
|
|
361
|
+
dataType: "boolean";
|
|
362
|
+
columnType: "PgBoolean";
|
|
363
|
+
data: boolean;
|
|
364
|
+
driverParam: boolean;
|
|
365
|
+
notNull: true;
|
|
366
|
+
hasDefault: true;
|
|
367
|
+
isPrimaryKey: false;
|
|
368
|
+
isAutoincrement: false;
|
|
369
|
+
hasRuntimeDefault: false;
|
|
370
|
+
enumValues: undefined;
|
|
371
|
+
baseColumn: never;
|
|
372
|
+
identity: undefined;
|
|
373
|
+
generated: undefined;
|
|
374
|
+
}, {}, {}>;
|
|
358
375
|
versionPin: import("drizzle-orm/pg-core").PgColumn<{
|
|
359
376
|
name: "version_pin";
|
|
360
377
|
tableName: "foundry_declared_orgs";
|
package/dist/store/schema.js
CHANGED
|
@@ -38,6 +38,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
38
38
|
// ServicesConfig (org.ts): false and null are the platform's shared services.
|
|
39
39
|
servicesOwn: boolean('services_own').notNull().default(false),
|
|
40
40
|
servicesSfuSlot: integer('services_sfu_slot'),
|
|
41
|
+
// SignInConfig.inApp (org.ts); migrations/0054.
|
|
42
|
+
signInInApp: boolean('signin_in_app').notNull().default(false),
|
|
41
43
|
// VersionPin (org.ts): null follows the platform. `migrations/0053_org_version_pin.sql`.
|
|
42
44
|
versionPin: jsonb('version_pin').$type(),
|
|
43
45
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|