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.
Files changed (75) hide show
  1. package/core/access/Access.module.ts +36 -0
  2. package/core/access/catalog/AccessCatalog.ts +73 -0
  3. package/core/access/catalog/IAccessCatalog.service.ts +17 -0
  4. package/core/access/catalog/moduleFields.ts +153 -0
  5. package/core/access/catalog/permissions.ts +127 -0
  6. package/core/access/check/Access.ts +191 -0
  7. package/core/access/check/IAccess.service.ts +20 -0
  8. package/core/access/check/accessConfig.ts +18 -0
  9. package/core/access/check/can.ts +21 -0
  10. package/core/access/check/conditions.ts +132 -0
  11. package/core/access/check/filter.ts +132 -0
  12. package/core/access/contracts/AccessPolicy.ts +29 -0
  13. package/core/access/contracts/AttributeLookup.ts +17 -0
  14. package/core/access/contracts/References.ts +33 -0
  15. package/core/access/contracts/errors.ts +13 -0
  16. package/core/access/contracts/types.ts +66 -0
  17. package/core/access/generated/deps.ts +14 -0
  18. package/core/access/http/AccessCatalog.controller.ts +51 -0
  19. package/core/access/index.ts +35 -0
  20. package/core/access/roles/RolesAccessPolicy.ts +64 -0
  21. package/core/access/roles/roles.ts +62 -0
  22. package/core/access/testing/index.ts +54 -0
  23. package/core/background/BackgroundService.ts +1 -1
  24. package/core/di/README.md +5 -5
  25. package/core/di/errors/ClassDependenciesMismatchError.ts +1 -1
  26. package/core/di/internal/GraphValidator.ts +1 -1
  27. package/core/di/module/autoDeps.ts +11 -1
  28. package/core/di/module/createContainer.ts +1 -1
  29. package/core/di/module/providerSelection.ts +1 -1
  30. package/core/grpc/{GrpcClient.ts → GrpcClientBase.ts} +1 -1
  31. package/core/grpc/grpcClients.ts +79 -0
  32. package/core/grpc/index.ts +2 -2
  33. package/core/http/options.ts +1 -1
  34. package/core/http-client/httpClients.ts +82 -0
  35. package/core/http-client/index.ts +11 -91
  36. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  37. package/core/scripts/di-generate-target.ts +9 -0
  38. package/core/scripts/di-generate.ts +177 -9
  39. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +15 -11
  40. package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
  41. package/core/workflow/definitions_module/access/Process.references.ts +27 -0
  42. package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +4 -3
  43. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +2 -2
  44. package/core/workflow/definitions_module/http/Processes.controller.ts +23 -14
  45. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +9 -0
  46. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +4 -0
  47. package/core/workflow/generated/deps.ts +56 -0
  48. package/core/workflow/index.ts +1 -1
  49. package/core/workflow/internal/WorkflowTools.ts +2 -2
  50. package/core/workflow/internal/http.ts +18 -17
  51. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +17 -34
  52. package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
  53. package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
  54. package/core/workflow/runtime_module/background/WorkflowWorker.ts +1 -10
  55. package/core/workflow/runtime_module/config/Runtime.config.ts +11 -1
  56. package/core/workflow/runtime_module/http/Events.controller.ts +5 -3
  57. package/core/workflow/runtime_module/http/Instances.controller.ts +25 -15
  58. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +4 -3
  59. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +3 -0
  60. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -5
  61. package/core/workflow/tasks_module/WorkflowTasks.module.ts +8 -10
  62. package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
  63. package/core/workflow/tasks_module/events/TaskEvents.ts +29 -8
  64. package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -12
  65. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +8 -3
  66. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +41 -42
  67. package/core/workflow/testing/index.ts +1 -0
  68. package/index.ts +1 -1
  69. package/library/http-client/{HttpClient.ts → HttpClientBase.ts} +5 -5
  70. package/library/http-client/HttpClientFactory.ts +5 -5
  71. package/library/http-client/index.ts +3 -3
  72. package/library/terminal/messages.ts +2 -2
  73. package/package.json +3 -1
  74. package/core/grpc/grpcClientProvider.ts +0 -9
  75. package/core/workflow/access/WorkflowAccess.ts +0 -52
