@wtfalch/foundry 0.16.15 → 0.17.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/apply.d.ts +22 -1
- package/dist/bootstrap/bootstrap.d.ts +6 -1
- package/dist/bootstrap/bootstrap.js +26 -3
- package/dist/bootstrap/declare.d.ts +11 -1
- package/dist/bootstrap/declare.js +12 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +7 -0
- package/dist/migrations/0038_calls_version_pin.sql +8 -0
- package/dist/migrations/0039_destroy_record.sql +7 -0
- package/dist/migrations/0040_company_included_uuid.sql +72 -0
- package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
- package/dist/migrations/0042_mutation_leases.sql +12 -0
- package/dist/org.d.ts +82 -5
- package/dist/org.js +62 -1
- package/dist/plan/adopt.d.ts +76 -0
- package/dist/plan/adopt.js +184 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +37 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +13 -5
- package/dist/plan/chat-matrix.d.ts +1 -1
- package/dist/plan/chat-matrix.js +11 -3
- package/dist/plan/core-files.d.ts +25 -0
- package/dist/plan/core-files.js +25 -0
- package/dist/plan/deploy-health.d.ts +136 -0
- package/dist/plan/deploy-health.js +153 -0
- package/dist/plan/diff.d.ts +11 -3
- package/dist/plan/diff.js +77 -71
- package/dist/plan/entry-ai-key.d.ts +3 -3
- package/dist/plan/entry-files-key.d.ts +6 -4
- package/dist/plan/entry-files-key.js +7 -10
- package/dist/plan/executor.d.ts +109 -28
- package/dist/plan/executor.js +213 -56
- package/dist/plan/hostnames.d.ts +6 -0
- package/dist/plan/hostnames.js +13 -0
- package/dist/plan/index.d.ts +5 -1
- package/dist/plan/index.js +3 -1
- package/dist/plan/live.d.ts +5 -0
- package/dist/plan/login-host.d.ts +1 -1
- package/dist/plan/login-host.js +6 -5
- package/dist/plan/plan-apply.d.ts +153 -3
- package/dist/plan/plan-apply.js +584 -69
- package/dist/plan/platform-diff.d.ts +103 -9
- package/dist/plan/platform-diff.js +192 -14
- package/dist/plan/platform-org.d.ts +25 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +38 -5
- package/dist/plan/platform-providers.js +592 -109
- package/dist/plan/providers-readers.d.ts +2 -0
- package/dist/plan/providers-readers.js +4 -0
- package/dist/plan/rollout.d.ts +52 -0
- package/dist/plan/rollout.js +37 -0
- package/dist/plan/step-system.d.ts +22 -0
- package/dist/plan/step-system.js +37 -0
- package/dist/plan.d.ts +3 -0
- package/dist/state.d.ts +13 -0
- package/dist/store/audit.js +15 -1
- package/dist/store/postgres.d.ts +1 -1
- package/dist/store/postgres.js +127 -14
- package/dist/store/schema.d.ts +134 -5
- package/dist/store/schema.js +12 -1
- package/dist/store.d.ts +47 -4
- package/dist/store.js +9 -1
- package/package.json +2 -2
package/dist/apply.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { OrgId } from './org.js';
|
|
2
2
|
import type { Plan } from './plan.js';
|
|
3
3
|
import type { AppliedState } from './state.js';
|
|
4
|
+
/** One destroy step starting, finishing or failing; `index` counts from 1 up to `total`. */
|
|
5
|
+
export interface DestroyProgress {
|
|
6
|
+
step: string;
|
|
7
|
+
index: number;
|
|
8
|
+
total: number;
|
|
9
|
+
status: 'started' | 'finished' | 'failed';
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* The plan/apply seam https://github.com/wtfalch/foundry/issues/19
|
|
6
14
|
* implements.
|
|
@@ -37,5 +45,18 @@ export interface PlanApply {
|
|
|
37
45
|
* declared org in place with whatever remains recorded, so a retried
|
|
38
46
|
* `destroy` only tears down what's left.
|
|
39
47
|
*/
|
|
40
|
-
destroy(id: OrgId, confirmedOrgName: string
|
|
48
|
+
destroy(id: OrgId, confirmedOrgName: string, options?: {
|
|
49
|
+
onProgress?: (event: DestroyProgress) => void;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* The steps `destroy` will run, in order, for a test org (same refusal as
|
|
53
|
+
* `destroy`: any other org throws). Read-only. A stopped destroy's
|
|
54
|
+
* finished and failed steps are `AppliedState.destroy`. `steps` are exactly
|
|
55
|
+
* what `destroy` runs, in order; `kept` lists the adopted resources it
|
|
56
|
+
* leaves alone (archon#271). Refuses when an adopted record cannot be read.
|
|
57
|
+
*/
|
|
58
|
+
planDestroy(id: OrgId): Promise<{
|
|
59
|
+
steps: string[];
|
|
60
|
+
kept: string[];
|
|
61
|
+
}>;
|
|
41
62
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Plan } from '../plan.js';
|
|
2
|
+
import type { PlatformLiveReaders, PlatformStepExecutor, StepSystem } from '../plan/index.js';
|
|
2
3
|
import type { FoundryPlatformStepExecutorOptions } from '../plan/platform-providers.js';
|
|
3
4
|
import type { FoundryStoreDb } from '../store/postgres.js';
|
|
4
5
|
import type { BootstrapCompany, BootstrapOwner } from './declare.js';
|
|
@@ -29,6 +30,8 @@ export interface RootCredentials {
|
|
|
29
30
|
/** What Archon's onboarding screen shows: one event per phase or round. */
|
|
30
31
|
export interface BootstrapProgress {
|
|
31
32
|
phase: 'declare' | 'plan' | 'apply' | 'done';
|
|
33
|
+
/** The system the event touches: the steps' own when a round's steps share one, `postgres` for a declare, `foundry` otherwise. */
|
|
34
|
+
system: StepSystem;
|
|
32
35
|
round?: number;
|
|
33
36
|
message: string;
|
|
34
37
|
steps?: string[];
|
|
@@ -100,6 +103,8 @@ export declare class Redactor {
|
|
|
100
103
|
addDeep(value: unknown, depth?: number, seen?: Set<unknown>): void;
|
|
101
104
|
text(input: string): string;
|
|
102
105
|
}
|
|
106
|
+
/** The one system every step of `plan` touches, or `foundry` when they differ or there are none. */
|
|
107
|
+
export declare function roundSystem(plan: Plan): StepSystem;
|
|
103
108
|
/** Wraps the secret-carrying ports inside one step's deps so what they deliver or resolve is redacted. */
|
|
104
109
|
export declare function guardStepDeps(executor: BootstrapExecutorDeps | undefined, redactor: Redactor, deliverSecret: (ref: string, secret: string) => Promise<void>, heldKeys: BootstrapPlatformDeps['heldKeys']): BootstrapExecutorDeps;
|
|
105
110
|
/**
|
|
@@ -61,6 +61,12 @@ const unusedOrgExecutor = {
|
|
|
61
61
|
throw new Error('bootstrapPlatform never destroys an org');
|
|
62
62
|
},
|
|
63
63
|
};
|
|
64
|
+
/** The one system every step of `plan` touches, or `foundry` when they differ or there are none. */
|
|
65
|
+
export function roundSystem(plan) {
|
|
66
|
+
const systems = new Set(plan.steps.map((s) => s.system));
|
|
67
|
+
const [only] = systems;
|
|
68
|
+
return systems.size === 1 && only ? only : 'foundry';
|
|
69
|
+
}
|
|
64
70
|
const unusedOrgReader = { listResources: async () => [] };
|
|
65
71
|
/** The audit log with every free-text field run through the redactor before it is stored. */
|
|
66
72
|
function redactingAudit(inner, redactor) {
|
|
@@ -76,6 +82,10 @@ function redactingAudit(inner, redactor) {
|
|
|
76
82
|
plan: plan(entry.plan),
|
|
77
83
|
...(entry.reason === undefined ? {} : { reason: redactor.text(entry.reason) }),
|
|
78
84
|
}),
|
|
85
|
+
recordAdoption: (entry) => inner.recordAdoption({
|
|
86
|
+
...entry,
|
|
87
|
+
...(entry.reason === undefined ? {} : { reason: redactor.text(entry.reason) }),
|
|
88
|
+
}),
|
|
79
89
|
recordDestroy: (entry) => inner.recordDestroy({
|
|
80
90
|
...entry,
|
|
81
91
|
...(entry.reason === undefined ? {} : { reason: redactor.text(entry.reason) }),
|
|
@@ -189,20 +199,29 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
189
199
|
const orgStore = createPostgresDeclaredOrgStore(deps.db);
|
|
190
200
|
const platformStore = createPostgresPlatformStore(deps.db);
|
|
191
201
|
if (await orgStore.get(COMPANY_INCLUDED_ORG_ID)) {
|
|
192
|
-
emit({ phase: 'declare', message: 'declared org already written' });
|
|
202
|
+
emit({ phase: 'declare', system: 'postgres', message: 'declared org already written' });
|
|
193
203
|
}
|
|
194
204
|
else {
|
|
195
205
|
await orgStore.create(declaredCompanyOrg(input.company, input.owner));
|
|
196
|
-
emit({
|
|
206
|
+
emit({
|
|
207
|
+
phase: 'declare',
|
|
208
|
+
system: 'postgres',
|
|
209
|
+
message: `declared org "${input.company.name}" written`,
|
|
210
|
+
});
|
|
197
211
|
}
|
|
198
212
|
const existing = await platformStore.get();
|
|
199
213
|
if (existing) {
|
|
200
|
-
emit({
|
|
214
|
+
emit({
|
|
215
|
+
phase: 'declare',
|
|
216
|
+
system: 'postgres',
|
|
217
|
+
message: `declared platform already written for ${existing.apex}`,
|
|
218
|
+
});
|
|
201
219
|
// Only serverIpv4, through the validating `put`; no other field is touched.
|
|
202
220
|
if (input.serverIpv4 && input.serverIpv4 !== existing.serverIpv4) {
|
|
203
221
|
await platformStore.put({ ...existing, serverIpv4: input.serverIpv4 });
|
|
204
222
|
emit({
|
|
205
223
|
phase: 'declare',
|
|
224
|
+
system: 'postgres',
|
|
206
225
|
message: `declared platform serverIpv4 set to ${input.serverIpv4}`,
|
|
207
226
|
});
|
|
208
227
|
}
|
|
@@ -211,6 +230,7 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
211
230
|
await platformStore.put(declaredPlatformFor(input.company.apex, input.serverIpv4));
|
|
212
231
|
emit({
|
|
213
232
|
phase: 'declare',
|
|
233
|
+
system: 'postgres',
|
|
214
234
|
message: `declared platform written for ${input.company.apex}`,
|
|
215
235
|
});
|
|
216
236
|
}
|
|
@@ -278,12 +298,14 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
278
298
|
maxRounds: deps.maxRounds,
|
|
279
299
|
onPlan: (round, plan) => emit({
|
|
280
300
|
phase: 'plan',
|
|
301
|
+
system: roundSystem(plan),
|
|
281
302
|
round,
|
|
282
303
|
message: `round ${round}: ${plan.steps.length} step(s) planned`,
|
|
283
304
|
steps: plan.steps.map((s) => s.description),
|
|
284
305
|
}),
|
|
285
306
|
onApplied: (round, plan) => emit({
|
|
286
307
|
phase: 'apply',
|
|
308
|
+
system: roundSystem(plan),
|
|
287
309
|
round,
|
|
288
310
|
message: `round ${round}: applied`,
|
|
289
311
|
steps: plan.steps.map((s) => s.description),
|
|
@@ -310,6 +332,7 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
310
332
|
};
|
|
311
333
|
emit({
|
|
312
334
|
phase: 'done',
|
|
335
|
+
system: 'foundry',
|
|
313
336
|
message: driven.failed
|
|
314
337
|
? `stopped: ${result.failed?.message}`
|
|
315
338
|
: `${result.applied.length} step(s) applied, ${blocked.length} blocked`,
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { DeclaredOrg, OrgId, OwnerIdentity } from '../org.js';
|
|
2
2
|
import type { DeclaredPlatform } from '../platform.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Company Included's own org id, the same literal `test/fakes.ts`'s
|
|
5
|
+
* `basePlatform()` uses for `ownerOrgId`. Must be a uuid: Archon's company
|
|
6
|
+
* pages resolve a declared org only by uuid (archon issue #40,
|
|
7
|
+
* `src/lib/companies/index.ts`'s `getCompany`/`isUuid`), so a non-uuid id
|
|
8
|
+
* 404s every one of them, Company Included's included (foundry#435).
|
|
9
|
+
* `migrations/0040_company_included_uuid.sql` rewrites the live row and
|
|
10
|
+
* every column that references it from the old literal
|
|
11
|
+
* `org_company_included` to this value; this constant and that migration's
|
|
12
|
+
* literal must agree.
|
|
13
|
+
*/
|
|
4
14
|
export declare const COMPANY_INCLUDED_ORG_ID: OrgId;
|
|
5
15
|
export declare const DEFAULT_GIT_REF = "main";
|
|
6
16
|
/**
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { validateOwners } from '../org.js';
|
|
2
2
|
import { PLATFORM_SERVICE_KEYS, defaultPlatformHostname } from '../platform.js';
|
|
3
3
|
import { VERSIONS } from './versions.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Company Included's own org id, the same literal `test/fakes.ts`'s
|
|
6
|
+
* `basePlatform()` uses for `ownerOrgId`. Must be a uuid: Archon's company
|
|
7
|
+
* pages resolve a declared org only by uuid (archon issue #40,
|
|
8
|
+
* `src/lib/companies/index.ts`'s `getCompany`/`isUuid`), so a non-uuid id
|
|
9
|
+
* 404s every one of them, Company Included's included (foundry#435).
|
|
10
|
+
* `migrations/0040_company_included_uuid.sql` rewrites the live row and
|
|
11
|
+
* every column that references it from the old literal
|
|
12
|
+
* `org_company_included` to this value; this constant and that migration's
|
|
13
|
+
* literal must agree.
|
|
14
|
+
*/
|
|
15
|
+
export const COMPANY_INCLUDED_ORG_ID = 'e4d20889-a63d-42b6-b670-2bdd4472449b';
|
|
6
16
|
export const DEFAULT_GIT_REF = 'main';
|
|
7
17
|
/**
|
|
8
18
|
* The Stalwart image tag the platform's mail step runs. `platform.mail.gitRef`
|
package/dist/index.d.ts
CHANGED
|
@@ -2,19 +2,24 @@ export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig
|
|
|
2
2
|
export { APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
3
3
|
export type { DeclaredPlatform, PlatformService, PlatformServiceKey } from './platform.js';
|
|
4
4
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
5
|
+
import { type PinnedVersions } from './bootstrap/versions.js';
|
|
6
|
+
export type { PinnedVersions };
|
|
7
|
+
export declare const PINNED_VERSIONS: Readonly<PinnedVersions>;
|
|
5
8
|
export type { RequestableField, CallsChange, StorageLimitChange, AiLimitChange, DomainChange, MailTierChange, RequestedChangeValue, ChangeClassification, RequestedChangeState, RequestedChange, } from './requested-changes.js';
|
|
6
9
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
7
10
|
export type { SlugRejectionReason, SlugRejection } from './slug.js';
|
|
8
11
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
9
|
-
export type { Checkpoint, LastApplied, AppliedState } from './state.js';
|
|
12
|
+
export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
|
|
10
13
|
export type { PlanStep, UnmanagedItem, Plan } from './plan.js';
|
|
11
|
-
export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
|
|
12
|
-
export
|
|
14
|
+
export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
|
|
15
|
+
export { StaleLeaseError } from './store.js';
|
|
16
|
+
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
13
17
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
14
18
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
15
19
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
16
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
17
|
-
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, PlanDetail, PlatformPlanDetail, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
21
|
+
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
22
|
+
export { AdoptRefusedError } from './plan/index.js';
|
|
18
23
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
19
24
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
20
25
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export { APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
|
|
2
2
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
3
|
+
// archon#273: the pinned platform service versions the planner deploys, so a
|
|
4
|
+
// host can show declared against running. Readonly: it is the planner's own
|
|
5
|
+
// object, not a copy.
|
|
6
|
+
import { VERSIONS } from './bootstrap/versions.js';
|
|
7
|
+
export const PINNED_VERSIONS = VERSIONS;
|
|
3
8
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
4
9
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
10
|
+
export { StaleLeaseError } from './store.js';
|
|
5
11
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
6
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
7
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
|
+
export { AdoptRefusedError } from './plan/index.js';
|
|
8
15
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
9
16
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
10
17
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- @wtfalch/foundry: #259 -- per-org calls Matrix version pin, holding an
|
|
2
|
+
-- org's Synapse/MAS release during an ordered rollout. Null holds no pin:
|
|
3
|
+
-- the stack follows versions.json, same as every org today. Mirrors
|
|
4
|
+
-- src/store/schema.ts's `declaredOrgs` table additions.
|
|
5
|
+
|
|
6
|
+
alter table foundry_declared_orgs
|
|
7
|
+
add column if not exists calls_version_pin_synapse text,
|
|
8
|
+
add column if not exists calls_version_pin_mas text;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- @wtfalch/foundry: which steps of a destroy have finished, and which failed.
|
|
2
|
+
-- Sits beside `0002_applied_state.sql`'s `checkpoints` (an apply's finished
|
|
3
|
+
-- steps) and is read the same way, from the org's applied state. `DestroyRecord`
|
|
4
|
+
-- in `state.ts`. Null until a destroy has started; a destroy that finishes
|
|
5
|
+
-- deletes the org and this row with it.
|
|
6
|
+
|
|
7
|
+
alter table foundry_applied_state add column if not exists destroy jsonb;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- @wtfalch/foundry: Company Included's declared org id was minted by hand as
|
|
2
|
+
-- the literal 'org_company_included' rather than a uuid (bootstrap/declare.ts).
|
|
3
|
+
-- Archon resolves a declared org only by uuid (archon issue #40, `isUuid` in
|
|
4
|
+
-- `src/lib/companies/index.ts`'s `getCompany`), so every company page for our
|
|
5
|
+
-- own company 404s (foundry#435). This rewrites the live row, and every
|
|
6
|
+
-- column across this package's own tables that carries that same literal, to
|
|
7
|
+
-- the uuid `declare.ts`'s `COMPANY_INCLUDED_ORG_ID` now exports. The two
|
|
8
|
+
-- literals below must agree with that constant.
|
|
9
|
+
--
|
|
10
|
+
-- Idempotent: every statement is a no-op once the row has already moved,
|
|
11
|
+
-- because each WHERE clause only ever matches the old literal. Safe for a
|
|
12
|
+
-- still-running old release during the rolling deploy: nothing outside this
|
|
13
|
+
-- package's own one-time `bootstrapPlatform` idempotency check
|
|
14
|
+
-- (`bootstrap.ts`'s `orgStore.get(COMPANY_INCLUDED_ORG_ID)`) reads this id as
|
|
15
|
+
-- a hardcoded literal rather than from the declared platform row it is
|
|
16
|
+
-- already running against (`plan/platform-providers.ts`'s
|
|
17
|
+
-- `context.platform.ownerOrgId`), and Company Included's platform is already
|
|
18
|
+
-- bootstrapped -- that check has nothing left to do either way.
|
|
19
|
+
--
|
|
20
|
+
-- `foundry_applied_state` and `foundry_requested_changes` both carry a
|
|
21
|
+
-- foreign key to `foundry_declared_orgs (id)` with no `ON UPDATE CASCADE`,
|
|
22
|
+
-- so the primary key cannot move while they still point at the old value.
|
|
23
|
+
-- Each is dropped and re-added around the rewrite rather than made
|
|
24
|
+
-- permanently cascading, which would let a future bug silently rename an
|
|
25
|
+
-- org's rows out from under its history instead of failing loudly.
|
|
26
|
+
-- (`foundry_adoptions` is gone -- 0032 dropped it -- so it needs no such
|
|
27
|
+
-- treatment here.)
|
|
28
|
+
|
|
29
|
+
alter table foundry_applied_state drop constraint if exists foundry_applied_state_org_fkey;
|
|
30
|
+
alter table foundry_requested_changes drop constraint if exists foundry_requested_changes_org_fkey;
|
|
31
|
+
|
|
32
|
+
update foundry_declared_orgs
|
|
33
|
+
set id = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
34
|
+
where id = 'org_company_included';
|
|
35
|
+
|
|
36
|
+
update foundry_applied_state
|
|
37
|
+
set org_id = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
38
|
+
where org_id = 'org_company_included';
|
|
39
|
+
|
|
40
|
+
update foundry_requested_changes
|
|
41
|
+
set org_id = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
42
|
+
where org_id = 'org_company_included';
|
|
43
|
+
|
|
44
|
+
update foundry_runs
|
|
45
|
+
set org_id = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
46
|
+
where org_id = 'org_company_included';
|
|
47
|
+
|
|
48
|
+
update foundry_audit_events
|
|
49
|
+
set org_id = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
50
|
+
where org_id = 'org_company_included';
|
|
51
|
+
|
|
52
|
+
update foundry_declared_platform
|
|
53
|
+
set owner_org_id = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
54
|
+
where owner_org_id = 'org_company_included';
|
|
55
|
+
|
|
56
|
+
update foundry_applied_versions
|
|
57
|
+
set service = 'e4d20889-a63d-42b6-b670-2bdd4472449b'
|
|
58
|
+
where service = 'org_company_included';
|
|
59
|
+
|
|
60
|
+
do $$
|
|
61
|
+
begin
|
|
62
|
+
if not exists (select 1 from pg_constraint where conname = 'foundry_applied_state_org_fkey') then
|
|
63
|
+
alter table foundry_applied_state
|
|
64
|
+
add constraint foundry_applied_state_org_fkey foreign key (org_id)
|
|
65
|
+
references foundry_declared_orgs (id);
|
|
66
|
+
end if;
|
|
67
|
+
if not exists (select 1 from pg_constraint where conname = 'foundry_requested_changes_org_fkey') then
|
|
68
|
+
alter table foundry_requested_changes
|
|
69
|
+
add constraint foundry_requested_changes_org_fkey foreign key (org_id)
|
|
70
|
+
references foundry_declared_orgs (id);
|
|
71
|
+
end if;
|
|
72
|
+
end $$;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
-- @wtfalch/foundry: migration 0040 moved Company Included's declared org id
|
|
2
|
+
-- from the literal 'org_company_included' to the uuid
|
|
3
|
+
-- 'e4d20889-a63d-42b6-b670-2bdd4472449b' in every org_id (and org_id-shaped)
|
|
4
|
+
-- column, but never looked inside a jsonb value. A hosting step's key is
|
|
5
|
+
-- `<orgId>:<entryKey>` (platform-diff.ts's `appStepKey`), recorded as
|
|
6
|
+
-- `foundry_applied_state.service_ids`'s own keys
|
|
7
|
+
-- (`coolify:app:<orgId>:<entryKey>`, `cloudflare:access:<orgId>:<entryKey>`)
|
|
8
|
+
-- and as `foundry_runs.step_ids` array entries of the identical shape. After
|
|
9
|
+
-- 0040, the planner looks for `coolify:app:e4d20889-...:<entry>`, finds
|
|
10
|
+
-- nothing under the still-literal key, and can plan to create Company
|
|
11
|
+
-- Included's Coolify apps a second time (#452).
|
|
12
|
+
--
|
|
13
|
+
-- `foundry_runs.checkpoints` carries the same step strings
|
|
14
|
+
-- (`runner/runner.ts`'s `Checkpoint.step`) and is compared by exact string
|
|
15
|
+
-- against `step_ids` to decide what a resumed run has already done
|
|
16
|
+
-- (`done.has(stepId)`); rewriting `step_ids` alone would make an
|
|
17
|
+
-- already-completed step look undone and run it again. `failure.step` names
|
|
18
|
+
-- which step actually failed, same shape. `foundry_applied_state.checkpoints`
|
|
19
|
+
-- and `.destroy` (`state.ts`'s `Checkpoint`/`DestroyRecord`) carry the same
|
|
20
|
+
-- step strings too, as history rather than anything matched against, but are
|
|
21
|
+
-- rewritten the same way so nothing here still reads as Company Included's
|
|
22
|
+
-- old id.
|
|
23
|
+
--
|
|
24
|
+
-- Every WHERE clause below matches only a key, array element or `step` field
|
|
25
|
+
-- that still contains the old literal, so a second run is a no-op.
|
|
26
|
+
|
|
27
|
+
update foundry_applied_state
|
|
28
|
+
set service_ids = (
|
|
29
|
+
select coalesce(
|
|
30
|
+
jsonb_object_agg(
|
|
31
|
+
replace(key, 'org_company_included', 'e4d20889-a63d-42b6-b670-2bdd4472449b'),
|
|
32
|
+
value
|
|
33
|
+
),
|
|
34
|
+
'{}'::jsonb
|
|
35
|
+
)
|
|
36
|
+
from jsonb_each(service_ids)
|
|
37
|
+
)
|
|
38
|
+
where exists (
|
|
39
|
+
select 1 from jsonb_object_keys(service_ids) k
|
|
40
|
+
where k like '%org_company_included%'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
update foundry_applied_state
|
|
44
|
+
set checkpoints = (
|
|
45
|
+
select coalesce(
|
|
46
|
+
jsonb_agg(
|
|
47
|
+
case
|
|
48
|
+
when (c ->> 'step') like '%org_company_included%'
|
|
49
|
+
then jsonb_set(c, '{step}',
|
|
50
|
+
to_jsonb(replace(c ->> 'step', 'org_company_included',
|
|
51
|
+
'e4d20889-a63d-42b6-b670-2bdd4472449b')))
|
|
52
|
+
else c
|
|
53
|
+
end
|
|
54
|
+
),
|
|
55
|
+
'[]'::jsonb
|
|
56
|
+
)
|
|
57
|
+
from jsonb_array_elements(checkpoints) as c
|
|
58
|
+
)
|
|
59
|
+
where exists (
|
|
60
|
+
select 1 from jsonb_array_elements(checkpoints) c
|
|
61
|
+
where (c ->> 'step') like '%org_company_included%'
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
update foundry_applied_state
|
|
65
|
+
set destroy = jsonb_set(
|
|
66
|
+
destroy,
|
|
67
|
+
'{steps}',
|
|
68
|
+
(select coalesce(
|
|
69
|
+
jsonb_agg(
|
|
70
|
+
case
|
|
71
|
+
when (s ->> 'step') like '%org_company_included%'
|
|
72
|
+
then jsonb_set(s, '{step}',
|
|
73
|
+
to_jsonb(replace(s ->> 'step', 'org_company_included',
|
|
74
|
+
'e4d20889-a63d-42b6-b670-2bdd4472449b')))
|
|
75
|
+
else s
|
|
76
|
+
end
|
|
77
|
+
),
|
|
78
|
+
'[]'::jsonb
|
|
79
|
+
)
|
|
80
|
+
from jsonb_array_elements(destroy -> 'steps') as s)
|
|
81
|
+
)
|
|
82
|
+
where destroy is not null
|
|
83
|
+
and exists (
|
|
84
|
+
select 1 from jsonb_array_elements(destroy -> 'steps') s
|
|
85
|
+
where (s ->> 'step') like '%org_company_included%'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
update foundry_runs
|
|
89
|
+
set step_ids = (
|
|
90
|
+
select coalesce(
|
|
91
|
+
jsonb_agg(
|
|
92
|
+
replace(elem, 'org_company_included', 'e4d20889-a63d-42b6-b670-2bdd4472449b')
|
|
93
|
+
),
|
|
94
|
+
'[]'::jsonb
|
|
95
|
+
)
|
|
96
|
+
from jsonb_array_elements_text(step_ids) as elem
|
|
97
|
+
)
|
|
98
|
+
where exists (
|
|
99
|
+
select 1 from jsonb_array_elements_text(step_ids) e
|
|
100
|
+
where e like '%org_company_included%'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
update foundry_runs
|
|
104
|
+
set checkpoints = (
|
|
105
|
+
select coalesce(
|
|
106
|
+
jsonb_agg(
|
|
107
|
+
case
|
|
108
|
+
when (c ->> 'step') like '%org_company_included%'
|
|
109
|
+
then jsonb_set(c, '{step}',
|
|
110
|
+
to_jsonb(replace(c ->> 'step', 'org_company_included',
|
|
111
|
+
'e4d20889-a63d-42b6-b670-2bdd4472449b')))
|
|
112
|
+
else c
|
|
113
|
+
end
|
|
114
|
+
),
|
|
115
|
+
'[]'::jsonb
|
|
116
|
+
)
|
|
117
|
+
from jsonb_array_elements(checkpoints) as c
|
|
118
|
+
)
|
|
119
|
+
where exists (
|
|
120
|
+
select 1 from jsonb_array_elements(checkpoints) c
|
|
121
|
+
where (c ->> 'step') like '%org_company_included%'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
update foundry_runs
|
|
125
|
+
set failure = jsonb_set(
|
|
126
|
+
failure,
|
|
127
|
+
'{step}',
|
|
128
|
+
to_jsonb(replace(failure ->> 'step', 'org_company_included',
|
|
129
|
+
'e4d20889-a63d-42b6-b670-2bdd4472449b'))
|
|
130
|
+
)
|
|
131
|
+
where failure is not null
|
|
132
|
+
and (failure ->> 'step') like '%org_company_included%';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the lock behind `FoundryPlanApply`'s apply, destroy, adopt
|
|
2
|
+
-- and release (#441). One row per scope: `company:<orgId>` or `platform`.
|
|
3
|
+
-- `token` is the fencing token; a row is never deleted, so it only rises, and
|
|
4
|
+
-- a stale holder's `putAppliedState` / `putServiceIds` is refused by
|
|
5
|
+
-- comparing against it. A released lease has `expires_at` set to now.
|
|
6
|
+
|
|
7
|
+
create table if not exists foundry_mutation_leases (
|
|
8
|
+
scope text primary key,
|
|
9
|
+
token bigint not null,
|
|
10
|
+
holder text not null,
|
|
11
|
+
expires_at timestamptz not null
|
|
12
|
+
);
|
package/dist/org.d.ts
CHANGED
|
@@ -129,6 +129,27 @@ export interface ChatConfig {
|
|
|
129
129
|
export interface CallsConfig {
|
|
130
130
|
enabled: boolean;
|
|
131
131
|
usesDomain: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* #259: holds this org's calls Matrix stack on an older Synapse/MAS
|
|
134
|
+
* release during an ordered rollout, instead of whatever
|
|
135
|
+
* `versions.json` currently pins. `null`/absent follows the pinned
|
|
136
|
+
* release, same as today.
|
|
137
|
+
*
|
|
138
|
+
* Declared and validated here; not yet read anywhere a tag is actually
|
|
139
|
+
* applied. `executor.ts`'s `coolifyCalls` only ever sets Synapse's/MAS's
|
|
140
|
+
* image tag at creation (its own comment: "#251: only on create --
|
|
141
|
+
* nothing here patches `docker_registry_image_tag` on an app
|
|
142
|
+
* `findOrCreateCallsApp` found already running") -- there is no patch-
|
|
143
|
+
* and-redeploy path for an existing org's stack yet, pinned or not. A
|
|
144
|
+
* rollout mechanism needs that path before this field can hold anything
|
|
145
|
+
* back; building it is outside this change (#259's own "Human steps"
|
|
146
|
+
* raises a related open question about Coolify's auto-deploy that bears
|
|
147
|
+
* on the same path).
|
|
148
|
+
*/
|
|
149
|
+
versionPin?: {
|
|
150
|
+
synapse?: string;
|
|
151
|
+
mas?: string;
|
|
152
|
+
} | null;
|
|
132
153
|
}
|
|
133
154
|
/**
|
|
134
155
|
* The issuer's per-service opt-in into the org's `domain` fact.
|
|
@@ -157,6 +178,17 @@ export interface IssuerConfig {
|
|
|
157
178
|
export interface SiteConfig {
|
|
158
179
|
enabled: boolean;
|
|
159
180
|
usesDomain: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* The org's own skin repo for `plan/executor.ts`'s `cmsSite` site app
|
|
183
|
+
* (#245) -- `null` (the default) builds the shared `@wtfalch/cms` repo's
|
|
184
|
+
* `apps/site`, same sample skin as before this field existed. Only the
|
|
185
|
+
* repo the site app builds from; the cms admin app behind it always
|
|
186
|
+
* builds from the shared repo. Where the skin finally lives (an
|
|
187
|
+
* org-owned repo vs. a cms theme document served at runtime) is still
|
|
188
|
+
* undecided in https://github.com/wtfalch/cms/issues/55 -- this field and
|
|
189
|
+
* its default are the only part built until that closes.
|
|
190
|
+
*/
|
|
191
|
+
repo?: string | null;
|
|
160
192
|
}
|
|
161
193
|
/**
|
|
162
194
|
* A ZITADEL OIDC application kind foundry can provision inside an org's
|
|
@@ -333,8 +365,10 @@ export declare const AI_ENTRY_ENV_NAMES: readonly ["AI_URL", "AI_ORGANISATION_ID
|
|
|
333
365
|
* env and one collection per name in `collections`. A name is uppercase
|
|
334
366
|
* `[A-Z0-9_]`; its id is delivered as `FILES_COLLECTION_<NAME>_ID`.
|
|
335
367
|
* `permissions` is a subset of {@link FILES_ENTRY_PERMISSIONS}, which is also
|
|
336
|
-
* the default. `shareHost` asks for `share.<domain
|
|
337
|
-
*
|
|
368
|
+
* the default. `shareHost` asks for `share.<domain>`: foundry routes and
|
|
369
|
+
* publishes that name through the org's tunnel, delivers `FILES_SHARE_ORIGIN`
|
|
370
|
+
* and passes it to files. The platform files runner must still serve the
|
|
371
|
+
* host (a Coolify domain and `FILES_PUBLIC_HOSTS`); the plan warns of that.
|
|
338
372
|
*/
|
|
339
373
|
export interface EntryFiles {
|
|
340
374
|
collections: string[];
|
|
@@ -356,8 +390,18 @@ export interface HostingEntryUses {
|
|
|
356
390
|
* looks like a secret. `{ from }` names a value the plan already mints.
|
|
357
391
|
*/
|
|
358
392
|
export type EnvValue = string | {
|
|
359
|
-
from: AppEnvSource
|
|
360
|
-
};
|
|
393
|
+
from: Exclude<AppEnvSource, 'sharedSecret'>;
|
|
394
|
+
} | EnvSharedSecret;
|
|
395
|
+
/**
|
|
396
|
+
* A secret foundry generates once and holds under an org-scoped ref. Every
|
|
397
|
+
* entry of the same org that names the same `name` gets the same value; an
|
|
398
|
+
* entry of another org never does. It is written to the entry's env only, and
|
|
399
|
+
* never reaches a plan, a log or an error.
|
|
400
|
+
*/
|
|
401
|
+
export interface EnvSharedSecret {
|
|
402
|
+
from: 'sharedSecret';
|
|
403
|
+
name: string;
|
|
404
|
+
}
|
|
361
405
|
/** The org's `app` entry: today's one app. */
|
|
362
406
|
export declare function appEntry(hosting: HostingConfig): HostingEntry | undefined;
|
|
363
407
|
/**
|
|
@@ -367,8 +411,19 @@ export declare function appEntry(hosting: HostingConfig): HostingEntry | undefin
|
|
|
367
411
|
* sends from. Each is read from `StepExecutionContext.recordedIds()` in
|
|
368
412
|
* `executor.ts`'s `pushAppEnv`, never minted again there.
|
|
369
413
|
*/
|
|
370
|
-
export type AppEnvSource = 'filesOrganisationId' | 'aiOrganisationId' | 'mailDomain';
|
|
414
|
+
export type AppEnvSource = 'filesOrganisationId' | 'aiOrganisationId' | 'mailDomain' | 'sharedSecret' | 'platformMailKey';
|
|
415
|
+
/**
|
|
416
|
+
* Two sources are secrets. `sharedSecret` is {@link EnvSharedSecret}.
|
|
417
|
+
* `platformMailKey` is the platform's held Lettermint key, handed over as the
|
|
418
|
+
* value of the declared env name (the platform's own apps call it
|
|
419
|
+
* `LETTERMINT_API_TOKEN`). Any hosting entry of any customer org may name it
|
|
420
|
+
* (William, 2026-10-04): mail is Lettermint, and the key is the platform's.
|
|
421
|
+
*/
|
|
371
422
|
export declare const APP_ENV_SOURCES: ReadonlySet<AppEnvSource>;
|
|
423
|
+
/** The sources whose value is a secret: resolved at apply time, never recorded. */
|
|
424
|
+
export declare const SECRET_APP_ENV_SOURCES: ReadonlySet<AppEnvSource>;
|
|
425
|
+
/** The held-key ref of an org's shared secret. */
|
|
426
|
+
export declare const sharedSecretRef: (orgId: string, name: string) => string;
|
|
372
427
|
/**
|
|
373
428
|
* `null` when `hosting` is valid; otherwise the reason it is not (#71), same
|
|
374
429
|
* shape as `validateBuyerIdentity` below.
|
|
@@ -405,8 +460,19 @@ export declare function validateDomain(domain: string): string | null;
|
|
|
405
460
|
* - `site.enabled` refuses an `issuerApps` entry named `cms`: the fixed cms
|
|
406
461
|
* client (#139, `plan/diff.ts`'s `planCmsClient`) already owns the
|
|
407
462
|
* `zitadel:app:cms` key, and the two would overwrite each other.
|
|
463
|
+
* - `site.repo`, if set, must be a repo under `wtfalch` (#245), the same
|
|
464
|
+
* `REPO` rule {@link validateHostingEntry} already holds `HostingEntry.repo`
|
|
465
|
+
* to.
|
|
408
466
|
*/
|
|
409
467
|
export declare function validateSite(org: DeclaredOrg): string | null;
|
|
468
|
+
/**
|
|
469
|
+
* #259: `calls.versionPin` holds this org's calls Matrix stack on an older
|
|
470
|
+
* release during an ordered rollout. A pin is a tag, held to the same
|
|
471
|
+
* "no floating tag" rule the pinned releases in `versions.json` already
|
|
472
|
+
* follow ({@link floatingTagBlocked}) -- a pin that floats would defeat the
|
|
473
|
+
* reason to pin at all.
|
|
474
|
+
*/
|
|
475
|
+
export declare function validateCalls(org: DeclaredOrg): string | null;
|
|
410
476
|
/**
|
|
411
477
|
* Per-org SSO. Decided to exist in #14 ("Also in scope") and its resolved
|
|
412
478
|
* research #3 ("per-org SSO code exists unused"); nothing about its
|
|
@@ -590,9 +656,20 @@ export declare function ownerMailboxRecorded(box: OwnerMailbox, recordedIds: Rea
|
|
|
590
656
|
* two rules `validateHosting` already applies to `issuerApps`' names, for
|
|
591
657
|
* the same reason: a duplicate or blank entry can never mean anything
|
|
592
658
|
* different from leaving it out.
|
|
659
|
+
*
|
|
660
|
+
* A domain plus `mail.mode === 'received'` is tier 2 (own domain, MX stays
|
|
661
|
+
* at Google or Microsoft), which William dropped on 2026-10-04 (#422):
|
|
662
|
+
* only tier 1 (no domain) and tier 3 (own domain, `hosted`) are offered.
|
|
663
|
+
* Refused unless `opts.allowExistingTier2` says this exact combination was
|
|
664
|
+
* already the stored state before this write -- `store/postgres.ts`'s
|
|
665
|
+
* `update` passes that for an org already grandfathered into tier 2, so a
|
|
666
|
+
* patch that does not touch `domain` or `mail.mode` still goes through.
|
|
667
|
+
* `create` never passes it: a brand new org cannot already be grandfathered.
|
|
593
668
|
*/
|
|
594
669
|
export declare function validateMail(org: Pick<DeclaredOrg, 'mail' | 'domain'> & {
|
|
595
670
|
owners?: readonly OwnerIdentity[];
|
|
671
|
+
}, opts?: {
|
|
672
|
+
allowExistingTier2?: boolean;
|
|
596
673
|
}): string | null;
|
|
597
674
|
/**
|
|
598
675
|
* The form an app foundry does not own takes. Decided in #7's `#15`
|