@wtfalch/foundry 0.32.0 → 0.34.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/bootstrap.d.ts +18 -1
- package/dist/bootstrap/bootstrap.js +167 -28
- package/dist/bootstrap/declare.d.ts +5 -1
- package/dist/bootstrap/declare.js +3 -1
- package/dist/bootstrap/index.d.ts +1 -1
- package/dist/bootstrap/versions.d.ts +4 -0
- package/dist/calls-sfu.d.ts +10 -0
- package/dist/calls-sfu.js +41 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.js +8 -3
- package/dist/migrations/0051_org_services.sql +13 -0
- package/dist/migrations/0052_platform_mail_relay.sql +8 -0
- package/dist/org.d.ts +24 -2
- package/dist/org.js +42 -0
- package/dist/plan/boule.d.ts +24 -1
- package/dist/plan/boule.js +29 -1
- package/dist/plan/calls-matrix.d.ts +9 -0
- package/dist/plan/calls-matrix.js +17 -0
- package/dist/plan/cloudflared-connector.d.ts +7 -1
- package/dist/plan/cloudflared-connector.js +14 -1
- package/dist/plan/diff.d.ts +32 -1
- package/dist/plan/diff.js +383 -26
- package/dist/plan/executor.d.ts +177 -7
- package/dist/plan/executor.js +738 -51
- package/dist/plan/health-check-settings.d.ts +28 -0
- package/dist/plan/health-check-settings.js +36 -0
- package/dist/plan/health-gaps.d.ts +33 -0
- package/dist/plan/health-gaps.js +89 -0
- package/dist/plan/health-targets.js +1 -1
- package/dist/plan/held-key-refs.d.ts +10 -0
- package/dist/plan/held-key-refs.js +10 -0
- package/dist/plan/index.d.ts +16 -6
- package/dist/plan/index.js +8 -3
- package/dist/plan/live.d.ts +5 -2
- package/dist/plan/live.js +15 -2
- package/dist/plan/locked-hostnames.d.ts +5 -2
- package/dist/plan/locked-hostnames.js +9 -1
- package/dist/plan/login-host.d.ts +62 -2
- package/dist/plan/login-host.js +104 -1
- package/dist/plan/mail-relay.d.ts +120 -0
- package/dist/plan/mail-relay.js +323 -0
- package/dist/plan/org-names.d.ts +24 -0
- package/dist/plan/org-names.js +52 -0
- package/dist/plan/own-diff.d.ts +169 -0
- package/dist/plan/own-diff.js +432 -0
- package/dist/plan/own-dns.d.ts +6 -0
- package/dist/plan/own-dns.js +15 -0
- package/dist/plan/own-limits.d.ts +22 -0
- package/dist/plan/own-limits.js +101 -0
- package/dist/plan/own-services.d.ts +193 -0
- package/dist/plan/own-services.js +167 -0
- package/dist/plan/own-sfu.d.ts +69 -0
- package/dist/plan/own-sfu.js +255 -0
- package/dist/plan/plan-apply.d.ts +101 -7
- package/dist/plan/plan-apply.js +216 -12
- package/dist/plan/platform-diff.d.ts +61 -12
- package/dist/plan/platform-diff.js +140 -34
- package/dist/plan/platform-org.d.ts +36 -0
- package/dist/plan/platform-org.js +96 -2
- package/dist/plan/platform-providers.d.ts +407 -12
- package/dist/plan/platform-providers.js +947 -422
- package/dist/plan/sign-in-org.d.ts +99 -0
- package/dist/plan/sign-in-org.js +151 -0
- package/dist/plan/zitadel-login-policy.js +1 -1
- package/dist/plan/zitadel-system-api.d.ts +22 -1
- package/dist/plan/zitadel-system-api.js +49 -8
- package/dist/plan.d.ts +18 -0
- package/dist/platform.d.ts +8 -0
- package/dist/slug.js +1 -0
- package/dist/store/postgres.js +51 -2
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +5 -0
- package/package.json +1 -1
- package/versions.json +3 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ZitadelPort } from './executor.js';
|
|
2
|
+
/** Scope id: the domain registered as primary on the company's sign-in org. */
|
|
3
|
+
export declare const ZITADEL_ORG_DOMAIN_SCOPE_KEY = "zitadel_org_domain";
|
|
4
|
+
/**
|
|
5
|
+
* Scope id: the id of the sign-in org whose self-registration foundry closed,
|
|
6
|
+
* recorded only after `closeSelfRegistration` returned. While it is missing or
|
|
7
|
+
* names another org, the company plan holds the close step and no owner invite.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY = "zitadel_org_registration_closed";
|
|
10
|
+
/** Closes self-registration on the org (idempotent) and records it under `scopeKey`, in that order. */
|
|
11
|
+
export declare function closeSignInOrgRegistration(zitadel: Pick<ZitadelPort, 'closeSelfRegistration'>, orgId: string, recordScopeId: (key: string, id: string) => void, scopeKey?: string): Promise<void>;
|
|
12
|
+
/** Scope id: the domain the sign-in server refused to make primary (its domain policy wants proof). Set instead of failing; the planner reports it as blocked. */
|
|
13
|
+
export declare const ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY = "zitadel_org_domain_blocked";
|
|
14
|
+
/** The fixed text of the blocked item for a refused sign-in domain. No cause, no value but names. */
|
|
15
|
+
export declare function signInDomainBlockedText(orgName: string, domain: string): string;
|
|
16
|
+
/** The domain an org's owners sign in on: its own, or `<slug>.oikos.host` when it has none. */
|
|
17
|
+
export declare function signInOrgDomain(org: {
|
|
18
|
+
slug: string;
|
|
19
|
+
domain: string | null;
|
|
20
|
+
}): string;
|
|
21
|
+
/**
|
|
22
|
+
* Adds `domain` to the ZITADEL org and makes it primary. Both calls are
|
|
23
|
+
* idempotent. A failure after the add leaves the domain on the org but not
|
|
24
|
+
* primary, and nothing is recorded, so the next run repeats both.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerSignInDomain(zitadel: Pick<ZitadelPort, 'addOrgDomain' | 'setPrimaryOrgDomain'>, orgId: string, domain: string, sleep?: (ms: number) => Promise<void>): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* {@link registerSignInDomain}, except that a refusal to make the domain
|
|
29
|
+
* primary (the sign-in server's domain policy wants proof) is recorded under
|
|
30
|
+
* `blockedScopeKey` and returns `false` instead of throwing on every apply.
|
|
31
|
+
* Any other failure throws. Returns `true` when the domain is primary.
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerSignInDomainOrBlock(zitadel: Pick<ZitadelPort, 'addOrgDomain' | 'setPrimaryOrgDomain'>, orgId: string, domain: string, recordScopeId: (key: string, id: string) => void, blockedScopeKey?: string): Promise<boolean>;
|
|
34
|
+
export interface EnsureSignInOrgOptions {
|
|
35
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'addOrgDomain' | 'setPrimaryOrgDomain' | 'closeSelfRegistration'>;
|
|
36
|
+
/** Where the successful close is recorded (the org id). Absent: not recorded; the caller closes on every run. */
|
|
37
|
+
registrationClosedScopeKey?: string;
|
|
38
|
+
/** Defaults to {@link ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY}. */
|
|
39
|
+
domainBlockedScopeKey?: string;
|
|
40
|
+
/** The ZITADEL org's name. A company uses its slug, so every caller finds the same org. */
|
|
41
|
+
name: string;
|
|
42
|
+
/** The domain to make primary, from {@link signInOrgDomain}. */
|
|
43
|
+
domain: string;
|
|
44
|
+
/** Where the ids are recorded. */
|
|
45
|
+
recordScopeId(key: string, id: string): void;
|
|
46
|
+
/** Called once the org id is recorded and before the domain is registered, so a crash between them does not lose the org. */
|
|
47
|
+
persistScopeIds?(): Promise<void>;
|
|
48
|
+
/** Defaults to `zitadel_org`. */
|
|
49
|
+
orgScopeKey?: string;
|
|
50
|
+
/** Defaults to {@link ZITADEL_ORG_DOMAIN_SCOPE_KEY}. */
|
|
51
|
+
domainScopeKey?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Finds or creates the company's sign-in organisation by name, registers
|
|
55
|
+
* `domain` on it and makes it primary, then records the org id and the domain.
|
|
56
|
+
* The domain is recorded last, only after both calls succeeded.
|
|
57
|
+
*/
|
|
58
|
+
export declare function ensureSignInOrg(options: EnsureSignInOrgOptions): Promise<{
|
|
59
|
+
orgId: string;
|
|
60
|
+
created: boolean;
|
|
61
|
+
domainBlocked?: true;
|
|
62
|
+
}>;
|
|
63
|
+
/** The user metadata key that holds an owner's personal address. */
|
|
64
|
+
export declare const PERSONAL_EMAIL_METADATA_KEY = "personal_email";
|
|
65
|
+
export interface InviteOwnerOptions {
|
|
66
|
+
zitadel: Pick<ZitadelPort, 'inviteOrgOwner' | 'setUserMetadata' | 'getUserMetadata'>;
|
|
67
|
+
mail: {
|
|
68
|
+
send(to: string, link: string): Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/** The sign-in org the account is made in. */
|
|
71
|
+
orgId: string;
|
|
72
|
+
/** The platform's login app, where the link lands (it serves `/verify`). */
|
|
73
|
+
authIssuer: string;
|
|
74
|
+
owner: {
|
|
75
|
+
givenName: string;
|
|
76
|
+
familyName: string;
|
|
77
|
+
/** The owner's work address: the account signs in as its local part. */
|
|
78
|
+
mailboxAddress: string;
|
|
79
|
+
/** Where the first-sign-in link is mailed and what the `personal_email` metadata holds. */
|
|
80
|
+
personalEmail: string;
|
|
81
|
+
};
|
|
82
|
+
/** The user id foundry recorded when it invited this owner before. An existing account with another id, and no personal_email metadata of the owner's, is refused. */
|
|
83
|
+
invitedUserId?: string;
|
|
84
|
+
/** Called with the invite code and the whole link before they are mailed, so the caller can mask them. */
|
|
85
|
+
onSecret?: (value: string) => void;
|
|
86
|
+
/** Names the step in errors, for example `zitadel:owner for acme`. */
|
|
87
|
+
label: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Makes the owner's account (username = local part of the work address, email =
|
|
91
|
+
* the work address), keeps the personal address as metadata, and mails the
|
|
92
|
+
* first-sign-in link to it. The invite code ZITADEL returns goes only into that
|
|
93
|
+
* mail (30 second timeout); no error holds it. A failure after the account
|
|
94
|
+
* exists leaves the account, and the next run finds it by username and mints a
|
|
95
|
+
* new code.
|
|
96
|
+
*/
|
|
97
|
+
export declare function inviteOwnerWithLink(options: InviteOwnerOptions): Promise<{
|
|
98
|
+
userId: string;
|
|
99
|
+
}>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Scope id: the domain registered as primary on the company's sign-in org. */
|
|
2
|
+
export const ZITADEL_ORG_DOMAIN_SCOPE_KEY = 'zitadel_org_domain';
|
|
3
|
+
/**
|
|
4
|
+
* Scope id: the id of the sign-in org whose self-registration foundry closed,
|
|
5
|
+
* recorded only after `closeSelfRegistration` returned. While it is missing or
|
|
6
|
+
* names another org, the company plan holds the close step and no owner invite.
|
|
7
|
+
*/
|
|
8
|
+
export const ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY = 'zitadel_org_registration_closed';
|
|
9
|
+
/** Closes self-registration on the org (idempotent) and records it under `scopeKey`, in that order. */
|
|
10
|
+
export async function closeSignInOrgRegistration(zitadel, orgId, recordScopeId, scopeKey = ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY) {
|
|
11
|
+
await zitadel.closeSelfRegistration(orgId);
|
|
12
|
+
recordScopeId(scopeKey, orgId);
|
|
13
|
+
}
|
|
14
|
+
/** Scope id: the domain the sign-in server refused to make primary (its domain policy wants proof). Set instead of failing; the planner reports it as blocked. */
|
|
15
|
+
export const ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY = 'zitadel_org_domain_blocked';
|
|
16
|
+
/** The fixed text of the blocked item for a refused sign-in domain. No cause, no value but names. */
|
|
17
|
+
export function signInDomainBlockedText(orgName, domain) {
|
|
18
|
+
return `${orgName}'s sign-in domain ${domain} is blocked, not planned: the sign-in server refused to make it primary because its domain policy wants proof of ownership. Set the policy ValidateOrgDomains to false (and UserLoginMustBeDomain to true) on the sign-in server, then remove the recorded key ${ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY}`;
|
|
19
|
+
}
|
|
20
|
+
/** True for the providers' "domain is not verified" refusal of `setPrimaryOrgDomain`. */
|
|
21
|
+
function isPrimaryDomainRefused(error) {
|
|
22
|
+
return error instanceof Error && /is not verified/.test(error.message);
|
|
23
|
+
}
|
|
24
|
+
/** True for the providers' "domain is not on org" answer of `setPrimaryOrgDomain`. */
|
|
25
|
+
function isDomainNotListed(error) {
|
|
26
|
+
return error instanceof Error && /is not on org/.test(error.message);
|
|
27
|
+
}
|
|
28
|
+
const DOMAIN_LISTED_TRIES = 10;
|
|
29
|
+
const DOMAIN_LISTED_WAIT_MS = 500;
|
|
30
|
+
/** The domain an org's owners sign in on: its own, or `<slug>.oikos.host` when it has none. */
|
|
31
|
+
export function signInOrgDomain(org) {
|
|
32
|
+
return (org.domain ?? `${org.slug}.oikos.host`).toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Adds `domain` to the ZITADEL org and makes it primary. Both calls are
|
|
36
|
+
* idempotent. A failure after the add leaves the domain on the org but not
|
|
37
|
+
* primary, and nothing is recorded, so the next run repeats both.
|
|
38
|
+
*/
|
|
39
|
+
export async function registerSignInDomain(zitadel, orgId, domain, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
40
|
+
await zitadel.addOrgDomain(orgId, domain);
|
|
41
|
+
// The sign-in server lists a domain a moment after it accepts the add, and
|
|
42
|
+
// `setPrimaryOrgDomain` reads that list first: ask again, for five seconds.
|
|
43
|
+
for (let attempt = 1;; attempt += 1) {
|
|
44
|
+
try {
|
|
45
|
+
await zitadel.setPrimaryOrgDomain(orgId, domain);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (attempt >= DOMAIN_LISTED_TRIES || !isDomainNotListed(error))
|
|
50
|
+
throw error;
|
|
51
|
+
await sleep(DOMAIN_LISTED_WAIT_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* {@link registerSignInDomain}, except that a refusal to make the domain
|
|
57
|
+
* primary (the sign-in server's domain policy wants proof) is recorded under
|
|
58
|
+
* `blockedScopeKey` and returns `false` instead of throwing on every apply.
|
|
59
|
+
* Any other failure throws. Returns `true` when the domain is primary.
|
|
60
|
+
*/
|
|
61
|
+
export async function registerSignInDomainOrBlock(zitadel, orgId, domain, recordScopeId, blockedScopeKey = ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY) {
|
|
62
|
+
try {
|
|
63
|
+
await registerSignInDomain(zitadel, orgId, domain);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!isPrimaryDomainRefused(error))
|
|
68
|
+
throw error;
|
|
69
|
+
recordScopeId(blockedScopeKey, domain);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Finds or creates the company's sign-in organisation by name, registers
|
|
75
|
+
* `domain` on it and makes it primary, then records the org id and the domain.
|
|
76
|
+
* The domain is recorded last, only after both calls succeeded.
|
|
77
|
+
*/
|
|
78
|
+
export async function ensureSignInOrg(options) {
|
|
79
|
+
const { zitadel, name, domain, recordScopeId } = options;
|
|
80
|
+
const { org, created } = await zitadel.findOrCreateOrg(name);
|
|
81
|
+
recordScopeId(options.orgScopeKey ?? 'zitadel_org', org.id);
|
|
82
|
+
await options.persistScopeIds?.();
|
|
83
|
+
// Before any owner invite can run: a stranger must not register the owner's
|
|
84
|
+
// username on an org that still inherits the instance's open registration.
|
|
85
|
+
// A failure here stops the step with the org recorded and no close recorded:
|
|
86
|
+
// the company plan then holds the close step and no invite until it succeeds.
|
|
87
|
+
await zitadel.closeSelfRegistration(org.id);
|
|
88
|
+
if (options.registrationClosedScopeKey) {
|
|
89
|
+
recordScopeId(options.registrationClosedScopeKey, org.id);
|
|
90
|
+
}
|
|
91
|
+
const registered = await registerSignInDomainOrBlock(zitadel, org.id, domain, recordScopeId, options.domainBlockedScopeKey);
|
|
92
|
+
if (!registered)
|
|
93
|
+
return { orgId: org.id, created, domainBlocked: true };
|
|
94
|
+
recordScopeId(options.domainScopeKey ?? ZITADEL_ORG_DOMAIN_SCOPE_KEY, domain);
|
|
95
|
+
return { orgId: org.id, created };
|
|
96
|
+
}
|
|
97
|
+
/** The user metadata key that holds an owner's personal address. */
|
|
98
|
+
export const PERSONAL_EMAIL_METADATA_KEY = 'personal_email';
|
|
99
|
+
/** How long the owner invite mail may take before the step gives up on it. */
|
|
100
|
+
const OWNER_INVITE_MAIL_TIMEOUT_MS = 30_000;
|
|
101
|
+
async function withTimeout(work, ms) {
|
|
102
|
+
let timer;
|
|
103
|
+
const timeout = new Promise((_, reject) => {
|
|
104
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
return await Promise.race([work, timeout]);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Makes the owner's account (username = local part of the work address, email =
|
|
115
|
+
* the work address), keeps the personal address as metadata, and mails the
|
|
116
|
+
* first-sign-in link to it. The invite code ZITADEL returns goes only into that
|
|
117
|
+
* mail (30 second timeout); no error holds it. A failure after the account
|
|
118
|
+
* exists leaves the account, and the next run finds it by username and mints a
|
|
119
|
+
* new code.
|
|
120
|
+
*/
|
|
121
|
+
export async function inviteOwnerWithLink(options) {
|
|
122
|
+
const { zitadel, mail, orgId, authIssuer, owner, label } = options;
|
|
123
|
+
const { userId, created, inviteCode } = await zitadel.inviteOrgOwner(orgId, {
|
|
124
|
+
givenName: owner.givenName,
|
|
125
|
+
familyName: owner.familyName,
|
|
126
|
+
email: owner.mailboxAddress,
|
|
127
|
+
username: owner.mailboxAddress.slice(0, owner.mailboxAddress.indexOf('@')),
|
|
128
|
+
}, undefined, true);
|
|
129
|
+
if (!created && !inviteCode && userId !== options.invitedUserId) {
|
|
130
|
+
// An account that already has a sign-in method and no record of ours: not adopted.
|
|
131
|
+
// `personal_email` is written only by this function, through the admin API.
|
|
132
|
+
const metadata = await zitadel.getUserMetadata(orgId, userId, PERSONAL_EMAIL_METADATA_KEY);
|
|
133
|
+
if (metadata?.toLowerCase() !== owner.personalEmail.toLowerCase()) {
|
|
134
|
+
throw new Error(`${label}: in ZITADEL org ${orgId} an account with this username already exists and was not invited by foundry`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await zitadel.setUserMetadata(orgId, userId, PERSONAL_EMAIL_METADATA_KEY, owner.personalEmail);
|
|
138
|
+
if (inviteCode) {
|
|
139
|
+
const link = `${authIssuer}/verify?userId=${encodeURIComponent(userId)}&code=${encodeURIComponent(inviteCode)}&organization=${encodeURIComponent(orgId)}&invite=true`;
|
|
140
|
+
options.onSecret?.(inviteCode);
|
|
141
|
+
options.onSecret?.(link);
|
|
142
|
+
try {
|
|
143
|
+
await withTimeout(mail.send(owner.personalEmail, link), OWNER_INVITE_MAIL_TIMEOUT_MS);
|
|
144
|
+
}
|
|
145
|
+
catch (_cause) {
|
|
146
|
+
// The mail port's own error may carry the link, and the link carries the code.
|
|
147
|
+
throw new Error(`${label}: the invite mail could not be sent; the account ${userId} exists and the next apply sends a new link`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { userId };
|
|
151
|
+
}
|
|
@@ -74,7 +74,7 @@ export function createZitadelLoginPolicy(options) {
|
|
|
74
74
|
const data = await call('GET', orgId, undefined);
|
|
75
75
|
const policy = data.policy ?? {};
|
|
76
76
|
const isDefault = data.isDefault === true || policy.isDefault === true;
|
|
77
|
-
if (!isDefault && policy.allowRegister
|
|
77
|
+
if (!isDefault && policy.allowRegister !== true) {
|
|
78
78
|
return { changed: false };
|
|
79
79
|
}
|
|
80
80
|
const settings = {};
|
|
@@ -36,13 +36,32 @@ export declare class CreateInstanceError extends Error {
|
|
|
36
36
|
constructor(kind: 'rejected' | 'exists' | 'uncertain', message: string);
|
|
37
37
|
}
|
|
38
38
|
/** What a person does when an instance exists without a delivered bearer. Nothing is deleted automatically. */
|
|
39
|
-
export declare function instanceWithoutBearerMessage(hostname: string, detail: string): string;
|
|
39
|
+
export declare function instanceWithoutBearerMessage(hostname: string, detail: string, label?: string): string;
|
|
40
40
|
export interface CreateInstanceRuntime {
|
|
41
41
|
fetch?: typeof fetch;
|
|
42
42
|
now: () => number;
|
|
43
43
|
/** Fetch timeout for the one create call (default 30s). */
|
|
44
44
|
timeoutMs?: number;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Waits until ZITADEL's system API answers, before the one create call. `/debug/healthz` and `/debug/ready`
|
|
48
|
+
* answer as soon as the HTTP server listens, but the gateway behind the system API reaches its gRPC server a
|
|
49
|
+
* moment later and answers 503 until then (seen: `POST /instances/_create` 503 about 100 ms after "server is
|
|
50
|
+
* listening"). A 503 on the create leaves the instance marked as possibly created, so the wait asks first with a
|
|
51
|
+
* read that changes nothing: `POST /system/v1/instances/_search`. Any answer other than a failed request or a
|
|
52
|
+
* 5xx counts (a 401 is the key being refused, which the create reports itself). Times out with an error that
|
|
53
|
+
* names the last answer.
|
|
54
|
+
*/
|
|
55
|
+
export declare function waitForSystemApi(input: {
|
|
56
|
+
customDomain: string;
|
|
57
|
+
privateKey: string;
|
|
58
|
+
label?: string;
|
|
59
|
+
}, runtime: CreateInstanceRuntime & {
|
|
60
|
+
sleep: (ms: number) => Promise<void>;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
intervalMs?: number;
|
|
63
|
+
requestTimeoutMs?: number;
|
|
64
|
+
}): Promise<void>;
|
|
46
65
|
/**
|
|
47
66
|
* Creates the platform's ZITADEL instance on `customDomain` with one machine
|
|
48
67
|
* owner and returns its id and the owner's PAT. Sends exactly one request,
|
|
@@ -54,6 +73,8 @@ export declare function createZitadelInstance(input: {
|
|
|
54
73
|
privateKey: string;
|
|
55
74
|
instanceName: string;
|
|
56
75
|
firstOrgName: string;
|
|
76
|
+
/** Names the target in error texts (default `platform:auth`). */
|
|
77
|
+
label?: string;
|
|
57
78
|
owner: {
|
|
58
79
|
userName: string;
|
|
59
80
|
name: string;
|
|
@@ -61,8 +61,48 @@ export class CreateInstanceError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
/** What a person does when an instance exists without a delivered bearer. Nothing is deleted automatically. */
|
|
64
|
-
export function instanceWithoutBearerMessage(hostname, detail) {
|
|
65
|
-
return
|
|
64
|
+
export function instanceWithoutBearerMessage(hostname, detail, label = 'platform:auth') {
|
|
65
|
+
return `${label}: a ZITADEL instance may already exist for ${hostname} without a delivered bearer (${detail}). To recover, delete the instance with the system API (DELETE /system/v1/instances/{id}) or redeploy the ZITADEL database, remove the recorded key zitadel_platform_instance_undelivered, then apply again`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Waits until ZITADEL's system API answers, before the one create call. `/debug/healthz` and `/debug/ready`
|
|
69
|
+
* answer as soon as the HTTP server listens, but the gateway behind the system API reaches its gRPC server a
|
|
70
|
+
* moment later and answers 503 until then (seen: `POST /instances/_create` 503 about 100 ms after "server is
|
|
71
|
+
* listening"). A 503 on the create leaves the instance marked as possibly created, so the wait asks first with a
|
|
72
|
+
* read that changes nothing: `POST /system/v1/instances/_search`. Any answer other than a failed request or a
|
|
73
|
+
* 5xx counts (a 401 is the key being refused, which the create reports itself). Times out with an error that
|
|
74
|
+
* names the last answer.
|
|
75
|
+
*/
|
|
76
|
+
export async function waitForSystemApi(input, runtime) {
|
|
77
|
+
const fetchImpl = runtime.fetch ?? globalThis.fetch;
|
|
78
|
+
const url = `https://${input.customDomain}`;
|
|
79
|
+
const timeout = runtime.timeoutMs ?? 300_000;
|
|
80
|
+
const interval = runtime.intervalMs ?? 2_000;
|
|
81
|
+
const deadline = runtime.now() + timeout;
|
|
82
|
+
let last = 'no answer';
|
|
83
|
+
for (;;) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetchImpl(`${url}/system/v1/instances/_search`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
signal: AbortSignal.timeout(runtime.requestTimeoutMs ?? 10_000),
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `Bearer ${signSystemApiJwt(input.privateKey, url, runtime.now())}`,
|
|
90
|
+
'content-type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: '{}',
|
|
93
|
+
});
|
|
94
|
+
if (response.status < 500)
|
|
95
|
+
return;
|
|
96
|
+
last = `HTTP ${response.status}`;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
last = 'the request failed';
|
|
100
|
+
}
|
|
101
|
+
if (runtime.now() >= deadline) {
|
|
102
|
+
throw new Error(`${input.label ?? 'platform:auth'}: ZITADEL's system API did not answer at ${url} within ${Math.round(timeout / 1000)}s (${last}); nothing was created, retry this apply`);
|
|
103
|
+
}
|
|
104
|
+
await runtime.sleep(interval);
|
|
105
|
+
}
|
|
66
106
|
}
|
|
67
107
|
/**
|
|
68
108
|
* Creates the platform's ZITADEL instance on `customDomain` with one machine
|
|
@@ -73,6 +113,7 @@ export function instanceWithoutBearerMessage(hostname, detail) {
|
|
|
73
113
|
export async function createZitadelInstance(input, runtime) {
|
|
74
114
|
const fetchImpl = runtime.fetch ?? globalThis.fetch;
|
|
75
115
|
const url = `https://${input.customDomain}`;
|
|
116
|
+
const label = input.label ?? 'platform:auth';
|
|
76
117
|
let response;
|
|
77
118
|
try {
|
|
78
119
|
response = await fetchImpl(`${url}/system/v1/instances/_create`, {
|
|
@@ -97,19 +138,19 @@ export async function createZitadelInstance(input, runtime) {
|
|
|
97
138
|
});
|
|
98
139
|
}
|
|
99
140
|
catch {
|
|
100
|
-
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the create request timed out or failed'));
|
|
141
|
+
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the create request timed out or failed', label));
|
|
101
142
|
}
|
|
102
143
|
if (response.status === 401) {
|
|
103
|
-
throw new CreateInstanceError('rejected',
|
|
144
|
+
throw new CreateInstanceError('rejected', `${label}: system API key rejected (HTTP 401); nothing was created`);
|
|
104
145
|
}
|
|
105
146
|
if (response.status === 409) {
|
|
106
|
-
throw new CreateInstanceError('exists', instanceWithoutBearerMessage(input.customDomain, 'ZITADEL answered 409, already exists'));
|
|
147
|
+
throw new CreateInstanceError('exists', instanceWithoutBearerMessage(input.customDomain, 'ZITADEL answered 409, already exists', label));
|
|
107
148
|
}
|
|
108
149
|
if (!response.ok) {
|
|
109
150
|
// A 4xx is a refusal before anything was created; a 5xx may not be.
|
|
110
151
|
throw new CreateInstanceError(response.status < 500 ? 'rejected' : 'uncertain', response.status < 500
|
|
111
|
-
?
|
|
112
|
-
: instanceWithoutBearerMessage(input.customDomain, `ZITADEL answered HTTP ${response.status}
|
|
152
|
+
? `${label}: ZITADEL refused to create the instance (HTTP ${response.status}); nothing was created`
|
|
153
|
+
: instanceWithoutBearerMessage(input.customDomain, `ZITADEL answered HTTP ${response.status}`, label));
|
|
113
154
|
}
|
|
114
155
|
let body = {};
|
|
115
156
|
try {
|
|
@@ -119,7 +160,7 @@ export async function createZitadelInstance(input, runtime) {
|
|
|
119
160
|
// Handled below.
|
|
120
161
|
}
|
|
121
162
|
if (!body.instanceId || !body.pat) {
|
|
122
|
-
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the answer had no instance id or token'));
|
|
163
|
+
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the answer had no instance id or token', label));
|
|
123
164
|
}
|
|
124
165
|
return { instanceId: body.instanceId, pat: body.pat };
|
|
125
166
|
}
|
package/dist/plan.d.ts
CHANGED
|
@@ -62,6 +62,24 @@ export interface Plan {
|
|
|
62
62
|
* this type with none.
|
|
63
63
|
*/
|
|
64
64
|
versionChanges?: VersionChangeItem[];
|
|
65
|
+
/**
|
|
66
|
+
* Things a person should see that block no step: an optional credential not
|
|
67
|
+
* held yet. Never in `unmanaged`, so a plan with no steps and no blocked
|
|
68
|
+
* item still reads as settled. Optional: a consumer that does not know it
|
|
69
|
+
* loses only the notes.
|
|
70
|
+
*/
|
|
71
|
+
notes?: PlanNote[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One advisory line of a {@link Plan}. It never stops a step and is never a
|
|
75
|
+
* {@link PlanStep}. It names a credential, never a value.
|
|
76
|
+
*/
|
|
77
|
+
export interface PlanNote {
|
|
78
|
+
/** Stable, e.g. `uptime:CHAT_ACCESS_TOKEN`. */
|
|
79
|
+
key: string;
|
|
80
|
+
/** The service or step this is about, e.g. `uptime`. */
|
|
81
|
+
service: string;
|
|
82
|
+
description: string;
|
|
65
83
|
}
|
|
66
84
|
/**
|
|
67
85
|
* The action kind a {@link VersionChangeItem} carries -- a distinct, typed
|
package/dist/platform.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface PlatformSfuService extends PlatformService {
|
|
|
31
31
|
*/
|
|
32
32
|
export interface PlatformMailService extends PlatformService {
|
|
33
33
|
lettermintProjectId?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* true: one shared relay container owns host port 25 and routes each
|
|
36
|
+
* message by recipient domain to a company's own mail server or to the
|
|
37
|
+
* platform's; the platform Stalwart then stops publishing port 25.
|
|
38
|
+
* false or unset (the default): the platform Stalwart binds host port 25
|
|
39
|
+
* directly, as it always has.
|
|
40
|
+
*/
|
|
41
|
+
relay?: boolean;
|
|
34
42
|
}
|
|
35
43
|
/**
|
|
36
44
|
* The uptime Worker (foundry#542): the outside prober Archon publishes as a
|
package/dist/slug.js
CHANGED
package/dist/store/postgres.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { and, asc, desc, eq, sql } from 'drizzle-orm';
|
|
2
|
-
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateCalls, validateDomain, validateHosting, validateMail, validateOwners, validateSite, } from '../org.js';
|
|
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, validateHosting, validateMail, validateOwners, validateServices, validateSite, } from '../org.js';
|
|
3
3
|
import { lockedHostnames } from '../plan/locked-hostnames.js';
|
|
4
|
+
import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
|
|
4
5
|
import { validatePlatform } from '../platform.js';
|
|
5
6
|
import { RequestedChangeError, classifyChange } from '../requested-changes.js';
|
|
6
7
|
import { validateSlug } from '../slug.js';
|
|
@@ -29,6 +30,7 @@ async function writeFenced(db, fence, write) {
|
|
|
29
30
|
/** `migrations/0004_slug_unique.sql` / `migrations/0015_domain_unique.sql`'s index names, read back off a caught unique-violation to tell which one fired. */
|
|
30
31
|
const SLUG_UNIQUE_INDEX = 'foundry_declared_orgs_slug_idx';
|
|
31
32
|
const DOMAIN_UNIQUE_INDEX = 'foundry_declared_orgs_domain_idx';
|
|
33
|
+
const SFU_SLOT_UNIQUE_INDEX = 'foundry_declared_orgs_sfu_slot_idx';
|
|
32
34
|
/** #27: a bigint-safe decimal string of a non-negative integer -- no sign, no decimal point, no leading zeros (so "010" never diffs against a live "10"). */
|
|
33
35
|
const NON_NEGATIVE_INTEGER = /^(0|[1-9]\d*)$/;
|
|
34
36
|
/**
|
|
@@ -135,6 +137,15 @@ function rowToOrg(row) {
|
|
|
135
137
|
: {}),
|
|
136
138
|
},
|
|
137
139
|
issuer: { usesDomain: row.issuerUsesDomain },
|
|
140
|
+
// Omitted while shared, as `calls.versionPin` is.
|
|
141
|
+
...(row.servicesOwn || row.servicesSfuSlot !== null
|
|
142
|
+
? {
|
|
143
|
+
services: {
|
|
144
|
+
own: row.servicesOwn,
|
|
145
|
+
...(row.servicesSfuSlot !== null ? { sfuSlot: row.servicesSfuSlot } : {}),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
138
149
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
139
150
|
hosting: {
|
|
140
151
|
enabled: row.hostingEnabled,
|
|
@@ -177,6 +188,8 @@ function orgToRow(org) {
|
|
|
177
188
|
callsVersionPinSynapse: org.calls.versionPin?.synapse ?? null,
|
|
178
189
|
callsVersionPinMas: org.calls.versionPin?.mas ?? null,
|
|
179
190
|
issuerUsesDomain: org.issuer.usesDomain,
|
|
191
|
+
servicesOwn: org.services?.own ?? false,
|
|
192
|
+
servicesSfuSlot: org.services?.sfuSlot ?? null,
|
|
180
193
|
siteEnabled: org.site.enabled,
|
|
181
194
|
siteUsesDomain: org.site.usesDomain,
|
|
182
195
|
hostingEnabled: org.hosting.enabled,
|
|
@@ -345,6 +358,30 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
345
358
|
if (reason)
|
|
346
359
|
throw new DeclaredOrgError(`calls invalid: ${reason} (#259)`);
|
|
347
360
|
}
|
|
361
|
+
// Own-service rules, on `create` and on the merged result of `update`; no
|
|
362
|
+
// two orgs share an `sfuSlot` (the unique index catches the race this misses).
|
|
363
|
+
async function requireValidServices(org) {
|
|
364
|
+
const reason = validateServices(org);
|
|
365
|
+
if (reason)
|
|
366
|
+
throw new DeclaredOrgError(`services invalid: ${reason}`);
|
|
367
|
+
const slot = org.services?.sfuSlot ?? null;
|
|
368
|
+
if (slot === null)
|
|
369
|
+
return;
|
|
370
|
+
const [taken] = await db
|
|
371
|
+
.select({ id: declaredOrgs.id })
|
|
372
|
+
.from(declaredOrgs)
|
|
373
|
+
.where(and(eq(declaredOrgs.servicesSfuSlot, slot), ne(declaredOrgs.id, org.id)))
|
|
374
|
+
.limit(1);
|
|
375
|
+
if (taken)
|
|
376
|
+
throw new DeclaredOrgError(`services invalid: sfuSlot ${slot} is already taken by another org`);
|
|
377
|
+
}
|
|
378
|
+
// No name foundry derives for this org may be one another org or the platform derives.
|
|
379
|
+
async function requireNoNameClash(org) {
|
|
380
|
+
const rows = await db.select().from(declaredOrgs).where(ne(declaredOrgs.id, org.id));
|
|
381
|
+
const [clash] = findNameClashes(org, rows.map(rowToOrg));
|
|
382
|
+
if (clash)
|
|
383
|
+
throw new DeclaredOrgError(nameClashMessage(org.slug, clash));
|
|
384
|
+
}
|
|
348
385
|
// `as`: `update` is overloaded in the interface (#351), which one object-literal method cannot express.
|
|
349
386
|
return {
|
|
350
387
|
async create(org) {
|
|
@@ -357,6 +394,8 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
357
394
|
requireValidOwner(org);
|
|
358
395
|
requireValidMail(org);
|
|
359
396
|
requireValidCalls(org);
|
|
397
|
+
await requireValidServices(org);
|
|
398
|
+
await requireNoNameClash(org);
|
|
360
399
|
try {
|
|
361
400
|
await db.insert(declaredOrgs).values(orgToRow(org));
|
|
362
401
|
}
|
|
@@ -371,6 +410,9 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
371
410
|
if (constraint === DOMAIN_UNIQUE_INDEX) {
|
|
372
411
|
throw new DeclaredOrgError(`domain '${org.domain}' is already taken by another org (#131)`);
|
|
373
412
|
}
|
|
413
|
+
if (constraint === SFU_SLOT_UNIQUE_INDEX) {
|
|
414
|
+
throw new DeclaredOrgError(`services invalid: sfuSlot ${org.services?.sfuSlot} is already taken by another org`);
|
|
415
|
+
}
|
|
374
416
|
if (constraint === SLUG_UNIQUE_INDEX || constraint !== undefined) {
|
|
375
417
|
throw new DeclaredOrgError(`slug '${org.slug}' is already taken by another org`);
|
|
376
418
|
}
|
|
@@ -461,6 +503,8 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
461
503
|
const wasTier2 = current.domain !== null && current.mail.mode === 'received';
|
|
462
504
|
requireValidMail(next, { allowExistingTier2: wasTier2 });
|
|
463
505
|
requireValidCalls(next);
|
|
506
|
+
await requireValidServices(next);
|
|
507
|
+
await requireNoNameClash(next);
|
|
464
508
|
try {
|
|
465
509
|
// #351: with `expectedVersion` the version check is part of this one
|
|
466
510
|
// statement, so an edit that lands after the read above makes it match
|
|
@@ -489,6 +533,9 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
489
533
|
if (constraint === DOMAIN_UNIQUE_INDEX) {
|
|
490
534
|
throw new DeclaredOrgError(`domain '${next.domain}' is already taken by another org (#131)`);
|
|
491
535
|
}
|
|
536
|
+
if (constraint === SFU_SLOT_UNIQUE_INDEX) {
|
|
537
|
+
throw new DeclaredOrgError(`services invalid: sfuSlot ${next.services?.sfuSlot} is already taken by another org`);
|
|
538
|
+
}
|
|
492
539
|
if (constraint === SLUG_UNIQUE_INDEX || constraint !== undefined) {
|
|
493
540
|
throw new DeclaredOrgError(`slug '${next.slug}' is already taken by another org`);
|
|
494
541
|
}
|
|
@@ -605,6 +652,7 @@ function rowToPlatform(row) {
|
|
|
605
652
|
hostname: row.mailHostname,
|
|
606
653
|
gitRef: row.mailGitRef,
|
|
607
654
|
...(row.mailLettermintProjectId ? { lettermintProjectId: row.mailLettermintProjectId } : {}),
|
|
655
|
+
...(row.mailRelay ? { relay: true } : {}),
|
|
608
656
|
},
|
|
609
657
|
sfu: {
|
|
610
658
|
enabled: row.sfuEnabled,
|
|
@@ -656,6 +704,7 @@ function platformToRow(platform) {
|
|
|
656
704
|
mailHostname: platform.mail.hostname,
|
|
657
705
|
mailGitRef: platform.mail.gitRef,
|
|
658
706
|
mailLettermintProjectId: platform.mail.lettermintProjectId ?? null,
|
|
707
|
+
mailRelay: platform.mail.relay === true,
|
|
659
708
|
sfuEnabled: platform.sfu.enabled,
|
|
660
709
|
sfuHostname: platform.sfu.hostname,
|
|
661
710
|
sfuGitRef: platform.sfu.gitRef,
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -321,6 +321,40 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
321
321
|
identity: undefined;
|
|
322
322
|
generated: undefined;
|
|
323
323
|
}, {}, {}>;
|
|
324
|
+
servicesOwn: import("drizzle-orm/pg-core").PgColumn<{
|
|
325
|
+
name: "services_own";
|
|
326
|
+
tableName: "foundry_declared_orgs";
|
|
327
|
+
dataType: "boolean";
|
|
328
|
+
columnType: "PgBoolean";
|
|
329
|
+
data: boolean;
|
|
330
|
+
driverParam: boolean;
|
|
331
|
+
notNull: true;
|
|
332
|
+
hasDefault: true;
|
|
333
|
+
isPrimaryKey: false;
|
|
334
|
+
isAutoincrement: false;
|
|
335
|
+
hasRuntimeDefault: false;
|
|
336
|
+
enumValues: undefined;
|
|
337
|
+
baseColumn: never;
|
|
338
|
+
identity: undefined;
|
|
339
|
+
generated: undefined;
|
|
340
|
+
}, {}, {}>;
|
|
341
|
+
servicesSfuSlot: import("drizzle-orm/pg-core").PgColumn<{
|
|
342
|
+
name: "services_sfu_slot";
|
|
343
|
+
tableName: "foundry_declared_orgs";
|
|
344
|
+
dataType: "number";
|
|
345
|
+
columnType: "PgInteger";
|
|
346
|
+
data: number;
|
|
347
|
+
driverParam: string | number;
|
|
348
|
+
notNull: false;
|
|
349
|
+
hasDefault: false;
|
|
350
|
+
isPrimaryKey: false;
|
|
351
|
+
isAutoincrement: false;
|
|
352
|
+
hasRuntimeDefault: false;
|
|
353
|
+
enumValues: undefined;
|
|
354
|
+
baseColumn: never;
|
|
355
|
+
identity: undefined;
|
|
356
|
+
generated: undefined;
|
|
357
|
+
}, {}, {}>;
|
|
324
358
|
siteEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
325
359
|
name: "site_enabled";
|
|
326
360
|
tableName: "foundry_declared_orgs";
|
|
@@ -1592,6 +1626,23 @@ export declare const foundryDeclaredPlatform: import("drizzle-orm/pg-core").PgTa
|
|
|
1592
1626
|
identity: undefined;
|
|
1593
1627
|
generated: undefined;
|
|
1594
1628
|
}, {}, {}>;
|
|
1629
|
+
mailRelay: import("drizzle-orm/pg-core").PgColumn<{
|
|
1630
|
+
name: "mail_relay";
|
|
1631
|
+
tableName: "foundry_declared_platform";
|
|
1632
|
+
dataType: "boolean";
|
|
1633
|
+
columnType: "PgBoolean";
|
|
1634
|
+
data: boolean;
|
|
1635
|
+
driverParam: boolean;
|
|
1636
|
+
notNull: true;
|
|
1637
|
+
hasDefault: true;
|
|
1638
|
+
isPrimaryKey: false;
|
|
1639
|
+
isAutoincrement: false;
|
|
1640
|
+
hasRuntimeDefault: false;
|
|
1641
|
+
enumValues: undefined;
|
|
1642
|
+
baseColumn: never;
|
|
1643
|
+
identity: undefined;
|
|
1644
|
+
generated: undefined;
|
|
1645
|
+
}, {}, {}>;
|
|
1595
1646
|
sfuEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
1596
1647
|
name: "sfu_enabled";
|
|
1597
1648
|
tableName: "foundry_declared_platform";
|
package/dist/store/schema.js
CHANGED
|
@@ -35,6 +35,9 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
35
35
|
callsVersionPinSynapse: text('calls_version_pin_synapse'),
|
|
36
36
|
callsVersionPinMas: text('calls_version_pin_mas'),
|
|
37
37
|
issuerUsesDomain: boolean('issuer_uses_domain').notNull().default(false),
|
|
38
|
+
// ServicesConfig (org.ts): false and null are the platform's shared services.
|
|
39
|
+
servicesOwn: boolean('services_own').notNull().default(false),
|
|
40
|
+
servicesSfuSlot: integer('services_sfu_slot'),
|
|
38
41
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|
|
39
42
|
siteEnabled: boolean('site_enabled').notNull().default(false),
|
|
40
43
|
siteUsesDomain: boolean('site_uses_domain').notNull().default(false),
|
|
@@ -162,6 +165,8 @@ export const foundryDeclaredPlatform = pgTable('foundry_declared_platform', {
|
|
|
162
165
|
mailGitRef: text('mail_git_ref').notNull().default(''),
|
|
163
166
|
/** #491, `migrations/0044_platform_mail_lettermint_project.sql`. Declared, never discovered. */
|
|
164
167
|
mailLettermintProjectId: text('mail_lettermint_project_id'),
|
|
168
|
+
/** `migrations/0052_platform_mail_relay.sql`. */
|
|
169
|
+
mailRelay: boolean('mail_relay').notNull().default(false),
|
|
165
170
|
sfuEnabled: boolean('sfu_enabled').notNull().default(false),
|
|
166
171
|
sfuHostname: text('sfu_hostname').notNull(),
|
|
167
172
|
sfuGitRef: text('sfu_git_ref').notNull().default(''),
|