@@ -1,30 +1,35 @@
1
1
  import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
- import { Controller, Get, Post, type HttpContext } from "../../../http";
2
+ import { can } from "../../../access";
3
+ import { Authorize, Controller, Get, Post, type HttpContext } from "../../../http";
3
4
  import type { WorkflowActor } from "../../contracts/types";
4
5
  import { WorkflowError } from "../../contracts/WorkflowError";
5
- import { assigneesOf, authorize, body, optionalString, pagingQuery, param } from "../../internal/http";
6
+ import { body, callerOf, optionalString, pagingQuery, param } from "../../internal/http";
6
7
  import type { TaskStatus } from "../model/WfTask.model";
7
8
  import { IWorkflowTasks, type TaskDetails } from "../services/IWorkflowTasks.service";
8
9
 
9
10
  /**
10
- * The caller's tasks (WORKFLOW-001 §5): those addressed to any of
11
- * `WorkflowAccess.assignees` or claimed by the caller. Another person's task
12
- * answers 404, as if it did not exist.
11
+ * The caller's tasks (WORKFLOW-002 §6.2): those addressed to any of the
12
+ * subject's `principals` in the task's partition or claimed by the caller.
13
+ * Another person's task answers 404, as if it did not exist.
13
14
  */
14
15
  @Controller("workflow/tasks")
