@k2b/cloud 0.46.0 → 0.48.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/package.json +8 -4
- package/scripts/build.ts +5 -1
- package/scripts/preload.ts +5 -1
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +8 -0
- package/src/_internal/define-app.ts +69 -2
- package/src/_internal/heartbeat.ts +21 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/postgres-application-name.ts +11 -0
- package/src/_internal/process-sync.ts +15 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/readiness.ts +6 -1
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/resource-principals.ts +4 -34
- package/src/_internal/workload-credential.ts +154 -0
- package/src/_internal/workload-protocol.ts +36 -0
- package/src/ai/application-tasks.ts +5 -2
- package/src/ai/chart-block.ts +36 -711
- package/src/ai/default-tool-contracts.ts +21 -259
- package/src/api/admin-app-domains.ts +62 -0
- package/src/api/admin-app-keys.ts +66 -0
- package/src/api/admin-core-settings.ts +2 -0
- package/src/api/admin-identity.ts +27 -1
- package/src/api/admin-settings-env.ts +120 -0
- package/src/api/app-bootstrap.ts +131 -0
- package/src/api/capabilities.ts +17 -4
- package/src/api/index.ts +8 -0
- package/src/api/settings-ops.ts +35 -0
- package/src/app-key.ts +125 -0
- package/src/browser/CloudResourceSearch.tsx +48 -41
- package/src/capabilities/discussion-providers.ts +3 -2
- package/src/capabilities/resource-readers.ts +13 -2
- package/src/capabilities/response.ts +6 -2
- package/src/capabilities/server-invoke.ts +73 -29
- package/src/capabilities/server.ts +1 -1
- package/src/capabilities/types.ts +1 -1
- package/src/cli/admin/app-credentials.ts +32 -7
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +226 -0
- package/src/cli/admin/index.ts +109 -84
- package/src/cli/admin/settings.ts +62 -0
- package/src/config/define-env.ts +63 -8
- package/src/config/env.ts +34 -22
- package/src/config/index.ts +12 -1
- package/src/config/workflow-ai.ts +28 -0
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +22 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/settings-env.ts +21 -0
- package/src/contracts/settings-types.ts +6 -0
- package/src/index.ts +10 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +60 -0
- package/src/runtime/clients.ts +72 -0
- package/src/runtime/connections.ts +46 -0
- package/src/runtime/resolve-connections.ts +91 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/client-ip.ts +1 -1
- package/src/server/index.ts +1 -0
- package/src/services/app-access-management.ts +1 -8
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-bootstrap.ts +134 -0
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-keys.ts +301 -0
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/gateway.ts +6 -0
- package/src/services/identity/index.ts +3 -0
- package/src/services/identity/resource-readers.ts +80 -0
- package/src/services/identity/runtime-config.ts +14 -6
- package/src/services/identity/workload-runtime.ts +253 -0
- package/src/services/index.ts +8 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +7 -5
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +8 -8
- package/src/services/outgoing-mail/resolve-host.ts +5 -4
- package/src/services/request-cache-redis.ts +2 -2
- package/src/services/service-account-credentials.ts +63 -21
- package/src/services/settings/api.ts +3 -3
- package/src/services/settings/binding.ts +45 -0
- package/src/services/settings/core-settings.ts +119 -1
- package/src/services/settings/crypto.ts +2 -2
- package/src/services/settings/defaults.ts +16 -2
- package/src/services/settings/env-status.ts +69 -0
- package/src/services/settings/index.ts +69 -26
- package/src/services/settings/store.ts +78 -8
- package/src/services/settings/validation.ts +40 -0
- package/src/shared/index.ts +11 -10
- package/src/shared/markdown/extensions/tables.ts +10 -1
- package/src/ssr/admin-navigation.ts +0 -1
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +57 -0
- package/src/workflows/ai/index.ts +1 -0
- package/src/workflows/ai/runtime.ts +16 -8
- package/src/workflows/ai/settings.ts +5 -0
- package/src/shared/markdown/formula.ts +0 -1283
package/src/cli/admin/index.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* them. The groups map to the admin UI areas, so a command and the page that
|
|
6
6
|
* shows the same data stay easy to find together.
|
|
7
7
|
*/
|
|
8
|
+
|
|
8
9
|
import { defineCliCommands } from "../commands";
|
|
10
|
+
import { type CloudCliContext, localizeCloudCliText } from "../index";
|
|
9
11
|
import { accountAdministrationCommands } from "./account-administration";
|
|
10
12
|
import { accountCategoryCommands } from "./account-categories";
|
|
11
13
|
import { agentCommands } from "./agents";
|
|
@@ -14,6 +16,8 @@ import { aiQuotaCommands } from "./ai-quotas";
|
|
|
14
16
|
import { aiSkillCommands } from "./ai-skills";
|
|
15
17
|
import { aiUsageCommands } from "./ai-usage";
|
|
16
18
|
import { appCredentialCommands } from "./app-credentials";
|
|
19
|
+
import { appDomainCommands } from "./app-domains";
|
|
20
|
+
import { appKeyCommands } from "./app-keys";
|
|
17
21
|
import { appSignInCommands } from "./app-sign-in";
|
|
18
22
|
import { dataCommands } from "./data";
|
|
19
23
|
import { documentationCommands } from "./documentation";
|
|
@@ -28,92 +32,113 @@ import { metricsCommands } from "./metrics";
|
|
|
28
32
|
import { natsCommands } from "./nats";
|
|
29
33
|
import { notificationCommands } from "./notifications";
|
|
30
34
|
import { outgoingMailCommands } from "./outgoing-mail";
|
|
35
|
+
import { settingsCommands } from "./settings";
|
|
31
36
|
import { syncCommands } from "./sync";
|
|
32
37
|
import { telemetryCommands } from "./telemetry";
|
|
33
38
|
import { webhookCommands } from "./webhooks";
|
|
34
39
|
import { workflowCommands } from "./workflows";
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
41
|
+
const build = (locale?: string) =>
|
|
42
|
+
defineCliCommands({
|
|
43
|
+
name: "admin",
|
|
44
|
+
summary: "Inspect and operate Cloud administration surfaces.",
|
|
45
|
+
groupSummaries: {
|
|
46
|
+
accounts: "Manage account policy and account administration.",
|
|
47
|
+
ai: "Manage AI models, quotas, Skills, and usage.",
|
|
48
|
+
"ai models": "Manage AI model configuration.",
|
|
49
|
+
"ai providers": "Manage AI provider connections.",
|
|
50
|
+
"ai variants": "Manage named model variants.",
|
|
51
|
+
"ai classes": "Manage model classes and tools.",
|
|
52
|
+
"ai quotas background": "Inspect and release the background emergency stop.",
|
|
53
|
+
"ai skills": "Inspect Assistant Skills and trusted templates.",
|
|
54
|
+
"ai usage": "Inspect AI runs and usage facets.",
|
|
55
|
+
announcements: "Manage platform announcements and banners.",
|
|
56
|
+
"app-credentials": "Manage per-application credentials for background work.",
|
|
57
|
+
agents: "Create, list, revoke, and re-key agent accounts with OAuth client credentials, and change their pictures.",
|
|
58
|
+
"agents avatar": "Show and change an agent's picture: its robot parts or a photo.",
|
|
59
|
+
"app-sign-in": "Manage app sign-in and pairing.",
|
|
60
|
+
apps: localizeCloudCliText(locale, {
|
|
61
|
+
en: "Inspect registered apps and manage app keys and domains.",
|
|
62
|
+
de: "Registrierte Apps prüfen und App-Schlüssel und App-Domains verwalten.",
|
|
63
|
+
}),
|
|
64
|
+
"apps domains": localizeCloudCliText(locale, {
|
|
65
|
+
en: "Assign app domains and inspect DNS, TLS and gateway reachability.",
|
|
66
|
+
de: "App-Domains zuordnen und DNS, TLS und Gateway-Erreichbarkeit prüfen.",
|
|
67
|
+
}),
|
|
68
|
+
"apps keys": "Create, list, and revoke Ed25519 app keys.",
|
|
69
|
+
documentation: "Manage the administration documentation website.",
|
|
70
|
+
geoip: "Replace and inspect sign-in location databases.",
|
|
71
|
+
jobs: "Inspect background jobs and their runs.",
|
|
72
|
+
logs: "Search, inspect, and clean up retained logs.",
|
|
73
|
+
metrics: "Inspect collectors and manage metrics access.",
|
|
74
|
+
"metrics tokens": "Manage metrics bearer tokens.",
|
|
75
|
+
nats: "Inspect NATS nodes, streams, and consumers.",
|
|
76
|
+
"nats consumers": "Inspect NATS consumers.",
|
|
77
|
+
"nats streams": "Find NATS streams and buckets.",
|
|
78
|
+
"notification-batches": "Manage account notification batches.",
|
|
79
|
+
"outgoing-mail": "Manage outgoing mail sender profiles, application access, and the send log.",
|
|
80
|
+
"outgoing-mail profiles": "Manage outgoing mail sender profiles.",
|
|
81
|
+
"outgoing-mail apps": "Manage application outgoing mail access.",
|
|
82
|
+
"outgoing-mail log": "Read and cancel application mail in the send log.",
|
|
83
|
+
"outgoing-mail retention": "Show or change how long the send log keeps mail.",
|
|
84
|
+
notifications: "Inspect and resend email notifications.",
|
|
85
|
+
postgres: "Inspect Postgres diagnostics.",
|
|
86
|
+
redis: "Inspect Valkey diagnostics.",
|
|
87
|
+
routes: "Inspect gateway routes.",
|
|
88
|
+
settings: "Inspect environment migration and change administrator settings.",
|
|
89
|
+
sync: "Inspect Sync resources, schedules, and dead letters.",
|
|
90
|
+
"sync dead-letters": "Inspect, replay, requeue, or remove dead letters.",
|
|
91
|
+
"sync resources": "List Sync resource declarations.",
|
|
92
|
+
"sync schedules": "Inspect and run Sync schedules.",
|
|
93
|
+
"sync schedules runs": "Read Sync schedule run results.",
|
|
94
|
+
telemetry: "Inspect request telemetry.",
|
|
95
|
+
webhooks: "Manage gateway health webhooks.",
|
|
96
|
+
workflows: "Inspect and operate workflow runs and effects.",
|
|
97
|
+
"app-sign-in config": "Manage the trusted app website and pairing policy.",
|
|
98
|
+
"accounts config": "Manage account types and sign-in visibility.",
|
|
99
|
+
"accounts administration": "Manage account requests and account/group follow-up notices.",
|
|
100
|
+
"ai quotas": "Manage direct Assistant chat limits and consumption.",
|
|
101
|
+
"ai quotas config": "Export and replace quota policies with revision checks.",
|
|
102
|
+
legal: "Manage Terms, Privacy, and Imprint.",
|
|
103
|
+
linux: "Prepare Linux identities.",
|
|
104
|
+
"linux config": "Manage Linux identity defaults and allocation range.",
|
|
105
|
+
},
|
|
106
|
+
commands: [
|
|
107
|
+
...appCredentialCommands,
|
|
108
|
+
...appKeyCommands,
|
|
109
|
+
...appDomainCommands(locale),
|
|
110
|
+
...agentCommands,
|
|
111
|
+
...documentationCommands,
|
|
112
|
+
...appSignInCommands,
|
|
113
|
+
...accountCategoryCommands,
|
|
114
|
+
...accountAdministrationCommands,
|
|
115
|
+
...aiUsageCommands,
|
|
116
|
+
...aiModelCommands,
|
|
117
|
+
...aiQuotaCommands,
|
|
118
|
+
...aiSkillCommands,
|
|
119
|
+
...instanceCommands,
|
|
120
|
+
...gatewayCommands,
|
|
121
|
+
...logCommands,
|
|
122
|
+
...telemetryCommands,
|
|
123
|
+
...jobCommands,
|
|
124
|
+
...legalCommands,
|
|
125
|
+
...geoipCommands,
|
|
126
|
+
...linuxCommands,
|
|
127
|
+
...workflowCommands,
|
|
128
|
+
...dataCommands,
|
|
129
|
+
...notificationCommands,
|
|
130
|
+
...outgoingMailCommands,
|
|
131
|
+
...webhookCommands,
|
|
132
|
+
...metricsCommands,
|
|
133
|
+
...natsCommands,
|
|
134
|
+
...syncCommands,
|
|
135
|
+
...settingsCommands,
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const admin = build();
|
|
140
|
+
export default {
|
|
141
|
+
...admin,
|
|
142
|
+
help: (locale?: string) => build(locale).help?.(locale) ?? "",
|
|
143
|
+
run: (ctx: CloudCliContext) => build(ctx.options.locale).run(ctx),
|
|
144
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { EnvSettingsStatusSchema } from "../../contracts/settings-env";
|
|
2
|
+
import { arg, command, confirmFlag, flag, printStructured } from "../index";
|
|
3
|
+
import { apiGet, apiJson } from "./shared";
|
|
4
|
+
|
|
5
|
+
export const settingsCommands = [
|
|
6
|
+
command("settings status", {
|
|
7
|
+
summary: "Check environment-backed settings across registered processes without printing values",
|
|
8
|
+
flags: { env: flag.boolean({ description: "Check stored values against each running application's environment" }) },
|
|
9
|
+
async run({ ctx, flags }) {
|
|
10
|
+
if (!flags.env) throw new Error("Use settings status --env.");
|
|
11
|
+
const result = EnvSettingsStatusSchema.parse(await apiGet(ctx, "/api/admin/core/settings/env/status"));
|
|
12
|
+
if (!printStructured(ctx, result)) {
|
|
13
|
+
for (const process of result.processes) {
|
|
14
|
+
ctx.print(`${process.appId}: ${process.status}`);
|
|
15
|
+
ctx.table(
|
|
16
|
+
process.settings,
|
|
17
|
+
[
|
|
18
|
+
"key",
|
|
19
|
+
"envVariable",
|
|
20
|
+
"storedValuePresent",
|
|
21
|
+
"envValuePresent",
|
|
22
|
+
"conflict",
|
|
23
|
+
"unreadableStoredValue",
|
|
24
|
+
"invalidEnvValue",
|
|
25
|
+
"needsAttention",
|
|
26
|
+
].map((key) => ({ key })),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result.needsAttention ? 1 : 0;
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
command("settings set", {
|
|
34
|
+
summary: "Set a whole administrator setting value",
|
|
35
|
+
args: { key: arg.required() },
|
|
36
|
+
flags: {
|
|
37
|
+
value: flag.string({ required: true, description: "JSON value (quote a text value as JSON)" }),
|
|
38
|
+
app: flag.string({ description: "Owning application ID; omit for platform settings" }),
|
|
39
|
+
replaceUnreadable: flag.boolean({ description: "Explicitly replace an unreadable stored row with this whole value" }),
|
|
40
|
+
yes: confirmFlag("Confirm setting change"),
|
|
41
|
+
},
|
|
42
|
+
async run({ ctx, args, flags }) {
|
|
43
|
+
if (!flags.yes) throw new Error("Setting changes require --yes.");
|
|
44
|
+
let value: unknown;
|
|
45
|
+
try {
|
|
46
|
+
value = JSON.parse(flags.value ?? "");
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error("--value must be valid JSON.");
|
|
49
|
+
}
|
|
50
|
+
const result = flags.app
|
|
51
|
+
? await apiJson(ctx, "PUT", `/api/admin/core/settings/env/${encodeURIComponent(flags.app)}/${encodeURIComponent(args.key)}`, {
|
|
52
|
+
value,
|
|
53
|
+
replaceUnreadable: flags.replaceUnreadable,
|
|
54
|
+
})
|
|
55
|
+
: await apiJson(ctx, "PUT", "/api/admin/core/settings", {
|
|
56
|
+
updates: { [args.key]: value },
|
|
57
|
+
replaceUnreadable: flags.replaceUnreadable ? [args.key] : [],
|
|
58
|
+
});
|
|
59
|
+
if (!printStructured(ctx, result)) ctx.print("Setting saved.");
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
];
|
package/src/config/define-env.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Every runtime environment variable is declared once with a schema, a
|
|
5
5
|
* default, a scope and a one-sentence doc. Registries read `process.env`
|
|
6
6
|
* lazily: each key is parsed on access, so tests can change variables and
|
|
7
|
-
* consumers never depend on an import-time snapshot.
|
|
7
|
+
* consumers never depend on an import-time snapshot. Configured secret files
|
|
8
|
+
* are also validated when the registry is created. `.env.example`,
|
|
8
9
|
* `.env.prod.example` and the configuration reference are generated from the
|
|
9
10
|
* `$specs` of every registry by `scripts/generate-config-docs.ts`.
|
|
10
11
|
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
11
13
|
import { z } from "zod";
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -17,7 +19,7 @@ import { z } from "zod";
|
|
|
17
19
|
export type EnvScope = "runtime" | "secret" | "development";
|
|
18
20
|
|
|
19
21
|
export type EnvSpec<T> = {
|
|
20
|
-
/** Parses the trimmed
|
|
22
|
+
/** Parses the raw value (trimmed for env, one trailing newline removed for files). Empty and unset values never reach the schema. */
|
|
21
23
|
schema: z.ZodType<T, unknown>;
|
|
22
24
|
/** Used when the variable is unset or empty; a function is evaluated on every access. */
|
|
23
25
|
default?: T | (() => T);
|
|
@@ -31,6 +33,8 @@ export type EnvSpec<T> = {
|
|
|
31
33
|
required?: boolean;
|
|
32
34
|
/** Alternative variable names, consulted after the canonical key. */
|
|
33
35
|
aliases?: string[];
|
|
36
|
+
/** Allow NAME_FILE to supply the secret. Configured files are validated when the registry is created. */
|
|
37
|
+
secretFile?: boolean;
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
export type EnvSpecs = Record<string, EnvSpec<unknown>>;
|
|
@@ -42,20 +46,59 @@ type EnvValue<S extends EnvSpec<unknown>> = S extends { default: unknown } | { r
|
|
|
42
46
|
export type EnvRegistry<S extends EnvSpecs> = { readonly [K in keyof S]: EnvValue<S[K]> } & {
|
|
43
47
|
readonly $specs: S;
|
|
44
48
|
readonly $keys: readonly string[];
|
|
49
|
+
/** Read only an explicitly supplied value, without the code default. */
|
|
50
|
+
readonly $readPresent: <K extends keyof S & string>(key: K) => z.output<S[K]["schema"]> | undefined;
|
|
45
51
|
};
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
/** Effective value source; empty values fall through to the default or unset. */
|
|
54
|
+
export type EnvSource = "env" | "alias" | "file" | "default" | "unset";
|
|
55
|
+
|
|
56
|
+
type RawEnv = { value: string | undefined; source: "env" | "alias" | "file" | "unset" };
|
|
57
|
+
|
|
58
|
+
// A mounted secret is a startup input, not a live configuration channel. A new
|
|
59
|
+
// path is still observed when tests or tooling deliberately change process.env.
|
|
60
|
+
const fileValues = new WeakMap<EnvSpec<unknown>, { path: string; value: string }>();
|
|
61
|
+
|
|
62
|
+
const rawValue = (key: string, spec: EnvSpec<unknown>): RawEnv => {
|
|
63
|
+
const names = [key, ...(spec.aliases ?? [])];
|
|
64
|
+
const fileKey = `${key}_FILE`;
|
|
65
|
+
const path = spec.secretFile ? process.env[fileKey]?.trim() || undefined : undefined;
|
|
66
|
+
if (path !== undefined) {
|
|
67
|
+
const conflict = names.find((name) => process.env[name]?.trim());
|
|
68
|
+
if (conflict) throw new Error(`${conflict} and ${fileKey} must not both be set`);
|
|
69
|
+
const cached = fileValues.get(spec);
|
|
70
|
+
let value = cached?.path === path ? cached.value : undefined;
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
try {
|
|
73
|
+
value = readFileSync(path, "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
// File errors must identify the source without including contents or an underlying error.
|
|
76
|
+
throw new Error(`${fileKey} could not read file ${JSON.stringify(path)}`);
|
|
77
|
+
}
|
|
78
|
+
if (value.endsWith("\r\n")) value = value.slice(0, -2);
|
|
79
|
+
else if (value.endsWith("\n")) value = value.slice(0, -1);
|
|
80
|
+
fileValues.set(spec, { path, value });
|
|
81
|
+
}
|
|
82
|
+
return { value: value === "" ? undefined : value, source: value === "" ? "unset" : "file" };
|
|
83
|
+
}
|
|
84
|
+
for (const name of names) {
|
|
49
85
|
const value = process.env[name]?.trim();
|
|
50
|
-
if (value) return value;
|
|
86
|
+
if (value) return { value, source: name === key ? "env" : "alias" };
|
|
51
87
|
}
|
|
52
|
-
return undefined;
|
|
88
|
+
return { value: undefined, source: "unset" };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Inspect the effective source without evaluating defaults; invalid file configuration throws as on value access. */
|
|
92
|
+
export const envSource = <S extends EnvSpecs>(registry: EnvRegistry<S>, key: keyof S & string): EnvSource => {
|
|
93
|
+
const spec = registry.$specs[key]!;
|
|
94
|
+
const { source } = rawValue(key, spec);
|
|
95
|
+
return source === "unset" && spec.default !== undefined ? "default" : source;
|
|
53
96
|
};
|
|
54
97
|
|
|
55
98
|
const isFactory = <T>(value: T | (() => T)): value is () => T => typeof value === "function";
|
|
56
99
|
|
|
57
100
|
export const readEnv = <T>(key: string, spec: EnvSpec<T>): T | undefined => {
|
|
58
|
-
const raw = rawValue(key, spec
|
|
101
|
+
const { value: raw } = rawValue(key, spec);
|
|
59
102
|
if (raw === undefined) {
|
|
60
103
|
if (spec.default !== undefined) return isFactory(spec.default) ? spec.default() : spec.default;
|
|
61
104
|
if (spec.required) throw new Error(`${key} is required`);
|
|
@@ -63,6 +106,7 @@ export const readEnv = <T>(key: string, spec: EnvSpec<T>): T | undefined => {
|
|
|
63
106
|
}
|
|
64
107
|
const parsed = spec.schema.safeParse(raw);
|
|
65
108
|
if (!parsed.success) {
|
|
109
|
+
if (spec.secretFile) throw new Error(`${key} is invalid`);
|
|
66
110
|
const reason = parsed.error.issues.map((issue) => issue.message).join("; ") || "is invalid";
|
|
67
111
|
throw new Error(`${key} ${reason}`);
|
|
68
112
|
}
|
|
@@ -71,8 +115,19 @@ export const readEnv = <T>(key: string, spec: EnvSpec<T>): T | undefined => {
|
|
|
71
115
|
|
|
72
116
|
export const defineEnv = <S extends EnvSpecs>(specs: S): EnvRegistry<S> => {
|
|
73
117
|
const keys = Object.freeze(Object.keys(specs));
|
|
74
|
-
const registry: Record<string, unknown> = {
|
|
118
|
+
const registry: Record<string, unknown> = {
|
|
119
|
+
$specs: specs,
|
|
120
|
+
$keys: keys,
|
|
121
|
+
$readPresent: (key: string) => {
|
|
122
|
+
const spec = specs[key];
|
|
123
|
+
if (!spec) throw new Error(`Unknown environment variable: ${key}`);
|
|
124
|
+
return readEnv(key, { ...spec, default: undefined, required: false });
|
|
125
|
+
},
|
|
126
|
+
};
|
|
75
127
|
for (const key of keys) {
|
|
128
|
+
const spec = specs[key]!;
|
|
129
|
+
// Fail during module initialization, even when this secret is first used by a later request.
|
|
130
|
+
if (spec.secretFile && process.env[`${key}_FILE`]?.trim()) readEnv(key, spec);
|
|
76
131
|
Object.defineProperty(registry, key, { enumerable: true, configurable: true, get: () => readEnv(key, specs[key]!) });
|
|
77
132
|
}
|
|
78
133
|
// The getters above are the only way to build a typed lazy object.
|
package/src/config/env.ts
CHANGED
|
@@ -4,23 +4,14 @@
|
|
|
4
4
|
* `src/env.ts`; values are parsed lazily on access (see `define-env.ts`).
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { DEFAULT_TRUSTED_PROXIES } from "../services/settings/validation";
|
|
7
8
|
import { defineEnv, envBoolean, envList } from "./define-env";
|
|
9
|
+
import { DEFAULT_WORKFLOW_AI_BUDGET, workflowAiBudgetSchema } from "./workflow-ai";
|
|
8
10
|
|
|
9
11
|
const syncReplicas = z
|
|
10
12
|
.enum(["1", "3", "5"], { error: "must be 1, 3, or 5" })
|
|
11
13
|
.transform((value): 1 | 3 | 5 => (value === "1" ? 1 : value === "3" ? 3 : 5));
|
|
12
14
|
|
|
13
|
-
const budgetSchema = z
|
|
14
|
-
.object({
|
|
15
|
-
maxStepsPerSource: z.number().int().positive(),
|
|
16
|
-
tasksPerMinute: z.number().int().positive(),
|
|
17
|
-
burst: z.number().int().positive(),
|
|
18
|
-
sourcesPer24Hours: z.number().int().positive(),
|
|
19
|
-
scopeConcurrent: z.number().int().positive(),
|
|
20
|
-
globalConcurrent: z.number().int().positive(),
|
|
21
|
-
})
|
|
22
|
-
.strict();
|
|
23
|
-
|
|
24
15
|
const registry = defineEnv({
|
|
25
16
|
// Process
|
|
26
17
|
NODE_ENV: {
|
|
@@ -35,6 +26,7 @@ const registry = defineEnv({
|
|
|
35
26
|
// Infrastructure
|
|
36
27
|
DATABASE_URL: {
|
|
37
28
|
schema: z.string(),
|
|
29
|
+
aliases: ["DATABASEURL", "TLS_DATABASE_URL", "POSTGRES_URL", "PGURL", "PG_URL", "TLS_POSTGRES_DATABASE_URL"],
|
|
38
30
|
example: "postgresql://ipa:ipa@localhost:5432/ipa",
|
|
39
31
|
doc: "Postgres connection read by Bun SQL and by application-owned pools.",
|
|
40
32
|
},
|
|
@@ -54,7 +46,7 @@ const registry = defineEnv({
|
|
|
54
46
|
schema: z.string(),
|
|
55
47
|
default: "",
|
|
56
48
|
example: "dev",
|
|
57
|
-
doc: "Sync namespace shared by every app of one installation;
|
|
49
|
+
doc: "Sync namespace shared by every app of one installation; separates NATS resource names, not access. Use one NATS account per installation.",
|
|
58
50
|
},
|
|
59
51
|
SYNC_REPLICAS: {
|
|
60
52
|
schema: syncReplicas,
|
|
@@ -88,9 +80,9 @@ const registry = defineEnv({
|
|
|
88
80
|
return z.NEVER;
|
|
89
81
|
}
|
|
90
82
|
})
|
|
91
|
-
.pipe(
|
|
92
|
-
default:
|
|
93
|
-
doc: "JSON scoped workflow inference budget: active or successful logical requests per source/24h, token-bucket admission per minute and burst, distinct sources per rolling 24h, outstanding tasks per scope and globally; capacity/rate overload uses bounded workflow retries, daily/source limits fail visibly, and cache hits consume no reservation.",
|
|
83
|
+
.pipe(workflowAiBudgetSchema),
|
|
84
|
+
default: DEFAULT_WORKFLOW_AI_BUDGET,
|
|
85
|
+
doc: "Deprecated: bootstraps `ai.workflow_budget` once when no stored value exists; remove after `cld admin settings status --env` succeeds. JSON scoped workflow inference budget: active or successful logical requests per source/24h, token-bucket admission per minute and burst, distinct sources per rolling 24h, outstanding tasks per scope and globally; capacity/rate overload uses bounded workflow retries, daily/source limits fail visibly, and cache hits consume no reservation.",
|
|
94
86
|
},
|
|
95
87
|
|
|
96
88
|
// Workflow inference: p95 ~4s in the pilot; 60s bounds a whole attempt,
|
|
@@ -98,12 +90,12 @@ const registry = defineEnv({
|
|
|
98
90
|
WORKFLOW_AI_ATTEMPT_TIMEOUT_MS: {
|
|
99
91
|
schema: z.coerce.number().int().positive(),
|
|
100
92
|
default: 60_000,
|
|
101
|
-
doc: "Absolute workflow AI attempt budget in milliseconds, including admission and repair; expiry aborts the provider and follows the bounded retry policy.",
|
|
93
|
+
doc: "Deprecated: bootstraps `ai.workflow_attempt_timeout_ms` once when no stored value exists; remove after `cld admin settings status --env` succeeds. Absolute workflow AI attempt budget in milliseconds, including admission and repair; expiry aborts the provider and follows the bounded retry policy.",
|
|
102
94
|
},
|
|
103
95
|
WORKFLOW_AI_QUEUE_TIMEOUT_MS: {
|
|
104
96
|
schema: z.coerce.number().int().positive(),
|
|
105
97
|
default: 60_000,
|
|
106
|
-
doc: "Maximum workflow AI queue wait in milliseconds; recovery fails overdue or lost deliveries visibly so an operator can retry the workflow.",
|
|
98
|
+
doc: "Deprecated: bootstraps `ai.workflow_queue_timeout_ms` once when no stored value exists; remove after `cld admin settings status --env` succeeds. Maximum workflow AI queue wait in milliseconds; recovery fails overdue or lost deliveries visibly so an operator can retry the workflow.",
|
|
107
99
|
},
|
|
108
100
|
|
|
109
101
|
// Installation
|
|
@@ -115,12 +107,13 @@ const registry = defineEnv({
|
|
|
115
107
|
GOTENBERG_URL: {
|
|
116
108
|
schema: z.string(),
|
|
117
109
|
example: "http://localhost:3001",
|
|
118
|
-
doc: "Internal Gotenberg base URL used to bootstrap the `gotenberg.url` PDF rendering setting; the stored setting wins once written.",
|
|
110
|
+
doc: "Deprecated: env only bootstraps `gotenberg.url`; remove after `cld admin settings status --env` succeeds. Internal Gotenberg base URL used to bootstrap the `gotenberg.url` PDF rendering setting; the stored setting wins once written.",
|
|
119
111
|
},
|
|
120
112
|
APP_SECRET: {
|
|
121
113
|
schema: z.string(),
|
|
122
114
|
default: "",
|
|
123
115
|
scope: "secret",
|
|
116
|
+
secretFile: true,
|
|
124
117
|
example: "change-me-generate-with-openssl-rand-hex-32",
|
|
125
118
|
doc: "Deployment-wide secret for at-rest settings encryption and signed cursors; generate once with `openssl rand -hex 32` and never change it after data has been written.",
|
|
126
119
|
},
|
|
@@ -128,51 +121,70 @@ const registry = defineEnv({
|
|
|
128
121
|
schema: z.string(),
|
|
129
122
|
default: "",
|
|
130
123
|
scope: "secret",
|
|
124
|
+
secretFile: true,
|
|
131
125
|
doc: "Core only: temporary first-administrator or recovery login token; remove it after normal administrator sign-in works.",
|
|
132
126
|
},
|
|
133
127
|
|
|
128
|
+
// Gateway trust boundary
|
|
129
|
+
GATEWAY_TRUSTED_PROXIES: {
|
|
130
|
+
schema: envList,
|
|
131
|
+
default: DEFAULT_TRUSTED_PROXIES,
|
|
132
|
+
doc: "Deprecated: bootstraps `gateway.trusted_proxies` once when no stored value exists; remove after `cld admin settings status --env` succeeds. Comma-separated IP addresses or CIDR ranges of the reverse proxies allowed to name the client in `X-Forwarded-For`; the default trusts loopback and private networks, which covers Traefik on a Docker network. Set the proxy's exact address or subnet when clients on private networks reach the gateway directly or through a proxy that appends to their `X-Forwarded-For`. Invalid bootstrap input with no stored row and invalid stored proxy lists stop gateway startup.",
|
|
133
|
+
},
|
|
134
|
+
|
|
134
135
|
// Outbound network
|
|
135
136
|
OUTGOING_MAIL_ALLOWED_TARGETS: {
|
|
136
137
|
schema: envList,
|
|
137
138
|
default: [],
|
|
138
139
|
scope: "runtime",
|
|
139
|
-
doc: "Comma-separated host names, IP addresses and CIDR ranges on internal networks allowed for outgoing mail SMTP and bounce IMAP connections; unset allows only public targets, an explicit value replaces this default, and invalid entries including `/0`
|
|
140
|
+
doc: "Deprecated: bootstraps `outgoing_mail.allowed_targets` once when no stored value exists; remove after `cld admin settings status --env` succeeds. Comma-separated host names, IP addresses and CIDR ranges on internal networks allowed for outgoing mail SMTP and bounce IMAP connections; unset allows only public targets, an explicit value replaces this default, and invalid entries including `/0` are ignored with a bootstrap warning.",
|
|
140
141
|
},
|
|
141
142
|
|
|
142
143
|
// Identity and OAuth
|
|
143
144
|
CLOUD_IDENTITY_KEY_ENCRYPTION_KEY: {
|
|
144
145
|
schema: z.string(),
|
|
145
146
|
scope: "secret",
|
|
147
|
+
secretFile: true,
|
|
146
148
|
example: "change-me-generate-with-openssl-rand-hex-32",
|
|
147
149
|
doc: "Core only: 64-hex KEK for platform identity signing keys stored in Postgres; generate once with `openssl rand -hex 32` and never distribute it to other apps.",
|
|
148
150
|
},
|
|
149
151
|
CLOUD_IDENTITY_NEXT_KEY: {
|
|
150
152
|
schema: z.string(),
|
|
151
153
|
scope: "secret",
|
|
154
|
+
secretFile: true,
|
|
152
155
|
doc: "Core only: pre-distributed next KEK before promoting it to current; cannot be combined with `CLOUD_IDENTITY_PREVIOUS_KEY`.",
|
|
153
156
|
},
|
|
154
157
|
CLOUD_IDENTITY_PREVIOUS_KEY: {
|
|
155
158
|
schema: z.string(),
|
|
156
159
|
scope: "secret",
|
|
160
|
+
secretFile: true,
|
|
157
161
|
doc: "Core only: old KEK set during an explicit rolling rewrap of stored identity signing keys, then removed.",
|
|
158
162
|
},
|
|
159
163
|
CLOUD_OAUTH_BROKER_SECRET: {
|
|
160
164
|
schema: z.string(),
|
|
161
165
|
scope: "secret",
|
|
166
|
+
secretFile: true,
|
|
162
167
|
doc: "Core and OAuth only: 64-hex secret that lets the OAuth app request token signatures from Core; generate independently with `openssl rand -hex 32`.",
|
|
163
168
|
},
|
|
169
|
+
CLOUD_APP_KEY: {
|
|
170
|
+
schema: z.string(),
|
|
171
|
+
scope: "secret",
|
|
172
|
+
secretFile: true,
|
|
173
|
+
doc: "App Ed25519 private key for startup connection bootstrap; alternatively mount CLOUD_APP_KEY_FILE. Requires the direct Core origin. Core and Gateway refuse app keys. Keep infrastructure env until the platform-client import migration is complete.",
|
|
174
|
+
},
|
|
164
175
|
CLOUD_CORE_INTERNAL_ORIGIN: {
|
|
165
176
|
schema: z.string(),
|
|
166
|
-
doc: "Direct Core listener origin for OAuth issuance, mandate-backed background work and internal callbacks; host
|
|
177
|
+
doc: "Direct Core listener origin for app connection bootstrap, OAuth issuance, mandate-backed background work and internal callbacks; never the gateway. Cross-host bootstrap requires verified TLS; install private CAs through NODE_EXTRA_CA_CERTS before the first fetch.",
|
|
167
178
|
},
|
|
168
179
|
CLOUD_APP_CREDENTIAL: {
|
|
169
180
|
schema: z.string(),
|
|
170
181
|
scope: "secret",
|
|
171
|
-
|
|
182
|
+
secretFile: true,
|
|
183
|
+
doc: "Optional host-run app override for the automatically delivered app workload credential; provision through Core's admin workload API with `identity:invoke`.",
|
|
172
184
|
},
|
|
173
185
|
CLOUD_IDENTITY_JWKS_ORIGIN: {
|
|
174
186
|
schema: z.string(),
|
|
175
|
-
doc: "Optional private origin of the Core listener for session and invocation JWKS; unset
|
|
187
|
+
doc: "Deprecated: use `CLOUD_CORE_INTERNAL_ORIGIN`; the explicit old origin remains honored for this release. Optional private origin of the Core listener for session and invocation JWKS; unset uses `CLOUD_CORE_INTERNAL_ORIGIN`, then the public issuer origin.",
|
|
176
188
|
},
|
|
177
189
|
CLOUD_OAUTH_JWKS_ORIGIN: {
|
|
178
190
|
schema: z.string(),
|
package/src/config/index.ts
CHANGED
|
@@ -4,5 +4,16 @@
|
|
|
4
4
|
* by application `src/env.ts` files. SSR config is per-app via defineApp().
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
defineEnv,
|
|
9
|
+
type EnvRegistry,
|
|
10
|
+
type EnvScope,
|
|
11
|
+
type EnvSource,
|
|
12
|
+
type EnvSpec,
|
|
13
|
+
type EnvSpecs,
|
|
14
|
+
envBoolean,
|
|
15
|
+
envList,
|
|
16
|
+
envSource,
|
|
17
|
+
envString,
|
|
18
|
+
} from "./define-env";
|
|
8
19
|
export { env } from "./env";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const workflowAiBudgetSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
maxStepsPerSource: z.number().int().positive(),
|
|
6
|
+
tasksPerMinute: z.number().int().positive(),
|
|
7
|
+
burst: z.number().int().positive(),
|
|
8
|
+
sourcesPer24Hours: z.number().int().positive(),
|
|
9
|
+
scopeConcurrent: z.number().int().positive(),
|
|
10
|
+
globalConcurrent: z.number().int().positive(),
|
|
11
|
+
})
|
|
12
|
+
.strict();
|
|
13
|
+
export const DEFAULT_WORKFLOW_AI_BUDGET = {
|
|
14
|
+
maxStepsPerSource: 2,
|
|
15
|
+
tasksPerMinute: 10,
|
|
16
|
+
burst: 20,
|
|
17
|
+
sourcesPer24Hours: 2_000,
|
|
18
|
+
scopeConcurrent: 1,
|
|
19
|
+
globalConcurrent: 4,
|
|
20
|
+
};
|
|
21
|
+
export const validateWorkflowAiBudget = (value: unknown): string | undefined => {
|
|
22
|
+
try {
|
|
23
|
+
if (typeof value === "string" && workflowAiBudgetSchema.safeParse(JSON.parse(value)).success) return;
|
|
24
|
+
} catch {
|
|
25
|
+
/* invalid JSON */
|
|
26
|
+
}
|
|
27
|
+
return "Workflow AI budget must be a JSON object with positive whole numbers for maxStepsPerSource, tasksPerMinute, burst, sourcesPer24Hours, scopeConcurrent and globalConcurrent, without extra fields";
|
|
28
|
+
};
|