@cullet/erp-core 1.0.4 → 1.0.6
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/dist/gate-types.d.ts +1 -1
- package/dist/gate-v1-payload.schema.js +43 -0
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/policies/engines/v1/gate/index.d.ts +4 -0
- package/dist/policy-service.js +14 -11
- package/dist/policy-service.js.map +1 -1
- package/dist/validation-code.js +43 -9
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.d.ts +15 -1
- package/meta.json +1 -1
- package/package.json +1 -1
- package/src/core/domain/entity.ts +6 -0
- package/src/core/errors/utils/index.ts +1 -0
- package/src/core/errors/utils/json-safe.ts +24 -6
- package/src/core/policies/defs/policy-semver.ts +12 -5
- package/src/core/policies/engines/condition-evaluator-reporter.ts +2 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +84 -0
- package/src/core/policies/utils/hash.ts +21 -9
- package/src/core/shared/hashing.ts +33 -4
- package/src/core/shared/immutable.ts +13 -2
- package/src/version.ts +1 -1
package/dist/validation-code.js
CHANGED
|
@@ -49,21 +49,31 @@ function serializationErrorCode(direction, category) {
|
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/core/errors/utils/json-safe.ts
|
|
51
51
|
const NON_SERIALIZABLE_PLACEHOLDER = "[NonSerializable]";
|
|
52
|
+
const CIRCULAR_REFERENCE_PLACEHOLDER = "[Circular]";
|
|
52
53
|
function isPlainObject(value) {
|
|
53
54
|
if (value === null || typeof value !== "object") return false;
|
|
54
55
|
const proto = Object.getPrototypeOf(value);
|
|
55
56
|
return proto === Object.prototype || proto === null;
|
|
56
57
|
}
|
|
57
|
-
function sanitizeValue(value) {
|
|
58
|
+
function sanitizeValue(value, seen) {
|
|
58
59
|
if (value === null) return null;
|
|
59
60
|
const type = typeof value;
|
|
60
61
|
if (type === "string" || type === "number" || type === "boolean") return value;
|
|
61
62
|
if (type === "bigint" || type === "function" || type === "symbol" || value === void 0) return NON_SERIALIZABLE_PLACEHOLDER;
|
|
62
|
-
if (Array.isArray(value))
|
|
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
|
+
}
|
|
63
70
|
if (value instanceof Date) return NON_SERIALIZABLE_PLACEHOLDER;
|
|
64
71
|
if (!isPlainObject(value)) return NON_SERIALIZABLE_PLACEHOLDER;
|
|
72
|
+
if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;
|
|
73
|
+
seen.add(value);
|
|
65
74
|
const result = {};
|
|
66
|
-
for (const [key, val] of Object.entries(value)) result[key] = sanitizeValue(val);
|
|
75
|
+
for (const [key, val] of Object.entries(value)) result[key] = sanitizeValue(val, seen);
|
|
76
|
+
seen.delete(value);
|
|
67
77
|
return result;
|
|
68
78
|
}
|
|
69
79
|
/**
|
|
@@ -72,14 +82,15 @@ function sanitizeValue(value) {
|
|
|
72
82
|
* **Allowed:** string, number, boolean, null, arrays, and plain objects.
|
|
73
83
|
*
|
|
74
84
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
75
|
-
* symbols, undefined
|
|
85
|
+
* symbols, undefined, and circular references (which would otherwise make
|
|
86
|
+
* `JSON.stringify` throw).
|
|
76
87
|
*
|
|
77
88
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
78
89
|
*/
|
|
79
90
|
function assertJsonSafeMetadata(input) {
|
|
80
91
|
if (input === void 0) return {};
|
|
81
92
|
if (!isPlainObject(input)) throw new TypeError("metadata must be a plain object (Record<string, unknown>).");
|
|
82
|
-
return sanitizeValue(input);
|
|
93
|
+
return sanitizeValue(input, /* @__PURE__ */ new WeakSet());
|
|
83
94
|
}
|
|
84
95
|
//#endregion
|
|
85
96
|
//#region src/core/errors/app-error.ts
|
|
@@ -122,22 +133,45 @@ var AppError = class extends Error {
|
|
|
122
133
|
};
|
|
123
134
|
//#endregion
|
|
124
135
|
//#region src/core/shared/hashing.ts
|
|
136
|
+
const CIRCULAR_STRUCTURE_MESSAGE = "Cannot stably stringify a circular structure";
|
|
125
137
|
function sha256Hex(value) {
|
|
126
138
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
127
139
|
}
|
|
128
|
-
function sortKeysDeep(value) {
|
|
129
|
-
if (Array.isArray(value))
|
|
140
|
+
function sortKeysDeep(value, seen) {
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
if (seen.has(value)) throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
143
|
+
seen.add(value);
|
|
144
|
+
const mapped = value.map((item) => sortKeysDeep(item, seen));
|
|
145
|
+
seen.delete(value);
|
|
146
|
+
return mapped;
|
|
147
|
+
}
|
|
130
148
|
if (value instanceof Date) return value;
|
|
131
149
|
if (value && typeof value === "object") {
|
|
150
|
+
if (seen.has(value)) throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
151
|
+
seen.add(value);
|
|
132
152
|
const record = value;
|
|
133
153
|
const ordered = Object.create(null);
|
|
134
|
-
for (const key of Object.keys(record).sort()) ordered[key] = sortKeysDeep(record[key]);
|
|
154
|
+
for (const key of Object.keys(record).sort()) ordered[key] = sortKeysDeep(record[key], seen);
|
|
155
|
+
seen.delete(value);
|
|
135
156
|
return ordered;
|
|
136
157
|
}
|
|
137
158
|
return value;
|
|
138
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
162
|
+
* does not affect the output.
|
|
163
|
+
*
|
|
164
|
+
* Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
|
|
165
|
+
* functions and symbols are dropped, non-finite numbers become `null`, `bigint`
|
|
166
|
+
* throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
|
|
167
|
+
* collapse to the same string — for collision-resistant hashing use
|
|
168
|
+
* `PolicyHashing.canonicalJson` (policies/utils), which rejects those values
|
|
169
|
+
* up front.
|
|
170
|
+
*
|
|
171
|
+
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
172
|
+
*/
|
|
139
173
|
function stableStringify(value) {
|
|
140
|
-
return JSON.stringify(sortKeysDeep(value));
|
|
174
|
+
return JSON.stringify(sortKeysDeep(value, /* @__PURE__ */ new WeakSet()));
|
|
141
175
|
}
|
|
142
176
|
function payloadHash(value) {
|
|
143
177
|
return sha256Hex(stableStringify(value));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation-code.js","names":[],"sources":["../src/core/errors/error-codes.ts","../src/core/errors/utils/json-safe.ts","../src/core/errors/app-error.ts","../src/core/shared/hashing.ts","../src/core/errors/unexpected-error.ts","../src/core/exceptions/validation-code.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 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\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst NON_SERIALIZABLE_PLACEHOLDER = \"[NonSerializable]\";\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\nfunction sanitizeValue(value: unknown): 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 return value.map(sanitizeValue);\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 const result: Record<string, JsonSafeValue> = {};\n for (const [key, val] of Object.entries(value)) {\n result[key] = sanitizeValue(val);\n }\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.\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) as JsonSafeRecord;\n}\n\nexport { assertJsonSafeMetadata, NON_SERIALIZABLE_PLACEHOLDER };\n","// Base application error for the domain/application layer.\n// Encapsulates a code, optional cause, and JSON-safe metadata.\n\nimport type { AppErrorOptions, ErrorSeverity, JsonSafeRecord } from \"./types\";\nimport { assertJsonSafeMetadata } from \"./utils\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AppError (abstract base class)\n// ─────────────────────────────────────────────────────────────────────────────\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 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 type: this.type,\n severity: this.severity,\n message: this.message,\n publicMessage: this.publicMessage,\n createdAtIso: this.createdAtIso,\n metadata: this.metadata,\n };\n\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","import { createHash } from \"node:crypto\";\n\nfunction sha256Hex(value: string): string {\n return createHash(\"sha256\").update(value, \"utf8\").digest(\"hex\");\n}\n\nfunction sortKeysDeep(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((item) => sortKeysDeep(item));\n }\n\n if (value instanceof Date) {\n return value;\n }\n\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>;\n const ordered = Object.create(null) as Record<string, unknown>;\n\n for (const key of Object.keys(record).sort()) {\n ordered[key] = sortKeysDeep(record[key]);\n }\n\n return ordered;\n }\n\n return value;\n}\n\nfunction stableStringify(value: unknown): string {\n return JSON.stringify(sortKeysDeep(value));\n}\n\nfunction payloadHash(value: unknown): string {\n return sha256Hex(stableStringify(value));\n}\n\nexport { payloadHash, sha256Hex, stableStringify };\n","import { AppError } from \"./app-error\";\nimport { ErrorCodes } from \"./error-codes\";\nimport type { AppErrorOptions } from \"./types\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// UnexpectedError\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass UnexpectedError extends AppError {\n constructor(\n message = \"Unexpected error\",\n cause?: unknown,\n options?: Omit<AppErrorOptions, \"cause\">,\n ) {\n super(message, ErrorCodes.unexpected, {\n cause,\n ...options,\n });\n }\n}\n\nexport { UnexpectedError };\n","// Object-oriented representation of validation codes (similar to Kotlin inline class).\n// Provides common reusable codes while allowing custom codes via `of`.\n\nclass ValidationCode {\n public readonly value: string;\n\n private constructor(value: string) {\n this.value = value;\n }\n\n // Factory for custom codes\n public static of(value: string): ValidationCode {\n return new ValidationCode(value);\n }\n\n // Common, domain-friendly codes\n public static readonly BLANK = new ValidationCode(\"blank\");\n public static readonly REQUIRED = new ValidationCode(\"required\");\n public static readonly INVALID_FORMAT = new ValidationCode(\n \"invalid_format\",\n );\n public static readonly INVALID_CHARACTERS = new ValidationCode(\n \"invalid_characters\",\n );\n public static readonly INVALID_LENGTH = new ValidationCode(\n \"invalid_length\",\n );\n public static readonly TOO_SHORT = new ValidationCode(\"too_short\");\n public static readonly TOO_LONG = new ValidationCode(\"too_long\");\n public static readonly OUT_OF_RANGE = new ValidationCode(\"out_of_range\");\n public static readonly INVALID_CHECKSUM = new ValidationCode(\n \"invalid_checksum\",\n );\n public static readonly INVALID_CHECKING_DIGIT = new ValidationCode(\n \"invalid_checking_digit\",\n );\n public static readonly ALREADY_EXISTS = new ValidationCode(\n \"already_exists\",\n );\n public static readonly NOT_FOUND = new ValidationCode(\"not_found\");\n public static readonly NOT_ALLOWED = new ValidationCode(\"not_allowed\");\n public static readonly UNEXPECTED = new ValidationCode(\"unexpected\");\n}\n\nexport { ValidationCode };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation-code.js","names":[],"sources":["../src/core/errors/error-codes.ts","../src/core/errors/utils/json-safe.ts","../src/core/errors/app-error.ts","../src/core/shared/hashing.ts","../src/core/errors/unexpected-error.ts","../src/core/exceptions/validation-code.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 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\";\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 { AppErrorOptions, ErrorSeverity, JsonSafeRecord } from \"./types\";\nimport { assertJsonSafeMetadata } from \"./utils\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AppError (abstract base class)\n// ─────────────────────────────────────────────────────────────────────────────\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 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 type: this.type,\n severity: this.severity,\n message: this.message,\n publicMessage: this.publicMessage,\n createdAtIso: this.createdAtIso,\n metadata: this.metadata,\n };\n\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","import { createHash } from \"node:crypto\";\n\nconst CIRCULAR_STRUCTURE_MESSAGE =\n \"Cannot stably stringify a circular structure\";\n\nfunction sha256Hex(value: string): string {\n return createHash(\"sha256\").update(value, \"utf8\").digest(\"hex\");\n}\n\n// `seen` holds the current traversal branch so true cycles are rejected while\n// repeated (acyclic) references are still serialized — matching JSON.stringify.\nfunction sortKeysDeep(value: unknown, seen: WeakSet<object>): unknown {\n if (Array.isArray(value)) {\n if (seen.has(value)) {\n throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);\n }\n seen.add(value);\n const mapped = value.map((item) => sortKeysDeep(item, seen));\n seen.delete(value);\n return mapped;\n }\n\n if (value instanceof Date) {\n return value;\n }\n\n if (value && typeof value === \"object\") {\n if (seen.has(value)) {\n throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);\n }\n seen.add(value);\n const record = value as Record<string, unknown>;\n const ordered = Object.create(null) as Record<string, unknown>;\n\n for (const key of Object.keys(record).sort()) {\n ordered[key] = sortKeysDeep(record[key], seen);\n }\n seen.delete(value);\n\n return ordered;\n }\n\n return value;\n}\n\n/**\n * Deterministic JSON string with object keys sorted recursively, so key order\n * does not affect the output.\n *\n * Follows `JSON.stringify` semantics for the values it serializes: `undefined`,\n * functions and symbols are dropped, non-finite numbers become `null`, `bigint`\n * throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore\n * collapse to the same string — for collision-resistant hashing use\n * `PolicyHashing.canonicalJson` (policies/utils), which rejects those values\n * up front.\n *\n * @throws {TypeError} On circular references (mirroring `JSON.stringify`).\n */\nfunction stableStringify(value: unknown): string {\n return JSON.stringify(sortKeysDeep(value, new WeakSet<object>()));\n}\n\nfunction payloadHash(value: unknown): string {\n return sha256Hex(stableStringify(value));\n}\n\nexport { payloadHash, sha256Hex, stableStringify };\n","import { AppError } from \"./app-error\";\nimport { ErrorCodes } from \"./error-codes\";\nimport type { AppErrorOptions } from \"./types\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// UnexpectedError\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass UnexpectedError extends AppError {\n constructor(\n message = \"Unexpected error\",\n cause?: unknown,\n options?: Omit<AppErrorOptions, \"cause\">,\n ) {\n super(message, ErrorCodes.unexpected, {\n cause,\n ...options,\n });\n }\n}\n\nexport { UnexpectedError };\n","// Object-oriented representation of validation codes (similar to Kotlin inline class).\n// Provides common reusable codes while allowing custom codes via `of`.\n\nclass ValidationCode {\n public readonly value: string;\n\n private constructor(value: string) {\n this.value = value;\n }\n\n // Factory for custom codes\n public static of(value: string): ValidationCode {\n return new ValidationCode(value);\n }\n\n // Common, domain-friendly codes\n public static readonly BLANK = new ValidationCode(\"blank\");\n public static readonly REQUIRED = new ValidationCode(\"required\");\n public static readonly INVALID_FORMAT = new ValidationCode(\n \"invalid_format\",\n );\n public static readonly INVALID_CHARACTERS = new ValidationCode(\n \"invalid_characters\",\n );\n public static readonly INVALID_LENGTH = new ValidationCode(\n \"invalid_length\",\n );\n public static readonly TOO_SHORT = new ValidationCode(\"too_short\");\n public static readonly TOO_LONG = new ValidationCode(\"too_long\");\n public static readonly OUT_OF_RANGE = new ValidationCode(\"out_of_range\");\n public static readonly INVALID_CHECKSUM = new ValidationCode(\n \"invalid_checksum\",\n );\n public static readonly INVALID_CHECKING_DIGIT = new ValidationCode(\n \"invalid_checking_digit\",\n );\n public static readonly ALREADY_EXISTS = new ValidationCode(\n \"already_exists\",\n );\n public static readonly NOT_FOUND = new ValidationCode(\"not_found\");\n public static readonly NOT_ALLOWED = new ValidationCode(\"not_allowed\");\n public static readonly UNEXPECTED = new ValidationCode(\"unexpected\");\n}\n\nexport { ValidationCode };\n"],"mappings":"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"}
|
|
@@ -95,7 +95,8 @@ declare function serializationErrorCode(direction: SerializationCodeDirection, c
|
|
|
95
95
|
* **Allowed:** string, number, boolean, null, arrays, and plain objects.
|
|
96
96
|
*
|
|
97
97
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
98
|
-
* symbols, undefined
|
|
98
|
+
* symbols, undefined, and circular references (which would otherwise make
|
|
99
|
+
* `JSON.stringify` throw).
|
|
99
100
|
*
|
|
100
101
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
101
102
|
*/
|
|
@@ -322,6 +323,19 @@ declare function translateUniqueViolationToDuplicate(input: {
|
|
|
322
323
|
//#endregion
|
|
323
324
|
//#region src/core/shared/hashing.d.ts
|
|
324
325
|
declare function sha256Hex(value: string): string;
|
|
326
|
+
/**
|
|
327
|
+
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
328
|
+
* does not affect the output.
|
|
329
|
+
*
|
|
330
|
+
* Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
|
|
331
|
+
* functions and symbols are dropped, non-finite numbers become `null`, `bigint`
|
|
332
|
+
* throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
|
|
333
|
+
* collapse to the same string — for collision-resistant hashing use
|
|
334
|
+
* `PolicyHashing.canonicalJson` (policies/utils), which rejects those values
|
|
335
|
+
* up front.
|
|
336
|
+
*
|
|
337
|
+
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
338
|
+
*/
|
|
325
339
|
declare function stableStringify(value: unknown): string;
|
|
326
340
|
declare function payloadHash(value: unknown): string;
|
|
327
341
|
//#endregion
|
package/meta.json
CHANGED
package/package.json
CHANGED
|
@@ -59,6 +59,12 @@ abstract class Entity<TIdentifier> {
|
|
|
59
59
|
protected markAsModified(updatedAt: Date = new Date()): number {
|
|
60
60
|
assertValidDate("updatedAt", updatedAt);
|
|
61
61
|
|
|
62
|
+
if (updatedAt.getTime() < this._createdAt.getTime()) {
|
|
63
|
+
throw new InvariantViolationException(
|
|
64
|
+
"updatedAt cannot be earlier than createdAt",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
this._updatedAt = cloneDate(updatedAt);
|
|
63
69
|
this._aggregateVersion += 1;
|
|
64
70
|
|
|
@@ -7,6 +7,7 @@ import type { JsonSafeRecord, JsonSafeValue } from "../types";
|
|
|
7
7
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
const NON_SERIALIZABLE_PLACEHOLDER = "[NonSerializable]";
|
|
10
|
+
const CIRCULAR_REFERENCE_PLACEHOLDER = "[Circular]";
|
|
10
11
|
|
|
11
12
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
13
|
// Internal helpers
|
|
@@ -18,7 +19,12 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
18
19
|
return proto === Object.prototype || proto === null;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// `seen` tracks the ancestors on the current traversal branch (a DFS path), not
|
|
23
|
+
// every object visited. This collapses true circular references to a placeholder
|
|
24
|
+
// while still serializing the same object referenced more than once across
|
|
25
|
+
// sibling branches (a DAG) — matching `JSON.stringify`, which only rejects
|
|
26
|
+
// cycles, not shared references.
|
|
27
|
+
function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
|
|
22
28
|
// Null passthrough
|
|
23
29
|
if (value === null) return null;
|
|
24
30
|
|
|
@@ -41,7 +47,11 @@ function sanitizeValue(value: unknown): JsonSafeValue {
|
|
|
41
47
|
|
|
42
48
|
// Arrays (recursive)
|
|
43
49
|
if (Array.isArray(value)) {
|
|
44
|
-
|
|
50
|
+
if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;
|
|
51
|
+
seen.add(value);
|
|
52
|
+
const sanitized = value.map((item) => sanitizeValue(item, seen));
|
|
53
|
+
seen.delete(value);
|
|
54
|
+
return sanitized;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
// Date → placeholder (could also use .toISOString() if preferred)
|
|
@@ -55,10 +65,13 @@ function sanitizeValue(value: unknown): JsonSafeValue {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
// Plain object (recursive)
|
|
68
|
+
if (seen.has(value)) return CIRCULAR_REFERENCE_PLACEHOLDER;
|
|
69
|
+
seen.add(value);
|
|
58
70
|
const result: Record<string, JsonSafeValue> = {};
|
|
59
71
|
for (const [key, val] of Object.entries(value)) {
|
|
60
|
-
result[key] = sanitizeValue(val);
|
|
72
|
+
result[key] = sanitizeValue(val, seen);
|
|
61
73
|
}
|
|
74
|
+
seen.delete(value);
|
|
62
75
|
return result;
|
|
63
76
|
}
|
|
64
77
|
|
|
@@ -72,7 +85,8 @@ function sanitizeValue(value: unknown): JsonSafeValue {
|
|
|
72
85
|
* **Allowed:** string, number, boolean, null, arrays, and plain objects.
|
|
73
86
|
*
|
|
74
87
|
* **Converted to placeholder:** Date, BigInt, class instances, functions,
|
|
75
|
-
* symbols, undefined
|
|
88
|
+
* symbols, undefined, and circular references (which would otherwise make
|
|
89
|
+
* `JSON.stringify` throw).
|
|
76
90
|
*
|
|
77
91
|
* @throws {TypeError} If `input` is not a plain object at the root level.
|
|
78
92
|
*/
|
|
@@ -87,7 +101,11 @@ function assertJsonSafeMetadata(input: unknown): JsonSafeRecord {
|
|
|
87
101
|
);
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
return sanitizeValue(input) as JsonSafeRecord;
|
|
104
|
+
return sanitizeValue(input, new WeakSet<object>()) as JsonSafeRecord;
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
export {
|
|
107
|
+
export {
|
|
108
|
+
assertJsonSafeMetadata,
|
|
109
|
+
CIRCULAR_REFERENCE_PLACEHOLDER,
|
|
110
|
+
NON_SERIALIZABLE_PLACEHOLDER,
|
|
111
|
+
};
|
|
@@ -12,16 +12,23 @@ interface ParsedSemver {
|
|
|
12
12
|
|
|
13
13
|
function parseSemver(version: string): ParsedSemver {
|
|
14
14
|
const withoutBuild = version.split("+")[0];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
// Split on the first hyphen only. A pre-release identifier may itself
|
|
17
|
+
// contain hyphens (e.g. "1.0.0-x-1"); `split("-")` would drop everything
|
|
18
|
+
// after the first one and corrupt the comparison.
|
|
19
|
+
const dashIndex = withoutBuild.indexOf("-");
|
|
20
|
+
const core =
|
|
21
|
+
dashIndex === -1 ? withoutBuild : withoutBuild.slice(0, dashIndex);
|
|
22
|
+
const preRelease =
|
|
23
|
+
dashIndex === -1 ? null : withoutBuild.slice(dashIndex + 1);
|
|
24
|
+
|
|
19
25
|
const [major, minor, patch] = core.split(".").map(Number) as [
|
|
20
26
|
number,
|
|
21
27
|
number,
|
|
22
28
|
number,
|
|
23
29
|
];
|
|
24
|
-
|
|
30
|
+
|
|
31
|
+
return { major, minor, patch, preRelease };
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
function comparePreRelease(a: string, b: string): number {
|
|
@@ -10,6 +10,8 @@ export type ConditionEvaluationReportTag =
|
|
|
10
10
|
| "NULLISH_NUMERIC_OPERAND_NOT_ALLOWED"
|
|
11
11
|
| "NULLISH_DATE_OPERAND_NOT_ALLOWED"
|
|
12
12
|
| "INVALID_DATE_OPERAND"
|
|
13
|
+
| "INVALID_NUMERIC_OPERAND"
|
|
14
|
+
| "INVALID_SET_OPERAND"
|
|
13
15
|
| "CONDITION_EVAL_THREW";
|
|
14
16
|
|
|
15
17
|
export interface ConditionEvaluationReport {
|
|
@@ -21,6 +21,8 @@ const NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG =
|
|
|
21
21
|
"NULLISH_NUMERIC_OPERAND_NOT_ALLOWED";
|
|
22
22
|
const NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG = "NULLISH_DATE_OPERAND_NOT_ALLOWED";
|
|
23
23
|
const INVALID_DATE_OPERAND_TAG = "INVALID_DATE_OPERAND";
|
|
24
|
+
const INVALID_NUMERIC_OPERAND_TAG = "INVALID_NUMERIC_OPERAND";
|
|
25
|
+
const INVALID_SET_OPERAND_TAG = "INVALID_SET_OPERAND";
|
|
24
26
|
const EMPTY_OR_CONDITION_TAG = "EMPTY_OR_CONDITION";
|
|
25
27
|
const EMPTY_AND_CONDITION_TAG = "EMPTY_AND_CONDITION";
|
|
26
28
|
const ISO_8601_UTC_DATE_PATTERN =
|
|
@@ -110,6 +112,18 @@ export class ConditionEvaluatorV1 {
|
|
|
110
112
|
return `${INVALID_DATE_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires Date or ISO 8601 UTC string operands`;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
private static buildInvalidNumericOperandMessage(
|
|
116
|
+
node: ConditionLeafNode,
|
|
117
|
+
): string {
|
|
118
|
+
return `${INVALID_NUMERIC_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires numeric operands`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private static buildInvalidSetOperandMessage(
|
|
122
|
+
node: ConditionLeafNode,
|
|
123
|
+
): string {
|
|
124
|
+
return `${INVALID_SET_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires an array operand`;
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
private static buildEmptyOrConditionMessage(): string {
|
|
114
128
|
return `${EMPTY_OR_CONDITION_TAG}: OR nodes must contain at least one child condition`;
|
|
115
129
|
}
|
|
@@ -224,6 +238,56 @@ export class ConditionEvaluatorV1 {
|
|
|
224
238
|
return Result.err(message);
|
|
225
239
|
}
|
|
226
240
|
|
|
241
|
+
private reportInvalidNumericOperand(
|
|
242
|
+
node: ConditionLeafNode,
|
|
243
|
+
actual: unknown,
|
|
244
|
+
): Result<boolean, string> {
|
|
245
|
+
const message =
|
|
246
|
+
ConditionEvaluatorV1.buildInvalidNumericOperandMessage(node);
|
|
247
|
+
|
|
248
|
+
this.options.reporter.error(
|
|
249
|
+
this.buildReport({
|
|
250
|
+
level: "error",
|
|
251
|
+
tag: INVALID_NUMERIC_OPERAND_TAG,
|
|
252
|
+
message,
|
|
253
|
+
details: {
|
|
254
|
+
field: node.field,
|
|
255
|
+
op: node.op,
|
|
256
|
+
actual,
|
|
257
|
+
expected: node.value,
|
|
258
|
+
node,
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
return Result.err(message);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private reportInvalidSetOperand(
|
|
267
|
+
node: ConditionLeafNode,
|
|
268
|
+
actual: unknown,
|
|
269
|
+
): Result<boolean, string> {
|
|
270
|
+
const message =
|
|
271
|
+
ConditionEvaluatorV1.buildInvalidSetOperandMessage(node);
|
|
272
|
+
|
|
273
|
+
this.options.reporter.error(
|
|
274
|
+
this.buildReport({
|
|
275
|
+
level: "error",
|
|
276
|
+
tag: INVALID_SET_OPERAND_TAG,
|
|
277
|
+
message,
|
|
278
|
+
details: {
|
|
279
|
+
field: node.field,
|
|
280
|
+
op: node.op,
|
|
281
|
+
actual,
|
|
282
|
+
expected: node.value,
|
|
283
|
+
node,
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return Result.err(message);
|
|
289
|
+
}
|
|
290
|
+
|
|
227
291
|
private evaluateDateRelationalNode(
|
|
228
292
|
node: ConditionLeafNode,
|
|
229
293
|
actual: unknown,
|
|
@@ -406,6 +470,19 @@ export class ConditionEvaluatorV1 {
|
|
|
406
470
|
return Result.err(message);
|
|
407
471
|
}
|
|
408
472
|
|
|
473
|
+
// null + allowNull: a relational comparison against a missing value
|
|
474
|
+
// never matches.
|
|
475
|
+
if (actual === null) {
|
|
476
|
+
return Result.ok(false);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Both operands must be numbers. A present but wrong-typed operand is a
|
|
480
|
+
// context/configuration error, surfaced like the date path instead of
|
|
481
|
+
// silently collapsing to "no match".
|
|
482
|
+
if (typeof actual !== "number" || typeof node.value !== "number") {
|
|
483
|
+
return this.reportInvalidNumericOperand(node, actual);
|
|
484
|
+
}
|
|
485
|
+
|
|
409
486
|
return Result.ok(this.evaluateOperator(actual, node.op, node.value));
|
|
410
487
|
}
|
|
411
488
|
|
|
@@ -443,6 +520,13 @@ export class ConditionEvaluatorV1 {
|
|
|
443
520
|
return this.evaluateNumericRelationalNode(node, actual);
|
|
444
521
|
}
|
|
445
522
|
|
|
523
|
+
if (
|
|
524
|
+
(node.op === "in" || node.op === "notIn") &&
|
|
525
|
+
!Array.isArray(node.value)
|
|
526
|
+
) {
|
|
527
|
+
return this.reportInvalidSetOperand(node, actual);
|
|
528
|
+
}
|
|
529
|
+
|
|
446
530
|
return Result.ok(
|
|
447
531
|
this.evaluateOperator(actual, node.op, node.value),
|
|
448
532
|
);
|
|
@@ -16,7 +16,11 @@ export class PolicyHashing {
|
|
|
16
16
|
return sha256Hex(input);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
private static assertHashable(
|
|
19
|
+
private static assertHashable(
|
|
20
|
+
value: unknown,
|
|
21
|
+
path: string,
|
|
22
|
+
seen: WeakSet<object>,
|
|
23
|
+
): void {
|
|
20
24
|
if (value === null) {
|
|
21
25
|
return;
|
|
22
26
|
}
|
|
@@ -54,22 +58,30 @@ export class PolicyHashing {
|
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
if (seen.has(value as object)) {
|
|
62
|
+
throw new TypeError(
|
|
63
|
+
`canonicalJson does not accept circular references (at ${path})`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
seen.add(value as object);
|
|
67
|
+
|
|
57
68
|
if (Array.isArray(value)) {
|
|
58
69
|
value.forEach((item, index) => {
|
|
59
|
-
PolicyHashing.assertHashable(item, `${path}[${index}]
|
|
70
|
+
PolicyHashing.assertHashable(item, `${path}[${index}]`, seen);
|
|
60
71
|
});
|
|
61
|
-
|
|
72
|
+
} else {
|
|
73
|
+
for (const [key, nested] of Object.entries(
|
|
74
|
+
value as Record<string, unknown>,
|
|
75
|
+
)) {
|
|
76
|
+
PolicyHashing.assertHashable(nested, `${path}.${key}`, seen);
|
|
77
|
+
}
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
value as Record<string, unknown>,
|
|
66
|
-
)) {
|
|
67
|
-
PolicyHashing.assertHashable(nested, `${path}.${key}`);
|
|
68
|
-
}
|
|
80
|
+
seen.delete(value as object);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
static canonicalJson(value: unknown): string {
|
|
72
|
-
PolicyHashing.assertHashable(value, "$");
|
|
84
|
+
PolicyHashing.assertHashable(value, "$", new WeakSet<object>());
|
|
73
85
|
|
|
74
86
|
return stableStringify(value);
|
|
75
87
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
const CIRCULAR_STRUCTURE_MESSAGE =
|
|
4
|
+
"Cannot stably stringify a circular structure";
|
|
5
|
+
|
|
3
6
|
function sha256Hex(value: string): string {
|
|
4
7
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
// `seen` holds the current traversal branch so true cycles are rejected while
|
|
11
|
+
// repeated (acyclic) references are still serialized — matching JSON.stringify.
|
|
12
|
+
function sortKeysDeep(value: unknown, seen: WeakSet<object>): unknown {
|
|
8
13
|
if (Array.isArray(value)) {
|
|
9
|
-
|
|
14
|
+
if (seen.has(value)) {
|
|
15
|
+
throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
16
|
+
}
|
|
17
|
+
seen.add(value);
|
|
18
|
+
const mapped = value.map((item) => sortKeysDeep(item, seen));
|
|
19
|
+
seen.delete(value);
|
|
20
|
+
return mapped;
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
if (value instanceof Date) {
|
|
@@ -14,12 +25,17 @@ function sortKeysDeep(value: unknown): unknown {
|
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
if (value && typeof value === "object") {
|
|
28
|
+
if (seen.has(value)) {
|
|
29
|
+
throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
|
|
30
|
+
}
|
|
31
|
+
seen.add(value);
|
|
17
32
|
const record = value as Record<string, unknown>;
|
|
18
33
|
const ordered = Object.create(null) as Record<string, unknown>;
|
|
19
34
|
|
|
20
35
|
for (const key of Object.keys(record).sort()) {
|
|
21
|
-
ordered[key] = sortKeysDeep(record[key]);
|
|
36
|
+
ordered[key] = sortKeysDeep(record[key], seen);
|
|
22
37
|
}
|
|
38
|
+
seen.delete(value);
|
|
23
39
|
|
|
24
40
|
return ordered;
|
|
25
41
|
}
|
|
@@ -27,8 +43,21 @@ function sortKeysDeep(value: unknown): unknown {
|
|
|
27
43
|
return value;
|
|
28
44
|
}
|
|
29
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Deterministic JSON string with object keys sorted recursively, so key order
|
|
48
|
+
* does not affect the output.
|
|
49
|
+
*
|
|
50
|
+
* Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
|
|
51
|
+
* functions and symbols are dropped, non-finite numbers become `null`, `bigint`
|
|
52
|
+
* throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
|
|
53
|
+
* collapse to the same string — for collision-resistant hashing use
|
|
54
|
+
* `PolicyHashing.canonicalJson` (policies/utils), which rejects those values
|
|
55
|
+
* up front.
|
|
56
|
+
*
|
|
57
|
+
* @throws {TypeError} On circular references (mirroring `JSON.stringify`).
|
|
58
|
+
*/
|
|
30
59
|
function stableStringify(value: unknown): string {
|
|
31
|
-
return JSON.stringify(sortKeysDeep(value));
|
|
60
|
+
return JSON.stringify(sortKeysDeep(value, new WeakSet<object>()));
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
function payloadHash(value: unknown): string {
|
|
@@ -14,13 +14,24 @@ type DeepReadonly<T> = T extends PrimitiveValue | Date
|
|
|
14
14
|
: { readonly [TKey in keyof T]: DeepReadonly<T[TKey]> };
|
|
15
15
|
|
|
16
16
|
function deepFreeze<T>(value: T): T {
|
|
17
|
+
return deepFreezeInternal(value, new WeakSet<object>());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function deepFreezeInternal<T>(value: T, seen: WeakSet<object>): T {
|
|
17
21
|
if (typeof value !== "object" || value === null || value instanceof Date) {
|
|
18
22
|
return value;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
// A value already on `seen` has been visited on this pass (cyclic graph or a
|
|
26
|
+
// shared reference); revisiting it would recurse forever.
|
|
27
|
+
if (seen.has(value)) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
seen.add(value);
|
|
31
|
+
|
|
21
32
|
if (Array.isArray(value)) {
|
|
22
33
|
for (const item of value) {
|
|
23
|
-
|
|
34
|
+
deepFreezeInternal(item, seen);
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
return Object.freeze(value);
|
|
@@ -29,7 +40,7 @@ function deepFreeze<T>(value: T): T {
|
|
|
29
40
|
const objectValue = value as Record<PropertyKey, object | PrimitiveValue>;
|
|
30
41
|
|
|
31
42
|
for (const propertyKey of Reflect.ownKeys(objectValue)) {
|
|
32
|
-
|
|
43
|
+
deepFreezeInternal(objectValue[propertyKey], seen);
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
return Object.freeze(value);
|
package/src/version.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// Mantemos a versao aqui, dentro de src/, para que a copia full-control seja
|
|
5
5
|
// auto-contida: ao copiar so o conteudo de src/, o entry nao depende de um
|
|
6
6
|
// `../package.json` que deixaria de existir no projeto consumidor.
|
|
7
|
-
export const version = "1.0.
|
|
7
|
+
export const version = "1.0.6";
|