@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.
Files changed (104) hide show
  1. package/package.json +8 -4
  2. package/scripts/build.ts +5 -1
  3. package/scripts/preload.ts +5 -1
  4. package/src/_internal/app-access-manager-sql.ts +10 -0
  5. package/src/_internal/bounded-json.ts +7 -1
  6. package/src/_internal/capabilities.ts +8 -0
  7. package/src/_internal/define-app.ts +69 -2
  8. package/src/_internal/heartbeat.ts +21 -0
  9. package/src/_internal/nats-connection.ts +10 -9
  10. package/src/_internal/postgres-application-name.ts +11 -0
  11. package/src/_internal/process-sync.ts +15 -6
  12. package/src/_internal/public-surface.ts +76 -0
  13. package/src/_internal/purpose-name.ts +7 -0
  14. package/src/_internal/readiness.ts +6 -1
  15. package/src/_internal/registry-validation.ts +1 -0
  16. package/src/_internal/registry.ts +49 -3
  17. package/src/_internal/resource-principals.ts +4 -34
  18. package/src/_internal/workload-credential.ts +154 -0
  19. package/src/_internal/workload-protocol.ts +36 -0
  20. package/src/ai/application-tasks.ts +5 -2
  21. package/src/ai/chart-block.ts +36 -711
  22. package/src/ai/default-tool-contracts.ts +21 -259
  23. package/src/api/admin-app-domains.ts +62 -0
  24. package/src/api/admin-app-keys.ts +66 -0
  25. package/src/api/admin-core-settings.ts +2 -0
  26. package/src/api/admin-identity.ts +27 -1
  27. package/src/api/admin-settings-env.ts +120 -0
  28. package/src/api/app-bootstrap.ts +131 -0
  29. package/src/api/capabilities.ts +17 -4
  30. package/src/api/index.ts +8 -0
  31. package/src/api/settings-ops.ts +35 -0
  32. package/src/app-key.ts +125 -0
  33. package/src/browser/CloudResourceSearch.tsx +48 -41
  34. package/src/capabilities/discussion-providers.ts +3 -2
  35. package/src/capabilities/resource-readers.ts +13 -2
  36. package/src/capabilities/response.ts +6 -2
  37. package/src/capabilities/server-invoke.ts +73 -29
  38. package/src/capabilities/server.ts +1 -1
  39. package/src/capabilities/types.ts +1 -1
  40. package/src/cli/admin/app-credentials.ts +32 -7
  41. package/src/cli/admin/app-domains.ts +140 -0
  42. package/src/cli/admin/app-keys.ts +226 -0
  43. package/src/cli/admin/index.ts +109 -84
  44. package/src/cli/admin/settings.ts +62 -0
  45. package/src/config/define-env.ts +63 -8
  46. package/src/config/env.ts +34 -22
  47. package/src/config/index.ts +12 -1
  48. package/src/config/workflow-ai.ts +28 -0
  49. package/src/contracts/app-domains.ts +91 -0
  50. package/src/contracts/app-status.ts +104 -0
  51. package/src/contracts/capabilities.ts +22 -0
  52. package/src/contracts/index.ts +1 -0
  53. package/src/contracts/registry.ts +2 -0
  54. package/src/contracts/settings-env.ts +21 -0
  55. package/src/contracts/settings-types.ts +6 -0
  56. package/src/index.ts +10 -0
  57. package/src/runtime/bootstrap-client.ts +195 -0
  58. package/src/runtime/bootstrap-protocol.ts +60 -0
  59. package/src/runtime/clients.ts +72 -0
  60. package/src/runtime/connections.ts +46 -0
  61. package/src/runtime/resolve-connections.ts +91 -0
  62. package/src/server/app-key.ts +14 -0
  63. package/src/server/client-ip.ts +1 -1
  64. package/src/server/index.ts +1 -0
  65. package/src/services/app-access-management.ts +1 -8
  66. package/src/services/app-admin-facts.ts +160 -0
  67. package/src/services/app-bootstrap.ts +134 -0
  68. package/src/services/app-domain-check.ts +152 -0
  69. package/src/services/app-domains.ts +172 -0
  70. package/src/services/app-keys.ts +301 -0
  71. package/src/services/downstream-budget.ts +2 -2
  72. package/src/services/gateway.ts +6 -0
  73. package/src/services/identity/index.ts +3 -0
  74. package/src/services/identity/resource-readers.ts +80 -0
  75. package/src/services/identity/runtime-config.ts +14 -6
  76. package/src/services/identity/workload-runtime.ts +253 -0
  77. package/src/services/index.ts +8 -1
  78. package/src/services/links/index.ts +16 -0
  79. package/src/services/links/runtime.ts +44 -0
  80. package/src/services/links/store.ts +105 -0
  81. package/src/services/logging/index.ts +7 -5
  82. package/src/services/logging/redaction.ts +6 -1
  83. package/src/services/logging/trace.ts +8 -8
  84. package/src/services/outgoing-mail/resolve-host.ts +5 -4
  85. package/src/services/request-cache-redis.ts +2 -2
  86. package/src/services/service-account-credentials.ts +63 -21
  87. package/src/services/settings/api.ts +3 -3
  88. package/src/services/settings/binding.ts +45 -0
  89. package/src/services/settings/core-settings.ts +119 -1
  90. package/src/services/settings/crypto.ts +2 -2
  91. package/src/services/settings/defaults.ts +16 -2
  92. package/src/services/settings/env-status.ts +69 -0
  93. package/src/services/settings/index.ts +69 -26
  94. package/src/services/settings/store.ts +78 -8
  95. package/src/services/settings/validation.ts +40 -0
  96. package/src/shared/index.ts +11 -10
  97. package/src/shared/markdown/extensions/tables.ts +10 -1
  98. package/src/ssr/admin-navigation.ts +0 -1
  99. package/src/ssr/platform-messages.ts +0 -2
  100. package/src/start.ts +57 -0
  101. package/src/workflows/ai/index.ts +1 -0
  102. package/src/workflows/ai/runtime.ts +16 -8
  103. package/src/workflows/ai/settings.ts +5 -0
  104. package/src/shared/markdown/formula.ts +0 -1283
