bazis 0.136.0 → 0.137.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 (75) hide show
  1. package/cli/main.ts +8 -0
  2. package/cli/parseCli.ts +9 -0
  3. package/cli/templates/module.ts +16 -19
  4. package/cli/templates/passport.ts +1 -1
  5. package/cli/upgrade/markColumns.ts +151 -0
  6. package/cli/upgrade/runUpgrade.ts +57 -0
  7. package/core/access/Access.module.ts +1 -1
  8. package/core/access/catalog/moduleFields.ts +16 -2
  9. package/core/access/catalog/permissions.ts +10 -0
  10. package/core/access/check/Access.ts +5 -5
  11. package/core/access/check/can.ts +2 -1
  12. package/core/access/index.ts +11 -1
  13. package/core/access/roles/roles.ts +4 -2
  14. package/core/agent/ToolRunner.ts +5 -0
  15. package/core/agent/session/entities.ts +45 -45
  16. package/core/http/Decorators/metadata.ts +3 -0
  17. package/core/http/Decorators/routes.ts +12 -0
  18. package/core/http/Errors/HttpError.ts +15 -1
  19. package/core/http/HttpServer.ts +19 -2
  20. package/core/http/Middleware/errorHandler.ts +16 -1
  21. package/core/http/OpenApi/openApiDocument.ts +43 -4
  22. package/core/http/Results/normalize.ts +38 -1
  23. package/core/http/Results/results.ts +5 -2
  24. package/core/http/Routing/RouterBuilder.ts +27 -2
  25. package/core/http/index.ts +3 -0
  26. package/core/http/middlewareTokens.ts +12 -0
  27. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  28. package/core/infra/connectors/postgres.ts +4 -2
  29. package/core/infra/index.ts +1 -1
  30. package/core/kernel/config/defineConfig.ts +55 -19
  31. package/core/kernel/index.ts +3 -0
  32. package/core/orm/buildOrmProviders.ts +2 -1
  33. package/core/orm/contextEntities.ts +36 -0
  34. package/core/orm/databaseConnector.ts +9 -3
  35. package/core/orm/databaseHttpError.ts +21 -0
  36. package/core/orm/index.ts +12 -1
  37. package/core/orm/ormModule.ts +17 -7
  38. package/core/orm/ownedStoreContributions.ts +2 -1
  39. package/core/scripts/di-generate.ts +61 -4
  40. package/core/tools/StandardTools.module.ts +1 -1
  41. package/core/tools/contracts/Tools.config.ts +13 -7
  42. package/core/tools/generated/deps.ts +3 -3
  43. package/core/tools/http/HttpRequest.tool.ts +1 -2
  44. package/core/tools/http/publicAddress.ts +2 -2
  45. package/core/tools/orm/OrmEntity.references.ts +7 -8
  46. package/core/workflow/contracts/Workflow.config.ts +5 -5
  47. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +2 -2
  48. package/core/workflow/definitions_module/http/Processes.controller.ts +2 -2
  49. package/core/workflow/definitions_module/internal/PublishedVersions.ts +39 -0
  50. package/core/workflow/definitions_module/model/WfProcess.model.ts +24 -11
  51. package/core/workflow/definitions_module/model/WfVersion.model.ts +7 -1
  52. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +4 -1
  53. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +17 -7
  54. package/core/workflow/generated/deps.ts +2 -1
  55. package/core/workflow/index.ts +9 -0
  56. package/core/workflow/internal/WorkflowLanguages.ts +1 -1
  57. package/core/workflow/internal/http.ts +2 -2
  58. package/core/workflow/runtime_module/background/WorkflowWorker.ts +6 -2
  59. package/core/workflow/runtime_module/http/Instances.controller.ts +1 -1
  60. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +43 -1
  61. package/core/workflow/runtime_module/model/WfHistory.model.ts +13 -6
  62. package/core/workflow/runtime_module/model/WfInstance.model.ts +30 -10
  63. package/core/workflow/runtime_module/model/WfJob.model.ts +22 -10
  64. package/core/workflow/runtime_module/model/WfSubscription.model.ts +8 -3
  65. package/core/workflow/runtime_module/model/WfToken.model.ts +16 -7
  66. package/core/workflow/tasks_module/model/WfTask.model.ts +25 -9
  67. package/library/openapi/codegen.ts +2 -2
  68. package/library/orm/DbContext.ts +37 -1
  69. package/library/orm/Metadata/ModelBuilder.ts +23 -7
  70. package/library/orm/Metadata/decorators.ts +23 -10
  71. package/library/orm/Metadata/types.ts +2 -0
  72. package/library/orm/Transactions/TransactionScopeCoordinator.ts +3 -3
  73. package/library/orm/errors.ts +39 -1
  74. package/library/orm/index.ts +1 -0
  75. package/package.json +1 -1
