@wtfalch/foundry 0.7.1 → 0.9.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 +12 -8
- package/dist/bootstrap/bootstrap.js +4 -12
- package/dist/bootstrap/declare.js +1 -5
- package/dist/bootstrap/versions.d.ts +8 -0
- package/dist/bootstrap/versions.js +31 -0
- package/dist/calls-sfu-deploy-support.d.ts +1 -1
- package/dist/calls-sfu-deploy-support.js +1 -1
- package/dist/calls-sfu-ssh-adapter.d.ts +1 -1
- package/dist/calls-sfu-ssh-adapter.js +1 -1
- package/dist/calls-sfu.d.ts +5 -4
- package/dist/calls-sfu.js +5 -4
- package/dist/index.d.ts +4 -5
- package/dist/index.js +6 -3
- package/dist/migrations/0024_audit_events.sql +1 -1
- package/dist/migrations/0025_audit_display.sql +1 -1
- package/dist/migrations/0026_audit_erase_email_guard.sql +1 -1
- package/dist/migrations/0027_audit_chain.sql +1 -1
- package/dist/migrations/0028_audit_rls.sql +2 -2
- package/dist/migrations/0029_hosting_entries.sql +109 -0
- package/dist/migrations-dir.d.ts +9 -0
- package/dist/migrations-dir.js +10 -0
- package/dist/org.d.ts +59 -27
- package/dist/org.js +98 -26
- package/dist/plan/calls-matrix.d.ts +2 -0
- package/dist/plan/calls-matrix.js +2 -0
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +100 -32
- package/dist/plan/executor.d.ts +324 -19
- package/dist/plan/executor.js +764 -34
- package/dist/plan/index.d.ts +1 -1
- package/dist/plan/index.js +1 -1
- package/dist/plan/login-host.d.ts +24 -0
- package/dist/plan/login-host.js +21 -0
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/plan-apply.js +26 -10
- package/dist/plan/platform-diff.d.ts +102 -12
- package/dist/plan/platform-diff.js +236 -15
- package/dist/plan/platform-providers.d.ts +43 -1
- package/dist/plan/platform-providers.js +245 -33
- package/dist/plan/zitadel-system-api.d.ts +64 -0
- package/dist/plan/zitadel-system-api.js +125 -0
- package/dist/plan.d.ts +43 -0
- package/dist/plan.js +16 -1
- package/dist/platform.d.ts +1 -1
- package/dist/store/audit.d.ts +13 -17
- package/dist/store/audit.js +14 -18
- package/dist/store/postgres.d.ts +14 -0
- package/dist/store/postgres.js +15 -18
- package/dist/store/schema.d.ts +20 -1
- package/dist/store/schema.js +7 -1
- package/package.json +6 -3
- package/versions.json +1 -1
- package/dist/migrate.d.ts +0 -38
- package/dist/migrate.js +0 -47
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { MigrateSql } from '../migrate.js';
|
|
2
1
|
import type { PlatformLiveReaders, PlatformStepExecutor } from '../plan/index.js';
|
|
3
2
|
import type { FoundryPlatformStepExecutorOptions } from '../plan/platform-providers.js';
|
|
4
3
|
import type { FoundryStoreDb } from '../store/postgres.js';
|
|
@@ -27,7 +26,7 @@ export interface RootCredentials {
|
|
|
27
26
|
}
|
|
28
27
|
/** What Archon's onboarding screen shows: one event per phase or round. */
|
|
29
28
|
export interface BootstrapProgress {
|
|
30
|
-
phase: '
|
|
29
|
+
phase: 'declare' | 'plan' | 'apply' | 'done';
|
|
31
30
|
round?: number;
|
|
32
31
|
message: string;
|
|
33
32
|
steps?: string[];
|
|
@@ -35,10 +34,8 @@ export interface BootstrapProgress {
|
|
|
35
34
|
/** Everything foundry's per-step executors take besides the two clients and `deliverSecret`. */
|
|
36
35
|
export type BootstrapExecutorDeps = Omit<FoundryPlatformStepExecutorOptions, 'coolify' | 'deliverSecret'>;
|
|
37
36
|
export interface BootstrapPlatformDeps {
|
|
38
|
-
/** Foundry's tables live here; the store, the audit log and the plan all use it. */
|
|
37
|
+
/** Foundry's tables live here, already migrated by the host; the store, the audit log and the plan all use it. */
|
|
39
38
|
db: FoundryStoreDb;
|
|
40
|
-
/** The same database, for the migration runner. */
|
|
41
|
-
migrateSql: MigrateSql;
|
|
42
39
|
rootCredentials: () => Promise<RootCredentials>;
|
|
43
40
|
deliverSecret: (ref: string, secret: string) => Promise<void>;
|
|
44
41
|
/**
|
|
@@ -52,6 +49,13 @@ export interface BootstrapPlatformDeps {
|
|
|
52
49
|
};
|
|
53
50
|
/** Per-step dependencies (auth, mail, files, ai, integrations, archon, sfu). A step without its own is reported blocked. */
|
|
54
51
|
executor?: BootstrapExecutorDeps;
|
|
52
|
+
/**
|
|
53
|
+
* Passed to `createPostgresAuditLog`. Omit it in a real deployment: `db` is
|
|
54
|
+
* the runtime role the host made with `ensureRuntimeRole`, and the default
|
|
55
|
+
* (on) checks that. Only a fixture without that role (PGlite, whose one user
|
|
56
|
+
* is a superuser) needs `false`.
|
|
57
|
+
*/
|
|
58
|
+
checkRuntimeRole?: boolean;
|
|
55
59
|
onProgress?: (event: BootstrapProgress) => void;
|
|
56
60
|
/** Rounds before the run gives up. Default 20. */
|
|
57
61
|
maxRounds?: number;
|
|
@@ -97,9 +101,9 @@ export declare class Redactor {
|
|
|
97
101
|
/** Wraps the secret-carrying ports inside one step's deps so what they deliver or resolve is redacted. */
|
|
98
102
|
export declare function guardStepDeps(executor: BootstrapExecutorDeps | undefined, redactor: Redactor, deliverSecret: (ref: string, secret: string) => Promise<void>, heldKeys: BootstrapPlatformDeps['heldKeys']): BootstrapExecutorDeps;
|
|
99
103
|
/**
|
|
100
|
-
* The platform's first apply, run from Archon's onboarding flow
|
|
101
|
-
*
|
|
102
|
-
* overwriting a row already there), then plans and applies round by round
|
|
104
|
+
* The platform's first apply, run from Archon's onboarding flow, against a
|
|
105
|
+
* database the host has already migrated: declares Company Included and the
|
|
106
|
+
* platform (never overwriting a row already there), then plans and applies round by round
|
|
103
107
|
* until every step is applied or reported blocked. Reads no env and no file:
|
|
104
108
|
* the database, root credentials, held keys and every per-step dependency
|
|
105
109
|
* come in through `deps`.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CloudflareClient } from '@wtfalch/providers/cloudflare';
|
|
2
2
|
import { CoolifyClient } from '@wtfalch/providers/coolify';
|
|
3
|
-
import { migrate } from '../migrate.js';
|
|
4
3
|
import { FoundryPlanApply, FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, } from '../plan/index.js';
|
|
5
4
|
import { createPostgresAuditLog } from '../store/audit.js';
|
|
6
5
|
import { createPostgresDeclaredOrgStore, createPostgresPlatformStore } from '../store/postgres.js';
|
|
@@ -140,9 +139,9 @@ function archonMasterKeys(guarded, credentials) {
|
|
|
140
139
|
};
|
|
141
140
|
}
|
|
142
141
|
/**
|
|
143
|
-
* The platform's first apply, run from Archon's onboarding flow
|
|
144
|
-
*
|
|
145
|
-
* overwriting a row already there), then plans and applies round by round
|
|
142
|
+
* The platform's first apply, run from Archon's onboarding flow, against a
|
|
143
|
+
* database the host has already migrated: declares Company Included and the
|
|
144
|
+
* platform (never overwriting a row already there), then plans and applies round by round
|
|
146
145
|
* until every step is applied or reported blocked. Reads no env and no file:
|
|
147
146
|
* the database, root credentials, held keys and every per-step dependency
|
|
148
147
|
* come in through `deps`.
|
|
@@ -187,13 +186,6 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
187
186
|
}
|
|
188
187
|
: {}),
|
|
189
188
|
};
|
|
190
|
-
const migrated = await migrate(deps.migrateSql);
|
|
191
|
-
emit({
|
|
192
|
-
phase: 'migrate',
|
|
193
|
-
message: migrated.applied.length === 0
|
|
194
|
-
? 'foundry tables already up to date'
|
|
195
|
-
: `migrated: ${migrated.applied.join(', ')}`,
|
|
196
|
-
});
|
|
197
189
|
const orgStore = createPostgresDeclaredOrgStore(deps.db);
|
|
198
190
|
const platformStore = createPostgresPlatformStore(deps.db);
|
|
199
191
|
if (await orgStore.get(COMPANY_INCLUDED_ORG_ID)) {
|
|
@@ -264,7 +256,7 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
264
256
|
},
|
|
265
257
|
executor: unusedOrgExecutor,
|
|
266
258
|
destroyer: unusedOrgExecutor,
|
|
267
|
-
audit: redactingAudit(createPostgresAuditLog(deps.db), redactor),
|
|
259
|
+
audit: redactingAudit(createPostgresAuditLog(deps.db, { checkRuntimeRole: deps.checkRuntimeRole }), redactor),
|
|
268
260
|
authz: allowPlatformApply,
|
|
269
261
|
actor: { id: 'bootstrap-platform', display: 'bootstrapPlatform' },
|
|
270
262
|
platform: { store: platformStore, readers: runtime.readers, executor },
|
|
@@ -63,15 +63,11 @@ export function declaredCompanyOrg(company, owner) {
|
|
|
63
63
|
issuer: { usesDomain: false },
|
|
64
64
|
site: { enabled: false, usesDomain: false },
|
|
65
65
|
hosting: {
|
|
66
|
-
appEnv: {},
|
|
67
66
|
enabled: false,
|
|
68
67
|
usesDomain: false,
|
|
69
|
-
database: false,
|
|
70
68
|
tunnel: false,
|
|
71
|
-
gitBranch: DEFAULT_GIT_REF,
|
|
72
|
-
buildPack: 'nixpacks',
|
|
73
|
-
accessEmails: [],
|
|
74
69
|
issuerApps: [],
|
|
70
|
+
entries: [],
|
|
75
71
|
},
|
|
76
72
|
sso: { enabled: false },
|
|
77
73
|
guests: { enabled: false },
|
|
@@ -15,3 +15,11 @@ export interface PinnedVersions {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function loadVersions(): PinnedVersions;
|
|
17
17
|
export declare const VERSIONS: PinnedVersions;
|
|
18
|
+
/**
|
|
19
|
+
* Why an image tag cannot be used, or `null`. #248: generalised out of what
|
|
20
|
+
* was `platform-providers.ts`'s `mailImageTagBlocked`, the one guard
|
|
21
|
+
* `versions.json` had before -- `livekit/livekit-server:latest` was the live
|
|
22
|
+
* case with none. `label` names the image in the message (`"Stalwart"`,
|
|
23
|
+
* `"ZITADEL"`, `"LiveKit"`, ...); `example` is a real pinned tag to suggest.
|
|
24
|
+
*/
|
|
25
|
+
export declare function floatingTagBlocked(label: string, tag: string, example: string): string | null;
|
|
@@ -10,3 +10,34 @@ export function loadVersions() {
|
|
|
10
10
|
return JSON.parse(readFileSync(VERSIONS_PATH, 'utf8'));
|
|
11
11
|
}
|
|
12
12
|
export const VERSIONS = loadVersions();
|
|
13
|
+
/**
|
|
14
|
+
* Tag spellings that resolve to whatever the registry currently has, not a
|
|
15
|
+
* fixed image -- a re-apply of the same declaration can silently run a
|
|
16
|
+
* different version with no diff shown anywhere (#248). `main`/`master` are
|
|
17
|
+
* default-branch Docker Hub aliases some of these images publish alongside
|
|
18
|
+
* their release tags, not git refs.
|
|
19
|
+
*/
|
|
20
|
+
const FLOATING_TAGS = new Set([
|
|
21
|
+
'latest',
|
|
22
|
+
'main',
|
|
23
|
+
'master',
|
|
24
|
+
'stable',
|
|
25
|
+
'dev',
|
|
26
|
+
'edge',
|
|
27
|
+
'nightly',
|
|
28
|
+
'head',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Why an image tag cannot be used, or `null`. #248: generalised out of what
|
|
32
|
+
* was `platform-providers.ts`'s `mailImageTagBlocked`, the one guard
|
|
33
|
+
* `versions.json` had before -- `livekit/livekit-server:latest` was the live
|
|
34
|
+
* case with none. `label` names the image in the message (`"Stalwart"`,
|
|
35
|
+
* `"ZITADEL"`, `"LiveKit"`, ...); `example` is a real pinned tag to suggest.
|
|
36
|
+
*/
|
|
37
|
+
export function floatingTagBlocked(label, tag, example) {
|
|
38
|
+
const trimmed = tag.trim();
|
|
39
|
+
if (trimmed === '' || FLOATING_TAGS.has(trimmed.toLowerCase())) {
|
|
40
|
+
return `the ${label} image tag "${tag}" floats; declare a pinned release (for example ${example})`;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* -- the one place both `scripts/calls-sfu-deploy.mjs` (#122) and the
|
|
8
8
|
* platform `sfu` step (`plan/platform-sfu.ts`, #170) use it. The real SSH
|
|
9
9
|
* session -- `ssh2`'s `Client`/`sftp`/`exec` wiring -- stays untested, same
|
|
10
|
-
* as `foundry-db-provision.mjs
|
|
10
|
+
* as `foundry-db-provision.mjs`: real I/O, no branch
|
|
11
11
|
* logic worth a fake session over.
|
|
12
12
|
*
|
|
13
13
|
* Each export here answers one finding from PR #125's review (#122):
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* -- the one place both `scripts/calls-sfu-deploy.mjs` (#122) and the
|
|
8
8
|
* platform `sfu` step (`plan/platform-sfu.ts`, #170) use it. The real SSH
|
|
9
9
|
* session -- `ssh2`'s `Client`/`sftp`/`exec` wiring -- stays untested, same
|
|
10
|
-
* as `foundry-db-provision.mjs
|
|
10
|
+
* as `foundry-db-provision.mjs`: real I/O, no branch
|
|
11
11
|
* logic worth a fake session over.
|
|
12
12
|
*
|
|
13
13
|
* Each export here answers one finding from PR #125's review (#122):
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* host-key refusal and the 0600 file mode without ever opening a socket.
|
|
12
12
|
* `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh` is the one real
|
|
13
13
|
* implementation, over `ssh2`, and stays untested for the same reason
|
|
14
|
-
* `foundry-db-provision.mjs
|
|
14
|
+
* `foundry-db-provision.mjs` does: real I/O, no
|
|
15
15
|
* branch logic worth a fake session over.
|
|
16
16
|
*/
|
|
17
17
|
import { type CallsSfuConfig, type CallsSfuHostPort } from './calls-sfu.js';
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* host-key refusal and the 0600 file mode without ever opening a socket.
|
|
12
12
|
* `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh` is the one real
|
|
13
13
|
* implementation, over `ssh2`, and stays untested for the same reason
|
|
14
|
-
* `foundry-db-provision.mjs
|
|
14
|
+
* `foundry-db-provision.mjs` does: real I/O, no
|
|
15
15
|
* branch logic worth a fake session over.
|
|
16
16
|
*/
|
|
17
17
|
import { dirname } from 'node:path';
|
package/dist/calls-sfu.d.ts
CHANGED
|
@@ -114,10 +114,11 @@ export declare const LIVEKIT_CONFIG_PATH = "/opt/livekit/livekit.yaml";
|
|
|
114
114
|
/** Holds `TUNNEL_TOKEN=...` -- the one secret the `cloudflared` compose service reads (`env_file:`, below). */
|
|
115
115
|
export declare const CLOUDFLARED_ENV_PATH = "/opt/livekit/cloudflared.env";
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
117
|
+
* Pinned to a release tag (#248) -- `versions.json`'s `livekit` entry ran
|
|
118
|
+
* `:latest` until then, the one live case `floatingTagBlocked`
|
|
119
|
+
* (`bootstrap/versions.ts`) exists for: a re-apply of the same declaration
|
|
120
|
+
* silently running a different version, with no diff shown anywhere.
|
|
121
|
+
* Pinned in `versions.json` (#247).
|
|
121
122
|
*/
|
|
122
123
|
export declare const LIVEKIT_IMAGE_REF: string;
|
|
123
124
|
/** Pinned to a release tag; equals `scripts/shared-ingress.mjs`'s own `CLOUDFLARED_IMAGE_REF` constant (duplicated, not imported -- this package and that top-level script sit in separate places in the tree). Pinned in `versions.json` (#247). */
|
package/dist/calls-sfu.js
CHANGED
|
@@ -104,10 +104,11 @@ export const LIVEKIT_CONFIG_PATH = '/opt/livekit/livekit.yaml';
|
|
|
104
104
|
/** Holds `TUNNEL_TOKEN=...` -- the one secret the `cloudflared` compose service reads (`env_file:`, below). */
|
|
105
105
|
export const CLOUDFLARED_ENV_PATH = '/opt/livekit/cloudflared.env';
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* Pinned to a release tag (#248) -- `versions.json`'s `livekit` entry ran
|
|
108
|
+
* `:latest` until then, the one live case `floatingTagBlocked`
|
|
109
|
+
* (`bootstrap/versions.ts`) exists for: a re-apply of the same declaration
|
|
110
|
+
* silently running a different version, with no diff shown anywhere.
|
|
111
|
+
* Pinned in `versions.json` (#247).
|
|
111
112
|
*/
|
|
112
113
|
export const LIVEKIT_IMAGE_REF = `livekit/livekit-server:${VERSIONS.livekit}`;
|
|
113
114
|
/** Pinned to a release tag; equals `scripts/shared-ingress.mjs`'s own `CLOUDFLARED_IMAGE_REF` constant (duplicated, not imported -- this package and that top-level script sit in separate places in the tree). Pinned in `versions.json` (#247). */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, DeclaredOrg, } from './org.js';
|
|
2
|
-
export { DEFAULT_MAIL_APPS, validateBuyerIdentity, validateHosting, validateMail, validateOwner, validateSite, } from './org.js';
|
|
1
|
+
export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, DeclaredOrg, } from './org.js';
|
|
2
|
+
export { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, appEntry, validateBuyerIdentity, validateHosting, validateMail, validateOwner, 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
5
|
export type { RequestableField, CallsChange, StorageLimitChange, AiLimitChange, DomainChange, MailTierChange, RequestedChangeValue, ChangeClassification, RequestedChangeState, RequestedChange, } from './requested-changes.js';
|
|
@@ -30,7 +30,7 @@ export { checkMailAppsAllowlist } from './plan/index.js';
|
|
|
30
30
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
31
31
|
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './plan/index.js';
|
|
32
32
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
33
|
-
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './plan/index.js';
|
|
33
|
+
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
34
34
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
35
35
|
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './plan/index.js';
|
|
36
36
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './plan/index.js';
|
|
@@ -43,8 +43,7 @@ export type { LivekitCredentials } from './calls-sfu-keys.js';
|
|
|
43
43
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
44
44
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './plan/index.js';
|
|
45
45
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
|
|
46
|
-
export
|
|
47
|
-
export { MIGRATIONS_DIR, migrate } from './migrate.js';
|
|
46
|
+
export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
|
|
48
47
|
export type { BootstrapCompany, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
|
|
49
48
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
50
49
|
export type { RunnerDb } from './runner/db.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_MAIL_APPS, validateBuyerIdentity, validateHosting, validateMail, validateOwner, validateSite, } from './org.js';
|
|
1
|
+
export { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, appEntry, validateBuyerIdentity, validateHosting, validateMail, validateOwner, validateSite, } from './org.js';
|
|
2
2
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
3
3
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
4
4
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
@@ -13,14 +13,17 @@ export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
|
13
13
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
14
14
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
15
15
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
16
|
-
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './plan/index.js';
|
|
16
|
+
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
17
17
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
18
18
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
|
|
19
19
|
export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
|
|
20
20
|
export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
|
|
21
21
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
22
22
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
|
|
23
|
-
|
|
23
|
+
// The shipped migrations' directory; hosts apply them with `@wtfalch/db`'s
|
|
24
|
+
// `runMigrationSources`. Also at `@wtfalch/foundry/migrations-dir`, which
|
|
25
|
+
// imports nothing else.
|
|
26
|
+
export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
|
|
24
27
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
25
28
|
export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
|
|
26
29
|
export { createJobRunner } from './runner/runner.js';
|
|
@@ -164,7 +164,7 @@ create or replace function audit_erase_person(subject text, pseudonym text, subj
|
|
|
164
164
|
returns integer
|
|
165
165
|
language plpgsql
|
|
166
166
|
security definer
|
|
167
|
-
set search_path
|
|
167
|
+
set search_path from current
|
|
168
168
|
as $$
|
|
169
169
|
declare
|
|
170
170
|
touched integer;
|
|
@@ -102,7 +102,7 @@ create or replace function audit_erase_person(subject text, pseudonym text, subj
|
|
|
102
102
|
returns integer
|
|
103
103
|
language plpgsql
|
|
104
104
|
security definer
|
|
105
|
-
set search_path
|
|
105
|
+
set search_path from current
|
|
106
106
|
as $$
|
|
107
107
|
declare
|
|
108
108
|
touched integer;
|
|
@@ -145,7 +145,7 @@ create or replace function audit_seal_erasure(row_id bigint, erasure_hash_value
|
|
|
145
145
|
returns void
|
|
146
146
|
language plpgsql
|
|
147
147
|
security definer
|
|
148
|
-
set search_path
|
|
148
|
+
set search_path from current
|
|
149
149
|
as $$
|
|
150
150
|
begin
|
|
151
151
|
update audit_events
|
|
@@ -74,7 +74,7 @@ returns text
|
|
|
74
74
|
language sql
|
|
75
75
|
stable
|
|
76
76
|
security definer
|
|
77
|
-
set search_path
|
|
77
|
+
set search_path from current
|
|
78
78
|
as $$
|
|
79
79
|
select row_hash from audit_events order by id desc limit 1
|
|
80
80
|
$$;
|
|
@@ -88,7 +88,7 @@ returns table (id bigint, row_hash text, erased_at timestamptz)
|
|
|
88
88
|
language sql
|
|
89
89
|
stable
|
|
90
90
|
security definer
|
|
91
|
-
set search_path
|
|
91
|
+
set search_path from current
|
|
92
92
|
as $$
|
|
93
93
|
select e.id, e.row_hash, e.erased_at
|
|
94
94
|
from audit_events e
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- @wtfalch/foundry: an org's hosting as a list of entries, and today's one app
|
|
2
|
+
-- moved onto it as the entry keyed 'app' (org.ts's `HostingConfig.entries`).
|
|
3
|
+
-- Mirrors `src/store/schema.ts`'s `hostingEntries`.
|
|
4
|
+
--
|
|
5
|
+
-- Copies and never drops: hosting_database, hosting_git_branch,
|
|
6
|
+
-- hosting_build_pack, hosting_access_emails, hosting_app_env and
|
|
7
|
+
-- bespoke_app_repo stay for an older reader. Safe to run twice: an org that
|
|
8
|
+
-- already has entries is skipped, and a renamed id key is no longer there to
|
|
9
|
+
-- rename. An org with hosting off but an app id recorded (a parked app) gets
|
|
10
|
+
-- its entry too, so re-enabling it finds its settings.
|
|
11
|
+
--
|
|
12
|
+
-- A row the new validation (org.ts's `validateHostingEntry`) would refuse
|
|
13
|
+
-- makes the migration fail, naming every such org id and column at once, before
|
|
14
|
+
-- anything changes: a row that cannot be saved is worse found weeks later
|
|
15
|
+
-- than as a failed deploy. An empty branch becomes 'main', its default.
|
|
16
|
+
|
|
17
|
+
do $$
|
|
18
|
+
declare
|
|
19
|
+
bad text;
|
|
20
|
+
begin
|
|
21
|
+
select string_agg(format('%s: %s', o.id, c.problem), '; ' order by o.id)
|
|
22
|
+
into bad
|
|
23
|
+
from foundry_declared_orgs o
|
|
24
|
+
cross join lateral (values
|
|
25
|
+
('bespoke_app_repo ' || quote_literal(o.bespoke_app_repo) || ' is not a repo under wtfalch',
|
|
26
|
+
o.bespoke_app_repo is not null and o.bespoke_app_repo !~ '^wtfalch/[A-Za-z0-9._-]+$'),
|
|
27
|
+
('hosting_build_pack ' || quote_literal(o.hosting_build_pack) || ' is not available for an entry',
|
|
28
|
+
o.hosting_build_pack not in ('dockerfile', 'nixpacks', 'static')),
|
|
29
|
+
('hosting_database or hosting_access_emails is set while hosting_enabled is off',
|
|
30
|
+
not o.hosting_enabled and (o.hosting_database or o.hosting_access_emails <> '[]'::jsonb)),
|
|
31
|
+
('hosting_access_emails holds an invalid email',
|
|
32
|
+
exists (select 1 from jsonb_array_elements_text(o.hosting_access_emails) e
|
|
33
|
+
where e !~ '^[^@[:space:]]+@[^@[:space:]]+$')),
|
|
34
|
+
('hosting_app_env names a variable foundry sets itself, or an unknown source',
|
|
35
|
+
exists (select 1 from jsonb_each_text(o.hosting_app_env) e
|
|
36
|
+
where e.key = ''
|
|
37
|
+
or e.key ~ '^(APP_URL|LOG_DIR|OWNER_USER_ID|AUTH_.*|DATABASE_URL.*)$'
|
|
38
|
+
or e.value not in ('filesOrganisationId', 'aiOrganisationId', 'mailDomain')))
|
|
39
|
+
) as c(problem, refused)
|
|
40
|
+
where c.refused
|
|
41
|
+
-- to_jsonb, because the column does not exist yet on a first run.
|
|
42
|
+
and coalesce(to_jsonb(o) -> 'hosting_entries', '[]'::jsonb) = '[]'::jsonb
|
|
43
|
+
and (o.hosting_enabled or exists (
|
|
44
|
+
select 1 from foundry_applied_state a
|
|
45
|
+
where (a.service_ids -> ('coolify:app:' || a.org_id)) is not null
|
|
46
|
+
and a.org_id = o.id));
|
|
47
|
+
if bad is not null then
|
|
48
|
+
raise exception 'migration 0029: cannot move hosting onto an entry, org %', bad;
|
|
49
|
+
end if;
|
|
50
|
+
end $$;
|
|
51
|
+
|
|
52
|
+
alter table foundry_declared_orgs
|
|
53
|
+
add column if not exists hosting_entries jsonb not null default '[]'::jsonb;
|
|
54
|
+
|
|
55
|
+
update foundry_declared_orgs
|
|
56
|
+
set hosting_entries = jsonb_build_array(jsonb_build_object(
|
|
57
|
+
'key', 'app',
|
|
58
|
+
'repo', bespoke_app_repo,
|
|
59
|
+
'folder', null,
|
|
60
|
+
'branch', coalesce(nullif(hosting_git_branch, ''), 'main'),
|
|
61
|
+
'buildPack', hosting_build_pack,
|
|
62
|
+
'port', null,
|
|
63
|
+
'healthPath', null,
|
|
64
|
+
'database', hosting_database,
|
|
65
|
+
'autoDeploy', true,
|
|
66
|
+
'accessEmails', hosting_access_emails,
|
|
67
|
+
'env', coalesce(
|
|
68
|
+
(select jsonb_object_agg(e.key, jsonb_build_object('from', e.value))
|
|
69
|
+
from jsonb_each_text(hosting_app_env) as e),
|
|
70
|
+
'{}'::jsonb)
|
|
71
|
+
))
|
|
72
|
+
where hosting_entries = '[]'::jsonb
|
|
73
|
+
and (hosting_enabled or exists (
|
|
74
|
+
select 1 from foundry_applied_state a
|
|
75
|
+
where a.org_id = foundry_declared_orgs.id
|
|
76
|
+
and (a.service_ids -> ('coolify:app:' || a.org_id)) is not null));
|
|
77
|
+
|
|
78
|
+
-- The recorded ids and applied-env markers are one key per org today; the
|
|
79
|
+
-- entry key joins them so a second app cannot collide with the first. A key
|
|
80
|
+
-- is renamed only when its new form is absent; if both exist the new one
|
|
81
|
+
-- stays and the old one is left as it is. Without
|
|
82
|
+
-- the rename the planner would not find the app it already made and would
|
|
83
|
+
-- plan it again, restarting it. last_applied_at and
|
|
84
|
+
-- last_applied_declared_org_version are not touched.
|
|
85
|
+
update foundry_applied_state
|
|
86
|
+
set service_ids = service_ids
|
|
87
|
+
- ('coolify:app:' || org_id)
|
|
88
|
+
|| jsonb_build_object('coolify:app:' || org_id || ':app', service_ids -> ('coolify:app:' || org_id))
|
|
89
|
+
where (service_ids -> ('coolify:app:' || org_id)) is not null
|
|
90
|
+
and (service_ids -> ('coolify:app:' || org_id || ':app')) is null;
|
|
91
|
+
|
|
92
|
+
update foundry_applied_state
|
|
93
|
+
set service_ids = service_ids
|
|
94
|
+
- ('cloudflare:access:' || org_id)
|
|
95
|
+
|| jsonb_build_object('cloudflare:access:' || org_id || ':app', service_ids -> ('cloudflare:access:' || org_id))
|
|
96
|
+
where (service_ids -> ('cloudflare:access:' || org_id)) is not null
|
|
97
|
+
and (service_ids -> ('cloudflare:access:' || org_id || ':app')) is null;
|
|
98
|
+
|
|
99
|
+
update foundry_applied_state
|
|
100
|
+
set service_ids = service_ids - 'app_env_applied'
|
|
101
|
+
|| jsonb_build_object('app_env_applied.app', service_ids -> 'app_env_applied')
|
|
102
|
+
where (service_ids -> 'app_env_applied') is not null
|
|
103
|
+
and (service_ids -> 'app_env_applied.app') is null;
|
|
104
|
+
|
|
105
|
+
update foundry_applied_state
|
|
106
|
+
set service_ids = service_ids - 'db_env_applied'
|
|
107
|
+
|| jsonb_build_object('db_env_applied.app', service_ids -> 'db_env_applied')
|
|
108
|
+
where (service_ids -> 'db_env_applied') is not null
|
|
109
|
+
and (service_ids -> 'db_env_applied.app') is null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path of the directory holding this package's numbered `.sql`
|
|
3
|
+
* migrations. The host passes it to `runMigrationSources` as
|
|
4
|
+
* `{ name: 'foundry', dir: migrationsDir }`.
|
|
5
|
+
*
|
|
6
|
+
* Its own subpath (`@wtfalch/foundry/migrations-dir`) and no other imports: a
|
|
7
|
+
* migration image imports this without the rest of the package.
|
|
8
|
+
*/
|
|
9
|
+
export declare const migrationsDir: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
/**
|
|
3
|
+
* Absolute path of the directory holding this package's numbered `.sql`
|
|
4
|
+
* migrations. The host passes it to `runMigrationSources` as
|
|
5
|
+
* `{ name: 'foundry', dir: migrationsDir }`.
|
|
6
|
+
*
|
|
7
|
+
* Its own subpath (`@wtfalch/foundry/migrations-dir`) and no other imports: a
|
|
8
|
+
* migration image imports this without the rest of the package.
|
|
9
|
+
*/
|
|
10
|
+
export const migrationsDir = fileURLToPath(new URL('./migrations/', import.meta.url));
|
package/dist/org.d.ts
CHANGED
|
@@ -184,54 +184,85 @@ export interface IssuerApp {
|
|
|
184
184
|
*/
|
|
185
185
|
export type HostingBuildPack = 'dockerfile' | 'nixpacks' | 'static' | 'dockercompose';
|
|
186
186
|
/**
|
|
187
|
-
* The org's
|
|
187
|
+
* The org's hosted apps and what runs beside them (#71, part of #14's
|
|
188
188
|
* adopted/standard/bespoke map) -- distinct from the cms site (`SiteConfig`
|
|
189
189
|
* above), which is `@wtfalch/cms` content, not the org's own application
|
|
190
190
|
* code. `usesDomain` follows the same opt-in-per-service rule as mail, chat,
|
|
191
191
|
* calls, the issuer and the site (#7 amendment 1): setting `DeclaredOrg.domain`
|
|
192
192
|
* alone never moves the app onto it or starts managing its Cloudflare zone.
|
|
193
193
|
*
|
|
194
|
-
* `
|
|
195
|
-
*
|
|
194
|
+
* `enabled`, `usesDomain`, `tunnel` and `issuerApps` are the org's own: one
|
|
195
|
+
* park switch, one hostname rule, one tunnel and one ZITADEL project for all
|
|
196
|
+
* its apps. Each app is one of `entries`. `tunnel` is meaningless without the
|
|
197
|
+
* apps running, so `validateHosting` below requires `enabled` before it can
|
|
196
198
|
* be true.
|
|
197
199
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* declarable; those two strings are their defaults now, kept here so an org
|
|
201
|
-
* that never mentions them still gets exactly what shipped before.
|
|
200
|
+
* Until the planner reads more than one entry, only the entry keyed
|
|
201
|
+
* {@link APP_ENTRY_KEY} is planned.
|
|
202
202
|
*/
|
|
203
203
|
export interface HostingConfig {
|
|
204
|
-
/** The org's
|
|
204
|
+
/** The org's apps run on Coolify. */
|
|
205
205
|
enabled: boolean;
|
|
206
206
|
/** Served on `DeclaredOrg.domain` (and its Cloudflare zone managed) instead of `<slug>.oikos.host` (`../slug.ts`). */
|
|
207
207
|
usesDomain: boolean;
|
|
208
|
-
/**
|
|
209
|
-
database: boolean;
|
|
210
|
-
/** The app is reached through a Cloudflare tunnel. Requires `enabled`. */
|
|
208
|
+
/** The apps are reached through a Cloudflare tunnel. Requires `enabled`. */
|
|
211
209
|
tunnel: boolean;
|
|
210
|
+
/** The org's declared ZITADEL OIDC applications (#71). */
|
|
211
|
+
issuerApps: IssuerApp[];
|
|
212
|
+
/** One entry per app the org runs. */
|
|
213
|
+
entries: HostingEntry[];
|
|
214
|
+
}
|
|
215
|
+
/** The key of the entry that is today's one app: the Coolify app named `<slug>`. */
|
|
216
|
+
export declare const APP_ENTRY_KEY = "app";
|
|
217
|
+
/**
|
|
218
|
+
* One app the org runs. `database` and `accessEmails` are meaningless
|
|
219
|
+
* without `HostingConfig.enabled`, so `validateHosting` requires it before
|
|
220
|
+
* either is set.
|
|
221
|
+
*/
|
|
222
|
+
export interface HostingEntry {
|
|
223
|
+
/** A DNS label, unique per org. It goes into step ids and scope keys, so it never holds `:` or `.`. */
|
|
224
|
+
key: string;
|
|
225
|
+
/** The repo under `wtfalch` the app builds from. `null` is the shared standard app (`executor.ts`'s `DEFAULT_APP_REPO`). */
|
|
226
|
+
repo: string | null;
|
|
227
|
+
/** The app's folder in `repo`. `null` is the repo root. */
|
|
228
|
+
folder: string | null;
|
|
212
229
|
/** The branch `coolify:app` builds and deploys from. Default: `'main'`. */
|
|
213
|
-
|
|
230
|
+
branch: string;
|
|
214
231
|
/** The Coolify build strategy `coolify:app` provisions with. Default: `'nixpacks'`. */
|
|
215
232
|
buildPack: HostingBuildPack;
|
|
216
|
-
/** The
|
|
217
|
-
|
|
233
|
+
/** The port the app listens on. `null` is Coolify's default. */
|
|
234
|
+
port: number | null;
|
|
235
|
+
/** The path that answers 200 when the app is healthy. `null` is none. */
|
|
236
|
+
healthPath: string | null;
|
|
237
|
+
/** A Postgres database beside the app. At most one entry per org. */
|
|
238
|
+
database: boolean;
|
|
239
|
+
/** Deploy on every push to `branch`. Default: `true`. */
|
|
240
|
+
autoDeploy: boolean;
|
|
218
241
|
/**
|
|
219
242
|
* Emails Cloudflare Access lets through to the app's hostname (#84). Empty
|
|
220
|
-
* means no Access gate.
|
|
243
|
+
* means no Access gate. Only the entry keyed {@link APP_ENTRY_KEY} may set it.
|
|
221
244
|
*/
|
|
222
245
|
accessEmails: string[];
|
|
223
246
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* them, so `pushAppEnv` pushes nothing from this map.
|
|
247
|
+
* The app's own env var names and what they hold (#243). Each bespoke app
|
|
248
|
+
* names the values the plan mints (Files, AI, mail) differently (lokessmie
|
|
249
|
+
* uses `STORAGE_*`/`AI_*`); `{ from }` declares its own name for one. `{}`
|
|
250
|
+
* means the app names none of them, so `pushAppEnv` pushes nothing from
|
|
251
|
+
* this map.
|
|
230
252
|
*/
|
|
231
|
-
|
|
253
|
+
env: Record<string, EnvValue>;
|
|
232
254
|
}
|
|
233
255
|
/**
|
|
234
|
-
*
|
|
256
|
+
* A plain string is pushed as it is, so `validateHosting` refuses one that
|
|
257
|
+
* looks like a secret. `{ from }` names a value the plan already mints.
|
|
258
|
+
*/
|
|
259
|
+
export type EnvValue = string | {
|
|
260
|
+
from: AppEnvSource;
|
|
261
|
+
};
|
|
262
|
+
/** The org's `app` entry: today's one app. */
|
|
263
|
+
export declare function appEntry(hosting: HostingConfig): HostingEntry | undefined;
|
|
264
|
+
/**
|
|
265
|
+
* The closed set of values the plan already mints that `HostingEntry.env`
|
|
235
266
|
* may map an app's env var name to (#243): the org's own Files organisation
|
|
236
267
|
* id, its AI organisation id, and the mail domain its Lettermint project
|
|
237
268
|
* sends from. Each is read from `StepExecutionContext.recordedIds()` in
|
|
@@ -243,12 +274,13 @@ export declare const APP_ENV_SOURCES: ReadonlySet<AppEnvSource>;
|
|
|
243
274
|
* `null` when `hosting` is valid; otherwise the reason it is not (#71), same
|
|
244
275
|
* shape as `validateBuyerIdentity` below.
|
|
245
276
|
*
|
|
246
|
-
* - `
|
|
247
|
-
* without the app itself running.
|
|
248
|
-
* - `gitBranch` is non-empty and `buildPack` is one of `HostingBuildPack`'s
|
|
249
|
-
* four values.
|
|
277
|
+
* - `tunnel` requires `enabled` -- it means nothing without the apps running.
|
|
250
278
|
* - Every `issuerApps[]` entry has a non-empty `name`, unique within the
|
|
251
279
|
* list, and a `kind` that is one of `IssuerAppKind`'s three values.
|
|
280
|
+
* - `enabled` requires an entry keyed {@link APP_ENTRY_KEY}.
|
|
281
|
+
* - Every `entries[]` entry follows {@link validateHostingEntry}; keys are
|
|
282
|
+
* unique, and at most one entry has a `database`, because the one
|
|
283
|
+
* `<slug>_rt` runtime role cannot serve two apps.
|
|
252
284
|
*/
|
|
253
285
|
export declare function validateHosting(hosting: HostingConfig): string | null;
|
|
254
286
|
export declare function validateDomain(domain: string): string | null;
|