@wtfalch/foundry 0.62.0 → 0.64.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/apps/index.d.ts +1 -0
- package/dist/apps/index.js +1 -0
- package/dist/apps/server-entry.d.ts +43 -0
- package/dist/apps/server-entry.js +99 -0
- package/dist/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/build-json.d.ts +25 -0
- package/dist/build-json.js +61 -0
- package/dist/catalogue/copy-en.js +10 -0
- package/dist/catalogue/integrations.js +10 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/customer-grants.d.ts +3 -0
- package/dist/modules/customer-grants.js +28 -0
- package/dist/modules/index.js +49 -1
- package/dist/modules/repository.d.ts +6 -0
- package/dist/modules/repository.js +40 -0
- package/dist/modules/types.d.ts +8 -0
- package/dist/native-build.d.ts +7 -27
- package/dist/native-build.js +7 -27
- package/dist/org.d.ts +19 -1
- package/dist/org.js +45 -3
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +61 -2
- package/dist/plan/blue-green.js +80 -6
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +21 -2
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +7 -0
- package/dist/plan/executor.d.ts +71 -1
- package/dist/plan/executor.js +491 -56
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +23 -1
- package/dist/plan/platform-diff.js +96 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +156 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
package/dist/apps/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { type ApplyWorkerAppMigrationsDeps, type ApplyWorkerAppMigrationsInput,
|
|
|
7
7
|
export { type ReleaseAsset, ReleaseAssetError, fetchReleaseAsset, parseAppRepo, releaseAssetOf, } from './github.js';
|
|
8
8
|
export { type WorkerAppMigrationRunner, migrationsUrl } from '../worker-app-migrations.js';
|
|
9
9
|
export { APPS_CLOUDFLARE_TOKEN_PERMISSIONS, type AppsCloudflare, type HyperdriveConnection, } from './cloudflare.js';
|
|
10
|
+
export { type ServerEntryContext, ServerEntryError, processUrlEnvName, serverEntriesOf, } from './server-entry.js';
|
package/dist/apps/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { RELEASE_MIGRATION_SOURCE, applyWorkerAppMigrations, migrationChecksum,
|
|
|
7
7
|
export { ReleaseAssetError, fetchReleaseAsset, parseAppRepo, releaseAssetOf, } from './github.js';
|
|
8
8
|
export { migrationsUrl } from '../worker-app-migrations.js';
|
|
9
9
|
export { APPS_CLOUDFLARE_TOKEN_PERMISSIONS, } from './cloudflare.js';
|
|
10
|
+
export { ServerEntryError, processUrlEnvName, serverEntriesOf, } from './server-entry.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ServerBuildJson } from '../build-json.js';
|
|
2
|
+
import { type AppManifest, type AppModules } from '../modules/index.js';
|
|
3
|
+
import { type EnvValue, type HostingEntry } from '../org.js';
|
|
4
|
+
/** Thrown by {@link serverEntriesOf}. The message is plain words for the company and never carries a secret. */
|
|
5
|
+
export declare class ServerEntryError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** What {@link serverEntriesOf} needs to know about the company and where the app runs. */
|
|
9
|
+
export interface ServerEntryContext {
|
|
10
|
+
/** The company's slug: an entry reaches another on the company network as `<slug>-<entry key>` (`<slug>` for `app`). */
|
|
11
|
+
slug: string;
|
|
12
|
+
/** The platform registry host (`registry.<platform domain>`), with a port when it has one. */
|
|
13
|
+
registryHost: string;
|
|
14
|
+
/** The app's repository, `<github owner>/<repo>`. The images must sit at `<registryHost>/<owner>/<repo>[-<process>]`, lowercased. */
|
|
15
|
+
repo: string;
|
|
16
|
+
/** The entry key of the app. Default `app`. A process entry is keyed `<appKey>-<process key>`. */
|
|
17
|
+
appKey?: string;
|
|
18
|
+
/** The names the app entry is served at (see `HostingEntry.hostnames`). Processes are internal. */
|
|
19
|
+
hostnames: string[];
|
|
20
|
+
/** The port the app listens on. */
|
|
21
|
+
port: number;
|
|
22
|
+
healthPath?: string | null;
|
|
23
|
+
/** The app copy's own public origin (`https://host`), which each process gets as `APP_URL` to call the app back. */
|
|
24
|
+
appUrl: string;
|
|
25
|
+
/** The manifest `values` as env, resolved by the caller. Every entry gets the same map. */
|
|
26
|
+
env?: Record<string, EnvValue>;
|
|
27
|
+
/** The modules with the company's answers applied (`checkManifestAnswers`). Default: the manifest's own. */
|
|
28
|
+
modules?: AppModules;
|
|
29
|
+
}
|
|
30
|
+
/** `PROCESS_<KEY>_URL`: the key uppercased, `-` as `_`. */
|
|
31
|
+
export declare const processUrlEnvName: (key: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* The hosting entries of one server app: the `app` entry and one internal entry per declared process, each
|
|
34
|
+
* running its image from `build`. Throws {@link ServerEntryError} in plain words when the build does not match
|
|
35
|
+
* the manifest, when an image is not at the app's own registry path (a company never points at another's
|
|
36
|
+
* image), or when a module refuses the entry.
|
|
37
|
+
*
|
|
38
|
+
* - The app entry is served at `ctx.hostnames`, gets the modules, `ctx.env`, and `PROCESS_<KEY>_URL` for each
|
|
39
|
+
* process that has a port (`http://<slug>-<entry key>:<port>`).
|
|
40
|
+
* - A process entry has no hostnames. It gets the same modules and `ctx.env`, and `APP_URL`. Its `database`
|
|
41
|
+
* stays false: foundry allows one database entry per company, so a process cannot hold a second one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function serverEntriesOf(manifest: AppManifest, build: ServerBuildJson, ctx: ServerEntryContext): HostingEntry[];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { checkServerBuild } from '../build-json.js';
|
|
2
|
+
import { applyModules } from '../modules/index.js';
|
|
3
|
+
import { APP_ENTRY_KEY, ENTRY_IMAGE_REPOSITORY } from '../org.js';
|
|
4
|
+
/** Thrown by {@link serverEntriesOf}. The message is plain words for the company and never carries a secret. */
|
|
5
|
+
export class ServerEntryError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'ServerEntryError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const HOST = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[0-9]{1,5})?$/;
|
|
12
|
+
const SLUG = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
13
|
+
const OWNER_REPO = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\/[A-Za-z0-9._-]+$/;
|
|
14
|
+
const ENTRY_KEY_MAX = 63;
|
|
15
|
+
/** `PROCESS_<KEY>_URL`: the key uppercased, `-` as `_`. */
|
|
16
|
+
export const processUrlEnvName = (key) => `PROCESS_${key.toUpperCase().replace(/-/g, '_')}_URL`;
|
|
17
|
+
const refuse = (message) => {
|
|
18
|
+
throw new ServerEntryError(message);
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The hosting entries of one server app: the `app` entry and one internal entry per declared process, each
|
|
22
|
+
* running its image from `build`. Throws {@link ServerEntryError} in plain words when the build does not match
|
|
23
|
+
* the manifest, when an image is not at the app's own registry path (a company never points at another's
|
|
24
|
+
* image), or when a module refuses the entry.
|
|
25
|
+
*
|
|
26
|
+
* - The app entry is served at `ctx.hostnames`, gets the modules, `ctx.env`, and `PROCESS_<KEY>_URL` for each
|
|
27
|
+
* process that has a port (`http://<slug>-<entry key>:<port>`).
|
|
28
|
+
* - A process entry has no hostnames. It gets the same modules and `ctx.env`, and `APP_URL`. Its `database`
|
|
29
|
+
* stays false: foundry allows one database entry per company, so a process cannot hold a second one.
|
|
30
|
+
*/
|
|
31
|
+
export function serverEntriesOf(manifest, build, ctx) {
|
|
32
|
+
if (manifest.runtime !== 'server')
|
|
33
|
+
refuse('This app does not run on a server.');
|
|
34
|
+
const processes = manifest.processes ?? [];
|
|
35
|
+
const problem = checkServerBuild(build, processes);
|
|
36
|
+
if (problem)
|
|
37
|
+
refuse(`The build does not match the app: ${problem}`);
|
|
38
|
+
const appKey = ctx.appKey ?? APP_ENTRY_KEY;
|
|
39
|
+
if (!SLUG.test(ctx.slug) || !HOST.test(ctx.registryHost) || !OWNER_REPO.test(ctx.repo)) {
|
|
40
|
+
refuse('The company name, registry or repository for this app is not written the way it must be.');
|
|
41
|
+
}
|
|
42
|
+
if (!/^https:\/\/[^/\s]+$/.test(ctx.appUrl)) {
|
|
43
|
+
refuse('The app address must be an https address with no path.');
|
|
44
|
+
}
|
|
45
|
+
const [owner = '', name = ''] = ctx.repo.split('/');
|
|
46
|
+
const base = `${ctx.registryHost}/${owner.toLowerCase()}/${name.toLowerCase()}`;
|
|
47
|
+
const imageFor = (processKey, suffix) => {
|
|
48
|
+
const image = build.images.find((i) => i.process === processKey);
|
|
49
|
+
if (!image)
|
|
50
|
+
return refuse(`The build has no image for "${processKey}".`);
|
|
51
|
+
// Exactly the app's own path: another owner's, another repo's or another host's image is never run.
|
|
52
|
+
if (image.repository !== `${base}${suffix}` || !ENTRY_IMAGE_REPOSITORY.test(image.repository)) {
|
|
53
|
+
return refuse(`The image for "${processKey}" is not at this app's own place in the registry, so it is not used.`);
|
|
54
|
+
}
|
|
55
|
+
return { repository: image.repository, digest: image.digest };
|
|
56
|
+
};
|
|
57
|
+
const internalName = (entryKey) => entryKey === APP_ENTRY_KEY ? ctx.slug : `${ctx.slug}-${entryKey}`;
|
|
58
|
+
const modules = ctx.modules ?? manifest.modules;
|
|
59
|
+
const make = (key, image, port, hostnames, env, healthPath) => {
|
|
60
|
+
if (key.length > ENTRY_KEY_MAX)
|
|
61
|
+
refuse(`The name "${key}" is too long.`);
|
|
62
|
+
const base = {
|
|
63
|
+
key,
|
|
64
|
+
// The app's source, as metadata: the image is what runs, and `uses.repo` makes its deploy key from this.
|
|
65
|
+
repo: ctx.repo,
|
|
66
|
+
folder: null,
|
|
67
|
+
branch: 'main',
|
|
68
|
+
buildPack: 'nixpacks',
|
|
69
|
+
port,
|
|
70
|
+
healthPath,
|
|
71
|
+
hostnames,
|
|
72
|
+
database: false,
|
|
73
|
+
autoDeploy: false,
|
|
74
|
+
gates: [],
|
|
75
|
+
env: { ...(ctx.env ?? {}), ...env },
|
|
76
|
+
...(image ? { image } : {}),
|
|
77
|
+
};
|
|
78
|
+
return base;
|
|
79
|
+
};
|
|
80
|
+
const processEntryKey = (processKey) => `${appKey}-${processKey}`;
|
|
81
|
+
const processEnv = {};
|
|
82
|
+
for (const p of processes) {
|
|
83
|
+
if (p.port !== undefined) {
|
|
84
|
+
processEnv[processUrlEnvName(p.key)] =
|
|
85
|
+
`http://${internalName(processEntryKey(p.key))}:${p.port}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const draft = [
|
|
89
|
+
make(appKey, imageFor('app', ''), ctx.port, ctx.hostnames, processEnv, ctx.healthPath ?? null),
|
|
90
|
+
...processes.map((p) => make(processEntryKey(p.key), imageFor(p.key, `-${p.key}`), p.port ?? null, [], { APP_URL: ctx.appUrl }, null)),
|
|
91
|
+
];
|
|
92
|
+
return draft.map((entry, index) => {
|
|
93
|
+
const applied = applyModules(entry, modules, { runtime: 'server' });
|
|
94
|
+
if (!applied.ok)
|
|
95
|
+
return refuse(applied.message);
|
|
96
|
+
// One database entry per company: a process reaches the app's, never a second one.
|
|
97
|
+
return index === 0 ? applied.entry : { ...applied.entry, database: false };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -53,6 +53,7 @@ const SERVICES_ENABLED_BY_DEFAULT = new Set([
|
|
|
53
53
|
'archon',
|
|
54
54
|
'edge',
|
|
55
55
|
'hc',
|
|
56
|
+
'registry',
|
|
56
57
|
]);
|
|
57
58
|
/**
|
|
58
59
|
* The services every company deploys its own copy of. Their platform entries
|
|
@@ -153,7 +154,9 @@ export function declaredPlatformFor(apex, serverIpv4) {
|
|
|
153
154
|
? DEFAULT_MAIL_IMAGE_TAG
|
|
154
155
|
: key === 'hc'
|
|
155
156
|
? DEFAULT_HC_IMAGE_TAG
|
|
156
|
-
:
|
|
157
|
+
: key === 'registry'
|
|
158
|
+
? VERSIONS.registry
|
|
159
|
+
: DEFAULT_GIT_REF,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
162
|
return {
|
|
@@ -12,6 +12,8 @@ export interface PinnedVersions {
|
|
|
12
12
|
cloudflared: string;
|
|
13
13
|
stalwart: string;
|
|
14
14
|
hc: string;
|
|
15
|
+
/** The platform's image registry (`registry:3`, Docker's `distribution`). */
|
|
16
|
+
registry: string;
|
|
15
17
|
/** The mail relay's base image (an exact Alpine release); Postfix comes from its package repository. */
|
|
16
18
|
alpine: string;
|
|
17
19
|
/** A static busybox, copied into the MAS image so its health check has an HTTP client. */
|
package/dist/build-json.d.ts
CHANGED
|
@@ -26,3 +26,28 @@ export declare class BuildJsonError extends Error {
|
|
|
26
26
|
export declare const buildJsonSchema: z.ZodType<BuildJson>;
|
|
27
27
|
/** Parses a build.json. Throws {@link BuildJsonError} for the first problem. */
|
|
28
28
|
export declare function parseBuildJson(input: unknown): BuildJson;
|
|
29
|
+
/** One image of a server app's build: `process` is `app` or the key of a declared helper process. */
|
|
30
|
+
export interface ServerBuildImage {
|
|
31
|
+
process: string;
|
|
32
|
+
repository: string;
|
|
33
|
+
digest: `sha256:${string}`;
|
|
34
|
+
}
|
|
35
|
+
/** The `build.json` of a server app: the images its GitHub built, chosen by `runtime: 'server'`. */
|
|
36
|
+
export interface ServerBuildJson {
|
|
37
|
+
runtime: 'server';
|
|
38
|
+
repo: string;
|
|
39
|
+
commit: string;
|
|
40
|
+
images: ServerBuildImage[];
|
|
41
|
+
}
|
|
42
|
+
export declare const serverBuildJsonSchema: z.ZodType<ServerBuildJson>;
|
|
43
|
+
/** Parses a server build.json. Throws {@link BuildJsonError} for the first problem; never carries a value. */
|
|
44
|
+
export declare function parseServerBuildJson(input: unknown): ServerBuildJson;
|
|
45
|
+
/** Parses either shape: `runtime: 'server'` picks the server one, anything else the Worker one. */
|
|
46
|
+
export declare function parseAnyBuildJson(input: unknown): BuildJson | ServerBuildJson;
|
|
47
|
+
/**
|
|
48
|
+
* Null when the build holds exactly one `app` image and one image per declared process, no more.
|
|
49
|
+
* Else a plain message naming the process key, never an image value.
|
|
50
|
+
*/
|
|
51
|
+
export declare function checkServerBuild(build: ServerBuildJson, processes: readonly {
|
|
52
|
+
key: string;
|
|
53
|
+
}[]): string | null;
|
package/dist/build-json.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { ENTRY_IMAGE_REPOSITORY } from './org.js';
|
|
2
3
|
import { WORKER_APP_COMPATIBILITY_FLAGS, WORKER_APP_INSTANCE_TYPES, WORKER_APP_MAX_INSTANCES, } from './worker-app-config.js';
|
|
3
4
|
/** Thrown by {@link parseBuildJson}. `path` is dotted into the build.json; the message never carries a value. */
|
|
4
5
|
export class BuildJsonError extends Error {
|
|
@@ -93,3 +94,63 @@ export function parseBuildJson(input) {
|
|
|
93
94
|
throw new BuildJsonError('missing_field', path);
|
|
94
95
|
throw new BuildJsonError('wrong_type', path);
|
|
95
96
|
}
|
|
97
|
+
const IMAGE_REPOSITORY = ENTRY_IMAGE_REPOSITORY;
|
|
98
|
+
export const serverBuildJsonSchema = z.strictObject({
|
|
99
|
+
runtime: z.literal('server'),
|
|
100
|
+
repo: text,
|
|
101
|
+
commit: text,
|
|
102
|
+
images: z.array(z.strictObject({
|
|
103
|
+
process: z.string().regex(/^[a-z][a-z0-9-]{0,30}$/, { message: 'wrong_type' }),
|
|
104
|
+
repository: z.string().regex(IMAGE_REPOSITORY, { message: 'wrong_type' }),
|
|
105
|
+
digest: z.string().regex(DIGEST, { message: 'wrong_type' }),
|
|
106
|
+
})),
|
|
107
|
+
});
|
|
108
|
+
function refuseBuildJson(input, error) {
|
|
109
|
+
const issue = error.issues[0];
|
|
110
|
+
if (!issue)
|
|
111
|
+
throw new BuildJsonError('wrong_type', '');
|
|
112
|
+
if (issue.code === 'unrecognized_keys') {
|
|
113
|
+
const base = dotted(issue.path);
|
|
114
|
+
const key = issue.keys[0] ?? '';
|
|
115
|
+
throw new BuildJsonError('unknown_field', base ? `${base}.${key}` : key);
|
|
116
|
+
}
|
|
117
|
+
const path = dotted(issue.path);
|
|
118
|
+
if (valueAt(input, issue.path) === undefined)
|
|
119
|
+
throw new BuildJsonError('missing_field', path);
|
|
120
|
+
throw new BuildJsonError('wrong_type', path);
|
|
121
|
+
}
|
|
122
|
+
/** Parses a server build.json. Throws {@link BuildJsonError} for the first problem; never carries a value. */
|
|
123
|
+
export function parseServerBuildJson(input) {
|
|
124
|
+
const result = serverBuildJsonSchema.safeParse(input);
|
|
125
|
+
if (result.success)
|
|
126
|
+
return result.data;
|
|
127
|
+
return refuseBuildJson(input, result.error);
|
|
128
|
+
}
|
|
129
|
+
/** Parses either shape: `runtime: 'server'` picks the server one, anything else the Worker one. */
|
|
130
|
+
export function parseAnyBuildJson(input) {
|
|
131
|
+
const runtime = input !== null && typeof input === 'object'
|
|
132
|
+
? input.runtime
|
|
133
|
+
: undefined;
|
|
134
|
+
return runtime === 'server' ? parseServerBuildJson(input) : parseBuildJson(input);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Null when the build holds exactly one `app` image and one image per declared process, no more.
|
|
138
|
+
* Else a plain message naming the process key, never an image value.
|
|
139
|
+
*/
|
|
140
|
+
export function checkServerBuild(build, processes) {
|
|
141
|
+
const wanted = new Set(['app', ...processes.map((p) => p.key)]);
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
for (const image of build.images) {
|
|
144
|
+
if (!wanted.has(image.process)) {
|
|
145
|
+
return `the build has an image for "${image.process}", which the app does not declare.`;
|
|
146
|
+
}
|
|
147
|
+
if (seen.has(image.process))
|
|
148
|
+
return `the build has two images for "${image.process}".`;
|
|
149
|
+
seen.add(image.process);
|
|
150
|
+
}
|
|
151
|
+
for (const key of wanted) {
|
|
152
|
+
if (!seen.has(key))
|
|
153
|
+
return `the build has no image for "${key}".`;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
@@ -82,6 +82,16 @@ const INTEGRATION_COPY = {
|
|
|
82
82
|
summary: 'Companies sign their own people in to the app.',
|
|
83
83
|
ours: 'Run by us',
|
|
84
84
|
},
|
|
85
|
+
'customer-grants': {
|
|
86
|
+
name: 'Permissions for customers',
|
|
87
|
+
summary: 'Your team gives each customer the permissions they need in the app, and we keep them in step.',
|
|
88
|
+
ours: 'Run by us',
|
|
89
|
+
},
|
|
90
|
+
repository: {
|
|
91
|
+
name: 'Repository',
|
|
92
|
+
summary: 'The app reads or updates the files in its repository, such as the pages it keeps there.',
|
|
93
|
+
ours: 'Run by us',
|
|
94
|
+
},
|
|
85
95
|
};
|
|
86
96
|
const SHARED_OUTPUTS = {
|
|
87
97
|
app: { url: "The app's web address.", host: "The app's host name.", name: "The app's name." },
|
|
@@ -81,6 +81,16 @@ const EXTRA = {
|
|
|
81
81
|
price: { class: 'free', meters: [] },
|
|
82
82
|
advanced: [],
|
|
83
83
|
},
|
|
84
|
+
'customer-grants': {
|
|
85
|
+
providers: [ours('customer-grants')],
|
|
86
|
+
price: { class: 'free', meters: [] },
|
|
87
|
+
advanced: [],
|
|
88
|
+
},
|
|
89
|
+
repository: {
|
|
90
|
+
providers: [ours('repository')],
|
|
91
|
+
price: { class: 'free', meters: [] },
|
|
92
|
+
advanced: [],
|
|
93
|
+
},
|
|
84
94
|
};
|
|
85
95
|
/** Every integration, one per app module, in the order modules are applied. */
|
|
86
96
|
export function integrations() {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,15 +8,15 @@ import { type PinnedVersions } from './bootstrap/versions.js';
|
|
|
8
8
|
export type { PinnedVersions };
|
|
9
9
|
export declare const PINNED_VERSIONS: Readonly<PinnedVersions>;
|
|
10
10
|
export type { RequestableField, CallsChange, CustomersChange, StorageLimitChange, AiLimitChange, DomainChange, MailTierChange, RequestedChangeValue, ChangeClassification, RequestedChangeState, RequestedChange, AppChange, BuildChange, FailureCode, RequestedBy, } from './requested-changes.js';
|
|
11
|
-
export type { BuildJson, BuildJsonErrorCode } from './build-json.js';
|
|
12
|
-
export { buildJsonSchema, parseBuildJson, BuildJsonError } from './build-json.js';
|
|
11
|
+
export type { BuildJson, BuildJsonErrorCode, ServerBuildImage, ServerBuildJson, } from './build-json.js';
|
|
12
|
+
export { buildJsonSchema, parseBuildJson, BuildJsonError, serverBuildJsonSchema, parseServerBuildJson, parseAnyBuildJson, checkServerBuild, } from './build-json.js';
|
|
13
13
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
14
14
|
export type { SlugRejectionReason, SlugRejection } from './slug.js';
|
|
15
15
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
16
16
|
export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
|
|
17
17
|
export type { PlanStep, UnmanagedItem, PlanNote, Plan, VersionChangeItem } from './plan.js';
|
|
18
|
-
export type { NativeBuildDispatchDeps
|
|
19
|
-
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild,
|
|
18
|
+
export type { NativeBuildDispatchDeps } from './native-build.js';
|
|
19
|
+
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, } from './native-build.js';
|
|
20
20
|
export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
|
|
21
21
|
export { StaleLeaseError } from './store.js';
|
|
22
22
|
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
@@ -40,6 +40,8 @@ export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError
|
|
|
40
40
|
export type { StageCopyInput, StageCopyPort, StageCopyProduction, StageCopyRunRecord, UndoStageCopyInput, } from './plan/index.js';
|
|
41
41
|
export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './plan/index.js';
|
|
42
42
|
export type { Runner, StageCopyPortDeps } from './plan/index.js';
|
|
43
|
+
export type { PlatformRegistryDeps, PlatformRegistryRuntime } from './plan/index.js';
|
|
44
|
+
export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './plan/index.js';
|
|
43
45
|
export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
|
|
44
46
|
export type { OrgPostgresMigrationConfirmation } from './plan/index.js';
|
|
45
47
|
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
@@ -59,6 +61,8 @@ export type { PlanOwnSfuOptions, SfuAppDeps, SfuAppInput } from './plan/index.js
|
|
|
59
61
|
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuCompose, } from './plan/index.js';
|
|
60
62
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, KeysRegistrarPort, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './plan/index.js';
|
|
61
63
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
64
|
+
export type { StageProbePort, ReleasePreviewMembers } from './plan/stage-guards.js';
|
|
65
|
+
export type { StageCopyHostConfig } from './plan/stage-copy-host.js';
|
|
62
66
|
export { NO_DOMAIN_ZONE, entryMachinePaths, ownHostnames, privatePageGateKey, workspaceHost, } from './plan/index.js';
|
|
63
67
|
export type { PrivatePageScope } from './plan/index.js';
|
|
64
68
|
export type { HostedLabelRefusal } from './hosted-label.js';
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,10 @@ export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, v
|
|
|
7
7
|
// object, not a copy.
|
|
8
8
|
import { VERSIONS } from './bootstrap/versions.js';
|
|
9
9
|
export const PINNED_VERSIONS = VERSIONS;
|
|
10
|
-
export { buildJsonSchema, parseBuildJson, BuildJsonError } from './build-json.js';
|
|
10
|
+
export { buildJsonSchema, parseBuildJson, BuildJsonError, serverBuildJsonSchema, parseServerBuildJson, parseAnyBuildJson, checkServerBuild, } from './build-json.js';
|
|
11
11
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
12
12
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
13
|
-
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild,
|
|
13
|
+
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, } from './native-build.js';
|
|
14
14
|
export { StaleLeaseError } from './store.js';
|
|
15
15
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
16
16
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
@@ -24,6 +24,7 @@ export { DumpError, dumpNow } from './plan/index.js';
|
|
|
24
24
|
export { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_IMAGE_REF, stageMailEnv, stageSinkAppName, } from './plan/index.js';
|
|
25
25
|
export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError, stageCopy, tableExcluded, undoStageCopy, } from './plan/index.js';
|
|
26
26
|
export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './plan/index.js';
|
|
27
|
+
export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './plan/index.js';
|
|
27
28
|
export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
|
|
28
29
|
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
29
30
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- @wtfalch/foundry: a company's Boule can request `kosmos` (turn `org.kosmos.enabled` on or off).
|
|
2
|
+
alter table foundry_requested_changes
|
|
3
|
+
drop constraint if exists foundry_requested_changes_field_check;
|
|
4
|
+
alter table foundry_requested_changes
|
|
5
|
+
add constraint foundry_requested_changes_field_check
|
|
6
|
+
check (field in ('calls', 'customers', 'storageLimit', 'aiLimit', 'domain', 'mailTier', 'app', 'build', 'bouleApps', 'kosmos'));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the platform's image registry (`registry:3`), declared with the rest of the
|
|
2
|
+
-- platform. Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` additions. Off for the
|
|
3
|
+
-- platform that already exists: foundry deploys nothing new until it is declared on. Backfills
|
|
4
|
+
-- `registry_hostname` the way `0047_platform_coolify.sql` backfilled `coolify_hostname`: the
|
|
5
|
+
-- column must not be empty (`src/platform.ts`'s `validatePlatform`).
|
|
6
|
+
|
|
7
|
+
alter table foundry_declared_platform
|
|
8
|
+
add column if not exists registry_enabled boolean not null default false,
|
|
9
|
+
add column if not exists registry_hostname text,
|
|
10
|
+
add column if not exists registry_git_ref text not null default '';
|
|
11
|
+
|
|
12
|
+
update foundry_declared_platform
|
|
13
|
+
set registry_hostname = 'registry.' || apex
|
|
14
|
+
where registry_hostname is null;
|
|
15
|
+
|
|
16
|
+
alter table foundry_declared_platform
|
|
17
|
+
alter column registry_hostname set not null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { problem, setUse } from './entry-parts.js';
|
|
2
|
+
/** `uses.customerGrants`: the company's team gives customers permissions in the app. */
|
|
3
|
+
export const customerGrants = {
|
|
4
|
+
view: {
|
|
5
|
+
id: 'customer-grants',
|
|
6
|
+
name: 'Permissions for customers',
|
|
7
|
+
line: 'Your team gives each customer the permissions they need in the app, and we keep them in step.',
|
|
8
|
+
category: 'People',
|
|
9
|
+
icon: 'shield',
|
|
10
|
+
runtimes: ['server'],
|
|
11
|
+
requires: ['sign-in'],
|
|
12
|
+
availability: 'ready',
|
|
13
|
+
fields: [],
|
|
14
|
+
adds: [
|
|
15
|
+
{
|
|
16
|
+
name: 'Permissions your team manages',
|
|
17
|
+
hint: 'Your team gives and takes back customer permissions without changing the app.',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
toEntry: (entry, values) => setUse(entry, 'customerGrants', values === null ? undefined : true),
|
|
22
|
+
fromEntry: (entry) => (entry.uses?.customerGrants === true ? {} : null),
|
|
23
|
+
check(_values, modules) {
|
|
24
|
+
if (modules['sign-in']?.users === 'customers')
|
|
25
|
+
return null;
|
|
26
|
+
return problem(null, 'Permissions for customers are for an app whose customers sign in. Set Sign-in to "Your customers".');
|
|
27
|
+
},
|
|
28
|
+
};
|
package/dist/modules/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SECRET_LIKE_NAME, WORKER_APP_ENV_NAME, WORKER_APP_RESERVED_NAMES, } fro
|
|
|
2
2
|
import { ai } from './ai.js';
|
|
3
3
|
import { assistants } from './assistants.js';
|
|
4
4
|
import { audit } from './audit.js';
|
|
5
|
+
import { customerGrants } from './customer-grants.js';
|
|
5
6
|
import { database } from './database.js';
|
|
6
7
|
import { email } from './email.js';
|
|
7
8
|
import { clone, isProblem } from './entry-parts.js';
|
|
@@ -9,6 +10,7 @@ import { files } from './files.js';
|
|
|
9
10
|
import { jobs } from './jobs.js';
|
|
10
11
|
import { organisations } from './organisations.js';
|
|
11
12
|
import { SECRET_OUTPUTS, referenceProblem } from './outputs.js';
|
|
13
|
+
import { repository } from './repository.js';
|
|
12
14
|
import { signIn } from './sign-in.js';
|
|
13
15
|
import { signedRecords } from './signed-records.js';
|
|
14
16
|
import { workspaces } from './workspaces.js';
|
|
@@ -29,6 +31,8 @@ export const MODULES = [
|
|
|
29
31
|
ai,
|
|
30
32
|
organisations,
|
|
31
33
|
audit,
|
|
34
|
+
customerGrants,
|
|
35
|
+
repository,
|
|
32
36
|
];
|
|
33
37
|
const byId = new Map(MODULES.map((module) => [module.view.id, module]));
|
|
34
38
|
/** The views of every module, JSON-safe, for Boule to render. */
|
|
@@ -340,7 +344,7 @@ function manifestV1AsV2(v1) {
|
|
|
340
344
|
}
|
|
341
345
|
return { v: 2, runtime: v1.runtime, modules: v1.modules, values };
|
|
342
346
|
}
|
|
343
|
-
const MANIFEST_V2_KEYS = ['v', 'runtime', 'modules', 'values', 'access'];
|
|
347
|
+
const MANIFEST_V2_KEYS = ['v', 'runtime', 'modules', 'values', 'access', 'processes'];
|
|
344
348
|
const VALUE_KEYS = {
|
|
345
349
|
generate: ['source', 'kind'],
|
|
346
350
|
reference: ['source', 'to', 'secret'],
|
|
@@ -368,11 +372,21 @@ const COPY_KEY = /^[a-z][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)+$/;
|
|
|
368
372
|
const REFERENCE = /^[a-z][a-z0-9-]*\.[A-Za-z][A-Za-z0-9-]*$/;
|
|
369
373
|
const HOST = /^(\*\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
370
374
|
const HOST_MAX = 20;
|
|
375
|
+
const PROCESS_MAX = 4;
|
|
376
|
+
const PROCESS_KEY = /^[a-z][a-z0-9-]{0,30}$/;
|
|
371
377
|
const WHY_MAX = 200;
|
|
372
378
|
const CHOICES_MAX = 50;
|
|
373
379
|
const ASKABLE_KINDS = ['text', 'longtext', 'path', 'choice', 'list'];
|
|
374
380
|
const isText = (value, max) => typeof value === 'string' && value.trim() !== '' && value.length <= max;
|
|
375
381
|
/** A plain sentence when `copy` is not a good label, help or copy key, else null. */
|
|
382
|
+
function isProcessFolder(folder) {
|
|
383
|
+
return (typeof folder === 'string' &&
|
|
384
|
+
folder.length > 0 &&
|
|
385
|
+
folder.length <= 200 &&
|
|
386
|
+
!folder.startsWith('/') &&
|
|
387
|
+
!folder.startsWith('\\') &&
|
|
388
|
+
!folder.split(/[\\/]/).includes('..'));
|
|
389
|
+
}
|
|
376
390
|
function copyProblem(name, raw, labelRequired) {
|
|
377
391
|
const { label, help, labelKey, helpKey } = raw;
|
|
378
392
|
if (labelRequired
|
|
@@ -670,9 +684,43 @@ function parseManifestV2(json) {
|
|
|
670
684
|
}
|
|
671
685
|
hosts.push({ host, why });
|
|
672
686
|
}
|
|
687
|
+
const processes = [];
|
|
688
|
+
if (json.processes !== undefined) {
|
|
689
|
+
if (runtime !== 'server') {
|
|
690
|
+
return refuse('"processes" is only for an app that runs on a server, not a Worker app.');
|
|
691
|
+
}
|
|
692
|
+
const raw = json.processes;
|
|
693
|
+
if (!Array.isArray(raw) || raw.length > PROCESS_MAX) {
|
|
694
|
+
return refuse(`"processes" must list at most ${PROCESS_MAX} helper processes.`);
|
|
695
|
+
}
|
|
696
|
+
const keys = new Set();
|
|
697
|
+
for (const item of raw) {
|
|
698
|
+
if (!isRecord(item) ||
|
|
699
|
+
Object.keys(item).some((k) => k !== 'key' && k !== 'folder' && k !== 'port')) {
|
|
700
|
+
return refuse('every helper process needs a "key" and a "folder", and may have a "port".');
|
|
701
|
+
}
|
|
702
|
+
const { key, folder, port } = item;
|
|
703
|
+
if (typeof key !== 'string' || !PROCESS_KEY.test(key) || key === 'app') {
|
|
704
|
+
return refuse('every helper process "key" must be 1 to 31 lowercase letters, digits or dashes, start with a letter, and not be "app".');
|
|
705
|
+
}
|
|
706
|
+
if (keys.has(key))
|
|
707
|
+
return refuse(`"processes" lists the key "${key}" twice.`);
|
|
708
|
+
keys.add(key);
|
|
709
|
+
if (!isProcessFolder(folder)) {
|
|
710
|
+
return refuse(`the "folder" of helper process "${key}" must be a path inside the repository, without "..", and not start with "/" or "\\".`);
|
|
711
|
+
}
|
|
712
|
+
if (port !== undefined &&
|
|
713
|
+
!(Number.isInteger(port) && port >= 1 && port <= 65535)) {
|
|
714
|
+
return refuse(`the "port" of helper process "${key}" must be a whole number from 1 to 65535.`);
|
|
715
|
+
}
|
|
716
|
+
processes.push(port === undefined ? { key, folder } : { key, folder, port: port });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
673
719
|
const manifest = { v: 2, runtime, modules: clone(modules), values };
|
|
674
720
|
if (json.access !== undefined)
|
|
675
721
|
manifest.access = { hosts };
|
|
722
|
+
if (json.processes !== undefined)
|
|
723
|
+
manifest.processes = processes;
|
|
676
724
|
return { ok: true, manifest };
|
|
677
725
|
}
|
|
678
726
|
/** `company-app.json` (v1 or v2) read and checked: what the app's code asks for. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AppModule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `uses.repo`: the app gets a key for its own repository. The entry's `repo` names the repository (it comes
|
|
4
|
+
* from the install, not from this module); `access` says whether the app may only read or also push.
|
|
5
|
+
*/
|
|
6
|
+
export declare const repository: AppModule;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { setUse } from './entry-parts.js';
|
|
2
|
+
/**
|
|
3
|
+
* `uses.repo`: the app gets a key for its own repository. The entry's `repo` names the repository (it comes
|
|
4
|
+
* from the install, not from this module); `access` says whether the app may only read or also push.
|
|
5
|
+
*/
|
|
6
|
+
export const repository = {
|
|
7
|
+
view: {
|
|
8
|
+
id: 'repository',
|
|
9
|
+
name: 'Repository',
|
|
10
|
+
line: 'The app reads or updates the files in its repository, such as the pages it keeps there.',
|
|
11
|
+
category: 'Data',
|
|
12
|
+
icon: 'git-branch',
|
|
13
|
+
runtimes: ['server'],
|
|
14
|
+
requires: [],
|
|
15
|
+
availability: 'ready',
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
key: 'access',
|
|
19
|
+
label: 'What the app may do',
|
|
20
|
+
kind: 'choice',
|
|
21
|
+
default: 'read',
|
|
22
|
+
choices: [
|
|
23
|
+
{ value: 'read', label: 'Read', help: 'The app reads the files.' },
|
|
24
|
+
{ value: 'write', label: 'Read and update', help: 'The app also saves changes.' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
adds: [
|
|
29
|
+
{
|
|
30
|
+
name: 'A key for the repository',
|
|
31
|
+
hint: 'Only this app holds it, and it opens only this repository.',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
toEntry: (entry, values) => setUse(entry, 'repo', values === null ? undefined : { access: values.access === 'write' ? 'write' : 'read' }),
|
|
36
|
+
fromEntry(entry) {
|
|
37
|
+
const used = entry.uses?.repo;
|
|
38
|
+
return used ? { access: used.access } : null;
|
|
39
|
+
},
|
|
40
|
+
};
|
package/dist/modules/types.d.ts
CHANGED
|
@@ -181,6 +181,12 @@ export interface ManifestHost {
|
|
|
181
181
|
export interface ManifestAccess {
|
|
182
182
|
hosts: ManifestHost[];
|
|
183
183
|
}
|
|
184
|
+
/** A helper process of a server app: built from `<folder>/Dockerfile`, next to the app itself. */
|
|
185
|
+
export interface ManifestProcess {
|
|
186
|
+
key: string;
|
|
187
|
+
folder: string;
|
|
188
|
+
port?: number;
|
|
189
|
+
}
|
|
184
190
|
/** `company-app.json` v2, and what every manifest (v1 too) is read as. */
|
|
185
191
|
export interface AppManifest {
|
|
186
192
|
v: 2;
|
|
@@ -189,6 +195,8 @@ export interface AppManifest {
|
|
|
189
195
|
values: Record<string, ManifestValue>;
|
|
190
196
|
/** Optional. Absent means the app may connect anywhere. */
|
|
191
197
|
access?: ManifestAccess;
|
|
198
|
+
/** Optional, server apps only: at most 4 helper processes. */
|
|
199
|
+
processes?: ManifestProcess[];
|
|
192
200
|
}
|
|
193
201
|
/** An answer to an `ask`: text, or a list of texts for `multiple`. */
|
|
194
202
|
export type ManifestAnswer = string | string[];
|