@@ -38,14 +38,23 @@ export function configEnum<const V extends readonly [string | number, ...(string
38
38
  return declared;
39
39
  }
40
40
  const invalidEnums = new WeakMap<object, KernelError>();
41
- type Cell = Primitive | SecretSpec | ConfigEnum;
41
+ export interface ConfigList { readonly __list: true; readonly default: readonly string[] }
42
+ /**
43
+ * A list of strings: `readonly string[]` in the code, comma-separated in the
44
+ * environment (`BAZIS_TOOLS__HTTP__ALLOW=api.example.com,*.example.org`). Items
45
+ * are trimmed, empty ones are dropped.
46
+ */
47
+ export function configList(defaultValue: readonly string[] = []): ConfigList {
48
+ return Object.freeze({ __list: true, default: Object.freeze([...defaultValue]) });
49
+ }
50
+ type Cell = Primitive | SecretSpec | ConfigEnum | ConfigList;
42
51
  type Defaults = Record<string, Cell>;
43
- export type ConfigValueType = Primitive | Secret;
52
+ export type ConfigValueType = Primitive | Secret | readonly string[];
44
53
  export type ConfigShape = Record<string, ConfigValueType>;
45
- type Resolved<V> = V extends SecretSpec ? Secret : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
54
+ type Resolved<V> = V extends SecretSpec ? Secret : V extends ConfigList ? readonly string[] : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
46
55
  type ResolveAll<D extends Defaults> = { [K in keyof D]: Resolved<D[K]> };
47
- type WidenCell<V> = V extends SecretSpec ? SecretSpec : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
48
- type CellFor<T> = [T] extends [Secret] ? SecretSpec : [T] extends [string] ? (string extends T ? string : ConfigEnum<T & string>) : [T] extends [number] ? (number extends T ? number : ConfigEnum<T & number>) : [T] extends [boolean] ? (boolean extends T ? boolean : never) : never;
56
+ type WidenCell<V> = V extends SecretSpec ? SecretSpec : V extends ConfigList ? readonly string[] : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
57
+ type CellFor<T> = [T] extends [Secret] ? SecretSpec : [T] extends [readonly string[]] ? ConfigList : [T] extends [string] ? (string extends T ? string : ConfigEnum<T & string>) : [T] extends [number] ? (number extends T ? number : ConfigEnum<T & number>) : [T] extends [boolean] ? (boolean extends T ? boolean : never) : never;
49
58
  type Declaration<T extends object> = { [K in keyof T]: CellFor<T[K]> };
50
59
  type OverrideFor<T> = T extends Secret ? SecretSpec : T;
51
60
  interface Rules<T extends object> {
@@ -76,18 +85,28 @@ export interface AppConfig<T extends object> extends ValidatableConfig {
76
85
  }
77
86
  export interface ConfigInspection {
78
87
  readonly key: string;
79
- readonly type: "string" | "number" | "boolean" | "secret";
88
+ readonly type: "string" | "number" | "boolean" | "secret" | "list";
80
89
  readonly env: readonly string[];
81
90
  readonly source: string;
82
91
  readonly value: Primitive;
83
92
  }
84
- export interface ConfigView<T extends object> extends AppConfig<T> {
93
+ /** The settings of one section as a service reads them. */
94
+ export interface ResolvedConfigView<T extends object> extends AppConfig<T> {
85
95
  readonly environment: Stand;
86
96
  inspect(): readonly ConfigInspection[];
87
97
  }
88
- export interface ConfigDefinition<T extends object> extends AppConfig<T> {
89
- readonly token: InjectionToken<ConfigView<T>>;
90
- resolve(environment?: Stand, configuration?: Configuration): ConfigView<T>;
98
+ /**
99
+ * The settings a service takes in its constructor. `T` is the declaration
100
+ * itself, `ConfigView<typeof mailConfig>`, so the settings are described once,
101
+ * by their defaults; or the shape of the settings, `ConfigView<MailConfig>`, for a
102
+ * declaration with an explicit type.
103
+ */
104
+ export type ConfigView<T extends object> = ResolvedConfigView<T extends ConfigDefinition<infer Shape, string> ? Shape : T>;
105
+ export interface ConfigDefinition<T extends object, P extends string = string> extends AppConfig<T> {
106
+ /** The name of the section: `mail`; `default` for a declaration without one. */
107
+ readonly prefix: P;
108
+ readonly token: InjectionToken<ResolvedConfigView<T>>;
109
+ resolve(environment?: Stand, configuration?: Configuration): ResolvedConfigView<T>;
91
110
  }
92
111
  const definitions = new WeakSet<object>();
93
112
  export function isConfigDefinition(config: ValidatableConfig): config is ConfigDefinition<object> {
@@ -118,8 +137,11 @@ function environmentNameProblem(name: string, claimed: ReadonlySet<string>): str
118
137
  return undefined;
119
138
  }
120
139
 
121
- function isSecret(cell: Cell): cell is SecretSpec { return typeof cell === "object" && cell !== null && "__secret" in cell; }
122
- function isEnum(cell: Cell): cell is ConfigEnum { return typeof cell === "object" && cell !== null && "__enum" in cell; }
140
+ function isSecret(cell: unknown): cell is SecretSpec { return typeof cell === "object" && cell !== null && "__secret" in cell; }
141
+ function isEnum(cell: unknown): cell is ConfigEnum { return typeof cell === "object" && cell !== null && "__enum" in cell; }
142
+ function isList(cell: unknown): cell is ConfigList { return typeof cell === "object" && cell !== null && "__list" in cell; }
143
+ /** `a, b,,c` → `["a", "b", "c"]`. */
144
+ function listItems(raw: string): readonly string[] { return Object.freeze(raw.split(",").map((item) => item.trim()).filter((item) => item !== "")); }
123
145
  function copyCell(cell: Cell): Cell {
124
146
  if (isSecret(cell)) return secret(cell.default);
125
147
  if (isEnum(cell)) {
@@ -127,20 +149,27 @@ function copyCell(cell: Cell): Cell {
127
149
  if (problem !== undefined) throw problem;
128
150
  return Object.freeze({ ...cell, values: Object.freeze([...cell.values]) });
129
151
  }
152
+ if (isList(cell)) {
153
+ if (!Array.isArray(cell.default) || cell.default.some((item) => typeof item !== "string")) throw new KernelError("configList requires a list of strings.");
154
+ return configList(cell.default);
155
+ }
156
+ // A list in an environment section is written as it is read: `test: { hosts: ["a"] }`.
157
+ if (Array.isArray(cell)) return copyCell(configList(cell as readonly string[]));
130
158
  if (["string", "number", "boolean"].includes(typeof cell)) return cell;
131
- throw new KernelError("Configuration supports only primitive values, secret() and configEnum().");
159
+ throw new KernelError("Configuration supports only primitive values, secret(), configEnum() and configList().");
132
160
  }
133
161
  function safeValue(key: string, value: ConfigValueType): Primitive {
134
- if (value instanceof Secret || (typeof value === "string" && isSensitiveKey(key))) return "***";
135
- if (typeof value !== "string") return value;
162
+ if (value instanceof Secret || ((typeof value === "string" || Array.isArray(value)) && isSensitiveKey(key))) return "***";
163
+ if (Array.isArray(value)) return value.join(", ");
164
+ if (typeof value !== "string") return value as Primitive;
136
165
  try {
137
166
  const url = new URL(value);
138
167
  if (url.username || url.password) { url.username = "***"; url.password = "***"; return redactSensitiveText(url.toString()); }
139
168
  } catch { /* Most strings are not URLs. */ }
140
169
  return redactSensitiveText(value);
141
170
  }
142
- export function defineConfig<const D extends Defaults>(schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>>;
143
- export function defineConfig<const D extends Defaults>(prefix: string, schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>>;
171
+ export function defineConfig<const D extends Defaults>(schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>, "default">;
172
+ export function defineConfig<const P extends string, const D extends Defaults>(prefix: P, schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>, P>;
144
173
  export function defineConfig<T extends object>(schema: SchemaFor<T>): ConfigDefinition<T>;
145
174
  export function defineConfig<T extends object>(prefix: string, schema: SchemaFor<T>): ConfigDefinition<T>;
146
175
  export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, maybeSchema?: ConfigSchema<Defaults>): ConfigDefinition<ConfigShape> {
@@ -157,6 +186,7 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
157
186
  // errors, instead of crashing the import of the file that declares it.
158
187
  const fail = (): never => { throw error; };
159
188
  definition = Object.freeze({
189
+ prefix: prefix || "default",
160
190
  token: Object.freeze(createToken<ConfigView<ConfigShape>>(`Config:${prefix || "default"}`)),
161
191
  resolve: fail,
162
192
  get: fail,
@@ -209,7 +239,7 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
209
239
  const base = schema.default[key]!;
210
240
  const override = schema[environment]?.[key];
211
241
  const cell = override ?? base;
212
- const type = isSecret(base) ? "secret" : isEnum(base) ? typeof base.default : typeof base;
242
+ const type = isSecret(base) ? "secret" : isList(base) ? "list" : isEnum(base) ? typeof base.default : typeof base;
213
243
  const candidates: { raw: string; source: string; priority: number; name: string }[] = [];
214
244
  for (const name of names.get(key)!) {
215
245
  const sourceKey = name.slice(ENV_PREFIX.length).toLowerCase().replaceAll("__", ".");
@@ -226,7 +256,7 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
226
256
  const variables = [...new Set(winners.map(candidate => candidate.name))];
227
257
  issue(key, "conflict", `conflicting environment aliases: ${variables.join(" and ")} are set to different values; set only one`);
228
258
  }
229
- const raw = selected?.raw ?? (isSecret(cell) || isEnum(cell) ? cell.default : cell);
259
+ const raw: unknown = selected?.raw ?? (isSecret(cell) || isEnum(cell) || isList(cell) ? cell.default : cell);
230
260
  let value: ConfigValueType | undefined;
231
261
  if (type === "secret") {
232
262
  // secret("") reads like an empty default but would mean "required":
@@ -238,6 +268,11 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
238
268
  // No "apiKey: …" in the hint: the console redaction would hide it.
239
269
  else if (environment === "production" && selected === undefined && override === undefined) issue(key, "development-secret", `production would use the development default secret from the code; set ${names.get(key)!.join(" or ")}, or declare ${key} in the production section`);
240
270
  else value = new Secret(raw);
271
+ } else if (type === "list") {
272
+ // From the environment a comma-separated string, from the code a list.
273
+ if (typeof raw === "string") value = listItems(raw);
274
+ else if (Array.isArray(raw) && (raw as unknown[]).every((item) => typeof item === "string")) value = Object.freeze([...(raw as string[])]);
275
+ else issue(key, "string", "expected a list of strings");
241
276
  } else if (type === "number") {
242
277
  const parsed = Number(raw);
243
278
  if (raw === undefined || (typeof raw === "string" && !raw.trim()) || !Number.isFinite(parsed)) issue(key, "number", `expected a finite number${got(key, raw)}${source(key, selected)}`, from(selected));
@@ -273,6 +308,7 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
273
308
  });
274
309
  };
275
310
  return Object.freeze({
311
+ prefix: prefix || "default",
276
312
  token: Object.freeze(createToken<ConfigView<ConfigShape>>(`Config:${prefix || "default"}`)),
277
313
  resolve,
278
314
  get: <K extends keyof ConfigShape>(key: K): ConfigShape[K] => resolve().get(key),
@@ -16,12 +16,15 @@ export { Secret } from "./config/Secret";
16
16
  export {
17
17
  defineConfig,
18
18
  configEnum,
19
+ configList,
19
20
  secret,
20
21
  type AppConfig,
21
22
  type ConfigDefinition,
22
23
  type ConfigView,
24
+ type ResolvedConfigView,
23
25
  type ConfigInspection,
24
26
  type ConfigEnum,
27
+ type ConfigList,
25
28
  type ConfigSchema,
26
29
  type ConfigShape,
27
30
  type ConfigValueType,
@@ -1,4 +1,5 @@
1
1
  import { DI, HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition, type ProviderDependencyList, type ServiceResolver } from "../di";
2
+ import { contextEntities } from "./contextEntities";
2
3
  import { getGeneratedClassDeps } from "../di/module/autoDeps";
3
4
  import { resolveDependencyList } from "../di/internal/resolveDependencyList";
4
5
  import { ConfigRegistry, HEALTH_CHECK, StartupReport } from "../kernel";
@@ -27,7 +28,7 @@ function createContextOptions<TContext extends DbContext>(
27
28
  ): DbContextOptions {
28
29
  return bindKeys(new DbContextOptions({
29
30
  provider,
30
- entities: config.entities ?? [],
31
+ entities: contextEntities(config),
31
32
  validateOnSave: config.validateOnSave,
32
33
  executionStrategy: config.executionStrategy,
33
34
  }), serviceProvider);
@@ -0,0 +1,36 @@
1
+ import { DbContext } from "../../library/orm";
2
+
3
+ type EntityClass = new () => object;
4
+ type ContextClass = abstract new (...parameters: never[]) => DbContext;
5
+
6
+ /** The sets of a context class are read once: a class does not change. */
7
+ const sets = new WeakMap<ContextClass, readonly EntityClass[] | Error>();
8
+
9
+ function setsOf(context: ContextClass): readonly EntityClass[] | Error {
10
+ let known = sets.get(context);
11
+ if (known === undefined) {
12
+ try {
13
+ known = DbContext.entitiesOf(context as unknown as Parameters<typeof DbContext.entitiesOf>[0]);
14
+ } catch (error) {
15
+ known = error instanceof Error ? error : new Error(String(error));
16
+ }
17
+ sets.set(context, known);
18
+ }
19
+ return known;
20
+ }
21
+
22
+ /**
23
+ * The entities of a context: the listed ones in their order, then those of its
24
+ * sets (`this.set(...)`). A context whose constructor cannot run without its
25
+ * dependencies has only the listed ones, and with none listed that is an error.
26
+ */
27
+ export function contextEntities(config: { readonly context?: ContextClass; readonly entities?: readonly EntityClass[] }): readonly EntityClass[] {
28
+ const listed = config.entities ?? [];
29
+ if (!config.context) return listed;
30
+ const ofSets = setsOf(config.context);
31
+ if (ofSets instanceof Error) {
32
+ if (listed.length === 0) throw ofSets;
33
+ return listed;
34
+ }
35
+ return ofSets.length === 0 ? listed : [...new Set([...listed, ...ofSets])];
36
+ }
@@ -1,7 +1,10 @@
1
1
  import { errorMessage, InfraError, reader, requireValue, postgresConnectionOptions, settingNames, type InfraConnector, type PostgresConfigShape } from "../infra";
2
- import { postgresAddress, serverVersion } from "../infra/connectors/postgres";
2
+ import { POSTGRES_SSL_MODES, postgresAddress, serverVersion, type PostgresSslMode } from "../infra/connectors/postgres";
3
3
  import type { AppConfig, ConfigRegistry } from "../kernel";
4
4
  import { postgres, type DatabaseProvider, type PostgresServerTimeouts } from "../../library/orm";
5
+ import { singletonValue } from "../di";
6
+ import { HTTP_ERROR_MAPPER } from "../http/middlewareTokens";
7
+ import { databaseHttpError } from "./databaseHttpError";
5
8
  import { DATABASE_PROVIDER } from "./DATABASE_PROVIDER";
6
9
 
7
10
  /** Connect before the servers and ORM features: open the database before anyone asks for it. */
@@ -13,7 +16,7 @@ export interface PostgresOrmConfigShape extends PostgresConfigShape {
13
16
  readonly connectionTimeout?: number;
14
17
  readonly idleTimeout?: number;
15
18
  readonly maxLifetime?: number;
16
- readonly tls?: "disable" | "allow" | "prefer" | "require" | "verify-ca" | "verify-full";
19
+ readonly tls?: PostgresSslMode;
17
20
  readonly tlsCa?: string;
18
21
  readonly operationTimeoutMs?: number;
19
22
  readonly cancellationTimeoutMs?: number;
@@ -43,7 +46,7 @@ function buildPostgresProvider<T extends PostgresOrmConfigShape>(config: AppConf
43
46
  // defineConfig keys are always present, so an empty tlsCa means "no additional CA".
44
47
  const tls = optional("tls"), configuredCa = optional("tlsCa"), ca = configuredCa === "" ? undefined : configuredCa;
45
48
  if (tls !== undefined) {
46
- if (typeof tls !== "string" || !["disable", "allow", "prefer", "require", "verify-ca", "verify-full"].includes(tls)) throw new InfraError("Invalid PostgreSQL configuration field: tls.");
49
+ if (typeof tls !== "string" || !(POSTGRES_SSL_MODES as readonly string[]).includes(tls)) throw new InfraError("Invalid PostgreSQL configuration field: tls.");
47
50
  options.tls = tls;
48
51
  }
49
52
  if (ca !== undefined) {
@@ -96,6 +99,9 @@ export function ormBazisConnect(
96
99
  token: DATABASE_PROVIDER,
97
100
  config,
98
101
  phase: DATABASE_PHASE,
102
+ // Database errors answer by their meaning (409, 404, 422, 503) without a setting in the application.
103
+ providers: [singletonValue(HTTP_ERROR_MAPPER, databaseHttpError)],
104
+ exports: [HTTP_ERROR_MAPPER],
99
105
  create(configs) {
100
106
  const c = reader(config, configs);
101
107
  target = `PostgreSQL at ${String(c.get("host"))}:${String(c.get("port"))} (database "${String(c.get("database"))}", user "${String(c.get("username"))}")`;
@@ -0,0 +1,21 @@
1
+ import { ConflictError, HttpError } from "../http/Errors/HttpError";
2
+ import { englishHttpMessage, STATUS_MESSAGE_KEYS } from "../http/Errors/messages";
3
+ import { UniqueViolationError } from "../../library/orm";
4
+ import { databaseErrorStatus } from "./databaseErrorStatus";
5
+
6
+ /**
7
+ * A database error as the HTTP answer for it: the statuses of
8
+ * {@link databaseErrorStatus}, and for a unique violation the text of
9
+ * `@Index({ conflict })` with the properties of the index in the details:
10
+ * `{ error, details: { code: "unique", fields: ["name"] } }`. The ORM registers
11
+ * it for every application with HTTP; the message never carries values of the row.
12
+ */
13
+ export function databaseHttpError(error: unknown): HttpError | undefined {
14
+ if (error instanceof UniqueViolationError) {
15
+ return new ConflictError(error.conflict, { code: "unique", fields: [...error.properties] });
16
+ }
17
+ const status = databaseErrorStatus(error);
18
+ if (status === undefined) return undefined;
19
+ const key = STATUS_MESSAGE_KEYS[status] ?? (status >= 500 ? "serverError" : "clientError");
20
+ return new HttpError(status, englishHttpMessage(key), undefined, { key });
21
+ }
package/core/orm/index.ts CHANGED
@@ -11,12 +11,14 @@ export { IRepository, repositoryFor } from "./repository";
11
11
  export { registerRepositories } from "./registerRepositories";
12
12
  export { paginate, type PageResult } from "./listQuery";
13
13
  export { databaseErrorStatus } from "./databaseErrorStatus";
14
+ export { databaseHttpError } from "./databaseHttpError";
14
15
  export { encryptedWith } from "./encryptedWith";
15
16
 
16
17
  import { registerModuleMetadataExpander, type BazisModuleRef } from "../di";
17
18
  import { registerModuleDescriber } from "../kernel/report/moduleTree";
18
19
  import { OrmOwnedStoreAdmissionError, type DbContext } from "../../library/orm";
19
20
  import { registerRepositoryEncapsulationHook } from "./encapsulationHook";
21
+ import { contextEntities } from "./contextEntities";
20
22
  import { ormModule, type OrmModuleConfig } from "./ormModule";
21
23
  import { readOwnedStoreRegistration, revalidateOwnedStoreRegistration } from "./ownedStoreContributions";
22
24
 
@@ -62,13 +64,22 @@ function declaredOwnedStore(config: OrmModuleConfig<DbContext>): unknown {
62
64
  return descriptor.value;
63
65
  }
64
66
 
67
+ /** For the startup screen only: a context whose sets cannot be read shows what is listed. */
68
+ function describedEntities(config: OrmModuleConfig<DbContext>): readonly unknown[] {
69
+ try {
70
+ return contextEntities(config);
71
+ } catch {
72
+ return config.entities ?? [];
73
+ }
74
+ }
75
+
65
76
  // The module tree of the startup screen: `Task, Comment · TaskDbContext`.
66
77
  registerModuleDescriber((metadata) => {
67
78
  const orm = (metadata as { ormBazis?: OrmModuleConfig<DbContext> | readonly OrmModuleConfig<DbContext>[] }).ormBazis;
68
79
  if (orm === undefined) return [];
69
80
  return (Array.isArray(orm) ? orm : [orm]).map((config: OrmModuleConfig<DbContext>) => ({
70
81
  kind: "table" as const,
71
- text: `${(config.entities ?? []).map((entity) => (entity as { name?: string }).name ?? "entity").join(", ") || "no entities"} · ${(config.context as { name?: string } | undefined)?.name ?? "DbContext"}`,
82
+ text: `${describedEntities(config).map((entity) => (entity as { name?: string }).name ?? "entity").join(", ") || "no entities"} · ${(config.context as { name?: string } | undefined)?.name ?? "DbContext"}`,
72
83
  }));
73
84
  });
74
85
 
@@ -7,6 +7,9 @@ import {
7
7
  type DbContextOptionsConfig,
8
8
  type Migration,
9
9
  } from "../../library/orm";
10
+ import { HTTP_ERROR_MAPPER } from "../http/middlewareTokens";
11
+ import { databaseHttpError } from "./databaseHttpError";
12
+ import { contextEntities } from "./contextEntities";
10
13
  import { DATABASE_PROVIDER } from "./DATABASE_PROVIDER";
11
14
  import { IRepository } from "./repository";
12
15
  import { registerRepositories } from "./registerRepositories";
@@ -40,7 +43,12 @@ type ContextClass<TContext extends DbContext> = new (options: DbContextOptions,
40
43
  export interface OrmModuleConfig<TContext extends DbContext> {
41
44
  /** Context class (a DbContext subclass). Omitted in the "connection" mode. */
42
45
  readonly context?: ContextClass<TContext>;
43
- /** Entities mapped by this context. */
46
+ /**
47
+ * Entities of the context besides those of its sets: by default the context
48
+ * maps what it declares with `this.set(...)`. List here only an entity without
49
+ * a set, or every entity of a context whose constructor cannot run without its
50
+ * dependencies.
51
+ */
44
52
  readonly entities?: readonly EntityClass[];
45
53
  /**
46
54
  * Database provider value (`postgres(...)`). Without it the context connects
@@ -86,7 +94,7 @@ function defineFeatureOrmModule<TContext extends DbContext>(
86
94
  providers,
87
95
  exports: withRepositories ? [config.context, IRepository] : [config.context],
88
96
  configure: withRepositories
89
- ? (di) => registerRepositories(di, config.context, config.entities ?? [])
97
+ ? (di) => registerRepositories(di, config.context, contextEntities(config))
90
98
  : undefined,
91
99
  };
92
100
  }
@@ -94,6 +102,8 @@ function defineFeatureOrmModule<TContext extends DbContext>(
94
102
  function defineConnectionModule(provider: DatabaseProvider, healthCheck: boolean): BazisModuleRef {
95
103
  const providers: ProviderDefinition[] = [
96
104
  singletonValue(DATABASE_PROVIDER, provider),
105
+ // Database errors answer by their meaning (409, 404, 422, 503) without a setting in the application.
106
+ singletonValue(HTTP_ERROR_MAPPER, databaseHttpError),
97
107
  DI.singleton(
98
108
  DI.factoryProvider(
99
109
  HOSTED_SERVICE,
@@ -112,7 +122,7 @@ function defineConnectionModule(provider: DatabaseProvider, healthCheck: boolean
112
122
  /** Single ORM registration entry point (see {@link OrmModuleConfig}). */
113
123
  export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TContext>): BazisModuleRef {
114
124
  if (config.ownedStore !== undefined) {
115
- if (!config.context || !config.entities || config.entities.length === 0) {
125
+ if (!config.context || contextEntities(config).length === 0) {
116
126
  throw new OrmError("ORM owned store requires a context and entities.");
117
127
  }
118
128
  if (config.provider) {
@@ -124,7 +134,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
124
134
  // Provider factories outlive this call. Keep their context/entity view
125
135
  // registration-local so a caller cannot mutate the descriptor afterwards
126
136
  // and silently change the runtime graph behind its owned receipt.
127
- const buildConfig = Object.freeze({ ...config, context: config.context, entities: Object.freeze([...config.entities]) }) as OrmBuildConfig<TContext>;
137
+ const buildConfig = Object.freeze({ ...config, context: config.context, entities: Object.freeze([...contextEntities(config)]) }) as OrmBuildConfig<TContext>;
128
138
  const module = defineFeatureOrmModule(buildConfig, buildOrmModuleProviders(buildConfig, DATABASE_PROVIDER));
129
139
  attachOwnedStoreRegistration(module, buildConfig.context, buildConfig.entities ?? [], config.ownedStore);
130
140
  return module;
@@ -148,7 +158,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
148
158
  // may replace them with a container-local snapshot.
149
159
  const standaloneOptions = new DbContextOptions({
150
160
  provider: config.provider,
151
- entities: config.entities ?? [],
161
+ entities: contextEntities(config),
152
162
  validateOnSave: config.validateOnSave,
153
163
  executionStrategy: config.executionStrategy,
154
164
  });
@@ -158,7 +168,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
158
168
  providers.push(buildOrmHealthCheck(config.provider));
159
169
  }
160
170
  const module = defineFeatureOrmModule(buildConfig, providers);
161
- attachOrmGraphContribution(module, buildConfig.context, buildConfig.entities ?? [], config, registration);
171
+ attachOrmGraphContribution(module, buildConfig.context, contextEntities(buildConfig), config, registration);
162
172
  return module;
163
173
  }
164
174
 
@@ -174,7 +184,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
174
184
  const module = defineFeatureOrmModule(buildConfig, providers);
175
185
  // `registration` is deliberately per ormModule call, not a token or public
176
186
  // identity. It links this factory to its container-local graph snapshot.
177
- attachOrmGraphContribution(module, buildConfig.context, buildConfig.entities ?? [], config, registration);
187
+ attachOrmGraphContribution(module, buildConfig.context, contextEntities(buildConfig), config, registration);
178
188
  return module;
179
189
  }
180
190
 
@@ -6,6 +6,7 @@ import {
6
6
  type BazisModuleMetadata,
7
7
  } from "../di";
8
8
  import { OrmModel, type DbContext, type OrmOwnedStoreDefinitionV1 } from "../../library/orm";
9
+ import { contextEntities } from "./contextEntities";
9
10
  import { compileExpectedSchema, type OrmExpectedSchema } from "../../library/orm/Schema/ExpectedSchema";
10
11
  import { isDefinedOrmOwnedStoreV1 } from "../../library/orm/Schema/OrmOwnedStore";
11
12
  import { defineOrmOwnedStoreV1 } from "../../library/orm/Schema/OrmOwnedStore";
@@ -159,7 +160,7 @@ export function ownedStoreRegistrations(snapshot: ModuleOwnedContributionSnapsho
159
160
  // view. Direct ordinary modules intentionally read their caller config at
160
161
  // build time, then both graph preflight and runtime factories use this copy.
161
162
  const source = entry.config as { readonly entities?: readonly EntityClass[]; readonly migrations?: readonly unknown[] } | undefined;
162
- const entities = source === undefined ? entry.entities : Object.freeze([...(source.entities ?? [])]);
163
+ const entities = source === undefined ? entry.entities : Object.freeze([...contextEntities({ context: entry.context, entities: source.entities })]);
163
164
  // Migration instances remain caller-owned; only the sequence membership and
164
165
  // order are part of this container's effective config snapshot.
165
166
  const migrations = source?.migrations;
@@ -189,6 +189,8 @@ interface ConstructorDependencies {
189
189
  readonly unsupportedParameter?: { readonly index: number; readonly name: string; readonly type: string };
190
190
  }
191
191
  let classDeps = new Map<ts.ClassDeclaration, readonly CollectedDependency[]>();
192
+ /** `permissions` of the target's modules: the exported constant and its file. */
193
+ let permissionDeclarations = new Map<string, { readonly name: string; readonly file: string }>();
192
194
  let inferredDiClasses = new Set<ts.ClassDeclaration>();
193
195
  let explicitlyBoundDiClasses = new Set<ts.ClassDeclaration>();
194
196
  // Known runtime token names: createToken descriptions + class names usable as class-tokens.
@@ -505,7 +507,7 @@ function collectDynamicImportDiagnostics(source: ts.SourceFile, target: string,
505
507
 
506
508
  function resetTargetAnalysis(files: Set<string>, generatedDir: string): void {
507
509
  activeGeneratedDir = generatedDir;
508
- classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set();
510
+ classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set(); permissionDeclarations = new Map();
509
511
  tokenDescriptions = new Set(); configPrefixesByType = new Map(); knownClassNames = new Set(); warnings = []; fatalErrors = [];
510
512
  httpBindings = {}; httpControllerFiles = {}; openApiSchemas = {}; openApiOperations = {};
511
513
  grpcBindings = new Map();
@@ -555,6 +557,8 @@ async function generateTarget(name: string, reachable: Set<string>, production:
555
557
  stageWrite(`${generatedDir}/httpRequestModels.ts`, requestModels);
556
558
  stageWrite(`${generatedDir}/httpListModels.ts`, listModels);
557
559
  stageWrite(`${generatedDir}/openapi.ts`, openApi);
560
+ if (permissionDeclarations.size > 0) stageWrite(`${generatedDir}/permissions.ts`, renderPermissions());
561
+ else stageRemove(`${generatedDir}/permissions.ts`);
558
562
  // Generated before 0.99.0, when agents were classes; nothing imports it now.
559
563
  stageRemove(`${generatedDir}/agentCatalog.ts`);
560
564
  stageWrite(`${generatedDir}/runtime.ts`, renderGeneratedRuntime(name));
@@ -708,6 +712,15 @@ function renderGeneratedRuntime(targetId: string): string {
708
712
  }
709
713
 
710
714
  /** Sources of this target as codegen saw them; read back by the generated runtime. */
715
+ /** Types only: the keys of the target's permissions for `can`, `access.can`, `access.filter` and `defineRoles`. */
716
+ function renderPermissions(): string {
717
+ const entries = [...permissionDeclarations.values()].sort((left, right) => left.file.localeCompare(right.file) || left.name.localeCompare(right.name));
718
+ const lines = ["// This file is auto-generated by `bazis codegen`.", "// Do not edit manually.", 'import type { PermissionsOf } from "bazis/core/access";'];
719
+ entries.forEach((entry, index) => lines.push(`import type { ${entry.name} as Permissions_${index} } from ${JSON.stringify(toModuleSpecifierFrom(activeGeneratedDir, entry.file))};`));
720
+ lines.push("", 'declare module "bazis/core/access" {', ` interface BazisPermissions extends ${entries.map((_, index) => `PermissionsOf<typeof Permissions_${index}>`).join(", ")} {}`, "}", "");
721
+ return lines.join("\n");
722
+ }
723
+
711
724
  function renderSourceFingerprint(): string {
712
725
  const files = sourceEntries.map((entry) => entry.filePath).sort();
713
726
  const framework = JSON.parse(readFileSync(path.join(FRAMEWORK_DIR, "package.json"), "utf8")).version as string;
@@ -832,6 +845,8 @@ function collectTokenDescriptions(source: ts.SourceFile): void {
832
845
  const visit = (node: ts.Node): void => {
833
846
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.arguments.length > 0) {
834
847
  const firstArg = node.arguments[0];
848
+ // defineConfig({ default: … }) without a section: its token is Config:default.
849
+ if (node.expression.text === "defineConfig" && firstArg && !ts.isStringLiteralLike(firstArg)) tokenDescriptions.add("Config:default");
835
850
  if (firstArg && ts.isStringLiteralLike(firstArg)) {
836
851
  if (node.expression.text === "createToken" || node.expression.text === "createOpenGenericTokenFamily") {
837
852
  tokenDescriptions.add(firstArg.text);
@@ -873,7 +888,9 @@ function resolveConfigViewDeps(): void {
873
888
  if (prefixes.length !== 1) {
874
889
  classDeps.delete(declaration);
875
890
  if (needsInferredDiDeps(declaration)) {
876
- fatalErrors.push(prefixes.length === 0
891
+ fatalErrors.push(typeName.startsWith("typeof ")
892
+ ? `BAZIS_DI_CONFIG_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", but ${typeName.slice(7)} is not a declaration whose section codegen can read. Declare it as defineConfig("name", { default: {...} }) with a literal name and without a type argument.`
893
+ : prefixes.length === 0
877
894
  ? `BAZIS_DI_CONFIG_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", but no defineConfig<${typeName}>(...) declaration was found. Declare the configuration with an explicit type argument, for example defineConfig<${typeName}>("name", { default: {...} }).`
878
895
  : `BAZIS_DI_CONFIG_AMBIGUOUS: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", which matches several declarations: ${prefixes.map((prefix) => `"${prefix}"`).join(", ")}. Give each declaration its own type.`);
879
896
  }
@@ -987,6 +1004,9 @@ function collectDiClassRegistrations(source: ts.SourceFile): void {
987
1004
  const classes = properties.get(name);
988
1005
  if (classes && ts.isArrayLiteralExpression(classes)) for (const entry of classes.elements) record(entry);
989
1006
  }
1007
+ // `references: { "orders.order": OrderReferences }`: the field registers the classes.
1008
+ const references = objectFor(properties.get("references"));
1009
+ if (references) for (const entry of propertiesOf(references).values()) record(entry);
990
1010
  const imports = properties.get("imports");
991
1011
  if (imports && ts.isArrayLiteralExpression(imports)) for (const entry of imports.elements) recordModule(entry);
992
1012
  };
@@ -1093,12 +1113,33 @@ function collectProviderFormErrors(source: ts.SourceFile): void {
1093
1113
  const target = call && callee(call);
1094
1114
  return target?.name === "ormEntityTools" && target.file?.endsWith("/core/tools/orm/ormEntityTools.ts") === true;
1095
1115
  };
1116
+ /**
1117
+ * `permissions: orderPermissions`: the exported constant goes to the generated
1118
+ * `permissions.ts`, which makes its keys the type of `can` and `defineRoles`.
1119
+ * A framework pack names its own in its sources.
1120
+ */
1121
+ const collectPermissions = (module: string, initializer: ts.Expression): void => {
1122
+ let symbol = ts.isIdentifier(initializer) ? checker.getSymbolAtLocation(initializer) : undefined;
1123
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
1124
+ const declaration = symbol?.valueDeclaration;
1125
+ const file = normalizeProjectPath((declaration ?? initializer).getSourceFile().fileName);
1126
+ // A framework pack names its permissions in its own sources.
1127
+ if (file.includes("/node_modules/") || isFrameworkPackPath(file)) return;
1128
+ const fileSymbol = declaration && checker.getSymbolAtLocation(declaration.getSourceFile());
1129
+ const exported = fileSymbol && symbol && checker.getExportsOfModule(fileSymbol).find((entry) => (entry.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(entry) : entry) === symbol);
1130
+ if (!declaration || !ts.isVariableDeclaration(declaration) || exported === undefined) {
1131
+ fatalErrors.push(`BAZIS_ACCESS_PERMISSIONS_FORM: ${sourceLocation(initializer)}: permissions of ${module}: "${text(initializer)}" — export the result of definePermissions(...) as a constant and name it here, so that its keys become the type of can(...) and defineRoles(...).`);
1132
+ return;
1133
+ }
1134
+ permissionDeclarations.set(`${file}\0${exported.name}`, { name: exported.name, file });
1135
+ };
1096
1136
  const checkModule = (module: string, argument: ts.Expression | undefined): void => {
1097
1137
  const object = argument && resolved(argument, ts.isObjectLiteralExpression);
1098
1138
  if (!object) return;
1099
1139
  for (const property of object.properties) {
1100
1140
  if (!ts.isPropertyAssignment(property)) continue;
1101
1141
  const field = property.name.getText().replace(/["']/g, "");
1142
+ if (field === "permissions") collectPermissions(module, property.initializer);
1102
1143
  if (field !== "providers" && !CLASS_LISTS.includes(field)) continue;
1103
1144
  const list = resolved(property.initializer, ts.isArrayLiteralExpression);
1104
1145
  if (!list) {
@@ -1182,6 +1223,13 @@ function readInheritedConstructorDeps(node: ts.ClassDeclaration): ConstructorDep
1182
1223
  return { deps };
1183
1224
  }
1184
1225
 
1226
+ /** The section of a `defineConfig("mail", …)` declaration by its type: the literal of its `prefix`. */
1227
+ function configPrefixOf(type: ts.Type): string | undefined {
1228
+ const prefix = type.getProperty("prefix");
1229
+ const literal = prefix && checker.getTypeOfSymbol(prefix);
1230
+ return literal?.isStringLiteral() ? literal.value : undefined;
1231
+ }
1232
+
1185
1233
  function dependencyFromType(type: ts.Type): CollectedDependency | undefined {
1186
1234
  if (type.flags & ts.TypeFlags.TypeParameter) return undefined;
1187
1235
  const symbol = type.getSymbol();
@@ -1191,8 +1239,11 @@ function dependencyFromType(type: ts.Type): CollectedDependency | undefined {
1191
1239
  const name = declaration?.name?.text ?? type.aliasSymbol?.name ?? symbol?.name;
1192
1240
  if (!name || BUILTIN_DEPENDENCY_NAMES.has(name)) return undefined;
1193
1241
  const arguments_ = type.flags & ts.TypeFlags.Object ? checker.getTypeArguments(type as ts.TypeReference) : [];
1194
- if (name === "ConfigView") {
1195
- const configType = arguments_[0];
1242
+ if (name === "ConfigView" || name === "ResolvedConfigView") {
1243
+ // ConfigView<typeof mailConfig>: the declaration carries the name of its section in its type.
1244
+ const declared = type.aliasTypeArguments?.[0] && configPrefixOf(type.aliasTypeArguments[0]);
1245
+ if (declared !== undefined) return { name: `Config:${declared}` };
1246
+ const configType = type.aliasTypeArguments?.[0] ?? arguments_[0];
1196
1247
  const configTypeName = configType && (configType.aliasSymbol?.name ?? configType.getSymbol()?.name);
1197
1248
  return configTypeName ? { name: `ConfigView<${configTypeName}>` } : undefined;
1198
1249
  }
@@ -1244,6 +1295,12 @@ function readConstructorTypeNames(node: ts.ClassDeclaration): ConstructorDepende
1244
1295
 
1245
1296
  if (refName === "ConfigView" && paramType && ts.isTypeReferenceNode(paramType)) {
1246
1297
  const configType = paramType.typeArguments?.[0];
1298
+ if (configType && ts.isTypeQueryNode(configType)) {
1299
+ // ConfigView<typeof mailConfig>: the declaration carries the name of its section in its type.
1300
+ const declared = configPrefixOf(checker.getTypeFromTypeNode(configType));
1301
+ deps.push({ name: declared === undefined ? `ConfigView<${configType.getText()}>` : `Config:${declared}`, ...optionalFlag });
1302
+ continue;
1303
+ }
1247
1304
  const configTypeName = configType ? getDependencyTypeName(configType) : undefined;
1248
1305
  if (configTypeName) {
1249
1306
  deps.push({ name: `ConfigView<${configTypeName}>`, ...optionalFlag });
@@ -16,7 +16,7 @@ import { ConfigSecretStore, SecretStore } from "./secrets/SecretStore";
16
16
  config: [toolsConfig],
17
17
  references: { "orm.entity": OrmEntityReferences, "orm.field": OrmFieldReferences },
18
18
  tools: [HttpRequestTool],
19
- providers: [singleton(OrmEntityReferences), singleton(OrmFieldReferences), singleton(SecretStore, ConfigSecretStore)],
19
+ providers: [singleton(SecretStore, ConfigSecretStore)],
20
20
  exports: [SecretStore],
21
21
  })
22
22
  export class StandardToolsModule {}
@@ -1,29 +1,35 @@
1
- import { defineConfig } from "../../kernel";
1
+ import { configList, defineConfig } from "../../kernel";
2
2
 
3
3
  /**
4
- * Settings of the standard Tools. `http.allow` is the comma-separated list of hosts
5
- * `http.request` may reach: `api.example.com`, `*.example.com`, `localhost:8080`.
4
+ * Settings of the standard Tools. `http.allow` is the list of hosts `http.request`
5
+ * may reach, comma-separated in the environment: `api.example.com`, `*.example.com`,
6
+ * `localhost:8080`.
6
7
  * Empty by default: requests go nowhere. `http.allowPrivateNetworks` lets it reach
7
8
  * private and loopback addresses of a listed host: intranet services, tests.
8
9
  * `http.callers` says who may call it: `workflow`, `agent`.
9
10
  */
10
11
  export interface ToolsSettings {
11
- readonly "http.allow": string;
12
- readonly "http.callers": string;
12
+ readonly "http.allow": readonly string[];
13
+ readonly "http.callers": readonly string[];
13
14
  readonly "http.allowPrivateNetworks": boolean;
14
15
  readonly "http.maxResponseBytes": number;
15
16
  readonly "http.timeoutMs": number;
16
17
  }
17
18
 
19
+ /** A host, a subdomain pattern or an IPv6 address in brackets, with an optional port. */
20
+ const HOST = /^(\*\.)?[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?(:\d{1,5})?$|^\[[0-9A-Fa-f:.]+\](:\d{1,5})?$/;
21
+
18
22
  export const toolsConfig = defineConfig<ToolsSettings>("tools", {
19
23
  default: {
20
- "http.allow": "",
21
- "http.callers": "workflow",
24
+ "http.allow": configList(),
25
+ "http.callers": configList(["workflow"]),
22
26
  "http.allowPrivateNetworks": false,
23
27
  "http.maxResponseBytes": 1_048_576,
24
28
  "http.timeoutMs": 30_000,
25
29
  },
26
30
  validate: {
31
+ "http.allow": (value) => value.find((item) => !HOST.test(item)) === undefined ? undefined : `must list hosts like api.example.com, *.example.com or localhost:8080; ${JSON.stringify(value.find((item) => !HOST.test(item)))} is not one`,
32
+ "http.callers": (value) => (value.every((item) => /^[a-z][a-z0-9_.-]{0,63}$/.test(item)) ? undefined : "must list caller ids like workflow, agent"),
27
33
  "http.maxResponseBytes": (value) => (Number.isSafeInteger(value) && value >= 1 && value <= 104_857_600 ? undefined : "must be an integer 1–104857600"),
28
34
  "http.timeoutMs": (value) => (Number.isSafeInteger(value) && value >= 1 && value <= 600_000 ? undefined : "must be an integer 1–600000"),
29
35
  },