@cullet/erp-core 1.3.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.
Files changed (166) hide show
  1. package/KIT_CONTEXT.md +5 -3
  2. package/README.md +160 -0
  3. package/dist/abac/index.cjs +7 -0
  4. package/dist/abac/index.d.cts +2 -0
  5. package/dist/abac/index.d.ts +2 -0
  6. package/dist/abac/index.js +2 -0
  7. package/dist/app-error.cjs +193 -0
  8. package/dist/app-error.cjs.map +1 -0
  9. package/dist/app-error.js +170 -0
  10. package/dist/app-error.js.map +1 -0
  11. package/dist/application/index.cjs +2 -1
  12. package/dist/application/index.d.cts +2 -1
  13. package/dist/application/index.d.ts +2 -1
  14. package/dist/application/index.js +2 -1
  15. package/dist/authorization-error.cjs +157 -0
  16. package/dist/authorization-error.cjs.map +1 -0
  17. package/dist/authorization-error.d.cts +113 -0
  18. package/dist/authorization-error.d.ts +113 -0
  19. package/dist/authorization-error.js +152 -0
  20. package/dist/authorization-error.js.map +1 -0
  21. package/dist/authorizer.port.d.cts +130 -0
  22. package/dist/authorizer.port.d.ts +130 -0
  23. package/dist/composite-authorizer.d.cts +216 -0
  24. package/dist/composite-authorizer.d.ts +216 -0
  25. package/dist/condition-evaluator.cjs +565 -0
  26. package/dist/condition-evaluator.cjs.map +1 -0
  27. package/dist/condition-evaluator.js +536 -0
  28. package/dist/condition-evaluator.js.map +1 -0
  29. package/dist/{gate-types.d.ts → core-config.d.cts} +78 -77
  30. package/dist/{gate-types.d.cts → core-config.d.ts} +78 -77
  31. package/dist/domain/index.cjs +2 -1
  32. package/dist/domain/index.d.cts +2 -1
  33. package/dist/domain/index.d.ts +2 -1
  34. package/dist/domain/index.js +2 -1
  35. package/dist/domain-event-contracts.cjs +3 -2
  36. package/dist/domain-event-contracts.cjs.map +1 -1
  37. package/dist/domain-event-contracts.js +2 -1
  38. package/dist/domain-event-contracts.js.map +1 -1
  39. package/dist/entity.cjs +126 -0
  40. package/dist/entity.cjs.map +1 -0
  41. package/dist/entity.js +115 -0
  42. package/dist/entity.js.map +1 -0
  43. package/dist/errors/index.cjs +8 -6
  44. package/dist/errors/index.d.cts +2 -1
  45. package/dist/errors/index.d.ts +2 -1
  46. package/dist/errors/index.js +4 -2
  47. package/dist/gate-engine-registry.cjs +5 -4
  48. package/dist/gate-engine-registry.cjs.map +1 -1
  49. package/dist/gate-engine-registry.d.cts +3 -2
  50. package/dist/gate-engine-registry.d.ts +3 -2
  51. package/dist/gate-engine-registry.js +2 -1
  52. package/dist/gate-engine-registry.js.map +1 -1
  53. package/dist/gate-v1-payload.schema.cjs +0 -562
  54. package/dist/gate-v1-payload.schema.cjs.map +1 -1
  55. package/dist/gate-v1-payload.schema.js +1 -533
  56. package/dist/gate-v1-payload.schema.js.map +1 -1
  57. package/dist/index.cjs +34 -16
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +19 -12
  60. package/dist/index.d.ts +19 -12
  61. package/dist/index.js +15 -9
  62. package/dist/index.js.map +1 -1
  63. package/dist/not-found-error.cjs +5 -5
  64. package/dist/not-found-error.cjs.map +1 -1
  65. package/dist/not-found-error.js +1 -1
  66. package/dist/outcome.d.cts +1 -83
  67. package/dist/outcome.d.ts +1 -83
  68. package/dist/parse-gate-payload.d.cts +2 -2
  69. package/dist/parse-gate-payload.d.ts +2 -2
  70. package/dist/path.d.cts +2 -2
  71. package/dist/path.d.ts +2 -2
  72. package/dist/policies/engines/index.d.cts +1 -1
  73. package/dist/policies/engines/index.d.ts +1 -1
  74. package/dist/policies/engines/v1/gate/index.cjs +5 -4
  75. package/dist/policies/engines/v1/gate/index.cjs.map +1 -1
  76. package/dist/policies/engines/v1/gate/index.d.cts +2 -2
  77. package/dist/policies/engines/v1/gate/index.d.ts +2 -2
  78. package/dist/policies/engines/v1/gate/index.js +2 -1
  79. package/dist/policies/engines/v1/gate/index.js.map +1 -1
  80. package/dist/policies/index.cjs +5 -5
  81. package/dist/policies/index.d.cts +3 -2
  82. package/dist/policies/index.d.ts +3 -2
  83. package/dist/policies/index.js +2 -2
  84. package/dist/policy-bridge.cjs +437 -0
  85. package/dist/policy-bridge.cjs.map +1 -0
  86. package/dist/policy-bridge.d.cts +185 -0
  87. package/dist/policy-bridge.d.ts +185 -0
  88. package/dist/policy-bridge.js +396 -0
  89. package/dist/policy-bridge.js.map +1 -0
  90. package/dist/policy-service.cjs +239 -239
  91. package/dist/policy-service.cjs.map +1 -1
  92. package/dist/policy-service.d.cts +2 -2
  93. package/dist/policy-service.d.ts +2 -2
  94. package/dist/policy-service.js +239 -239
  95. package/dist/policy-service.js.map +1 -1
  96. package/dist/rbac/index.cjs +9 -0
  97. package/dist/rbac/index.d.cts +3 -0
  98. package/dist/rbac/index.d.ts +3 -0
  99. package/dist/rbac/index.js +2 -0
  100. package/dist/requested-by.cjs +54 -0
  101. package/dist/requested-by.cjs.map +1 -0
  102. package/dist/requested-by.d.cts +25 -0
  103. package/dist/requested-by.d.ts +25 -0
  104. package/dist/requested-by.js +49 -0
  105. package/dist/requested-by.js.map +1 -0
  106. package/dist/result/index.d.cts +2 -1
  107. package/dist/result/index.d.ts +2 -1
  108. package/dist/result.d.cts +84 -0
  109. package/dist/result.d.ts +84 -0
  110. package/dist/rule.cjs +327 -0
  111. package/dist/rule.cjs.map +1 -0
  112. package/dist/rule.js +298 -0
  113. package/dist/rule.js.map +1 -0
  114. package/dist/temporal-use-case.cjs +1 -53
  115. package/dist/temporal-use-case.cjs.map +1 -1
  116. package/dist/temporal-use-case.d.cts +5 -27
  117. package/dist/temporal-use-case.d.ts +5 -27
  118. package/dist/temporal-use-case.js +2 -48
  119. package/dist/temporal-use-case.js.map +1 -1
  120. package/dist/unexpected-error.cjs +3 -192
  121. package/dist/unexpected-error.cjs.map +1 -1
  122. package/dist/unexpected-error.js +2 -167
  123. package/dist/unexpected-error.js.map +1 -1
  124. package/dist/uuid-identifier.d.cts +2 -85
  125. package/dist/uuid-identifier.d.ts +2 -85
  126. package/dist/validation-error.cjs +33 -166
  127. package/dist/validation-error.cjs.map +1 -1
  128. package/dist/validation-error.d.cts +2 -98
  129. package/dist/validation-error.d.ts +2 -98
  130. package/dist/validation-error.js +7 -134
  131. package/dist/validation-error.js.map +1 -1
  132. package/dist/value-object.cjs +0 -123
  133. package/dist/value-object.cjs.map +1 -1
  134. package/dist/value-object.d.cts +89 -0
  135. package/dist/value-object.d.ts +89 -0
  136. package/dist/value-object.js +1 -112
  137. package/dist/value-object.js.map +1 -1
  138. package/meta.json +18 -4
  139. package/package.json +27 -1
  140. package/src/abac/index.ts +1 -0
  141. package/src/core/abac/abac-request.ts +29 -0
  142. package/src/core/abac/attributes.ts +39 -0
  143. package/src/core/abac/authorizer.ts +108 -0
  144. package/src/core/abac/combining.ts +63 -0
  145. package/src/core/abac/composite-authorizer.ts +45 -0
  146. package/src/core/abac/domain/policy-set.ts +52 -0
  147. package/src/core/abac/domain/rule.ts +197 -0
  148. package/src/core/abac/index.ts +21 -0
  149. package/src/core/application/ports/abac-authorizer.port.ts +20 -0
  150. package/src/core/application/ports/authorizer.port.ts +22 -0
  151. package/src/core/errors/authorization-error.ts +26 -0
  152. package/src/core/errors/error-codes.ts +1 -0
  153. package/src/core/index.ts +7 -0
  154. package/src/core/rbac/access-request.ts +32 -0
  155. package/src/core/rbac/authorizer.ts +91 -0
  156. package/src/core/rbac/domain/grant.ts +96 -0
  157. package/src/core/rbac/domain/permission-set.ts +67 -0
  158. package/src/core/rbac/domain/permission.ts +119 -0
  159. package/src/core/rbac/domain/role.ts +101 -0
  160. package/src/core/rbac/domain/scope.ts +84 -0
  161. package/src/core/rbac/index.ts +15 -0
  162. package/src/core/rbac/policy-bridge.ts +44 -0
  163. package/src/examples/application/authorize-cancel-order-abac.example.ts +107 -0
  164. package/src/examples/application/authorize-cancel-order.example.ts +101 -0
  165. package/src/rbac/index.ts +1 -0
  166. 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** — o kit mantém suporte temporal no código-base, mas a API pública principal nao publica um `Timeline<T>` nem helpers temporais dedicados no barrel raiz.
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 específicos do seu domínio, sempre com verbo mínimo.
33
- - `AuthorizerPort` / `AuthenticatorPort` quando aplicável.
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,2 @@
1
+ import { a as abacContext, i as AbacAuthorizer, n as AbacPolicySet, r as CompositeAuthorizer, t as AbacRule } from "../rule.js";
2
+ export { AbacAuthorizer, AbacPolicySet, AbacRule, CompositeAuthorizer, 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,170 @@
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
+ export { serializationErrorCode as i, assertJsonSafeMetadata as n, ErrorCodes as r, AppError as t };
169
+
170
+ //# sourceMappingURL=app-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-error.js","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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"}