15
16
  export class TasksController {
16
17
  constructor(private readonly tasks: IWorkflowTasks) {}
17
18
 
18
19
  @Get()
20
+ @Authorize(can("workflow.tasks.work"))
19
21
  async list(ctx: HttpContext) {
20
22
  const partition = optionalString(ctx.query("partition"), "partition");
21
- const actor = await authorize(ctx, "tasks.work", { partition: partition ?? null });
23
+ const { access, actor } = await callerOf(ctx);
22
24
  const status = ctx.query("status");
25
+ const statuses = status === undefined ? {} : { status: status.split(",") as TaskStatus[] };
23
26
  const page = await this.tasks.list({
24
27
  ...pagingQuery(ctx),
25
- assignees: await assigneesOf(ctx, actor),
28
+ ...statuses,
29
+ // Addressees are per partition and counted now, not when the task was created.
30
+ addressees: await access.principals(partition === undefined ? await this.tasks.partitions(statuses) : [partition]),
26
31
  actor,
27
- ...(status === undefined ? {} : { status: status.split(",") as TaskStatus[] }),
32
+ access: await access.filter("workflow.tasks.work"),
28
33
  partition,
29
34
  processKey: optionalString(ctx.query("processKey"), "processKey"),
30
35
  });
@@ -32,35 +37,39 @@ export class TasksController {
32
37
  }
33
38
 
34
39
  @Get(":id")
40
+ @Authorize(can("workflow.tasks.work"))
35
41
  async get(ctx: HttpContext) {
36
42
  return (await this.visible(ctx)).task;
37
43
  }
38
44
 
39
45
  @Post(":id/claim")
46
+ @Authorize(can("workflow.tasks.work"))
40
47
  async claim(ctx: HttpContext) {
41
48
  const { task, actor } = await this.visible(ctx);
42
49
  return await this.tasks.claim(task.id, actor);
43
50
  }
44
51
 
45
52
  @Post(":id/release")
53
+ @Authorize(can("workflow.tasks.work"))
46
54
  async release(ctx: HttpContext) {
47
55
  const { task, actor } = await this.visible(ctx);
48
56
  return await this.tasks.release(task.id, actor);
49
57
  }
50
58
 
51
59
  @Post(":id/complete")
60
+ @Authorize(can("workflow.tasks.work"))
52
61
  async complete(ctx: HttpContext) {
53
62
  const { task, actor } = await this.visible(ctx);
54
63
  const request = await body(ctx);
55
64
  return await this.tasks.complete(task.id, request.output ?? {}, actor);
56
65
  }
57
66
 
58
- /** The task if the caller may see it: addressed to one of their assignees or claimed by them. */
67
+ /** The task if the caller may see it: addressed to one of their principals or claimed by them. */
59
68
  private async visible(ctx: HttpContext): Promise<{ task: TaskDetails; actor: WorkflowActor }> {
60
69
  const task = await this.tasks.get(param(ctx, "id"));
61
- const actor = await authorize(ctx, "tasks.work", { processKey: task.processKey, partition: task.partition, instanceId: task.instanceId, taskId: task.id });
62
- const assignees = await assigneesOf(ctx, actor);
63
- if (!assignees.includes(task.assignee) && task.claimedBy !== actor.id) throw new WorkflowError("not_found", `Task ${task.id} does not exist`);
70
+ const { access, actor } = await callerOf(ctx, "workflow.tasks.work", { scope: task.partition, process: task.processKey });
71
+ const addressees = await access.principals([task.partition]);
72
+ if (!addressees.some(({ principals }) => principals.includes(task.assignee)) && task.claimedBy !== actor.id) throw new WorkflowError("not_found", `Task ${task.id} does not exist`);
64
73
  return { task, actor };
65
74
  }
66
75
  }
@@ -1,4 +1,5 @@
1
1
  import type { FormSpec } from "../../../../library/workflow";
2
+ import type { AccessFilter, ScopePrincipals } from "../../../access";
2
3
  import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
3
4
  import type { TaskStatus } from "../model/WfTask.model";
4
5
 
@@ -23,10 +24,12 @@ export interface TaskDetails {
23
24
  }
24
25
 
25
26
  export interface TaskListQuery extends WorkflowPaging {
26
- /** Tasks addressed to any of these (`role:support`, `user:42`), plus those the actor claimed. */
27
- readonly assignees: readonly string[];
27
+ /** Tasks addressed to these (`role:support`, `user:42`) in their partition, plus those the actor claimed. */
28
+ readonly addressees: readonly ScopePrincipals[];
28
29
  /** Whose claimed tasks count as theirs. */
29
30
  readonly actor?: WorkflowActor;
31
+ /** Which tasks the caller may see, from `IAccess`; without it the calling code has decided. */
32
+ readonly access?: AccessFilter;
30
33
  /** `open` and `claimed` by default. */
31
34
  readonly status?: readonly TaskStatus[];
32
35
  readonly partition?: string;
@@ -40,8 +43,10 @@ export interface TaskListQuery extends WorkflowPaging {
40
43
  * by another person), `wrong_status`, `invalid_input` with the form issues.
41
44
  */
42
45
  export abstract class IWorkflowTasks {
43
- /** The tasks of these assignees, oldest first. */
46
+ /** The tasks of these addressees, oldest first. */
44
47
  abstract list(query: TaskListQuery): Promise<WorkflowPage<TaskDetails>>;
48
+ /** The partitions that have tasks in these statuses (`open` and `claimed` by default): where to ask for addressees. */
49
+ abstract partitions(query?: { readonly status?: readonly TaskStatus[] }): Promise<readonly (string | null)[]>;
45
50
  abstract get(taskId: string): Promise<TaskDetails>;
46
51
  /** Takes an open task: nobody else can complete it until it is released. */
47
52
  abstract claim(taskId: string, actor: WorkflowActor): Promise<TaskDetails>;
@@ -1,8 +1,7 @@
1
- import { UniqueViolationError } from "../../../orm";
1
+ import { grantsToWhere, type ScopePrincipals } from "../../../access";
2
2
  import { isUuid, pagingOf, type WorkflowActor, type WorkflowPage } from "../../contracts/types";
3
3
  import type { WorkflowClock } from "../../contracts/WorkflowClock";
4
4
  import { WorkflowError } from "../../contracts/WorkflowError";
5
- import type { WorkflowInstancesDeleted, WorkflowTaskRequested, WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
6
5
  import type { IWorkflowRuntime } from "../../runtime_module/services/IWorkflowRuntime.service";
7
6
  import type { TasksDbContext } from "../model/Tasks.dbContext";
8
7
  import { WfTask, type TaskStatus } from "../model/WfTask.model";
@@ -21,17 +20,22 @@ export class WorkflowTasks extends IWorkflowTasks {
21
20
 
22
21
  async list(query: TaskListQuery): Promise<WorkflowPage<TaskDetails>> {
23
22
  const { limit, offset } = pagingOf(query);
24
- const statuses = [...(query.status ?? ["open", "claimed"])];
25
- if (statuses.length === 0 || statuses.some((status) => !STATUSES.has(status))) throw new WorkflowError("invalid_input", "status must be open, claimed, completed or cancelled");
26
- const assignees = [...query.assignees];
23
+ const statuses = statusesOf(query.status);
24
+ const groups = groupByPrincipals(query.addressees);
27
25
  const mine = query.actor?.id;
28
- if (assignees.length === 0 && mine === undefined) return { items: [], total: 0 };
26
+ const where = query.access === undefined ? "all" : grantsToWhere(query.access);
27
+ if (where === "none" || (groups.length === 0 && mine === undefined)) return { items: [], total: 0 };
29
28
  let source = this.db.tasks.asNoTracking().where((t) => t.status.in(statuses));
30
- source = mine === undefined
31
- ? source.where((t) => t.assignee.in(assignees))
32
- : assignees.length === 0
33
- ? source.where((t) => t.claimedById.eq(mine))
34
- : source.where((t) => t.assignee.in(assignees).or(t.claimedById.eq(mine)));
29
+ if (where !== "all") source = source.where(() => where);
30
+ source = source.where((t) => {
31
+ const theirs = groups.map(({ scopes, principals }) => {
32
+ const named = scopes.filter((scope): scope is string => scope !== null);
33
+ const partition = !scopes.includes(null) ? t.partition.in(named) : named.length === 0 ? t.partition.isNull() : t.partition.isNull().or(t.partition.in(named));
34
+ return t.assignee.in(principals).and(partition);
35
+ });
36
+ if (mine !== undefined) theirs.push(t.claimedById.eq(mine));
37
+ return theirs.reduce((left, right) => left.or(right));
38
+ });
35
39
  if (query.partition !== undefined) source = source.where((t) => t.partition.eq(query.partition!));
36
40
  if (query.processKey !== undefined) source = source.where((t) => t.processKey.eq(query.processKey!));
37
41
  const total = await source.count();
@@ -39,6 +43,12 @@ export class WorkflowTasks extends IWorkflowTasks {
39
43
  return { items: rows.map(detailsOf), total };
40
44
  }
41
45
 
46
+ async partitions(query: { readonly status?: readonly TaskStatus[] } = {}): Promise<readonly (string | null)[]> {
47
+ const statuses = statusesOf(query.status);
48
+ const rows = await this.db.database.querySqlRaw(`SELECT DISTINCT "partition" FROM "bazis_wf_task_items" WHERE "status" IN (${statuses.map((_, index) => `{${index}}`).join(", ")})`, ...statuses);
49
+ return rows.map((row) => row.partition as string | null);
50
+ }
51
+
42
52
  async get(taskId: string): Promise<TaskDetails> {
43
53
  return detailsOf(await this.task(taskId, false));
44
54
  }
@@ -91,37 +101,6 @@ export class WorkflowTasks extends IWorkflowTasks {
91
101
  return await this.get(task.id);
92
102
  }
93
103
 
94
- /** A step started waiting for a person: its task. Repeated events of one token make one task. */
95
- async requested(event: WorkflowTaskRequested): Promise<void> {
96
- const assignee = "role" in event.assignee ? `role:${event.assignee.role}` : `user:${event.assignee.user}`;
97
- this.db.tasks.add(Object.assign(new WfTask(), {
98
- tokenId: event.tokenId,
99
- instanceId: event.instanceId,
100
- processKey: event.processKey,
101
- nodeId: event.nodeId,
102
- partition: event.partition,
103
- title: event.title,
104
- assignee,
105
- form: event.form,
106
- dueAt: event.dueAt,
107
- }));
108
- try {
109
- await this.db.saveChanges();
110
- } catch (error) {
111
- if (!(error instanceof UniqueViolationError)) throw error;
112
- }
113
- }
114
-
115
- /** The step closed otherwise than by its task: the task is cancelled. */
116
- async closed(event: WorkflowTokenClosed): Promise<void> {
117
- await this.close(event.tokenId, "cancelled");
118
- }
119
-
120
- /** Retention deleted the instances: their tasks go too. */
121
- async deleted(event: WorkflowInstancesDeleted): Promise<void> {
122
- if (event.instanceIds.length > 0) await this.db.tasks.asNoTracking().where((t) => t.instanceId.in([...event.instanceIds])).executeDelete();
123
- }
124
-
125
104
  private async close(tokenId: string, status: "cancelled"): Promise<void> {
126
105
  await this.db.tasks.asNoTracking().where((t) => t.tokenId.eq(tokenId).and(t.status.in(["open", "claimed"]))).executeUpdate({ status });
127
106
  }
@@ -142,6 +121,26 @@ export class WorkflowTasks extends IWorkflowTasks {
142
121
  }
143
122
  }
144
123
 
124
+ function statusesOf(status: readonly TaskStatus[] | undefined): TaskStatus[] {
125
+ const statuses = [...(status ?? ["open", "claimed"] as const)];
126
+ if (statuses.length === 0 || statuses.some((item) => !STATUSES.has(item))) throw new WorkflowError("invalid_input", "status must be open, claimed, completed or cancelled");
127
+ return statuses;
128
+ }
129
+
130
+ /** Partitions with the same addressees go together: usually all of them, so the query stays short. */
131
+ function groupByPrincipals(addressees: readonly ScopePrincipals[]): { scopes: (string | null)[]; principals: string[] }[] {
132
+ const groups = new Map<string, { scopes: (string | null)[]; principals: string[] }>();
133
+ for (const { scope, principals } of addressees) {
134
+ if (principals.length === 0) continue;
135
+ const list = [...new Set(principals)].sort();
136
+ const key = JSON.stringify(list);
137
+ const group = groups.get(key);
138
+ if (group === undefined) groups.set(key, { scopes: [scope], principals: list });
139
+ else group.scopes.push(scope);
140
+ }
141
+ return [...groups.values()];
142
+ }
143
+
145
144
  function notFound(taskId: string): WorkflowError {
146
145
  return new WorkflowError("not_found", `Task ${taskId} does not exist`);
147
146
  }
@@ -1,4 +1,5 @@
1
1
  import { DI } from "../../di";
2
+ import "../generated/deps";
2
3
  import { startTestApp, type TestApp, type TestAppOptions } from "../../testing";
3
4
  import { durationMs } from "../../../library/workflow";
4
5
  import { WorkflowClock } from "../contracts/WorkflowClock";
package/index.ts CHANGED
@@ -41,7 +41,7 @@ export * as validation from "./library/validation";
41
41
  // Golden-path application API.
42
42
  export {
43
43
  GrpcController, GrpcMethod, GrpcError, GrpcStatus, grpcModule, grpcService, loadGrpcPackage,
44
- GrpcClient, grpcClientProvider,
44
+ GrpcClient, GrpcClientBase,
45
45
  type GrpcClientOptions, type GrpcClientTlsOptions, type GrpcCallOptions, type GrpcResponse, type GrpcResponseStream,
46
46
  type GrpcContext, type GrpcModuleOptions,
47
47
  } from "./core/grpc";
@@ -78,7 +78,7 @@ function delay(ms: number, signal?: AbortSignal): Promise<void> {
78
78
  * Basic `auth`, `withCredentials`, idempotent retries with backoff, and
79
79
  * pluggable correlation-header propagation. No dependency on the Bazis core.
80
80
  */
81
- export class HttpClient {
81
+ export class HttpClientBase {
82
82
  public readonly defaults: RequestConfig;
83
83
  public readonly interceptors: {
84
84
  readonly request: InterceptorManager<ResolvedRequestConfig>;
@@ -95,12 +95,12 @@ export class HttpClient {
95
95
  }
96
96
 
97
97
  /** Creates a new client whose defaults are this client's defaults merged with `config`. */
98
- public create(config: RequestConfig = {}): HttpClient {
99
- return new HttpClient(this.mergeConfig(this.defaults, config));
98
+ public create(config: RequestConfig = {}): HttpClientBase {
99
+ return new HttpClientBase(this.mergeConfig(this.defaults, config));
100
100
  }
101
101
 
102
- public static create(config: RequestConfig = {}): HttpClient {
103
- return new HttpClient(config);
102
+ public static create(config: RequestConfig = {}): HttpClientBase {
103
+ return new HttpClientBase(config);
104
104
  }
105
105
 
106
106
  public request<T = unknown>(config: RequestConfig): Promise<HttpResponse<T>>;
@@ -1,4 +1,4 @@
1
- import { HttpClient } from "./HttpClient";
1
+ import { HttpClientBase } from "./HttpClientBase";
2
2
  import { HttpClientConfigError } from "./errors";
3
3
  import type { RequestConfig } from "./types";
4
4
 
@@ -7,7 +7,7 @@ import type { RequestConfig } from "./types";
7
7
  * An abstract class, so a service can take it by type in its constructor.
8
8
  */
9
9
  export abstract class HttpClientFactory {
10
- public abstract createClient(name?: string): HttpClient;
10
+ public abstract createClient(name?: string): HttpClientBase;
11
11
  }
12
12
 
13
13
  class ConfiguredHttpClientFactory extends HttpClientFactory {
@@ -18,15 +18,15 @@ class ConfiguredHttpClientFactory extends HttpClientFactory {
18
18
  super();
19
19
  }
20
20
 
21
- public createClient(name?: string): HttpClient {
21
+ public createClient(name?: string): HttpClientBase {
22
22
  if (name === undefined) {
23
- return new HttpClient(this.defaults);
23
+ return new HttpClientBase(this.defaults);
24
24
  }
25
25
  const config = this.named.get(name);
26
26
  if (config === undefined) {
27
27
  throw new HttpClientConfigError(name, [...this.named.keys()]);
28
28
  }
29
- return new HttpClient(this.defaults).create(config);
29
+ return new HttpClientBase(this.defaults).create(config);
30
30
  }
31
31
  }
32
32
 
@@ -4,13 +4,13 @@
4
4
  * Bun, browsers and Node 18+.
5
5
  *
6
6
  * ```ts
7
- * import { HttpClient } from "@/library/http-client";
7
+ * import { HttpClientBase } from "@/library/http-client";
8
8
  *
9
- * const api = new HttpClient({ baseUrl: "https://api.example.com", timeoutMs: 5000 });
9
+ * const api = new HttpClientBase({ baseUrl: "https://api.example.com", timeoutMs: 5000 });
10
10
  * const { data } = await api.get<User[]>("/users", { params: { page: 2 } });
11
11
  * ```
12
12
  */
13
- export { HttpClient } from "./HttpClient";
13
+ export { HttpClientBase } from "./HttpClientBase";
14
14
  export { InspectableRedirectProtocol } from "./redirectProtocol";
15
15
  export { HttpClientFactoryBuilder, HttpClientFactory } from "./HttpClientFactory";
16
16
  export { HttpClientError, HttpClientConfigError, HttpErrorCode } from "./errors";
@@ -211,7 +211,7 @@ const en = {
211
211
  outputs: "generated",
212
212
  sqlFix: "inject IPostgres from bazis/core/infra instead of Bun's SQL",
213
213
  redisFix: "inject IRedis from bazis/core/infra instead of Bun's RedisClient",
214
- unknownDepFix: "register the class in a module, declare a token with createToken, or bind it with an explicit factory",
214
+ unknownDepFix: "register the class in a module or declare a token with createToken",
215
215
  run: "run",
216
216
  },
217
217
  dev: {
@@ -388,7 +388,7 @@ const ru: Messages = {
388
388
  outputs: "сгенерировано",
389
389
  sqlFix: "внедряйте IPostgres из bazis/core/infra вместо SQL из Bun",
390
390
  redisFix: "внедряйте IRedis из bazis/core/infra вместо RedisClient из Bun",
391
- unknownDepFix: "зарегистрируйте класс в модуле, объявите токен через createToken или привяжите явной фабрикой",
391
+ unknownDepFix: "зарегистрируйте класс в модуле или объявите токен через createToken",
392
392
  run: "запуск",
393
393
  },
394
394
  dev: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.128.0",
3
+ "version": "0.130.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",
@@ -28,6 +28,8 @@
28
28
  "module": "./index.ts",
29
29
  "exports": {
30
30
  ".": "./index.ts",
31
+ "./core/access": "./core/access/index.ts",
32
+ "./core/access/testing": "./core/access/testing/index.ts",
31
33
  "./core/agent": "./core/agent/index.ts",
32
34
  "./core/app": "./core/app/index.ts",
33
35
  "./core/background": "./core/background/index.ts",
@@ -1,9 +0,0 @@
1
- import { singletonFactory, type ProviderDefinition, type Token } from "../di";
2
- import { GrpcClient } from "./GrpcClient";
3
- import type { GrpcClientOptions } from "./clientContracts";
4
- import type { ServiceDefinition } from "./serviceDefinition";
5
-
6
- /** Container-owned singleton: lazy network connection and automatic dispose(). */
7
- export function grpcClientProvider(service: ServiceDefinition, options: GrpcClientOptions, token: Token<GrpcClient> = GrpcClient): ProviderDefinition<GrpcClient> {
8
- return singletonFactory(token, [], () => new GrpcClient(service, options));
9
- }
@@ -1,52 +0,0 @@
1
- import type { HttpContext } from "../../http";
2
- import type { WorkflowActor } from "../contracts/types";
3
-
4
- export type WorkflowAction =
5
- | "processes.read"
6
- | "processes.manage"
7
- | "processes.publish"
8
- | "instances.start"
9
- | "instances.read"
10
- | "instances.manage"
11
- | "events.emit"
12
- | "tasks.work";
13
-
14
- /** What an action is about; the fields known for the route. */
15
- export interface WorkflowAccessTarget {
16
- readonly processKey?: string;
17
- readonly partition?: string | null;
18
- readonly instanceId?: string;
19
- readonly taskId?: string;
20
- }
21
-
22
- /**
23
- * Rights and identity of the workflow HTTP (WORKFLOW-001 §6). The framework's
24
- * controllers do not know how the application signs people in, so they ask this
25
- * port. It is the DI token too: register the application's implementation in any
26
- * module, `DI.singleton(DI.classProvider(WorkflowAccess, AppWorkflowAccess))`.
27
- * Without one every workflow route answers 403. The services never ask it: the
28
- * application's code that calls them decides itself.
29
- */
30
- export abstract class WorkflowAccess {
31
- /** Who makes the request; `undefined` answers 401. */
32
- abstract actor(ctx: HttpContext): WorkflowActor | undefined | Promise<WorkflowActor | undefined>;
33
-
34
- /** Whether the actor may do `action` on `target`; `false` answers 403. */
35
- abstract can(ctx: HttpContext, action: WorkflowAction, target: WorkflowAccessTarget): boolean | Promise<boolean>;
36
-
37
- /** Whom the user's tasks are addressed to: `user:<id>`, `role:<code>`. */
38
- assignees(_ctx: HttpContext, actor: WorkflowActor): readonly string[] | Promise<readonly string[]> {
39
- return [`user:${actor.id}`];
40
- }
41
- }
42
-
43
- /** The default: nothing is allowed until the application says otherwise. */
44
- export class DenyWorkflowAccess extends WorkflowAccess {
45
- actor(): undefined {
46
- return undefined;
47
- }
48
-
49
- can(): boolean {
50
- return false;
51
- }
52
- }