@cullet/erp-core 1.2.0 → 1.4.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/KIT_CONTEXT.md +5 -3
- package/README.md +160 -0
- package/dist/abac/index.cjs +7 -0
- package/dist/abac/index.d.cts +2 -0
- package/dist/abac/index.d.ts +2 -0
- package/dist/abac/index.js +2 -0
- package/dist/app-error.cjs +193 -0
- package/dist/app-error.cjs.map +1 -0
- package/dist/app-error.d.cts +108 -0
- package/dist/app-error.js +170 -0
- package/dist/app-error.js.map +1 -0
- package/dist/application/index.cjs +27 -0
- package/dist/application/index.d.cts +3 -0
- package/dist/application/index.d.ts +2 -1
- package/dist/application/index.js +2 -1
- package/dist/authorization-error.cjs +157 -0
- package/dist/authorization-error.cjs.map +1 -0
- package/dist/authorization-error.d.cts +113 -0
- package/dist/authorization-error.d.ts +113 -0
- package/dist/authorization-error.js +152 -0
- package/dist/authorization-error.js.map +1 -0
- package/dist/authorizer.port.d.cts +130 -0
- package/dist/authorizer.port.d.ts +130 -0
- package/dist/composite-authorizer.d.cts +216 -0
- package/dist/composite-authorizer.d.ts +216 -0
- package/dist/condition-evaluator.cjs +565 -0
- package/dist/condition-evaluator.cjs.map +1 -0
- package/dist/condition-evaluator.js +536 -0
- package/dist/condition-evaluator.js.map +1 -0
- package/dist/core-config.d.cts +172 -0
- package/dist/{gate-types.d.ts → core-config.d.ts} +78 -77
- package/dist/domain/index.cjs +17 -0
- package/dist/domain/index.d.cts +4 -0
- package/dist/domain/index.d.ts +2 -1
- package/dist/domain/index.js +2 -1
- package/dist/domain-event-contracts.cjs +34 -0
- package/dist/domain-event-contracts.cjs.map +1 -0
- package/dist/domain-event-contracts.d.cts +27 -0
- package/dist/domain-event-contracts.js +2 -1
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +17 -0
- package/dist/domain-exception.cjs.map +1 -0
- package/dist/domain-exception.d.cts +7 -0
- package/dist/entity.cjs +126 -0
- package/dist/entity.cjs.map +1 -0
- package/dist/entity.js +115 -0
- package/dist/entity.js.map +1 -0
- package/dist/errors/index.cjs +43 -0
- package/dist/errors/index.d.cts +4 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +4 -2
- package/dist/exceptions/index.cjs +17 -0
- package/dist/exceptions/index.d.cts +5 -0
- package/dist/exceptions/validation-field.cjs +3 -0
- package/dist/exceptions/validation-field.d.cts +2 -0
- package/dist/gate-engine-registry.cjs +309 -0
- package/dist/gate-engine-registry.cjs.map +1 -0
- package/dist/gate-engine-registry.d.cts +82 -0
- package/dist/gate-engine-registry.d.ts +3 -2
- package/dist/gate-engine-registry.js +2 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +76 -0
- package/dist/gate-v1-payload.schema.cjs.map +1 -0
- package/dist/gate-v1-payload.schema.js +1 -533
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +66 -0
- package/dist/hashing.cjs.map +1 -0
- package/dist/immutable.cjs +77 -0
- package/dist/immutable.cjs.map +1 -0
- package/dist/immutable.d.cts +6 -0
- package/dist/index.cjs +181 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +37 -0
- package/dist/index.d.ts +19 -12
- package/dist/index.js +15 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +47 -0
- package/dist/invalid-state-transition-exception.cjs.map +1 -0
- package/dist/invariant-violation-exception.cjs +16 -0
- package/dist/invariant-violation-exception.cjs.map +1 -0
- package/dist/not-found-error.cjs +65 -0
- package/dist/not-found-error.cjs.map +1 -0
- package/dist/not-found-error.js +1 -1
- package/dist/outcome.cjs +97 -0
- package/dist/outcome.cjs.map +1 -0
- package/dist/outcome.d.cts +58 -0
- package/dist/outcome.d.ts +1 -83
- package/dist/parse-gate-payload.d.cts +62 -0
- package/dist/parse-gate-payload.d.ts +2 -2
- package/dist/path.d.cts +90 -0
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +79 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +85 -0
- package/dist/plugins/index.cjs +3 -0
- package/dist/plugins/index.d.cts +2 -0
- package/dist/policies/engines/index.cjs +10 -0
- package/dist/policies/engines/index.d.cts +4 -0
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.cjs +92 -0
- package/dist/policies/engines/v1/gate/index.cjs.map +1 -0
- package/dist/policies/engines/v1/gate/index.d.cts +121 -0
- package/dist/policies/engines/v1/gate/index.d.ts +2 -2
- package/dist/policies/engines/v1/gate/index.js +2 -1
- package/dist/policies/engines/v1/gate/index.js.map +1 -1
- package/dist/policies/index.cjs +41 -0
- package/dist/policies/index.d.cts +8 -0
- package/dist/policies/index.d.ts +3 -2
- package/dist/policies/index.js +2 -2
- package/dist/policy-bridge.cjs +437 -0
- package/dist/policy-bridge.cjs.map +1 -0
- package/dist/policy-bridge.d.cts +185 -0
- package/dist/policy-bridge.d.ts +185 -0
- package/dist/policy-bridge.js +396 -0
- package/dist/policy-bridge.js.map +1 -0
- package/dist/policy-service.cjs +1190 -0
- package/dist/policy-service.cjs.map +1 -0
- package/dist/policy-service.d.cts +559 -0
- package/dist/policy-service.d.ts +2 -2
- package/dist/policy-service.js +239 -239
- package/dist/policy-service.js.map +1 -1
- package/dist/rbac/index.cjs +9 -0
- package/dist/rbac/index.d.cts +3 -0
- package/dist/rbac/index.d.ts +3 -0
- package/dist/rbac/index.js +2 -0
- package/dist/requested-by.cjs +54 -0
- package/dist/requested-by.cjs.map +1 -0
- package/dist/requested-by.d.cts +25 -0
- package/dist/requested-by.d.ts +25 -0
- package/dist/requested-by.js +49 -0
- package/dist/requested-by.js.map +1 -0
- package/dist/result/index.cjs +7 -0
- package/dist/result/index.d.cts +3 -0
- package/dist/result/index.d.ts +2 -1
- package/dist/result.cjs +135 -0
- package/dist/result.cjs.map +1 -0
- package/dist/result.d.cts +84 -0
- package/dist/result.d.ts +84 -0
- package/dist/rule.cjs +327 -0
- package/dist/rule.cjs.map +1 -0
- package/dist/rule.js +298 -0
- package/dist/rule.js.map +1 -0
- package/dist/ruleset-registry.cjs +47 -0
- package/dist/ruleset-registry.cjs.map +1 -0
- package/dist/rulesets/index.cjs +3 -0
- package/dist/rulesets/index.d.cts +2 -0
- package/dist/temporal-guards.cjs +32 -0
- package/dist/temporal-guards.cjs.map +1 -0
- package/dist/temporal-use-case.cjs +139 -0
- package/dist/temporal-use-case.cjs.map +1 -0
- package/dist/temporal-use-case.d.cts +282 -0
- package/dist/temporal-use-case.d.ts +5 -27
- package/dist/temporal-use-case.js +2 -48
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +19 -0
- package/dist/unexpected-error.cjs.map +1 -0
- package/dist/unexpected-error.js +2 -167
- package/dist/unexpected-error.js.map +1 -1
- package/dist/use-case.cjs +96 -0
- package/dist/use-case.cjs.map +1 -0
- package/dist/uuid-identifier.cjs +65 -0
- package/dist/uuid-identifier.cjs.map +1 -0
- package/dist/uuid-identifier.d.cts +147 -0
- package/dist/uuid-identifier.d.ts +2 -85
- package/dist/validation-code.cjs +60 -0
- package/dist/validation-code.cjs.map +1 -0
- package/dist/validation-code.d.cts +23 -0
- package/dist/validation-error.cjs +887 -0
- package/dist/validation-error.cjs.map +1 -0
- package/dist/validation-error.d.cts +681 -0
- package/dist/validation-error.d.ts +2 -98
- package/dist/validation-error.js +7 -134
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +41 -0
- package/dist/validation-exception.cjs.map +1 -0
- package/dist/validation-exception.d.cts +50 -0
- package/dist/validation-field.cjs +28 -0
- package/dist/validation-field.cjs.map +1 -0
- package/dist/validation-field.d.cts +12 -0
- package/dist/value-object-ruleset.contracts.d.cts +36 -0
- package/dist/value-object.cjs +85 -0
- package/dist/value-object.cjs.map +1 -0
- package/dist/value-object.d.cts +89 -0
- package/dist/value-object.d.ts +89 -0
- package/dist/value-object.js +1 -112
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +10 -0
- package/dist/versioning/index.cjs +7 -0
- package/dist/versioning/index.d.cts +3 -0
- package/meta.json +19 -4
- package/package.json +173 -28
- package/src/abac/index.ts +1 -0
- package/src/core/abac/abac-request.ts +29 -0
- package/src/core/abac/attributes.ts +39 -0
- package/src/core/abac/authorizer.ts +108 -0
- package/src/core/abac/combining.ts +63 -0
- package/src/core/abac/composite-authorizer.ts +45 -0
- package/src/core/abac/domain/policy-set.ts +52 -0
- package/src/core/abac/domain/rule.ts +197 -0
- package/src/core/abac/index.ts +21 -0
- package/src/core/application/ports/abac-authorizer.port.ts +20 -0
- package/src/core/application/ports/authorizer.port.ts +22 -0
- package/src/core/errors/authorization-error.ts +26 -0
- package/src/core/errors/error-codes.ts +1 -0
- package/src/core/index.ts +7 -0
- package/src/core/rbac/access-request.ts +32 -0
- package/src/core/rbac/authorizer.ts +91 -0
- package/src/core/rbac/domain/grant.ts +96 -0
- package/src/core/rbac/domain/permission-set.ts +67 -0
- package/src/core/rbac/domain/permission.ts +119 -0
- package/src/core/rbac/domain/role.ts +101 -0
- package/src/core/rbac/domain/scope.ts +84 -0
- package/src/core/rbac/index.ts +15 -0
- package/src/core/rbac/policy-bridge.ts +44 -0
- package/src/examples/application/authorize-cancel-order-abac.example.ts +107 -0
- package/src/examples/application/authorize-cancel-order.example.ts +101 -0
- package/src/rbac/index.ts +1 -0
- package/src/version.ts +1 -1
package/KIT_CONTEXT.md
CHANGED
|
@@ -12,13 +12,15 @@ Núcleo arquitetural para sistemas ERP (e domínios transacionais com temporalid
|
|
|
12
12
|
- **`result/`** — `Result<T, E>` e `Outcome` para retorno tipado da camada de aplicação.
|
|
13
13
|
- **`application/`** — `UseCase` base, `commands/`, `queries/`, `ports/`. Casos de uso consomem portas e retornam `Result`.
|
|
14
14
|
- **`policies/`** — catálogo, definições, resolução e avaliação de policies declarativas.
|
|
15
|
+
- **`rbac/`** — RBAC zod-free (`./rbac`): `Permission`/`Role`/`Grant`/`Scope`, decisor `RbacAuthorizer`, `AuthorizerPort`.
|
|
16
|
+
- **`abac/`** — ABAC zod-free (`./abac`): `AbacRule`/`AbacPolicySet` + combinação, decisor `AbacAuthorizer`, `AbacAuthorizerPort`, `CompositeAuthorizer`.
|
|
15
17
|
- **`versioning/`** — utilidades de versionamento de agregados temporais.
|
|
16
18
|
|
|
17
19
|
## [key-decisions] Decisões-chave
|
|
18
20
|
|
|
19
21
|
- **Domínio lança, aplicação retorna `Result`, infra traduz.** Não há `Result` no domínio.
|
|
20
22
|
- **Sem lib de log/observabilidade no runtime.** Portas vivem em `application/ports/`; adapters são opt-in.
|
|
21
|
-
- **Temporalidade interna ao kit** —
|
|
23
|
+
- **Temporalidade interna ao kit** — suporte temporal no código-base, mas a API pública não publica `Timeline<T>` nem helpers temporais no barrel raiz.
|
|
22
24
|
- **Policies como dados**, não como if-statements: avaliáveis, compostas, serializáveis.
|
|
23
25
|
- **Policies permitem disable sem remover código**: `PolicyDefinition` aceita `enabled: false` e o repositório ignora definições desabilitadas.
|
|
24
26
|
- **Composição isolada é de primeira classe**: use `new CoreConfig()`, `new ContextResolverRegistry()` e `registerNamespacedContextResolversIn(...)` quando precisar evitar singletons compartilhados.
|
|
@@ -29,8 +31,8 @@ Núcleo arquitetural para sistemas ERP (e domínios transacionais com temporalid
|
|
|
29
31
|
Implemente as portas em `application/ports/` para conectar a stack real:
|
|
30
32
|
|
|
31
33
|
- `LoggerPort`, `MetricsPort`, `TracerPort` — observabilidade.
|
|
32
|
-
- Repositórios
|
|
33
|
-
- `AuthorizerPort
|
|
34
|
+
- Repositórios do seu domínio, sempre com verbo mínimo.
|
|
35
|
+
- `AuthorizerPort`/`AbacAuthorizerPort`/`AuthenticatorPort`.
|
|
34
36
|
|
|
35
37
|
## [non-goals] Não-objetivos
|
|
36
38
|
|
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ Para o sumário prompt-friendly veja [`KIT_CONTEXT.md`](./KIT_CONTEXT.md). Para
|
|
|
13
13
|
- **Erros de aplicação** — `AppError` discriminada por `code`: `ValidationError`, `NotFoundError`, `ConflictError`, `AuthorizationError`, `IntegrationError`.
|
|
14
14
|
- **Result** — `Result<T, E>` e `Outcome` para retorno tipado da aplicação.
|
|
15
15
|
- **Policies** — `PolicyCatalog`, `PolicyDefinition`, `PolicyResolver`, `PolicyService` e tipos associados para avaliação declarativa.
|
|
16
|
+
- **RBAC** — primitivas puras e zod-free no subpath [`./rbac`](#rbac-controle-de-acesso-por-papéis): `Permission`, `Role`, `Grant`, `Scope`, o decisor `RbacAuthorizer` e a `AuthorizerPort`. Veja a [seção dedicada](#rbac-controle-de-acesso-por-papéis).
|
|
17
|
+
- **ABAC** — primitivas puras e zod-free no subpath [`./abac`](#abac-controle-de-acesso-por-atributos): `AbacRule`/`AbacPolicySet` com algoritmos de combinação, o decisor `AbacAuthorizer`, a `AbacAuthorizerPort` e o `CompositeAuthorizer`. Veja a [seção dedicada](#abac-controle-de-acesso-por-atributos).
|
|
16
18
|
- **Application** — `UseCase`/`Command` (CQS, entrada `CommandInput` com `RequestedBy`), portas de persistência (`Repository` e a variante `ResultRepository`), `PolicyPort`, portas de observabilidade (`LoggerPort`, `MetricsPort`, `TracerPort`) e `mapPolicyEvaluationError`. Veja o [exemplo end-to-end](#exemplo-end-to-end).
|
|
17
19
|
- **Exemplos** — rulesets de referência em `examples/rulesets/`, fora da superfície principal de domínio.
|
|
18
20
|
|
|
@@ -173,6 +175,164 @@ permite sinalizar not-found / concorrência otimista sem quebrar o fluxo. Uma
|
|
|
173
175
|
implementação in-memory de referência está em
|
|
174
176
|
[`src/examples/application/in-memory-account-repository.example.ts`](./src/examples/application/in-memory-account-repository.example.ts).
|
|
175
177
|
|
|
178
|
+
## RBAC (controle de acesso por papéis)
|
|
179
|
+
|
|
180
|
+
Primitivas puras e **zod-free** para responder "este ator pode executar esta
|
|
181
|
+
ação neste recurso?". O subpath `@cullet/erp-core/rbac` não puxa `zod` — só
|
|
182
|
+
instale `zod` se também for usar `./policies`. O decisor `RbacAuthorizer` é 100%
|
|
183
|
+
puro (sem I/O) e devolve `Result`; carregar os `Grant`s do ator é responsabilidade
|
|
184
|
+
do consumidor, atrás de uma `AuthorizerPort` — simétrico ao `PolicyPort`.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import {
|
|
188
|
+
Permission,
|
|
189
|
+
Role,
|
|
190
|
+
Grant,
|
|
191
|
+
Scope,
|
|
192
|
+
RbacAuthorizer,
|
|
193
|
+
type AuthorizerPort,
|
|
194
|
+
type AccessRequest,
|
|
195
|
+
} from "@cullet/erp-core/rbac";
|
|
196
|
+
import { RequestedBy } from "@cullet/erp-core";
|
|
197
|
+
import { Result } from "@cullet/erp-core/result";
|
|
198
|
+
import type { AuthorizationError } from "@cullet/erp-core/errors";
|
|
199
|
+
|
|
200
|
+
// 1. Papéis e permissões são dados do SEU domínio — o kit só dá os tipos.
|
|
201
|
+
const ROLES = {
|
|
202
|
+
cashier: Role.of("cashier", [
|
|
203
|
+
Permission.of("orders:read"),
|
|
204
|
+
Permission.of("orders:cancel"),
|
|
205
|
+
]),
|
|
206
|
+
manager: Role.of("manager", [Permission.of("orders:*")]),
|
|
207
|
+
admin: Role.of("admin", [Permission.of("*:*")]), // super-permissão
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// 2. Adapter da porta: carrega os Grants (do SEU store) e delega ao decisor puro.
|
|
211
|
+
class RbacAuthorizerAdapter implements AuthorizerPort {
|
|
212
|
+
private readonly engine = new RbacAuthorizer();
|
|
213
|
+
|
|
214
|
+
constructor(
|
|
215
|
+
private readonly loadGrants: (
|
|
216
|
+
subject: string,
|
|
217
|
+
) => Promise<readonly Grant[]>,
|
|
218
|
+
) {}
|
|
219
|
+
|
|
220
|
+
async authorize(
|
|
221
|
+
request: AccessRequest,
|
|
222
|
+
): Promise<Result<void, AuthorizationError>> {
|
|
223
|
+
const grants = await this.loadGrants(request.actor.raw);
|
|
224
|
+
const decision = this.engine.authorize(request, grants);
|
|
225
|
+
return decision.isErr()
|
|
226
|
+
? Result.err(decision.error)
|
|
227
|
+
: Result.ok(undefined);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 3. No caso de uso, injete só a porta e pergunte antes de mutar.
|
|
232
|
+
const authorizer = new RbacAuthorizerAdapter(async (subject) => [
|
|
233
|
+
Grant.of({
|
|
234
|
+
subject: RequestedBy.parse(subject),
|
|
235
|
+
role: ROLES.cashier,
|
|
236
|
+
scope: Scope.of("school:42"),
|
|
237
|
+
}),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const decision = await authorizer.authorize({
|
|
241
|
+
actor: RequestedBy.fromUser("550e8400-e29b-41d4-a716-446655440000"),
|
|
242
|
+
action: "order.cancel",
|
|
243
|
+
required: Permission.of("orders:cancel"),
|
|
244
|
+
resource: { type: "Order", id: "order-1" },
|
|
245
|
+
scope: Scope.of("school:42"),
|
|
246
|
+
});
|
|
247
|
+
// decision.isErr() → AuthorizationError com `reason`/`code`/`metadata` prontos
|
|
248
|
+
// para a borda HTTP traduzir em 403.
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
O `reason` discriminado (`missing_role` / `missing_capability` / `out_of_scope`)
|
|
252
|
+
e o `code` (`sec.authz.*`) já vêm preenchidos para a borda mapear sem re-derivar
|
|
253
|
+
nada. Para regras declarativas além de "tem a permissão" (ABAC), `rbacContextFields(actor, grants)`
|
|
254
|
+
projeta `actor.roles`/`actor.permissions` no `seed.fields` de uma policy de gate
|
|
255
|
+
(aí sim com `./policies` + `zod`). O exemplo interno compilado e testado está em
|
|
256
|
+
[`src/examples/application/authorize-cancel-order.example.ts`](./src/examples/application/authorize-cancel-order.example.ts).
|
|
257
|
+
|
|
258
|
+
## ABAC (controle de acesso por atributos)
|
|
259
|
+
|
|
260
|
+
Quando a decisão depende de **atributos** do sujeito, do recurso e do ambiente
|
|
261
|
+
(não só de um papel estático), o subpath `@cullet/erp-core/abac` — também
|
|
262
|
+
**zod-free** — dá um decisor puro que avalia regras `PERMIT`/`DENY` sobre esses
|
|
263
|
+
atributos. `AbacRule` reusa a mesma DSL de condição do gate; `AbacPolicySet`
|
|
264
|
+
combina as regras por um algoritmo (`deny-overrides` por padrão, mais
|
|
265
|
+
`permit-overrides` / `first-applicable`) e é **fechado por padrão**. O decisor
|
|
266
|
+
reusa o avaliador de condição puro do gate — sem arrastar `zod`.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import {
|
|
270
|
+
AbacRule,
|
|
271
|
+
AbacPolicySet,
|
|
272
|
+
AbacAuthorizer,
|
|
273
|
+
type AbacAuthorizerPort,
|
|
274
|
+
type AbacRequest,
|
|
275
|
+
} from "@cullet/erp-core/abac";
|
|
276
|
+
import { RequestedBy } from "@cullet/erp-core";
|
|
277
|
+
import { Result } from "@cullet/erp-core/result";
|
|
278
|
+
import type { AuthorizationError } from "@cullet/erp-core/errors";
|
|
279
|
+
|
|
280
|
+
// 1. Regras são dados do SEU domínio (condição na DSL de gate, sobre atributos).
|
|
281
|
+
const POLICIES = AbacPolicySet.of([
|
|
282
|
+
AbacRule.of({
|
|
283
|
+
id: "order.cancel.open-in-hours",
|
|
284
|
+
version: 1,
|
|
285
|
+
effect: "PERMIT",
|
|
286
|
+
condition: {
|
|
287
|
+
and: [
|
|
288
|
+
{ field: "resource.status", op: "eq", value: "OPEN" },
|
|
289
|
+
{ field: "env.businessHours", op: "eq", value: true },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
AbacRule.of({
|
|
294
|
+
id: "order.cancel.deny-locked",
|
|
295
|
+
version: 1,
|
|
296
|
+
effect: "DENY", // deny-overrides ⇒ trava explícita vence qualquer PERMIT
|
|
297
|
+
condition: { field: "resource.locked", op: "eq", value: true },
|
|
298
|
+
}),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// 2. Adapter da porta: resolve atributos dinâmicos e delega ao decisor puro.
|
|
302
|
+
class AbacAuthorizerAdapter implements AbacAuthorizerPort {
|
|
303
|
+
private readonly engine = new AbacAuthorizer();
|
|
304
|
+
|
|
305
|
+
async authorize(
|
|
306
|
+
request: AbacRequest,
|
|
307
|
+
): Promise<Result<void, AuthorizationError>> {
|
|
308
|
+
const decision = this.engine.authorize(request, POLICIES);
|
|
309
|
+
return decision.isErr()
|
|
310
|
+
? Result.err(decision.error)
|
|
311
|
+
: Result.ok(undefined);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const authorizer = new AbacAuthorizerAdapter();
|
|
316
|
+
const decision = await authorizer.authorize({
|
|
317
|
+
actor: RequestedBy.fromUser("550e8400-e29b-41d4-a716-446655440000"),
|
|
318
|
+
action: "order.cancel",
|
|
319
|
+
resource: { type: "Order", id: "order-1" },
|
|
320
|
+
attributes: {
|
|
321
|
+
resource: { status: "OPEN", locked: false },
|
|
322
|
+
environment: { businessHours: true },
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
// decision.isErr() → AuthorizationError (`policy_denied`, ou `forbidden` no
|
|
326
|
+
// fail-closed) pronto para a borda traduzir em 403.
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**RBAC ou ABAC?** RBAC responde "o ator _tem_ a capacidade?" (papel→permissão
|
|
330
|
+
estático); ABAC responde "…_neste_ contexto?" (atributos do recurso/ambiente). Os
|
|
331
|
+
dois compõem: o `CompositeAuthorizer` roda o RBAC e, se passar, refina com o ABAC.
|
|
332
|
+
Para regras geridas como **dado** (versionadas, por tenant, vindas de JSON), use o
|
|
333
|
+
motor `./policies` (aí sim com `zod`). O exemplo interno compilado e testado está
|
|
334
|
+
em [`src/examples/application/authorize-cancel-order-abac.example.ts`](./src/examples/application/authorize-cancel-order-abac.example.ts).
|
|
335
|
+
|
|
176
336
|
## Composicao sem singletons
|
|
177
337
|
|
|
178
338
|
Para isolamento por tenant, request ou teste, prefira instancias locais em vez
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_rule = require("../rule.cjs");
|
|
3
|
+
exports.AbacAuthorizer = require_rule.AbacAuthorizer;
|
|
4
|
+
exports.AbacPolicySet = require_rule.AbacPolicySet;
|
|
5
|
+
exports.AbacRule = require_rule.AbacRule;
|
|
6
|
+
exports.CompositeAuthorizer = require_rule.CompositeAuthorizer;
|
|
7
|
+
exports.abacContext = require_rule.abacContext;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as AbacPolicySet, c as AbacRuleProps, d as AbacAuthorizerPort, f as AbacAttributes, i as AbacDecision, l as RuleEffect, n as CompositeAuthorizer, o as CombiningAlgorithm, p as AbacRequest, r as AbacAuthorizer, s as AbacRule, t as CompositeAccessRequest, u as abacContext } from "../composite-authorizer.cjs";
|
|
2
|
+
export { AbacAttributes, AbacAuthorizer, AbacAuthorizerPort, AbacDecision, AbacPolicySet, AbacRequest, AbacRule, AbacRuleProps, CombiningAlgorithm, CompositeAccessRequest, CompositeAuthorizer, RuleEffect, abacContext };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as AbacPolicySet, c as AbacRuleProps, d as AbacAuthorizerPort, f as AbacAttributes, i as AbacDecision, l as RuleEffect, n as CompositeAuthorizer, o as CombiningAlgorithm, p as AbacRequest, r as AbacAuthorizer, s as AbacRule, t as CompositeAccessRequest, u as abacContext } from "../composite-authorizer.js";
|
|
2
|
+
export { AbacAttributes, AbacAuthorizer, AbacAuthorizerPort, AbacDecision, AbacPolicySet, AbacRequest, AbacRule, AbacRuleProps, CombiningAlgorithm, CompositeAccessRequest, CompositeAuthorizer, RuleEffect, abacContext };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
//#region src/core/errors/error-codes.ts
|
|
2
|
+
const ErrorCodes = {
|
|
3
|
+
authentication: {
|
|
4
|
+
missingToken: "sec.authn.missing_token",
|
|
5
|
+
invalidToken: "sec.authn.invalid_token",
|
|
6
|
+
expiredToken: "sec.authn.expired_token",
|
|
7
|
+
invalidCredentials: "sec.authn.invalid_credentials"
|
|
8
|
+
},
|
|
9
|
+
authorization: {
|
|
10
|
+
forbidden: "sec.authz.forbidden",
|
|
11
|
+
policyDenied: "sec.authz.policy_denied",
|
|
12
|
+
missingRole: "sec.authz.missing_role",
|
|
13
|
+
missingCapability: "sec.authz.missing_capability",
|
|
14
|
+
outOfScope: "sec.authz.out_of_scope"
|
|
15
|
+
},
|
|
16
|
+
businessRuleViolation: "business_rule_violation",
|
|
17
|
+
conflict: {
|
|
18
|
+
alreadyExists: "conf.already_exists",
|
|
19
|
+
duplicate: "conf.duplicate",
|
|
20
|
+
uniqueViolation: "conf.unique_violation"
|
|
21
|
+
},
|
|
22
|
+
idempotency: {
|
|
23
|
+
keyMissing: "idemp.key_missing",
|
|
24
|
+
inProgress: "idemp.in_progress",
|
|
25
|
+
payloadMismatch: "idemp.payload_mismatch",
|
|
26
|
+
replayNotSupported: "idemp.replay_not_supported"
|
|
27
|
+
},
|
|
28
|
+
integration: {
|
|
29
|
+
timeout: "int.timeout",
|
|
30
|
+
unreachable: "int.unreachable",
|
|
31
|
+
badResponse: "int.bad_response"
|
|
32
|
+
},
|
|
33
|
+
legacyIncompatible: "legacy_incompatible",
|
|
34
|
+
notFound: "not_found",
|
|
35
|
+
serialization: {
|
|
36
|
+
deserializePrefix: "ser.des",
|
|
37
|
+
serializePrefix: "ser.ser"
|
|
38
|
+
},
|
|
39
|
+
temporal: {
|
|
40
|
+
expired: "tmp.expired",
|
|
41
|
+
notYetValid: "tmp.not_yet_valid"
|
|
42
|
+
},
|
|
43
|
+
unexpected: "unexpected",
|
|
44
|
+
validation: "validation_error"
|
|
45
|
+
};
|
|
46
|
+
function serializationErrorCode(direction, category) {
|
|
47
|
+
return `${direction === "deserialize" ? ErrorCodes.serialization.deserializePrefix : ErrorCodes.serialization.serializePrefix}.${category}`;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/core/errors/utils/json-safe.ts
|
|
51
|
+
const NON_SERIALIZABLE_PLACEHOLDER = "[NonSerializable]";
|
|
52
|
+
const CIRCULAR_REFERENCE_PLACEHOLDER = "[Circular]";
|
|
53
|
+
function isPlainObject(value) {
|
|
54
|
+
if (value === null || typeof value !== "object") return false;
|
|
55
|
+
const proto = Object.getPrototypeOf(value);
|
|
56
|
+
return proto === Object.prototype || proto === null;
|
|
57
|
+
}
|
|
58
|
+
function sanitizeValue(value, seen) {
|
|
59
|
+
if (value === null) return null;
|
|
60
|
+
const type = typeof value;
|
|
61
|
+
if (type === "string" || type === "number" || type === "boolean") return value;
|
|
62
|
+
if (type === "bigint" || type === "function" || type === "symbol" || value === void 0) return NON_SERIALIZABLE_PLACEHOLDER;
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;
|
|
65
|
+
seen.add(value);
|
|
66
|
+
const sanitized = value.map((item) => sanitizeValue(item, seen));
|
|
67
|
+
seen.delete(value);
|
|
68
|
+
return sanitized;
|
|
69
|
+
}
|
|
70
|
+
if (value instanceof Date) return NON_SERIALIZABLE_PLACEHOLDER;
|
|
71
|
+
if (!isPlainObject(value)) return NON_SERIALIZABLE_PLACEHOLDER;
|
|
72
|
+
if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;
|
|
73
|
+
seen.add(value);
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const [key, val] of Object.entries(value)) result[key] = sanitizeValue(val, seen);
|
|
76
|
+
seen.delete(value);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ensures metadata is JSON-serializable.
|
|
81
|
+
*
|
|
82
|
+
* **Allowed:** string, number, boolean, null, arrays, and plain objects.
|
|
83
|
+
*
|
|
84
|
+
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
85
|
+
* symbols, undefined, and circular references (which would otherwise make
|
|
86
|
+
* `JSON.stringify` throw).
|
|
87
|
+
*
|
|
88
|
+
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
89
|
+
*/
|
|
90
|
+
function assertJsonSafeMetadata(input) {
|
|
91
|
+
if (input === void 0) return {};
|
|
92
|
+
if (!isPlainObject(input)) throw new TypeError("metadata must be a plain object (Record<string, unknown>).");
|
|
93
|
+
return sanitizeValue(input, /* @__PURE__ */ new WeakSet());
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/core/errors/app-error.ts
|
|
97
|
+
/**
|
|
98
|
+
* Root of the application/domain error hierarchy. Every error the system raises
|
|
99
|
+
* on purpose extends `AppError`, which gives callers a single `instanceof` to
|
|
100
|
+
* catch and a uniform, serializable shape to log and transport.
|
|
101
|
+
*
|
|
102
|
+
* Beyond the native `Error` message it carries a stable `code` (the contract the
|
|
103
|
+
* outside world matches on), an optional non-leaking `publicMessage`, a
|
|
104
|
+
* severity, JSON-safe `metadata`, and the correlation/request/command ids that
|
|
105
|
+
* stitch an error back to the request that produced it. The metadata is
|
|
106
|
+
* validated as JSON-safe on construction, so a logger can serialize any
|
|
107
|
+
* `AppError` without hitting a circular reference or a non-serializable value.
|
|
108
|
+
*
|
|
109
|
+
* Abstract on purpose: callers should throw a specific subclass (e.g.
|
|
110
|
+
* {@link ValidationError}, {@link NotFoundError}) so the `code` and shape are
|
|
111
|
+
* meaningful, never a bare `AppError`.
|
|
112
|
+
*/
|
|
113
|
+
var AppError = class extends Error {
|
|
114
|
+
/**
|
|
115
|
+
* Builds the common error envelope shared by every subclass.
|
|
116
|
+
*
|
|
117
|
+
* `name` is taken from `new.target` so the thrown instance reports its
|
|
118
|
+
* concrete subclass name (not `"AppError"`), and the prototype is re-pinned
|
|
119
|
+
* via `setPrototypeOf` so `instanceof` keeps working after transpilation to
|
|
120
|
+
* older targets where extending built-ins breaks the chain. `createdAtIso`
|
|
121
|
+
* defaults to now, and `metadata` is validated as JSON-safe so the error is
|
|
122
|
+
* always serializable.
|
|
123
|
+
*
|
|
124
|
+
* Declared `protected`: instantiate a concrete subclass, never `AppError`.
|
|
125
|
+
*
|
|
126
|
+
* @param message - The internal, developer-facing message.
|
|
127
|
+
* @param code - The stable machine-readable code callers match on.
|
|
128
|
+
* @param options - Optional cause, metadata, severity, and correlation ids.
|
|
129
|
+
* @throws When `options.metadata` contains a value that is not JSON-safe.
|
|
130
|
+
*/
|
|
131
|
+
constructor(message, code, options) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.name = new.target.name;
|
|
134
|
+
this.code = code;
|
|
135
|
+
this.cause = options?.cause;
|
|
136
|
+
this.type = options?.type;
|
|
137
|
+
this.severity = options?.severity;
|
|
138
|
+
this.correlationId = options?.correlationId;
|
|
139
|
+
this.requestId = options?.requestId;
|
|
140
|
+
this.commandId = options?.commandId;
|
|
141
|
+
this.createdAtIso = options?.createdAtIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
142
|
+
this.publicMessage = options?.publicMessage;
|
|
143
|
+
this.metadata = options?.metadata ? assertJsonSafeMetadata(options.metadata) : void 0;
|
|
144
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns a JSON-safe representation of the error.
|
|
148
|
+
* Does NOT include `cause` by default (to avoid leaking internal details).
|
|
149
|
+
*/
|
|
150
|
+
toJSON() {
|
|
151
|
+
const payload = {
|
|
152
|
+
name: this.name,
|
|
153
|
+
code: this.code,
|
|
154
|
+
message: this.message,
|
|
155
|
+
createdAtIso: this.createdAtIso
|
|
156
|
+
};
|
|
157
|
+
if (this.type !== void 0) payload.type = this.type;
|
|
158
|
+
if (this.severity !== void 0) payload.severity = this.severity;
|
|
159
|
+
if (this.publicMessage !== void 0) payload.publicMessage = this.publicMessage;
|
|
160
|
+
if (this.metadata !== void 0) payload.metadata = this.metadata;
|
|
161
|
+
if (this.correlationId !== void 0) payload.correlationId = this.correlationId;
|
|
162
|
+
if (this.requestId !== void 0) payload.requestId = this.requestId;
|
|
163
|
+
if (this.commandId !== void 0) payload.commandId = this.commandId;
|
|
164
|
+
return payload;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
Object.defineProperty(exports, "AppError", {
|
|
169
|
+
enumerable: true,
|
|
170
|
+
get: function() {
|
|
171
|
+
return AppError;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
Object.defineProperty(exports, "ErrorCodes", {
|
|
175
|
+
enumerable: true,
|
|
176
|
+
get: function() {
|
|
177
|
+
return ErrorCodes;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
Object.defineProperty(exports, "assertJsonSafeMetadata", {
|
|
181
|
+
enumerable: true,
|
|
182
|
+
get: function() {
|
|
183
|
+
return assertJsonSafeMetadata;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
Object.defineProperty(exports, "serializationErrorCode", {
|
|
187
|
+
enumerable: true,
|
|
188
|
+
get: function() {
|
|
189
|
+
return serializationErrorCode;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
//# sourceMappingURL=app-error.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-error.cjs","names":[],"sources":["../src/core/errors/error-codes.ts","../src/core/errors/utils/json-safe.ts","../src/core/errors/app-error.ts"],"sourcesContent":["const ErrorCodes = {\n authentication: {\n missingToken: \"sec.authn.missing_token\",\n invalidToken: \"sec.authn.invalid_token\",\n expiredToken: \"sec.authn.expired_token\",\n invalidCredentials: \"sec.authn.invalid_credentials\",\n },\n authorization: {\n forbidden: \"sec.authz.forbidden\",\n policyDenied: \"sec.authz.policy_denied\",\n missingRole: \"sec.authz.missing_role\",\n missingCapability: \"sec.authz.missing_capability\",\n outOfScope: \"sec.authz.out_of_scope\",\n },\n businessRuleViolation: \"business_rule_violation\",\n conflict: {\n alreadyExists: \"conf.already_exists\",\n duplicate: \"conf.duplicate\",\n uniqueViolation: \"conf.unique_violation\",\n },\n idempotency: {\n keyMissing: \"idemp.key_missing\",\n inProgress: \"idemp.in_progress\",\n payloadMismatch: \"idemp.payload_mismatch\",\n replayNotSupported: \"idemp.replay_not_supported\",\n },\n integration: {\n timeout: \"int.timeout\",\n unreachable: \"int.unreachable\",\n badResponse: \"int.bad_response\",\n },\n legacyIncompatible: \"legacy_incompatible\",\n notFound: \"not_found\",\n serialization: {\n deserializePrefix: \"ser.des\",\n serializePrefix: \"ser.ser\",\n },\n temporal: {\n expired: \"tmp.expired\",\n notYetValid: \"tmp.not_yet_valid\",\n },\n unexpected: \"unexpected\",\n validation: \"validation_error\",\n} as const;\n\ntype SerializationCodeDirection = \"deserialize\" | \"serialize\";\n\nfunction serializationErrorCode(\n direction: SerializationCodeDirection,\n category: string,\n): string {\n const prefix =\n direction === \"deserialize\"\n ? ErrorCodes.serialization.deserializePrefix\n : ErrorCodes.serialization.serializePrefix;\n\n return `${prefix}.${category}`;\n}\n\nexport { ErrorCodes, serializationErrorCode };\nexport type { SerializationCodeDirection };\n","// Utilities for ensuring metadata is JSON-serializable.\n\nimport type { JsonSafeRecord, JsonSafeValue } from \"../types.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst NON_SERIALIZABLE_PLACEHOLDER = \"[NonSerializable]\";\nconst CIRCULAR_REFERENCE_PLACEHOLDER = \"[Circular]\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Internal helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n// `seen` tracks the ancestors on the current traversal branch (a DFS path), not\n// every object visited. This collapses true circular references to a placeholder\n// while still serializing the same object referenced more than once across\n// sibling branches (a DAG) — matching `JSON.stringify`, which only rejects\n// cycles, not shared references.\nfunction sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {\n // Null passthrough\n if (value === null) return null;\n\n const type = typeof value;\n\n // Primitives\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n return value as JsonSafeValue;\n }\n\n // Non-serializable primitives\n if (\n type === \"bigint\" ||\n type === \"function\" ||\n type === \"symbol\" ||\n value === undefined\n ) {\n return NON_SERIALIZABLE_PLACEHOLDER;\n }\n\n // Arrays (recursive)\n if (Array.isArray(value)) {\n if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;\n seen.add(value);\n const sanitized = value.map((item) => sanitizeValue(item, seen));\n seen.delete(value);\n return sanitized;\n }\n\n // Date → placeholder (could also use .toISOString() if preferred)\n if (value instanceof Date) {\n return NON_SERIALIZABLE_PLACEHOLDER;\n }\n\n // Class instances and non-plain objects → placeholder\n if (!isPlainObject(value)) {\n return NON_SERIALIZABLE_PLACEHOLDER;\n }\n\n // Plain object (recursive)\n if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;\n seen.add(value);\n const result: Record<string, JsonSafeValue> = {};\n for (const [key, val] of Object.entries(value)) {\n result[key] = sanitizeValue(val, seen);\n }\n seen.delete(value);\n return result;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Public API\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Ensures metadata is JSON-serializable.\n *\n * **Allowed:** string, number, boolean, null, arrays, and plain objects.\n *\n * **Converted to placeholder:** Date, BigInt, class instances, functions,\n * symbols, undefined, and circular references (which would otherwise make\n * `JSON.stringify` throw).\n *\n * @throws {TypeError} If `input` is not a plain object at the root level.\n */\nfunction assertJsonSafeMetadata(input: unknown): JsonSafeRecord {\n if (input === undefined) {\n return {};\n }\n\n if (!isPlainObject(input)) {\n throw new TypeError(\n \"metadata must be a plain object (Record<string, unknown>).\",\n );\n }\n\n return sanitizeValue(input, new WeakSet<object>()) as JsonSafeRecord;\n}\n\nexport {\n assertJsonSafeMetadata,\n CIRCULAR_REFERENCE_PLACEHOLDER,\n NON_SERIALIZABLE_PLACEHOLDER,\n};\n","// Base application error for the domain/application layer.\n// Encapsulates a code, optional cause, and JSON-safe metadata.\n\nimport type {\n AppErrorOptions,\n ErrorSeverity,\n JsonSafeRecord,\n} from \"./types.js\";\nimport { assertJsonSafeMetadata } from \"./utils/index.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AppError (abstract base class)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Root of the application/domain error hierarchy. Every error the system raises\n * on purpose extends `AppError`, which gives callers a single `instanceof` to\n * catch and a uniform, serializable shape to log and transport.\n *\n * Beyond the native `Error` message it carries a stable `code` (the contract the\n * outside world matches on), an optional non-leaking `publicMessage`, a\n * severity, JSON-safe `metadata`, and the correlation/request/command ids that\n * stitch an error back to the request that produced it. The metadata is\n * validated as JSON-safe on construction, so a logger can serialize any\n * `AppError` without hitting a circular reference or a non-serializable value.\n *\n * Abstract on purpose: callers should throw a specific subclass (e.g.\n * {@link ValidationError}, {@link NotFoundError}) so the `code` and shape are\n * meaningful, never a bare `AppError`.\n */\nabstract class AppError extends Error {\n public readonly code: string;\n public readonly cause?: unknown;\n public readonly metadata?: JsonSafeRecord;\n public readonly type?: string;\n public readonly severity?: ErrorSeverity;\n public readonly createdAtIso: string;\n public readonly publicMessage?: string;\n /**\n * Identifies a single execution \"story\" in the system. All operations\n * related to the same logical flow must share the same correlationId.\n */\n public readonly correlationId?: string;\n /**\n * Identifies a specific technical request attempt (each retry may produce\n * a new requestId), even when the correlationId stays the same.\n */\n public readonly requestId?: string;\n /**\n * Marks the business intent / idempotency key; stays the same across\n * technical retries and multiple requests that represent the same intent.\n */\n public readonly commandId?: string;\n\n /**\n * Builds the common error envelope shared by every subclass.\n *\n * `name` is taken from `new.target` so the thrown instance reports its\n * concrete subclass name (not `\"AppError\"`), and the prototype is re-pinned\n * via `setPrototypeOf` so `instanceof` keeps working after transpilation to\n * older targets where extending built-ins breaks the chain. `createdAtIso`\n * defaults to now, and `metadata` is validated as JSON-safe so the error is\n * always serializable.\n *\n * Declared `protected`: instantiate a concrete subclass, never `AppError`.\n *\n * @param message - The internal, developer-facing message.\n * @param code - The stable machine-readable code callers match on.\n * @param options - Optional cause, metadata, severity, and correlation ids.\n * @throws When `options.metadata` contains a value that is not JSON-safe.\n */\n protected constructor(\n message: string,\n code: string,\n options?: AppErrorOptions,\n ) {\n super(message);\n\n this.name = new.target.name;\n this.code = code;\n this.cause = options?.cause;\n this.type = options?.type;\n this.severity = options?.severity;\n this.correlationId = options?.correlationId;\n this.requestId = options?.requestId;\n this.commandId = options?.commandId;\n this.createdAtIso = options?.createdAtIso ?? new Date().toISOString();\n this.publicMessage = options?.publicMessage;\n\n this.metadata = options?.metadata\n ? assertJsonSafeMetadata(options.metadata)\n : undefined;\n\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Returns a JSON-safe representation of the error.\n * Does NOT include `cause` by default (to avoid leaking internal details).\n */\n public toJSON(): Record<string, unknown> {\n const payload: Record<string, unknown> = {\n name: this.name,\n code: this.code,\n message: this.message,\n createdAtIso: this.createdAtIso,\n };\n\n // Optional fields are emitted only when defined, so the serialized\n // shape never carries `undefined`-valued keys (consistent with how the\n // correlation/request/command ids are handled).\n if (this.type !== undefined) payload.type = this.type;\n if (this.severity !== undefined) payload.severity = this.severity;\n if (this.publicMessage !== undefined)\n payload.publicMessage = this.publicMessage;\n if (this.metadata !== undefined) payload.metadata = this.metadata;\n if (this.correlationId !== undefined)\n payload.correlationId = this.correlationId;\n if (this.requestId !== undefined) payload.requestId = this.requestId;\n if (this.commandId !== undefined) payload.commandId = this.commandId;\n\n return payload;\n }\n}\n\nexport { AppError };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//#region src/core/errors/types.d.ts
|
|
2
|
+
type ErrorSeverity = "low" | "medium" | "high" | "critical";
|
|
3
|
+
type JsonSafePrimitive = string | number | boolean | null;
|
|
4
|
+
type JsonSafeValue = JsonSafePrimitive | JsonSafeValue[] | {
|
|
5
|
+
[key: string]: JsonSafeValue;
|
|
6
|
+
};
|
|
7
|
+
type JsonSafeRecord = Record<string, JsonSafeValue>;
|
|
8
|
+
/**
|
|
9
|
+
* Options accepted by the AppError constructor.
|
|
10
|
+
* Subclasses can extend or pick from this type.
|
|
11
|
+
*/
|
|
12
|
+
type AppErrorOptions = {
|
|
13
|
+
/** Original error or exception that caused this error. */
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
/** Arbitrary key-value metadata (will be sanitized to JSON-safe values). */
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
/** Error category/type for grouping (e.g., "authentication", "validation"). */
|
|
18
|
+
type?: string;
|
|
19
|
+
/** severity level for alerting/logging prioritization. */
|
|
20
|
+
severity?: ErrorSeverity;
|
|
21
|
+
/**
|
|
22
|
+
* Identifies a single execution "story" in the system. All operations
|
|
23
|
+
* related to the same logical flow must share the same correlationId.
|
|
24
|
+
*/
|
|
25
|
+
correlationId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Identifies a specific technical request attempt (each retry may produce
|
|
28
|
+
* a new requestId), even when the correlationId stays the same.
|
|
29
|
+
*/
|
|
30
|
+
requestId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Identifies a unique business intent (idempotency key); stays the same
|
|
33
|
+
* across technical retries and multiple requests that represent the same intent.
|
|
34
|
+
*/
|
|
35
|
+
commandId?: string;
|
|
36
|
+
/** ISO timestamp of when the error was created (defaults to now). */
|
|
37
|
+
createdAtIso?: string;
|
|
38
|
+
/** Safe message that can be exposed to end users (no internal details). */
|
|
39
|
+
publicMessage?: string;
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/core/errors/app-error.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Root of the application/domain error hierarchy. Every error the system raises
|
|
45
|
+
* on purpose extends `AppError`, which gives callers a single `instanceof` to
|
|
46
|
+
* catch and a uniform, serializable shape to log and transport.
|
|
47
|
+
*
|
|
48
|
+
* Beyond the native `Error` message it carries a stable `code` (the contract the
|
|
49
|
+
* outside world matches on), an optional non-leaking `publicMessage`, a
|
|
50
|
+
* severity, JSON-safe `metadata`, and the correlation/request/command ids that
|
|
51
|
+
* stitch an error back to the request that produced it. The metadata is
|
|
52
|
+
* validated as JSON-safe on construction, so a logger can serialize any
|
|
53
|
+
* `AppError` without hitting a circular reference or a non-serializable value.
|
|
54
|
+
*
|
|
55
|
+
* Abstract on purpose: callers should throw a specific subclass (e.g.
|
|
56
|
+
* {@link ValidationError}, {@link NotFoundError}) so the `code` and shape are
|
|
57
|
+
* meaningful, never a bare `AppError`.
|
|
58
|
+
*/
|
|
59
|
+
declare abstract class AppError extends Error {
|
|
60
|
+
readonly code: string;
|
|
61
|
+
readonly cause?: unknown;
|
|
62
|
+
readonly metadata?: JsonSafeRecord;
|
|
63
|
+
readonly type?: string;
|
|
64
|
+
readonly severity?: ErrorSeverity;
|
|
65
|
+
readonly createdAtIso: string;
|
|
66
|
+
readonly publicMessage?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Identifies a single execution "story" in the system. All operations
|
|
69
|
+
* related to the same logical flow must share the same correlationId.
|
|
70
|
+
*/
|
|
71
|
+
readonly correlationId?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Identifies a specific technical request attempt (each retry may produce
|
|
74
|
+
* a new requestId), even when the correlationId stays the same.
|
|
75
|
+
*/
|
|
76
|
+
readonly requestId?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Marks the business intent / idempotency key; stays the same across
|
|
79
|
+
* technical retries and multiple requests that represent the same intent.
|
|
80
|
+
*/
|
|
81
|
+
readonly commandId?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Builds the common error envelope shared by every subclass.
|
|
84
|
+
*
|
|
85
|
+
* `name` is taken from `new.target` so the thrown instance reports its
|
|
86
|
+
* concrete subclass name (not `"AppError"`), and the prototype is re-pinned
|
|
87
|
+
* via `setPrototypeOf` so `instanceof` keeps working after transpilation to
|
|
88
|
+
* older targets where extending built-ins breaks the chain. `createdAtIso`
|
|
89
|
+
* defaults to now, and `metadata` is validated as JSON-safe so the error is
|
|
90
|
+
* always serializable.
|
|
91
|
+
*
|
|
92
|
+
* Declared `protected`: instantiate a concrete subclass, never `AppError`.
|
|
93
|
+
*
|
|
94
|
+
* @param message - The internal, developer-facing message.
|
|
95
|
+
* @param code - The stable machine-readable code callers match on.
|
|
96
|
+
* @param options - Optional cause, metadata, severity, and correlation ids.
|
|
97
|
+
* @throws When `options.metadata` contains a value that is not JSON-safe.
|
|
98
|
+
*/
|
|
99
|
+
protected constructor(message: string, code: string, options?: AppErrorOptions);
|
|
100
|
+
/**
|
|
101
|
+
* Returns a JSON-safe representation of the error.
|
|
102
|
+
* Does NOT include `cause` by default (to avoid leaking internal details).
|
|
103
|
+
*/
|
|
104
|
+
toJSON(): Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { JsonSafeRecord as a, JsonSafePrimitive as i, AppErrorOptions as n, JsonSafeValue as o, ErrorSeverity as r, AppError as t };
|
|
108
|
+
//# sourceMappingURL=app-error.d.cts.map
|