@cullet/erp-core 1.0.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 +39 -0
- package/README.md +110 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/exceptions/validation-field.d.ts +2 -0
- package/dist/exceptions/validation-field.js +2 -0
- package/dist/gate-engine-registry.js +260 -0
- package/dist/gate-engine-registry.js.map +1 -0
- package/dist/gate-types.d.ts +307 -0
- package/dist/gate-v1-payload.schema.js +677 -0
- package/dist/gate-v1-payload.schema.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/outcome.js +92 -0
- package/dist/outcome.js.map +1 -0
- package/dist/parse-gate-payload.d.ts +136 -0
- package/dist/policies/engines/index.d.ts +3 -0
- package/dist/policies/engines/index.js +2 -0
- package/dist/policies/engines/v1/gate/index.d.ts +115 -0
- package/dist/policies/engines/v1/gate/index.js +84 -0
- package/dist/policies/engines/v1/gate/index.js.map +1 -0
- package/dist/policies/index.d.ts +4 -0
- package/dist/policies/index.js +5 -0
- package/dist/policy-service.d.ts +541 -0
- package/dist/policy-service.js +992 -0
- package/dist/policy-service.js.map +1 -0
- package/dist/validation-code.js +210 -0
- package/dist/validation-code.js.map +1 -0
- package/dist/validation-error.d.ts +686 -0
- package/dist/validation-error.js +757 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/validation-field.d.ts +12 -0
- package/dist/validation-field.js +23 -0
- package/dist/validation-field.js.map +1 -0
- package/meta.json +66 -0
- package/package.json +59 -0
- package/src/core/application/commands/command.ts +28 -0
- package/src/core/application/commands/index.ts +2 -0
- package/src/core/application/commands/requested-by.ts +97 -0
- package/src/core/application/index.ts +31 -0
- package/src/core/application/policy-error-mapper.ts +60 -0
- package/src/core/application/ports/index.ts +14 -0
- package/src/core/application/ports/logger.port.ts +10 -0
- package/src/core/application/ports/metrics.port.ts +9 -0
- package/src/core/application/ports/policy-port.ts +20 -0
- package/src/core/application/ports/repository.port.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +14 -0
- package/src/core/application/ports/tracer.port.ts +16 -0
- package/src/core/application/queries/index.ts +2 -0
- package/src/core/application/queries/query.ts +53 -0
- package/src/core/application/temporal/index.ts +11 -0
- package/src/core/application/temporal/temporal-context.ts +41 -0
- package/src/core/application/temporal/temporal-use-case.ts +41 -0
- package/src/core/application/use-case.ts +24 -0
- package/src/core/config/core-config.instance.ts +14 -0
- package/src/core/config/core-config.ts +87 -0
- package/src/core/config/index.ts +12 -0
- package/src/core/config/policy-reporter.ts +60 -0
- package/src/core/config/silent-policy-reporter.ts +5 -0
- package/src/core/domain/entity.ts +69 -0
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +16 -0
- package/src/core/domain/rulesets/ruleset-registry.ts +71 -0
- package/src/core/domain/rulesets/ruleset.contracts.ts +15 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +13 -0
- package/src/core/domain/temporal/index.ts +18 -0
- package/src/core/domain/temporal/temporal-range.ts +53 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +47 -0
- package/src/core/domain/temporal/transaction-time.ts +60 -0
- package/src/core/domain/temporal/valid-time.ts +57 -0
- package/src/core/domain/value-object.ts +30 -0
- package/src/core/errors/app-error.ts +85 -0
- package/src/core/errors/authentication-error.ts +171 -0
- package/src/core/errors/authorization-error.ts +204 -0
- package/src/core/errors/business-rule-violation-error.ts +28 -0
- package/src/core/errors/conflict-error.ts +246 -0
- package/src/core/errors/error-codes.ts +60 -0
- package/src/core/errors/idempotency-error.ts +372 -0
- package/src/core/errors/index.ts +99 -0
- package/src/core/errors/integration-error.ts +157 -0
- package/src/core/errors/legacy-incompatible-error.ts +26 -0
- package/src/core/errors/not-found-error.ts +27 -0
- package/src/core/errors/serialization-error.ts +233 -0
- package/src/core/errors/temporal-error.ts +145 -0
- package/src/core/errors/types.ts +74 -0
- package/src/core/errors/unexpected-error.ts +22 -0
- package/src/core/errors/utils/index.ts +4 -0
- package/src/core/errors/utils/json-safe.ts +93 -0
- package/src/core/errors/validation-error.ts +34 -0
- package/src/core/exceptions/business-rule-violation-exception.ts +12 -0
- package/src/core/exceptions/domain-exception.ts +9 -0
- package/src/core/exceptions/entity-not-found-exception.ts +12 -0
- package/src/core/exceptions/invalid-state-transition-exception.ts +15 -0
- package/src/core/exceptions/invariant-violation-exception.ts +9 -0
- package/src/core/exceptions/validation-code.ts +39 -0
- package/src/core/exceptions/validation-exception.ts +39 -0
- package/src/core/exceptions/validation-field.ts +32 -0
- package/src/core/index.ts +15 -0
- package/src/core/policies/asof/asof.ts +68 -0
- package/src/core/policies/asof/index.ts +2 -0
- package/src/core/policies/catalog/catalog-types.ts +12 -0
- package/src/core/policies/catalog/catalog.instance.ts +21 -0
- package/src/core/policies/catalog/index.ts +11 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +220 -0
- package/src/core/policies/catalog/policy-catalog.ts +124 -0
- package/src/core/policies/catalog/policy-key.ts +78 -0
- package/src/core/policies/context/context-builder.ts +391 -0
- package/src/core/policies/context/context-registry.instance.ts +26 -0
- package/src/core/policies/context/context-registry.ts +95 -0
- package/src/core/policies/context/context-resolver.ts +31 -0
- package/src/core/policies/context/context-seed.ts +32 -0
- package/src/core/policies/context/index.ts +21 -0
- package/src/core/policies/context/path.ts +152 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +73 -0
- package/src/core/policies/defs/index.ts +20 -0
- package/src/core/policies/defs/policy-definition-repository.ts +8 -0
- package/src/core/policies/defs/policy-definition.ts +192 -0
- package/src/core/policies/defs/policy-payload.contracts.ts +13 -0
- package/src/core/policies/defs/policy-scope.ts +53 -0
- package/src/core/policies/defs/policy-semver.ts +83 -0
- package/src/core/policies/engines/compute-engine-registry.ts +57 -0
- package/src/core/policies/engines/compute-evaluator-registry.ts +28 -0
- package/src/core/policies/engines/compute-payload.ts +5 -0
- package/src/core/policies/engines/compute-registry.ts +67 -0
- package/src/core/policies/engines/compute-types.ts +51 -0
- package/src/core/policies/engines/condition-evaluator-reporter.ts +34 -0
- package/src/core/policies/engines/gate-engine-registry.ts +38 -0
- package/src/core/policies/engines/gate-payload.ts +5 -0
- package/src/core/policies/engines/gate-types.ts +63 -0
- package/src/core/policies/engines/index.ts +48 -0
- package/src/core/policies/engines/parse-compute-payload.ts +71 -0
- package/src/core/policies/engines/parse-gate-payload.ts +68 -0
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +55 -0
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +75 -0
- package/src/core/policies/engines/v1/compute/index.ts +12 -0
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +54 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +592 -0
- package/src/core/policies/engines/v1/condition-schema.ts +50 -0
- package/src/core/policies/engines/v1/condition-types.ts +41 -0
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +157 -0
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +12 -0
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +46 -0
- package/src/core/policies/engines/v1/gate/index.ts +9 -0
- package/src/core/policies/engines/v1/index.ts +13 -0
- package/src/core/policies/index.ts +132 -0
- package/src/core/policies/package/policy-package.ts +17 -0
- package/src/core/policies/policy-ids.ts +60 -0
- package/src/core/policies/resolver/index.ts +1 -0
- package/src/core/policies/resolver/policy-resolver.ts +57 -0
- package/src/core/policies/service/index.ts +10 -0
- package/src/core/policies/service/policy-evaluation-error.ts +148 -0
- package/src/core/policies/service/policy-service.ts +493 -0
- package/src/core/policies/utils/date.ts +7 -0
- package/src/core/policies/utils/hash.ts +90 -0
- package/src/core/policies/utils/index.ts +3 -0
- package/src/core/policies/utils/result.ts +4 -0
- package/src/core/result/outcome.ts +158 -0
- package/src/core/result/result.ts +153 -0
- package/src/core/shared/aggregate-version.ts +11 -0
- package/src/core/shared/hashing.ts +38 -0
- package/src/core/shared/immutable.ts +46 -0
- package/src/core/shared/temporal-guards.ts +22 -0
- package/src/core/versioning/domain-event-contracts.ts +73 -0
- package/src/core/versioning/version.ts +34 -0
- package/src/errors/index.ts +1 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +31 -0
- package/src/examples/rulesets/entity/order-invariants-v1.ts +48 -0
- package/src/examples/rulesets/entity/order-invariants-v2.ts +28 -0
- package/src/examples/rulesets/entity/order.ts +162 -0
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +52 -0
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/cpf.ts +37 -0
- package/src/examples/rulesets/value-object/customer.ts +67 -0
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +37 -0
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/person-name.ts +39 -0
- package/src/exceptions/validation-field.ts +1 -0
- package/src/index.ts +11 -0
- package/src/policies/engines/index.ts +1 -0
- package/src/policies/engines/v1/gate/index.ts +1 -0
- package/src/policies/index.ts +1 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../../exceptions/invariant-violation-exception";
|
|
2
|
+
import { makeImmutable } from "../../shared/immutable";
|
|
3
|
+
import { assertValidDate } from "../../shared/temporal-guards";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents transaction time (Transaction Time).
|
|
7
|
+
* Defines when the system learned about or recorded a piece of information.
|
|
8
|
+
* Useful for auditing and reconstructing system state at a specific past moment.
|
|
9
|
+
*/
|
|
10
|
+
interface TransactionTime {
|
|
11
|
+
/** When the information was first recorded in the system. */
|
|
12
|
+
readonly recordedAt: Date;
|
|
13
|
+
/** When this information was superseded or invalidated by a newer version. */
|
|
14
|
+
readonly supersededAt?: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CreateTransactionTimeInput = TransactionTime;
|
|
18
|
+
|
|
19
|
+
function assertTransactionTime(
|
|
20
|
+
txTime: TransactionTime,
|
|
21
|
+
fieldName: string = "txTime",
|
|
22
|
+
): void {
|
|
23
|
+
assertValidDate(`${fieldName}.recordedAt`, txTime.recordedAt);
|
|
24
|
+
|
|
25
|
+
if (txTime.supersededAt === undefined) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assertValidDate(`${fieldName}.supersededAt`, txTime.supersededAt);
|
|
30
|
+
|
|
31
|
+
if (txTime.supersededAt.getTime() <= txTime.recordedAt.getTime()) {
|
|
32
|
+
throw new InvariantViolationException(
|
|
33
|
+
`${fieldName}.supersededAt must be later than ${fieldName}.recordedAt`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createTransactionTime(
|
|
39
|
+
input: CreateTransactionTimeInput,
|
|
40
|
+
): TransactionTime {
|
|
41
|
+
assertTransactionTime(input, "txTime");
|
|
42
|
+
|
|
43
|
+
if (input.supersededAt === undefined) {
|
|
44
|
+
return makeImmutable({
|
|
45
|
+
recordedAt: input.recordedAt,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return makeImmutable({
|
|
50
|
+
recordedAt: input.recordedAt,
|
|
51
|
+
supersededAt: input.supersededAt,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
assertTransactionTime,
|
|
57
|
+
createTransactionTime,
|
|
58
|
+
type CreateTransactionTimeInput,
|
|
59
|
+
type TransactionTime,
|
|
60
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { InvariantViolationException } from "../../exceptions/invariant-violation-exception";
|
|
2
|
+
import { makeImmutable } from "../../shared/immutable";
|
|
3
|
+
import { assertValidDate } from "../../shared/temporal-guards";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents business time (Valid Time).
|
|
7
|
+
* Defines when a piece of information is considered true or in effect in the real world.
|
|
8
|
+
*/
|
|
9
|
+
interface ValidTime {
|
|
10
|
+
/** Start of the validity window (inclusive). */
|
|
11
|
+
readonly from: Date;
|
|
12
|
+
/** End of the validity window (exclusive). When absent, the information is still in effect. */
|
|
13
|
+
readonly to?: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type CreateValidTimeInput = ValidTime;
|
|
17
|
+
|
|
18
|
+
function assertValidTime(
|
|
19
|
+
validTime: ValidTime,
|
|
20
|
+
fieldName: string = "validTime",
|
|
21
|
+
): void {
|
|
22
|
+
assertValidDate(`${fieldName}.from`, validTime.from);
|
|
23
|
+
|
|
24
|
+
if (validTime.to === undefined) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
assertValidDate(`${fieldName}.to`, validTime.to);
|
|
29
|
+
|
|
30
|
+
if (validTime.to.getTime() <= validTime.from.getTime()) {
|
|
31
|
+
throw new InvariantViolationException(
|
|
32
|
+
`${fieldName}.to must be later than ${fieldName}.from`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createValidTime(input: CreateValidTimeInput): ValidTime {
|
|
38
|
+
assertValidTime(input, "validTime");
|
|
39
|
+
|
|
40
|
+
if (input.to === undefined) {
|
|
41
|
+
return makeImmutable({
|
|
42
|
+
from: input.from,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return makeImmutable({
|
|
47
|
+
from: input.from,
|
|
48
|
+
to: input.to,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
assertValidTime,
|
|
54
|
+
createValidTime,
|
|
55
|
+
type CreateValidTimeInput,
|
|
56
|
+
type ValidTime,
|
|
57
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type DeepReadonly, makeImmutable } from "../shared/immutable";
|
|
2
|
+
import { type ContractVersion, version } from "../versioning/version";
|
|
3
|
+
|
|
4
|
+
@version("1.0")
|
|
5
|
+
abstract class ValueObject<T, P> {
|
|
6
|
+
public static readonly CONTRACT_VERSION: ContractVersion;
|
|
7
|
+
public readonly value: DeepReadonly<T>;
|
|
8
|
+
|
|
9
|
+
protected constructor(value: T) {
|
|
10
|
+
this.value = makeImmutable(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public get contractVersion(): ContractVersion {
|
|
14
|
+
return ValueObject.CONTRACT_VERSION;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected finalize(): void {
|
|
18
|
+
Object.freeze(this);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public toJSON(): P {
|
|
22
|
+
return this.toPrimitive();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public abstract equals(other: this): boolean;
|
|
26
|
+
|
|
27
|
+
public abstract toPrimitive(): P;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { type DeepReadonly, ValueObject };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Base application error for the domain/application layer.
|
|
2
|
+
// Encapsulates a code, optional cause, and JSON-safe metadata.
|
|
3
|
+
|
|
4
|
+
import type { AppErrorOptions, ErrorSeverity, JsonSafeRecord } from "./types";
|
|
5
|
+
import { assertJsonSafeMetadata } from "./utils";
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
// AppError (abstract base class)
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
abstract class AppError extends Error {
|
|
12
|
+
public readonly code: string;
|
|
13
|
+
public readonly cause?: unknown;
|
|
14
|
+
public readonly metadata?: JsonSafeRecord;
|
|
15
|
+
public readonly type?: string;
|
|
16
|
+
public readonly severity?: ErrorSeverity;
|
|
17
|
+
public readonly createdAtIso: string;
|
|
18
|
+
public readonly publicMessage?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Identifies a single execution "story" in the system. All operations
|
|
21
|
+
* related to the same logical flow must share the same correlationId.
|
|
22
|
+
*/
|
|
23
|
+
public readonly correlationId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Identifies a specific technical request attempt (each retry may produce
|
|
26
|
+
* a new requestId), even when the correlationId stays the same.
|
|
27
|
+
*/
|
|
28
|
+
public readonly requestId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Marks the business intent / idempotency key; stays the same across
|
|
31
|
+
* technical retries and multiple requests that represent the same intent.
|
|
32
|
+
*/
|
|
33
|
+
public readonly commandId?: string;
|
|
34
|
+
|
|
35
|
+
protected constructor(
|
|
36
|
+
message: string,
|
|
37
|
+
code: string,
|
|
38
|
+
options?: AppErrorOptions,
|
|
39
|
+
) {
|
|
40
|
+
super(message);
|
|
41
|
+
|
|
42
|
+
this.name = new.target.name;
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.cause = options?.cause;
|
|
45
|
+
this.type = options?.type;
|
|
46
|
+
this.severity = options?.severity;
|
|
47
|
+
this.correlationId = options?.correlationId;
|
|
48
|
+
this.requestId = options?.requestId;
|
|
49
|
+
this.commandId = options?.commandId;
|
|
50
|
+
this.createdAtIso = options?.createdAtIso ?? new Date().toISOString();
|
|
51
|
+
this.publicMessage = options?.publicMessage;
|
|
52
|
+
|
|
53
|
+
this.metadata = options?.metadata
|
|
54
|
+
? assertJsonSafeMetadata(options.metadata)
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns a JSON-safe representation of the error.
|
|
62
|
+
* Does NOT include `cause` by default (to avoid leaking internal details).
|
|
63
|
+
*/
|
|
64
|
+
public toJSON(): Record<string, unknown> {
|
|
65
|
+
const payload: Record<string, unknown> = {
|
|
66
|
+
name: this.name,
|
|
67
|
+
code: this.code,
|
|
68
|
+
type: this.type,
|
|
69
|
+
severity: this.severity,
|
|
70
|
+
message: this.message,
|
|
71
|
+
publicMessage: this.publicMessage,
|
|
72
|
+
createdAtIso: this.createdAtIso,
|
|
73
|
+
metadata: this.metadata,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (this.correlationId !== undefined)
|
|
77
|
+
payload.correlationId = this.correlationId;
|
|
78
|
+
if (this.requestId !== undefined) payload.requestId = this.requestId;
|
|
79
|
+
if (this.commandId !== undefined) payload.commandId = this.commandId;
|
|
80
|
+
|
|
81
|
+
return payload;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { AppError };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { AppError } from "./app-error";
|
|
2
|
+
import { ErrorCodes } from "./error-codes";
|
|
3
|
+
import type { AppErrorOptions, ErrorSeverity } from "./types";
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Types
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
type AuthenticationErrorReason =
|
|
10
|
+
| "missing_token"
|
|
11
|
+
| "malformed_token"
|
|
12
|
+
| "invalid_token"
|
|
13
|
+
| "expired_token"
|
|
14
|
+
| "revoked_token"
|
|
15
|
+
| "invalid_credentials"
|
|
16
|
+
| "session_not_found"
|
|
17
|
+
| "session_expired"
|
|
18
|
+
| "unknown";
|
|
19
|
+
|
|
20
|
+
type AuthenticationErrorMetadata = {
|
|
21
|
+
reason: AuthenticationErrorReason;
|
|
22
|
+
authScheme?: "bearer" | "cookie" | "basic" | "unknown";
|
|
23
|
+
tokenLocation?: "header" | "cookie" | "query" | "unknown";
|
|
24
|
+
expiresAtIso?: string;
|
|
25
|
+
correlationId?: string;
|
|
26
|
+
requestId?: string;
|
|
27
|
+
details?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type AuthenticationErrorFactoryOptions = Omit<
|
|
31
|
+
AuthenticationErrorMetadata,
|
|
32
|
+
"reason"
|
|
33
|
+
> &
|
|
34
|
+
Omit<AppErrorOptions, "metadata">;
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// AuthenticationError
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
class AuthenticationError extends AppError {
|
|
41
|
+
public readonly reason: AuthenticationErrorReason;
|
|
42
|
+
|
|
43
|
+
private constructor(params: {
|
|
44
|
+
message: string;
|
|
45
|
+
code: string;
|
|
46
|
+
reason: AuthenticationErrorReason;
|
|
47
|
+
metadata?: Omit<AuthenticationErrorMetadata, "reason">;
|
|
48
|
+
cause?: unknown;
|
|
49
|
+
type?: string;
|
|
50
|
+
severity?: ErrorSeverity;
|
|
51
|
+
correlationId?: string;
|
|
52
|
+
requestId?: string;
|
|
53
|
+
commandId?: string;
|
|
54
|
+
createdAtIso?: string;
|
|
55
|
+
publicMessage?: string;
|
|
56
|
+
}) {
|
|
57
|
+
super(params.message, params.code, {
|
|
58
|
+
cause: params.cause,
|
|
59
|
+
metadata: {
|
|
60
|
+
reason: params.reason,
|
|
61
|
+
...(params.metadata ?? {}),
|
|
62
|
+
},
|
|
63
|
+
type: params.type,
|
|
64
|
+
severity: params.severity,
|
|
65
|
+
correlationId: params.correlationId,
|
|
66
|
+
requestId: params.requestId,
|
|
67
|
+
commandId: params.commandId,
|
|
68
|
+
createdAtIso: params.createdAtIso,
|
|
69
|
+
publicMessage: params.publicMessage,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.reason = params.reason;
|
|
73
|
+
Object.freeze(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Factory methods
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
static missingToken(
|
|
81
|
+
options?: AuthenticationErrorFactoryOptions,
|
|
82
|
+
): AuthenticationError {
|
|
83
|
+
return new AuthenticationError({
|
|
84
|
+
message: "Missing credentials",
|
|
85
|
+
code: ErrorCodes.authentication.missingToken,
|
|
86
|
+
reason: "missing_token",
|
|
87
|
+
metadata: compactMetadata(options),
|
|
88
|
+
...extractAppErrorOptions(options),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static invalidToken(
|
|
93
|
+
options?: AuthenticationErrorFactoryOptions & { cause?: unknown },
|
|
94
|
+
): AuthenticationError {
|
|
95
|
+
return new AuthenticationError({
|
|
96
|
+
message: "Invalid credentials",
|
|
97
|
+
code: ErrorCodes.authentication.invalidToken,
|
|
98
|
+
reason: "invalid_token",
|
|
99
|
+
metadata: compactMetadata({
|
|
100
|
+
authScheme: options?.authScheme,
|
|
101
|
+
tokenLocation: options?.tokenLocation,
|
|
102
|
+
details: options?.details,
|
|
103
|
+
correlationId: options?.correlationId,
|
|
104
|
+
requestId: options?.requestId,
|
|
105
|
+
}),
|
|
106
|
+
...extractAppErrorOptions(options),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static expiredToken(
|
|
111
|
+
options?: AuthenticationErrorFactoryOptions & { sessionId?: string },
|
|
112
|
+
): AuthenticationError {
|
|
113
|
+
return new AuthenticationError({
|
|
114
|
+
message: "Expired credentials",
|
|
115
|
+
code: ErrorCodes.authentication.expiredToken,
|
|
116
|
+
reason: "expired_token",
|
|
117
|
+
metadata: compactMetadata(options),
|
|
118
|
+
...extractAppErrorOptions(options),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static invalidCredentials(
|
|
123
|
+
options?: Pick<
|
|
124
|
+
AuthenticationErrorFactoryOptions,
|
|
125
|
+
| "correlationId"
|
|
126
|
+
| "requestId"
|
|
127
|
+
| "type"
|
|
128
|
+
| "severity"
|
|
129
|
+
| "createdAtIso"
|
|
130
|
+
| "publicMessage"
|
|
131
|
+
>,
|
|
132
|
+
): AuthenticationError {
|
|
133
|
+
return new AuthenticationError({
|
|
134
|
+
message: "Invalid username or password",
|
|
135
|
+
code: ErrorCodes.authentication.invalidCredentials,
|
|
136
|
+
reason: "invalid_credentials",
|
|
137
|
+
metadata: compactMetadata(options),
|
|
138
|
+
...extractAppErrorOptions(options),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
144
|
+
// Helpers
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function extractAppErrorOptions(options?: Partial<AppErrorOptions>) {
|
|
148
|
+
return {
|
|
149
|
+
cause: options?.cause,
|
|
150
|
+
type: options?.type,
|
|
151
|
+
severity: options?.severity,
|
|
152
|
+
correlationId: options?.correlationId,
|
|
153
|
+
requestId: options?.requestId,
|
|
154
|
+
commandId: options?.commandId,
|
|
155
|
+
createdAtIso: options?.createdAtIso,
|
|
156
|
+
publicMessage: options?.publicMessage,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function compactMetadata<T extends Record<string, unknown>>(
|
|
161
|
+
input?: T,
|
|
162
|
+
): T | undefined {
|
|
163
|
+
if (!input) return undefined;
|
|
164
|
+
|
|
165
|
+
return Object.fromEntries(
|
|
166
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
167
|
+
) as T;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { AuthenticationError };
|
|
171
|
+
export type { AuthenticationErrorMetadata, AuthenticationErrorReason };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AppError } from "./app-error";
|
|
2
|
+
import { ErrorCodes } from "./error-codes";
|
|
3
|
+
import type { AppErrorOptions, ErrorSeverity } from "./types";
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Types
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
type AuthorizationErrorReason =
|
|
10
|
+
| "forbidden"
|
|
11
|
+
| "missing_role"
|
|
12
|
+
| "missing_capability"
|
|
13
|
+
| "out_of_scope"
|
|
14
|
+
| "outside_time_window"
|
|
15
|
+
| "policy_denied"
|
|
16
|
+
| "unknown";
|
|
17
|
+
|
|
18
|
+
type AuthorizationRequirement = {
|
|
19
|
+
role?: string;
|
|
20
|
+
capability?: string;
|
|
21
|
+
scope?: string; // e.g.: "school:{id}" or "class:{id}"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AuthorizationErrorMetadata = {
|
|
25
|
+
reason: AuthorizationErrorReason;
|
|
26
|
+
|
|
27
|
+
/** Stable action you evaluated (not an HTTP route — a "business action") */
|
|
28
|
+
action: string;
|
|
29
|
+
|
|
30
|
+
/** Resource identification (without leaking the full payload) */
|
|
31
|
+
resource?: { type: string; id?: string };
|
|
32
|
+
|
|
33
|
+
/** Optional: actor (do not include sensitive data) */
|
|
34
|
+
actor?: { userId: string; role?: string };
|
|
35
|
+
|
|
36
|
+
/** Optional: expected requirement */
|
|
37
|
+
required?: AuthorizationRequirement;
|
|
38
|
+
|
|
39
|
+
/** Optional: policy (when you already have PolicyEvaluation or similar) */
|
|
40
|
+
policyId?: string;
|
|
41
|
+
policyVersion?: number;
|
|
42
|
+
evaluatedAtIso?: string;
|
|
43
|
+
decision?: "allow" | "deny";
|
|
44
|
+
reasonCode?: string;
|
|
45
|
+
|
|
46
|
+
/** Optional: additional non-sensitive info */
|
|
47
|
+
details?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for AuthorizationError factory methods.
|
|
52
|
+
* Combines metadata fields (except 'reason') with common AppError options.
|
|
53
|
+
*/
|
|
54
|
+
type AuthorizationErrorFactoryOptions = Omit<
|
|
55
|
+
AuthorizationErrorMetadata,
|
|
56
|
+
"reason"
|
|
57
|
+
> &
|
|
58
|
+
Omit<AppErrorOptions, "metadata">;
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// AuthorizationError
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
class AuthorizationError extends AppError {
|
|
65
|
+
public readonly reason: AuthorizationErrorReason;
|
|
66
|
+
|
|
67
|
+
private constructor(params: {
|
|
68
|
+
message: string;
|
|
69
|
+
code: string;
|
|
70
|
+
reason: AuthorizationErrorReason;
|
|
71
|
+
metadata: AuthorizationErrorMetadata;
|
|
72
|
+
cause?: unknown;
|
|
73
|
+
type?: string;
|
|
74
|
+
severity?: ErrorSeverity;
|
|
75
|
+
correlationId?: string;
|
|
76
|
+
requestId?: string;
|
|
77
|
+
commandId?: string;
|
|
78
|
+
createdAtIso?: string;
|
|
79
|
+
publicMessage?: string;
|
|
80
|
+
}) {
|
|
81
|
+
super(params.message, params.code, {
|
|
82
|
+
cause: params.cause,
|
|
83
|
+
metadata: params.metadata,
|
|
84
|
+
type: params.type,
|
|
85
|
+
severity: params.severity,
|
|
86
|
+
correlationId: params.correlationId,
|
|
87
|
+
requestId: params.requestId,
|
|
88
|
+
commandId: params.commandId,
|
|
89
|
+
createdAtIso: params.createdAtIso,
|
|
90
|
+
publicMessage: params.publicMessage,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.reason = params.reason;
|
|
94
|
+
Object.freeze(this);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// Factories
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
static forbidden(
|
|
102
|
+
input: AuthorizationErrorFactoryOptions,
|
|
103
|
+
): AuthorizationError {
|
|
104
|
+
return new AuthorizationError({
|
|
105
|
+
message: "Action not allowed",
|
|
106
|
+
code: ErrorCodes.authorization.forbidden,
|
|
107
|
+
reason: "forbidden",
|
|
108
|
+
metadata: { reason: "forbidden", ...extractMetadataOnly(input) },
|
|
109
|
+
...extractAppErrorOptions(input),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static policyDenied(
|
|
114
|
+
input: AuthorizationErrorFactoryOptions & {
|
|
115
|
+
policyId: string;
|
|
116
|
+
policyVersion: number;
|
|
117
|
+
evaluatedAtIso: string;
|
|
118
|
+
},
|
|
119
|
+
): AuthorizationError {
|
|
120
|
+
return new AuthorizationError({
|
|
121
|
+
message: "Action denied by policy",
|
|
122
|
+
code: ErrorCodes.authorization.policyDenied,
|
|
123
|
+
reason: "policy_denied",
|
|
124
|
+
metadata: {
|
|
125
|
+
reason: "policy_denied",
|
|
126
|
+
...extractMetadataOnly(input),
|
|
127
|
+
decision: "deny",
|
|
128
|
+
},
|
|
129
|
+
...extractAppErrorOptions(input),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static missingCapability(
|
|
134
|
+
input: AuthorizationErrorFactoryOptions & {
|
|
135
|
+
required: AuthorizationRequirement;
|
|
136
|
+
},
|
|
137
|
+
): AuthorizationError {
|
|
138
|
+
return new AuthorizationError({
|
|
139
|
+
message: "Insufficient capability to perform the action",
|
|
140
|
+
code: ErrorCodes.authorization.missingCapability,
|
|
141
|
+
reason: "missing_capability",
|
|
142
|
+
metadata: {
|
|
143
|
+
reason: "missing_capability",
|
|
144
|
+
...extractMetadataOnly(input),
|
|
145
|
+
},
|
|
146
|
+
...extractAppErrorOptions(input),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static outOfScope(
|
|
151
|
+
input: AuthorizationErrorFactoryOptions & {
|
|
152
|
+
required?: AuthorizationRequirement;
|
|
153
|
+
},
|
|
154
|
+
): AuthorizationError {
|
|
155
|
+
return new AuthorizationError({
|
|
156
|
+
message: "Action outside the allowed scope",
|
|
157
|
+
code: ErrorCodes.authorization.outOfScope,
|
|
158
|
+
reason: "out_of_scope",
|
|
159
|
+
metadata: { reason: "out_of_scope", ...extractMetadataOnly(input) },
|
|
160
|
+
...extractAppErrorOptions(input),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
// Helpers
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function extractAppErrorOptions(options?: Partial<AppErrorOptions>) {
|
|
170
|
+
return {
|
|
171
|
+
cause: options?.cause,
|
|
172
|
+
type: options?.type,
|
|
173
|
+
severity: options?.severity,
|
|
174
|
+
correlationId: options?.correlationId,
|
|
175
|
+
requestId: options?.requestId,
|
|
176
|
+
commandId: options?.commandId,
|
|
177
|
+
createdAtIso: options?.createdAtIso,
|
|
178
|
+
publicMessage: options?.publicMessage,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function extractMetadataOnly(
|
|
183
|
+
input: AuthorizationErrorFactoryOptions,
|
|
184
|
+
): Omit<AuthorizationErrorMetadata, "reason"> {
|
|
185
|
+
const {
|
|
186
|
+
cause,
|
|
187
|
+
type,
|
|
188
|
+
severity,
|
|
189
|
+
correlationId,
|
|
190
|
+
requestId,
|
|
191
|
+
commandId,
|
|
192
|
+
createdAtIso,
|
|
193
|
+
publicMessage,
|
|
194
|
+
...metadata
|
|
195
|
+
} = input;
|
|
196
|
+
return metadata;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { AuthorizationError };
|
|
200
|
+
export type {
|
|
201
|
+
AuthorizationErrorMetadata,
|
|
202
|
+
AuthorizationErrorReason,
|
|
203
|
+
AuthorizationRequirement,
|
|
204
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AppError } from "./app-error";
|
|
2
|
+
import { ErrorCodes } from "./error-codes";
|
|
3
|
+
import type { AppErrorOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// BusinessRuleViolationError
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
class BusinessRuleViolationError extends AppError {
|
|
10
|
+
constructor(
|
|
11
|
+
rule: string,
|
|
12
|
+
message: string,
|
|
13
|
+
detail?: Record<string, unknown>,
|
|
14
|
+
options?: AppErrorOptions,
|
|
15
|
+
) {
|
|
16
|
+
const baseMetadata = detail ? { rule, detail } : { rule };
|
|
17
|
+
const mergedMetadata = options?.metadata
|
|
18
|
+
? { ...baseMetadata, ...options.metadata }
|
|
19
|
+
: baseMetadata;
|
|
20
|
+
|
|
21
|
+
super(message, ErrorCodes.businessRuleViolation, {
|
|
22
|
+
...options,
|
|
23
|
+
metadata: mergedMetadata,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { BusinessRuleViolationError };
|