bazis 0.128.0 → 0.130.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/core/access/Access.module.ts +36 -0
- package/core/access/catalog/AccessCatalog.ts +73 -0
- package/core/access/catalog/IAccessCatalog.service.ts +17 -0
- package/core/access/catalog/moduleFields.ts +153 -0
- package/core/access/catalog/permissions.ts +127 -0
- package/core/access/check/Access.ts +191 -0
- package/core/access/check/IAccess.service.ts +20 -0
- package/core/access/check/accessConfig.ts +18 -0
- package/core/access/check/can.ts +21 -0
- package/core/access/check/conditions.ts +132 -0
- package/core/access/check/filter.ts +132 -0
- package/core/access/contracts/AccessPolicy.ts +29 -0
- package/core/access/contracts/AttributeLookup.ts +17 -0
- package/core/access/contracts/References.ts +33 -0
- package/core/access/contracts/errors.ts +13 -0
- package/core/access/contracts/types.ts +66 -0
- package/core/access/generated/deps.ts +14 -0
- package/core/access/http/AccessCatalog.controller.ts +51 -0
- package/core/access/index.ts +35 -0
- package/core/access/roles/RolesAccessPolicy.ts +64 -0
- package/core/access/roles/roles.ts +62 -0
- package/core/access/testing/index.ts +54 -0
- package/core/background/BackgroundService.ts +1 -1
- package/core/di/README.md +5 -5
- package/core/di/errors/ClassDependenciesMismatchError.ts +1 -1
- package/core/di/internal/GraphValidator.ts +1 -1
- package/core/di/module/autoDeps.ts +11 -1
- package/core/di/module/createContainer.ts +1 -1
- package/core/di/module/providerSelection.ts +1 -1
- package/core/grpc/{GrpcClient.ts → GrpcClientBase.ts} +1 -1
- package/core/grpc/grpcClients.ts +79 -0
- package/core/grpc/index.ts +2 -2
- package/core/http/options.ts +1 -1
- package/core/http-client/httpClients.ts +82 -0
- package/core/http-client/index.ts +11 -91
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/di-generate-target.ts +9 -0
- package/core/scripts/di-generate.ts +177 -9
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +15 -11
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
- package/core/workflow/definitions_module/access/Process.references.ts +27 -0
- package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +4 -3
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +2 -2
- package/core/workflow/definitions_module/http/Processes.controller.ts +23 -14
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +9 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +4 -0
- package/core/workflow/generated/deps.ts +56 -0
- package/core/workflow/index.ts +1 -1
- package/core/workflow/internal/WorkflowTools.ts +2 -2
- package/core/workflow/internal/http.ts +18 -17
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +17 -34
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +1 -10
- package/core/workflow/runtime_module/config/Runtime.config.ts +11 -1
- package/core/workflow/runtime_module/http/Events.controller.ts +5 -3
- package/core/workflow/runtime_module/http/Instances.controller.ts +25 -15
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +4 -3
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +3 -0
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -5
- package/core/workflow/tasks_module/WorkflowTasks.module.ts +8 -10
- package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
- package/core/workflow/tasks_module/events/TaskEvents.ts +29 -8
- package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -12
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +8 -3
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +41 -42
- package/core/workflow/testing/index.ts +1 -0
- package/index.ts +1 -1
- package/library/http-client/{HttpClient.ts → HttpClientBase.ts} +5 -5
- package/library/http-client/HttpClientFactory.ts +5 -5
- package/library/http-client/index.ts +3 -3
- package/library/terminal/messages.ts +2 -2
- package/package.json +3 -1
- package/core/grpc/grpcClientProvider.ts +0 -9
- package/core/workflow/access/WorkflowAccess.ts +0 -52
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers of access (ACCESS-001 §8): a rights system whose grants are set
|
|
3
|
+
* right in the test.
|
|
4
|
+
*/
|
|
5
|
+
import { PRINCIPAL_STATE_KEY, type AuthorizeContext, type RequestPrincipal } from "../../http";
|
|
6
|
+
import { AccessPolicy, type ScopePrincipals } from "../contracts/AccessPolicy";
|
|
7
|
+
import type { AccessSubject, Grants, Scope } from "../contracts/types";
|
|
8
|
+
|
|
9
|
+
export interface StaticAccessOptions {
|
|
10
|
+
/** Subject → permission → grants; a permission not listed gives nothing. */
|
|
11
|
+
readonly grants: Readonly<Record<string, Readonly<Record<string, Grants>>>>;
|
|
12
|
+
/** Subject → the owner ids meaning it; by default `sub` after the last `:`. */
|
|
13
|
+
readonly owners?: Readonly<Record<string, readonly string[]>>;
|
|
14
|
+
/** Subject → its addressees in every partition; by default `user:<owner id>`. */
|
|
15
|
+
readonly principals?: Readonly<Record<string, readonly string[]>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A rights system for tests: the subject is the principal's `subject` of the
|
|
20
|
+
* route check, grants come from the table.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* singletonValue(AccessPolicy, new StaticAccessPolicy({
|
|
24
|
+
* grants: { "user:1": { "orders.read": [{ scopes: "all", where: { own: true } }] } },
|
|
25
|
+
* }))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class StaticAccessPolicy extends AccessPolicy {
|
|
29
|
+
constructor(private readonly options: StaticAccessOptions) {
|
|
30
|
+
super();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
subject(ctx: AuthorizeContext): AccessSubject | undefined {
|
|
34
|
+
const sub = (ctx.state.get(PRINCIPAL_STATE_KEY) as RequestPrincipal | undefined)?.subject;
|
|
35
|
+
return sub === undefined || sub === "" ? undefined : { sub };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
grants(subject: AccessSubject, permission: string): Grants {
|
|
39
|
+
return this.options.grants[subject.sub]?.[permission] ?? [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ownerIds(subject: AccessSubject): readonly string[] {
|
|
43
|
+
return this.options.owners?.[subject.sub] ?? [ownerId(subject.sub)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async principals(subject: AccessSubject, scopes: readonly Scope[]): Promise<readonly ScopePrincipals[]> {
|
|
47
|
+
const principals = this.options.principals?.[subject.sub] ?? [`user:${ownerId(subject.sub)}`];
|
|
48
|
+
return scopes.map((scope) => ({ scope, principals }));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ownerId(sub: string): string {
|
|
53
|
+
return sub.slice(sub.lastIndexOf(":") + 1);
|
|
54
|
+
}
|
|
@@ -37,7 +37,7 @@ const DEFAULT_STOP_TIMEOUT_MS = 5_000;
|
|
|
37
37
|
*
|
|
38
38
|
* ```ts
|
|
39
39
|
* class Heartbeat extends BackgroundService {
|
|
40
|
-
* constructor(private readonly api:
|
|
40
|
+
* constructor(private readonly api: HttpClientBase) { super({ stopTimeoutMs: 2000 }); }
|
|
41
41
|
* protected async execute(signal: AbortSignal) {
|
|
42
42
|
* while (!signal.aborted) {
|
|
43
43
|
* await this.api.post("/heartbeat");
|
package/core/di/README.md
CHANGED
|
@@ -77,11 +77,11 @@ contracts are described in [MODULE.md](MODULE.md).
|
|
|
77
77
|
|
|
78
78
|
## Style Rule (DI usage)
|
|
79
79
|
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
- `providers` of a module use only the agreed forms: `scoped/singleton/transient(IService, Service)`
|
|
81
|
+
(or the class alone), `singletonValue(TOKEN, value)`, `singletonAsyncFactory(TOKEN, [deps], async factory)`
|
|
82
|
+
and `...withEventSubscribers(Class)`. Codegen refuses anything else (`BAZIS_DI_PROVIDER_FORM`).
|
|
83
|
+
- The low-level path (`DI.classProvider(...)`, `DI.factoryProvider(...)`) stays for the kernel's own
|
|
84
|
+
plumbing and test `overrides`, never in a module's `providers`.
|
|
85
85
|
|
|
86
86
|
## Contributing Checklist
|
|
87
87
|
|
|
@@ -9,7 +9,7 @@ export class ClassDependenciesMismatchError extends DiError {
|
|
|
9
9
|
declaredDeps: number,
|
|
10
10
|
) {
|
|
11
11
|
super(
|
|
12
|
-
`Class provider "${tokenToDebugName(token)}" requires at least ${requiredConstructorParams} constructor deps, but only ${declaredDeps} declared. Constructor dependencies are wired by codegen: if the constructor was added or changed after the last run, run \`bazis codegen\` (bazis dev, bazis test and bazis build run it automatically)
|
|
12
|
+
`Class provider "${tokenToDebugName(token)}" requires at least ${requiredConstructorParams} constructor deps, but only ${declaredDeps} declared. Constructor dependencies are wired by codegen: if the constructor was added or changed after the last run, run \`bazis codegen\` (bazis dev, bazis test and bazis build run it automatically).`,
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -77,7 +77,7 @@ export class GraphValidator {
|
|
|
77
77
|
const shape = this.context.classShape(registration.provider);
|
|
78
78
|
if (shape && shape.required > shape.declared) {
|
|
79
79
|
issues.add(
|
|
80
|
-
`Class provider "${tokenToDebugName(registration.token)}" requires at least ${shape.required} constructor deps, but only ${shape.declared} declared. Constructor dependencies are wired by codegen: if the constructor was added or changed after the last run, run \`bazis codegen\` (bazis dev, bazis test and bazis build run it automatically)
|
|
80
|
+
`Class provider "${tokenToDebugName(registration.token)}" requires at least ${shape.required} constructor deps, but only ${shape.declared} declared. Constructor dependencies are wired by codegen: if the constructor was added or changed after the last run, run \`bazis codegen\` (bazis dev, bazis test and bazis build run it automatically).`,
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -14,10 +14,20 @@ export function registerGeneratedClassDeps(target: Class<unknown>, deps: readonl
|
|
|
14
14
|
targetClassDeps.set(target, deps);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// The framework's own classes: their wiring ships inside the package
|
|
18
|
+
// (`<pack>/generated/deps.ts`), because an application's codegen skips
|
|
19
|
+
// node_modules. Not part of a target, so target rollbacks keep it.
|
|
20
|
+
const frameworkClassDeps = new Map<Class<unknown>, readonly GeneratedClassDependency[]>();
|
|
21
|
+
|
|
22
|
+
/** Called by the generated `deps.ts` of a framework pack when the pack is imported. */
|
|
23
|
+
export function registerFrameworkClassDeps(entries: readonly (readonly [Class<unknown>, readonly GeneratedClassDependency[]])[]): void {
|
|
24
|
+
for (const [target, deps] of entries) frameworkClassDeps.set(target, deps);
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
export function getGeneratedClassDeps(useClass: Class<unknown>): ProviderDependencyList | undefined {
|
|
18
28
|
// Names are not class identities: package consumers may use the same names
|
|
19
29
|
// as the application that generated the framework's compatibility files.
|
|
20
|
-
const dependencies = targetClassDeps.get(useClass);
|
|
30
|
+
const dependencies = targetClassDeps.get(useClass) ?? frameworkClassDeps.get(useClass);
|
|
21
31
|
if (dependencies === undefined) {
|
|
22
32
|
return undefined;
|
|
23
33
|
}
|
|
@@ -325,6 +325,6 @@ function assertProviderDefinition(value: unknown, moduleName: string, index: num
|
|
|
325
325
|
throw new InvalidProviderError(
|
|
326
326
|
name,
|
|
327
327
|
`Module "${moduleName}": providers[${index}] is not a provider registration${provide === undefined ? "" : ` ({ provide: ${name} })`}. `
|
|
328
|
-
+ `Register it with singleton(${name}), scoped(${name}), transient(${name}), singletonValue(TOKEN, value)
|
|
328
|
+
+ `Register it with singleton(${name}), scoped(${name}), transient(${name}), or singletonValue(TOKEN, value).`,
|
|
329
329
|
);
|
|
330
330
|
}
|
|
@@ -116,7 +116,7 @@ function selectionMessage(
|
|
|
116
116
|
// No colon after "token": the console redaction would read "token: the" as a secret.
|
|
117
117
|
return `${head}, but ${subject} is registered in ${modules}, and the application uses one implementation per token,`
|
|
118
118
|
+ ` the last registered one, from "${owner.name}", which "${record.name}" cannot see.`
|
|
119
|
-
+ ` Register ${subject} in one module, or give
|
|
119
|
+
+ ` Register ${subject} in one module, or give each implementation its own contract.`;
|
|
120
120
|
}
|
|
121
121
|
return `${head}, but the application uses the implementation from "${owner.name}", which "${record.name}" cannot see.`
|
|
122
122
|
+ ` Add "${owner.name}" to the imports of "${record.name}" and export ${subject} from it.`;
|
|
@@ -8,7 +8,7 @@ import { Metadata } from "./Metadata";
|
|
|
8
8
|
import type { MethodDefinition, ServiceDefinition } from "./serviceDefinition";
|
|
9
9
|
|
|
10
10
|
/** Standard gRPC/HTTP2 client using only built-in networking and Bazis codecs. */
|
|
11
|
-
export class
|
|
11
|
+
export class GrpcClientBase {
|
|
12
12
|
private readonly methods = new Map<string, MethodDefinition | null>();
|
|
13
13
|
private readonly sessions = new Map<ClientHttp2Session, Set<GrpcClientCall<unknown>>>();
|
|
14
14
|
private readonly calls = new Set<GrpcClientCall<unknown>>();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DI, registerModuleMetadataExpander, type BazisModuleMetadata, type BazisModuleRef, type ServiceResolver } from "../di";
|
|
2
|
+
import type { ConfigDefinition } from "../kernel";
|
|
3
|
+
import type { GrpcClientOptions } from "./clientContracts";
|
|
4
|
+
import { GrpcClientBase } from "./GrpcClientBase";
|
|
5
|
+
import { GrpcSetupError } from "./GrpcSetupError";
|
|
6
|
+
import type { ServiceDefinition } from "./serviceDefinition";
|
|
7
|
+
|
|
8
|
+
/** The settings of a client in the configuration: the address and, if declared, the timeout. */
|
|
9
|
+
export interface GrpcClientConfig {
|
|
10
|
+
address: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Fixed options of a client, and the configuration its address and timeout come from. */
|
|
15
|
+
export interface GrpcClientDeclaration extends Partial<GrpcClientOptions> {
|
|
16
|
+
readonly config?: ConfigDefinition<GrpcClientConfig>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type GrpcClientClass = new (service: ServiceDefinition, options: GrpcClientOptions) => GrpcClientBase;
|
|
20
|
+
|
|
21
|
+
const declarations = new WeakMap<object, { readonly service: ServiceDefinition; readonly declaration: GrpcClientDeclaration }>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A client of another gRPC service, declared like a controller: the class
|
|
25
|
+
* extends `GrpcClientBase`, the module lists it in `grpcClients`, and services
|
|
26
|
+
* take it by type. The framework creates it with the address and timeout of
|
|
27
|
+
* `config` and closes it when the application stops.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* @GrpcClient(UsersRpc, { config: usersRpcConfig })
|
|
31
|
+
* export class UsersClient extends GrpcClientBase {}
|
|
32
|
+
*
|
|
33
|
+
* @Module({ grpcClients: [UsersClient], providers: [scoped(UsersApi)] })
|
|
34
|
+
* export class ReportsModule {}
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function GrpcClient(service: ServiceDefinition, declaration: GrpcClientDeclaration = {}) {
|
|
38
|
+
return (target: GrpcClientClass, _context: ClassDecoratorContext): void => {
|
|
39
|
+
declarations.set(target, { service, declaration });
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
registerModuleMetadataExpander((metadata: BazisModuleMetadata): readonly BazisModuleRef[] => {
|
|
44
|
+
const clients = (metadata as BazisModuleMetadata & { readonly grpcClients?: unknown }).grpcClients;
|
|
45
|
+
if (clients === undefined) return [];
|
|
46
|
+
const owner = typeof metadata === "function" ? (metadata as { name: string }).name : "<module>";
|
|
47
|
+
if (!Array.isArray(clients)) throw new GrpcSetupError(`${owner}: grpcClients must list classes declared with @GrpcClient.`);
|
|
48
|
+
const configs = new Set<ConfigDefinition<GrpcClientConfig>>();
|
|
49
|
+
const providers = clients.map((client: unknown) => {
|
|
50
|
+
const found = typeof client === "function" ? declarations.get(client) : undefined;
|
|
51
|
+
if (found === undefined || !((client as GrpcClientClass).prototype instanceof GrpcClientBase)) {
|
|
52
|
+
throw new GrpcSetupError(`${owner}: ${typeof client === "function" ? client.name : String(client)} in grpcClients must extend GrpcClientBase and be declared with @GrpcClient(service, …).`);
|
|
53
|
+
}
|
|
54
|
+
if (found.declaration.config !== undefined) configs.add(found.declaration.config);
|
|
55
|
+
const Client = client as GrpcClientClass;
|
|
56
|
+
return DI.singleton(DI.factoryProviderWithResolver(Client, [], (resolver) => new Client(found.service, optionsOf(Client.name, found.declaration, resolver))));
|
|
57
|
+
});
|
|
58
|
+
return [{ config: [...configs], providers, exports: clients }];
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function optionsOf(name: string, declaration: GrpcClientDeclaration, resolver: ServiceResolver): GrpcClientOptions {
|
|
62
|
+
const { config, ...fixed } = declaration;
|
|
63
|
+
const settings = config === undefined ? {} : {
|
|
64
|
+
address: resolver.resolve(config.token).get("address"),
|
|
65
|
+
...(config.has("timeoutMs") ? { timeoutMs: resolver.resolve(config.token).get("timeoutMs") } : {}),
|
|
66
|
+
};
|
|
67
|
+
const options = { ...fixed, ...settings };
|
|
68
|
+
if (typeof options.address !== "string" || options.address === "") throw new GrpcSetupError(`${name}: the gRPC client needs an address: in @GrpcClient(…, { address }) or in its config.`);
|
|
69
|
+
return options as GrpcClientOptions;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Augment the declaring file, not the "../di" re-export: re-export targets
|
|
73
|
+
// depend on program file order and silently stop merging in some projects.
|
|
74
|
+
declare module "../di/module/types/BazisModule" {
|
|
75
|
+
interface BazisModuleMetadata {
|
|
76
|
+
/** Clients of other gRPC services, declared with `@GrpcClient`: injected by their class. */
|
|
77
|
+
readonly grpcClients?: readonly (abstract new (...args: never[]) => GrpcClientBase)[];
|
|
78
|
+
}
|
|
79
|
+
}
|
package/core/grpc/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ export { GrpcError } from "./GrpcError";
|
|
|
3
3
|
export { GrpcSetupError } from "./GrpcSetupError";
|
|
4
4
|
export { grpcModule } from "./Grpc.module";
|
|
5
5
|
export { GrpcServer } from "./GrpcServer";
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
6
|
+
export { GrpcClientBase } from "./GrpcClientBase";
|
|
7
|
+
export { GrpcClient, type GrpcClientConfig, type GrpcClientDeclaration } from "./grpcClients";
|
|
8
8
|
export type { GrpcClientOptions, GrpcClientTlsOptions, GrpcCallOptions, GrpcResponse, GrpcResponseStream } from "./clientContracts";
|
|
9
9
|
export { grpcService, loadGrpcPackage } from "./protobuf";
|
|
10
10
|
export type { GrpcContext, GrpcModuleOptions } from "./contracts";
|
package/core/http/options.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface ApiVersioningOptions {
|
|
|
54
54
|
export interface HttpModuleOptions {
|
|
55
55
|
/**
|
|
56
56
|
* Permit explicitly negotiated redirects for exact browser-side limits.
|
|
57
|
-
* The
|
|
57
|
+
* The HttpClientBase must also enable inspectableRedirects. Default: false.
|
|
58
58
|
* Authorized/CORS-allowed clients can then inspect each redirect Location.
|
|
59
59
|
*/
|
|
60
60
|
readonly inspectableRedirects?: boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DI, registerModuleMetadataExpander, type BazisModuleMetadata, type BazisModuleRef, type ServiceResolver } from "../di";
|
|
2
|
+
import { getOutboundCorrelationHeaders, type ConfigDefinition } from "../kernel";
|
|
3
|
+
import { HttpClientBase, HttpClientError, type RequestConfig } from "../../library/http-client";
|
|
4
|
+
|
|
5
|
+
/** The settings of a client in the configuration: the base URL and, if declared, the limits. */
|
|
6
|
+
export interface HttpClientConfig {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
maxResponseBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Fixed request defaults of a client, and the configuration its base URL and limits come from. */
|
|
13
|
+
export interface HttpClientDeclaration extends RequestConfig {
|
|
14
|
+
readonly config?: ConfigDefinition<HttpClientConfig>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type HttpClientClass = new (defaults: RequestConfig) => HttpClientBase;
|
|
18
|
+
|
|
19
|
+
/** Server-side safety defaults of a declared client. */
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
const declarations = new WeakMap<object, HttpClientDeclaration>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A client of another HTTP service, declared like a controller: the class
|
|
27
|
+
* extends `HttpClientBase`, the module lists it in `httpClients`, and services
|
|
28
|
+
* take it by type. The framework creates it with the base URL and limits of
|
|
29
|
+
* `config`, a 30 s timeout and a 16 MiB response limit by default, and the
|
|
30
|
+
* kernel's correlation headers.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* @HttpClient({ config: weatherConfig })
|
|
34
|
+
* export class WeatherClient extends HttpClientBase {}
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function HttpClient(declaration: HttpClientDeclaration = {}) {
|
|
38
|
+
return (target: HttpClientClass, _context: ClassDecoratorContext): void => {
|
|
39
|
+
declarations.set(target, declaration);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
registerModuleMetadataExpander((metadata: BazisModuleMetadata): readonly BazisModuleRef[] => {
|
|
44
|
+
const clients = (metadata as BazisModuleMetadata & { readonly httpClients?: unknown }).httpClients;
|
|
45
|
+
if (clients === undefined) return [];
|
|
46
|
+
const owner = typeof metadata === "function" ? (metadata as { name: string }).name : "<module>";
|
|
47
|
+
if (!Array.isArray(clients)) throw new HttpClientError(`${owner}: httpClients must list classes declared with @HttpClient.`);
|
|
48
|
+
const configs = new Set<ConfigDefinition<HttpClientConfig>>();
|
|
49
|
+
const providers = clients.map((client: unknown) => {
|
|
50
|
+
const declaration = typeof client === "function" ? declarations.get(client) : undefined;
|
|
51
|
+
if (declaration === undefined || !((client as HttpClientClass).prototype instanceof HttpClientBase)) {
|
|
52
|
+
throw new HttpClientError(`${owner}: ${typeof client === "function" ? client.name : String(client)} in httpClients must extend HttpClientBase and be declared with @HttpClient(…).`);
|
|
53
|
+
}
|
|
54
|
+
if (declaration.config !== undefined) configs.add(declaration.config);
|
|
55
|
+
const Client = client as HttpClientClass;
|
|
56
|
+
return DI.singleton(DI.factoryProviderWithResolver(Client, [], (resolver) => new Client(defaultsOf(declaration, resolver))));
|
|
57
|
+
});
|
|
58
|
+
return [{ config: [...configs], providers, exports: clients }];
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function defaultsOf(declaration: HttpClientDeclaration, resolver: ServiceResolver): RequestConfig {
|
|
62
|
+
const { config, ...fixed } = declaration;
|
|
63
|
+
const view = config === undefined ? undefined : resolver.resolve(config.token);
|
|
64
|
+
return {
|
|
65
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
66
|
+
maxResponseBytes: DEFAULT_MAX_RESPONSE_BYTES,
|
|
67
|
+
correlationHeaders: getOutboundCorrelationHeaders,
|
|
68
|
+
...fixed,
|
|
69
|
+
...(view === undefined ? {} : { baseUrl: view.get("baseUrl") }),
|
|
70
|
+
...(view !== undefined && config!.has("timeoutMs") ? { timeoutMs: view.get("timeoutMs") } : {}),
|
|
71
|
+
...(view !== undefined && config!.has("maxResponseBytes") ? { maxResponseBytes: view.get("maxResponseBytes") } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Augment the declaring file, not the "../di" re-export: re-export targets
|
|
76
|
+
// depend on program file order and silently stop merging in some projects.
|
|
77
|
+
declare module "../di/module/types/BazisModule" {
|
|
78
|
+
interface BazisModuleMetadata {
|
|
79
|
+
/** Clients of other HTTP services, declared with `@HttpClient`: injected by their class. */
|
|
80
|
+
readonly httpClients?: readonly (abstract new (...args: never[]) => HttpClientBase)[];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -2,102 +2,22 @@
|
|
|
2
2
|
* Core DI/correlation integration for the pure `@/library/http-client`.
|
|
3
3
|
*
|
|
4
4
|
* The HTTP engine lives in the library (framework-agnostic). This module only
|
|
5
|
-
* wires it into the runtime:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* wires it into the runtime: clients of other services are declared like
|
|
6
|
+
* controllers — a class with `@HttpClient`, listed in `httpClients` of a
|
|
7
|
+
* module — and get the kernel's correlation headers. All library symbols are
|
|
8
|
+
* re-exported for convenience.
|
|
8
9
|
*
|
|
9
10
|
* ```ts
|
|
10
|
-
* import { HttpClient,
|
|
11
|
+
* import { HttpClient, HttpClientBase } from "bazis/core/http-client";
|
|
11
12
|
*
|
|
12
|
-
* @
|
|
13
|
+
* @HttpClient({ config: weatherConfig })
|
|
14
|
+
* export class WeatherClient extends HttpClientBase {}
|
|
15
|
+
*
|
|
16
|
+
* @Module({ httpClients: [WeatherClient], providers: [scoped(WeatherService)] })
|
|
13
17
|
* class WeatherModule {}
|
|
14
18
|
*
|
|
15
|
-
* class WeatherService { constructor(private readonly
|
|
19
|
+
* class WeatherService { constructor(private readonly weather: WeatherClient) {} }
|
|
16
20
|
* ```
|
|
17
21
|
*/
|
|
18
|
-
import { DI, Module, type BazisModule, type ProviderDefinition } from "../di";
|
|
19
|
-
import { getOutboundCorrelationHeaders } from "../kernel";
|
|
20
|
-
import {
|
|
21
|
-
HttpClient,
|
|
22
|
-
HttpClientFactory,
|
|
23
|
-
HttpClientFactoryBuilder,
|
|
24
|
-
type HeaderBag,
|
|
25
|
-
type RequestConfig,
|
|
26
|
-
} from "../../library/http-client";
|
|
27
|
-
|
|
28
22
|
export * from "../../library/http-client";
|
|
29
|
-
|
|
30
|
-
export interface HttpClientModuleConfig {
|
|
31
|
-
/** Named clients: `HttpClientFactory.createClient(name)`. */
|
|
32
|
-
readonly clients?: Readonly<Record<string, RequestConfig>>;
|
|
33
|
-
/** Defaults for the injectable `HttpClient` and `createClient()`. */
|
|
34
|
-
readonly default?: RequestConfig;
|
|
35
|
-
/** Correlation header source. Default: the kernel async request context. */
|
|
36
|
-
readonly correlationHeaders?: () => HeaderBag;
|
|
37
|
-
/** Default per-request timeout in ms (server-side safety). Default: 30000. */
|
|
38
|
-
readonly timeoutMs?: number;
|
|
39
|
-
/** Default maximum response body size. Default: 16 MiB. `0` disables it. */
|
|
40
|
-
readonly maxResponseBytes?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Registers the outbound HTTP client in DI with correlation propagation wired
|
|
45
|
-
* to the kernel request context. A service takes it by type:
|
|
46
|
-
* - `HttpClient` — the default client;
|
|
47
|
-
* - `HttpClientFactory` — named clients (fail-fast on unknown names).
|
|
48
|
-
*/
|
|
49
|
-
export function httpClientModule(config: HttpClientModuleConfig = {}): BazisModule {
|
|
50
|
-
const correlationHeaders = config.correlationHeaders ?? getOutboundCorrelationHeaders;
|
|
51
|
-
const moduleTimeoutMs = resolveLimit(config.timeoutMs, 30_000, "httpClientModule timeoutMs");
|
|
52
|
-
const moduleMaxResponseBytes = resolveLimit(
|
|
53
|
-
config.maxResponseBytes,
|
|
54
|
-
16 * 1024 * 1024,
|
|
55
|
-
"httpClientModule maxResponseBytes",
|
|
56
|
-
);
|
|
57
|
-
const defaultConfig = config.default ?? {};
|
|
58
|
-
const defaults: RequestConfig = {
|
|
59
|
-
...defaultConfig,
|
|
60
|
-
timeoutMs: resolveLimit(defaultConfig.timeoutMs, moduleTimeoutMs, "httpClientModule default.timeoutMs"),
|
|
61
|
-
maxResponseBytes: resolveLimit(
|
|
62
|
-
defaultConfig.maxResponseBytes,
|
|
63
|
-
moduleMaxResponseBytes,
|
|
64
|
-
"httpClientModule default.maxResponseBytes",
|
|
65
|
-
),
|
|
66
|
-
correlationHeaders: defaultConfig.correlationHeaders ?? correlationHeaders,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const builder = new HttpClientFactoryBuilder();
|
|
70
|
-
builder.useDefault(defaults);
|
|
71
|
-
for (const [name, clientConfig] of Object.entries(config.clients ?? {})) {
|
|
72
|
-
builder.addClient(name, {
|
|
73
|
-
...clientConfig,
|
|
74
|
-
timeoutMs: resolveLimit(clientConfig.timeoutMs, defaults.timeoutMs!, `httpClientModule clients.${name}.timeoutMs`),
|
|
75
|
-
maxResponseBytes: resolveLimit(
|
|
76
|
-
clientConfig.maxResponseBytes,
|
|
77
|
-
defaults.maxResponseBytes!,
|
|
78
|
-
`httpClientModule clients.${name}.maxResponseBytes`,
|
|
79
|
-
),
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
const factory = builder.build();
|
|
83
|
-
|
|
84
|
-
const client = factory.createClient();
|
|
85
|
-
// One factory and one default client, injected by type.
|
|
86
|
-
const providers: ProviderDefinition[] = [
|
|
87
|
-
DI.singleton(DI.valueProvider(HttpClientFactory, factory)),
|
|
88
|
-
DI.singleton(DI.valueProvider(HttpClient, client)),
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
@Module({ providers })
|
|
92
|
-
class HttpClientModule {}
|
|
93
|
-
|
|
94
|
-
return HttpClientModule;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function resolveLimit(value: number | undefined, fallback: number, field: string): number {
|
|
98
|
-
const resolved = value ?? fallback;
|
|
99
|
-
if (!Number.isSafeInteger(resolved) || resolved < 0) {
|
|
100
|
-
throw new TypeError(`${field} must be a non-negative safe integer`);
|
|
101
|
-
}
|
|
102
|
-
return resolved;
|
|
103
|
-
}
|
|
23
|
+
export { HttpClient, type HttpClientConfig, type HttpClientDeclaration } from "./httpClients";
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.130.0" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -10,6 +10,11 @@ export interface CodegenConfig {
|
|
|
10
10
|
readonly version: 1;
|
|
11
11
|
readonly defaultTarget: string;
|
|
12
12
|
readonly targets: Readonly<Record<string, TargetConfig>>;
|
|
13
|
+
/**
|
|
14
|
+
* The framework's own checkout only: packs whose DI wiring ships inside the
|
|
15
|
+
* package as `<pack>/generated/deps.ts`, imported by the pack's `index.ts`.
|
|
16
|
+
*/
|
|
17
|
+
readonly framework?: { readonly packs: readonly string[] };
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
const TARGET_NAME = /^[a-z][a-z0-9-]*$/;
|
|
@@ -34,6 +39,10 @@ export async function readCodegenConfig(): Promise<CodegenConfig> {
|
|
|
34
39
|
for (const candidate of [...target.entrypoints, ...(target.applicationParts ?? [])]) normalizeConfiguredPath(candidate);
|
|
35
40
|
}
|
|
36
41
|
if (!Object.hasOwn(value.targets, value.defaultTarget)) throw new Error("BAZIS_CODEGEN_CONFIG_INVALID: defaultTarget is undeclared");
|
|
42
|
+
if (value.framework !== undefined) {
|
|
43
|
+
if (!value.framework || !Array.isArray(value.framework.packs)) throw new Error("BAZIS_CODEGEN_CONFIG_INVALID: framework.packs must be a list of directories");
|
|
44
|
+
for (const pack of value.framework.packs) normalizeConfiguredPath(pack);
|
|
45
|
+
}
|
|
37
46
|
return value as CodegenConfig;
|
|
38
47
|
}
|
|
39
48
|
|