@wtfalch/foundry 0.67.0 → 0.68.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/build-json.d.ts +16 -1
- package/dist/build-json.js +21 -5
- package/dist/modules/assistants.js +2 -2
- package/dist/org.js +2 -2
- package/dist/plan/blue-green.d.ts +6 -0
- package/dist/plan/blue-green.js +7 -0
- package/dist/plan/diff.js +1 -0
- package/dist/plan/plan-apply.js +6 -1
- package/dist/plan/platform-providers.js +7 -0
- package/package.json +1 -1
package/dist/build-json.d.ts
CHANGED
|
@@ -5,6 +5,17 @@ import { type WorkerAppConfig } from './worker-app-config.js';
|
|
|
5
5
|
* app. Same shapes as `WorkerAppConfig`; `repo` and `commit` say where the build came from.
|
|
6
6
|
* Whether `artifactUrl` is an asset of that release is checked by the reader that fetched it.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* A container's image in a build.json: a registry image (`repository` and `digest`), or an asset of the
|
|
10
|
+
* release (`asset` is the file name, `sha256` its hex digest) that the reader resolves to a registry image.
|
|
11
|
+
*/
|
|
12
|
+
export type BuildJsonContainerImage = NonNullable<WorkerAppConfig['containers']>[number]['image'] | {
|
|
13
|
+
asset: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
};
|
|
16
|
+
export type BuildJsonContainer = Omit<NonNullable<WorkerAppConfig['containers']>[number], 'image'> & {
|
|
17
|
+
image: BuildJsonContainerImage;
|
|
18
|
+
};
|
|
8
19
|
export interface BuildJson {
|
|
9
20
|
repo: string;
|
|
10
21
|
commit: string;
|
|
@@ -13,7 +24,11 @@ export interface BuildJson {
|
|
|
13
24
|
compatibilityDate?: string;
|
|
14
25
|
compatibilityFlags?: 'nodejs_compat'[];
|
|
15
26
|
durableObjects: WorkerAppConfig['durableObjects'];
|
|
16
|
-
containers?:
|
|
27
|
+
containers?: BuildJsonContainer[];
|
|
28
|
+
/** Cloudflare cron triggers: at most 3 standard five-field cron expressions, each once. */
|
|
29
|
+
triggers?: {
|
|
30
|
+
crons: string[];
|
|
31
|
+
};
|
|
17
32
|
migrationsSha256?: string;
|
|
18
33
|
}
|
|
19
34
|
export type BuildJsonErrorCode = 'unknown_field' | 'missing_field' | 'wrong_type' | 'not_https' | 'bad_sha256';
|
package/dist/build-json.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { workerAppCronProblem } from './entry-rules.js';
|
|
2
3
|
import { ENTRY_IMAGE_REPOSITORY } from './org.js';
|
|
3
|
-
import { WORKER_APP_COMPATIBILITY_FLAGS, WORKER_APP_INSTANCE_TYPES, WORKER_APP_MAX_INSTANCES, } from './worker-app-config.js';
|
|
4
|
+
import { WORKER_APP_COMPATIBILITY_FLAGS, WORKER_APP_CRONS_MAX, WORKER_APP_INSTANCE_TYPES, WORKER_APP_MAX_INSTANCES, } from './worker-app-config.js';
|
|
4
5
|
/** Thrown by {@link parseBuildJson}. `path` is dotted into the build.json; the message never carries a value. */
|
|
5
6
|
export class BuildJsonError extends Error {
|
|
6
7
|
code;
|
|
@@ -39,15 +40,29 @@ const durableObjects = z.strictObject({
|
|
|
39
40
|
});
|
|
40
41
|
const container = z.strictObject({
|
|
41
42
|
className: z.string(),
|
|
42
|
-
image: z.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
image: z.union([
|
|
44
|
+
z.strictObject({
|
|
45
|
+
repository: z.string(),
|
|
46
|
+
digest: z.string().regex(DIGEST, { message: 'wrong_type' }),
|
|
47
|
+
}),
|
|
48
|
+
z.strictObject({ asset: text, sha256 }),
|
|
49
|
+
]),
|
|
46
50
|
instanceType: z.string().refine((v) => WORKER_APP_INSTANCE_TYPES.has(v), {
|
|
47
51
|
message: 'wrong_type',
|
|
48
52
|
}),
|
|
49
53
|
maxInstances: z.number().int().min(1).max(WORKER_APP_MAX_INSTANCES),
|
|
50
54
|
});
|
|
55
|
+
const cron = z.string().superRefine((value, ctx) => {
|
|
56
|
+
if (workerAppCronProblem(value) !== null)
|
|
57
|
+
ctx.addIssue({ code: 'custom', message: 'wrong_type' });
|
|
58
|
+
});
|
|
59
|
+
const triggers = z
|
|
60
|
+
.strictObject({ crons: z.array(cron).max(WORKER_APP_CRONS_MAX) })
|
|
61
|
+
.superRefine((value, ctx) => {
|
|
62
|
+
if (new Set(value.crons).size !== value.crons.length) {
|
|
63
|
+
ctx.addIssue({ code: 'custom', path: ['crons'], message: 'wrong_type' });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
51
66
|
export const buildJsonSchema = z.strictObject({
|
|
52
67
|
repo: text,
|
|
53
68
|
commit: text,
|
|
@@ -59,6 +74,7 @@ export const buildJsonSchema = z.strictObject({
|
|
|
59
74
|
.optional(),
|
|
60
75
|
durableObjects,
|
|
61
76
|
containers: z.array(container).optional(),
|
|
77
|
+
triggers: triggers.optional(),
|
|
62
78
|
migrationsSha256: sha256.optional(),
|
|
63
79
|
});
|
|
64
80
|
function valueAt(input, path) {
|
|
@@ -39,8 +39,8 @@ export const assistants = {
|
|
|
39
39
|
},
|
|
40
40
|
check(values, modules) {
|
|
41
41
|
const signIn = modules['sign-in'];
|
|
42
|
-
if (signIn?.users !== 'customers'
|
|
43
|
-
return problem(null, 'Help for AI assistants needs Sign-in for your customers
|
|
42
|
+
if (signIn?.users !== 'customers') {
|
|
43
|
+
return problem(null, 'Help for AI assistants needs Sign-in for your customers: each customer connects their own assistant. In Sign-in, choose "Your customers". They can sign up themselves or be invited.');
|
|
44
44
|
}
|
|
45
45
|
const onboarding = text(values, 'onboardingPath');
|
|
46
46
|
if (onboarding !== undefined && !isPagePath(onboarding)) {
|
package/dist/org.js
CHANGED
|
@@ -922,8 +922,8 @@ function validateEntryAuth(entry, hosting, domain) {
|
|
|
922
922
|
return refusal('auth_needs_exact_hostname', `hosting.entries.${entry.key}.hostnames`, `${at} needs an exact hostname to sign in at`);
|
|
923
923
|
}
|
|
924
924
|
if (auth.mcp !== undefined) {
|
|
925
|
-
if (auth.users !== 'customers'
|
|
926
|
-
return refusal('mcp_needs_open_customers', `hosting.entries.${entry.key}.uses.auth.mcp`, `${at} mcp needs users 'customers'
|
|
925
|
+
if (auth.users !== 'customers') {
|
|
926
|
+
return refusal('mcp_needs_open_customers', `hosting.entries.${entry.key}.uses.auth.mcp`, `${at} mcp needs users 'customers': an MCP client signs the customer in, and registration may be open or closed`);
|
|
927
927
|
}
|
|
928
928
|
if (typeof auth.mcp?.path !== 'string' ||
|
|
929
929
|
!MCP_PATH.test(auth.mcp.path) ||
|
|
@@ -379,6 +379,12 @@ export interface PlanBlueGreenInput {
|
|
|
379
379
|
probes: boolean;
|
|
380
380
|
access: boolean;
|
|
381
381
|
};
|
|
382
|
+
/**
|
|
383
|
+
* The platform's own company (`DeclaredPlatform.ownerOrgId`). Boule and Agora are our own products,
|
|
384
|
+
* so they plan blue/green only for this company (#1019); any other company keeps them as one plain
|
|
385
|
+
* app instead. Absent: not gated, for a caller with no platform in scope.
|
|
386
|
+
*/
|
|
387
|
+
ownerOrgId?: string | null;
|
|
382
388
|
}
|
|
383
389
|
/**
|
|
384
390
|
* Plans the blue/green parts of an org: per usable server, one structure step (the apps, the balancer, the
|
package/dist/plan/blue-green.js
CHANGED
|
@@ -589,6 +589,13 @@ export function planBlueGreen(input) {
|
|
|
589
589
|
description: `${org.slug}'s ${id} app blue/green is blocked, not planned: ${why}`,
|
|
590
590
|
key: `bg:${id}:${part}`,
|
|
591
591
|
});
|
|
592
|
+
if (input.ownerOrgId !== undefined &&
|
|
593
|
+
builtin &&
|
|
594
|
+
(id === 'boule' || id === 'agora') &&
|
|
595
|
+
org.id !== input.ownerOrgId) {
|
|
596
|
+
blocked("only the platform's own company runs Boule and Agora blue/green; it runs as one plain app", 'owner');
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
592
599
|
if (entry?.buildPack === 'dockercompose')
|
|
593
600
|
continue;
|
|
594
601
|
if (colorAppName(subject.base, 'green').length > MAX_NAME) {
|
package/dist/plan/diff.js
CHANGED
|
@@ -571,6 +571,7 @@ export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost
|
|
|
571
571
|
servers: options.blueGreenServers,
|
|
572
572
|
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
573
573
|
accessProviderId: platformReadiness.accessProviderId ?? null,
|
|
574
|
+
ownerOrgId: platformReadiness.platformOwnerOrgId,
|
|
574
575
|
...(options.stageSinkDown ? { stageSinkDown: true } : {}),
|
|
575
576
|
...(options.stageGuardPorts ? { stageGuardPorts: options.stageGuardPorts } : {}),
|
|
576
577
|
appPlanned: (entryKey) => plannedSteps.some((p) => p.action.system === 'coolify' &&
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1179,7 +1179,12 @@ export class FoundryPlanApply {
|
|
|
1179
1179
|
const primaryUuid = context.recordedIds()[PRIMARY_SERVER_SCOPE_KEY];
|
|
1180
1180
|
if (!store || !this.executor.releaseHost || !primaryUuid)
|
|
1181
1181
|
return out;
|
|
1182
|
-
|
|
1182
|
+
// #1019: Boule and Agora are our own products, so their blue/green release only runs for the
|
|
1183
|
+
// platform's own company; any other company's declaration was never planned as a structure, and a
|
|
1184
|
+
// leftover one is not run here either.
|
|
1185
|
+
const declaredPlatform = await this.platformDeps?.store.get();
|
|
1186
|
+
const ownerOrgId = declaredPlatform?.ownerOrgId ?? null;
|
|
1187
|
+
const subjects = blueGreenSubjects(org, context.recordedIds()).filter((s) => (s.builtin !== 'boule' && s.builtin !== 'agora') || ownerOrgId === org.id);
|
|
1183
1188
|
// A copy waits while any subject of the org has a release running: the runner must stay free to roll back.
|
|
1184
1189
|
if (copies === 'claim' && releaseRunning(subjects, context.recordedIds()))
|
|
1185
1190
|
return out;
|
|
@@ -2147,6 +2147,13 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2147
2147
|
const heldMailKey = await deps.heldKeys?.resolve(target.refs.brokerMailKey);
|
|
2148
2148
|
if (heldMailKey)
|
|
2149
2149
|
runtime.onSecret?.(heldMailKey);
|
|
2150
|
+
// Auth 0.35.0: the login app mails verification and invite codes itself (it throws without
|
|
2151
|
+
// `LETTERMINT_API_TOKEN` and sends nothing without `LOGIN_MAIL_FROM`). Both the login and the
|
|
2152
|
+
// in-app login app read `loginEnvs`, so both get the held key and this stack's sender address.
|
|
2153
|
+
if (heldMailKey)
|
|
2154
|
+
loginEnvs.push({ key: 'LETTERMINT_API_TOKEN', value: heldMailKey });
|
|
2155
|
+
if (input.smtpSender)
|
|
2156
|
+
loginEnvs.push({ key: 'LOGIN_MAIL_FROM', value: input.smtpSender });
|
|
2150
2157
|
// `AUTH_APPS` and `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` are not pushed here
|
|
2151
2158
|
// -- this function's own doc comment, "Known gaps". `LETTERMINT_API_TOKEN`
|
|
2152
2159
|
// is, once held (#210): pushing it costs nothing even while the broker
|