@@ -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
- export default defineCliCommands({
37
- name: "admin",
38
- summary: "Inspect and operate Cloud administration surfaces.",
39
- groupSummaries: {
40
- accounts: "Manage account policy and account administration.",
41
- ai: "Manage AI models, quotas, Skills, and usage.",
42
- "ai models": "Manage AI model configuration.",
43
- "ai providers": "Manage AI provider connections.",
44
- "ai variants": "Manage named model variants.",
45
- "ai classes": "Manage model classes and tools.",
46
- "ai quotas background": "Inspect and release the background emergency stop.",
47
- "ai skills": "Inspect Assistant Skills and trusted templates.",
48
- "ai usage": "Inspect AI runs and usage facets.",
49
- announcements: "Manage platform announcements and banners.",
50
- "app-credentials": "Manage per-application credentials for background work.",
51
- agents: "Create, list, revoke, and re-key agent accounts with OAuth client credentials, and change their pictures.",
52
- "agents avatar": "Show and change an agent's picture: its robot parts or a photo.",
53
- "app-sign-in": "Manage app sign-in and pairing.",
54
- apps: "Inspect and remove registered apps.",
55
- documentation: "Manage the administration documentation website.",
56
- geoip: "Replace and inspect sign-in location databases.",
57
- jobs: "Inspect background jobs and their runs.",
58
- logs: "Search, inspect, and clean up retained logs.",
59
- metrics: "Inspect collectors and manage metrics access.",
60
- "metrics tokens": "Manage metrics bearer tokens.",
61
- nats: "Inspect NATS nodes, streams, and consumers.",
62
- "nats consumers": "Inspect NATS consumers.",
63
- "nats streams": "Find NATS streams and buckets.",
64
- "notification-batches": "Manage account notification batches.",
65
- "outgoing-mail": "Manage outgoing mail sender profiles, application access, and the send log.",
66
- "outgoing-mail profiles": "Manage outgoing mail sender profiles.",
67
- "outgoing-mail apps": "Manage application outgoing mail access.",
68
- "outgoing-mail log": "Read and cancel application mail in the send log.",
69
- "outgoing-mail retention": "Show or change how long the send log keeps mail.",
70
- notifications: "Inspect and resend email notifications.",
71
- postgres: "Inspect Postgres diagnostics.",
72
- redis: "Inspect Valkey diagnostics.",
73
- routes: "Inspect gateway routes.",
74
- sync: "Inspect Sync resources, schedules, and dead letters.",
75
- "sync dead-letters": "Inspect, replay, requeue, or remove dead letters.",
76
- "sync resources": "List Sync resource declarations.",
77
- "sync schedules": "Inspect and run Sync schedules.",
78
- "sync schedules runs": "Read Sync schedule run results.",
79
- telemetry: "Inspect request telemetry.",
80
- webhooks: "Manage gateway health webhooks.",
81
- workflows: "Inspect and operate workflow runs and effects.",
82
- "app-sign-in config": "Manage the trusted app website and pairing policy.",
83
- "accounts config": "Manage account types and sign-in visibility.",
84
- "accounts administration": "Manage account requests and account/group follow-up notices.",
85
- "ai quotas": "Manage direct Assistant chat limits and consumption.",
86
- "ai quotas config": "Export and replace quota policies with revision checks.",
87
- legal: "Manage Terms, Privacy, and Imprint.",
88
- linux: "Prepare Linux identities.",
89
- "linux config": "Manage Linux identity defaults and allocation range.",
90
- },
91
- commands: [
92
- ...appCredentialCommands,
93
- ...agentCommands,
94
- ...documentationCommands,
95
- ...appSignInCommands,
96
- ...accountCategoryCommands,
97
- ...accountAdministrationCommands,
98
- ...aiUsageCommands,
99
- ...aiModelCommands,
100
- ...aiQuotaCommands,
101
- ...aiSkillCommands,
102
- ...instanceCommands,
103
- ...gatewayCommands,
104
- ...logCommands,
105
- ...telemetryCommands,
106
- ...jobCommands,
107
- ...legalCommands,
108
- ...geoipCommands,
109
- ...linuxCommands,
110
- ...workflowCommands,
111
- ...dataCommands,
112
- ...notificationCommands,
113
- ...outgoingMailCommands,
114
- ...webhookCommands,
115
- ...metricsCommands,
116
- ...natsCommands,
117
- ...syncCommands,
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
+ ];
@@ -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. `.env.example`,
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 raw value. Empty and unset values never reach the schema. */
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
- const rawValue = (key: string, aliases: string[] = []): string | undefined => {
48
- for (const name of [key, ...aliases]) {
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.aliases);
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> = { $specs: specs, $keys: keys };
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; isolates its NATS resources from other installations on the same cluster.",
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(budgetSchema),
92
- default: { maxStepsPerSource: 2, tasksPerMinute: 10, burst: 20, sourcesPer24Hours: 2_000, scopeConcurrent: 1, globalConcurrent: 4 },
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` stop the owning process at startup.",
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-run callers use the Core port, not the gateway.",
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
- doc: "App-bound workload credential provisioned through Core's admin workload API; background apps such as Mail need one with `identity:invoke`.",
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 fetches keys from the public issuer origin.",
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(),
@@ -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 { defineEnv, type EnvRegistry, type EnvScope, type EnvSpec, type EnvSpecs, envBoolean, envList, envString } from "./define-env";
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
+ };