@logbrew/sdk 0.1.36 → 0.1.38

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.
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { boundedIdentity, optionalText } = require("./issue-text.cjs");
5
+ const { optionalEntry, rejectUnknownKeys, requireObject } = require("./validation.cjs");
6
+
7
+ /**
8
+ * @import { IssueDiagnosticEvidence, IssueImpactEvidence, IssueLikelyFixArea } from "./index.cjs"
9
+ * @import { DeepReadonly } from "./validation.cjs"
10
+ * @typedef {"capturedFields" | "missingFields" | "redactedFields" | "truncatedFields"} EvidenceFieldKey
11
+ * @typedef {Pick<IssueDiagnosticEvidence, EvidenceFieldKey>} EvidenceFieldLists
12
+ */
13
+
14
+ const MAX_DIAGNOSTIC_IDENTITY_LENGTH = 256;
15
+ const MAX_DIAGNOSTIC_CAUSE_LENGTH = 1024;
16
+ const MAX_DIAGNOSTIC_OUTCOME_LENGTH = 512;
17
+ const MAX_DIAGNOSTIC_FIELDS = 32;
18
+ const MIN_DIAGNOSTIC_FIELDS = 1;
19
+ const MIN_COORDINATE = 1;
20
+ const MAX_COORDINATE = 2_147_483_647;
21
+ const NO_FIELDS = 0;
22
+ const EVIDENCE_FIELD_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u;
23
+ const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:\//u;
24
+ /** @type {readonly EvidenceFieldKey[]} */
25
+ const EVIDENCE_FIELD_KEYS = ["capturedFields", "missingFields", "redactedFields", "truncatedFields"];
26
+ const EVIDENCE_KEYS = new Set(["likelyRootCause", "likelyFixArea", "impact", ...EVIDENCE_FIELD_KEYS]);
27
+ const FIX_AREA_KEYS = new Set(["component", "module", "function", "file", "line", "column", "inApp"]);
28
+ const IMPACT_KEYS = new Set(["affectedUserSegment", "failedAction", "userVisibleOutcome"]);
29
+ /** @type {readonly ["component", "module", "function"]} */
30
+ const FIX_AREA_IDENTITY_KEYS = ["component", "module", "function"];
31
+ /** @type {readonly ["affectedUserSegment", "failedAction"]} */
32
+ const IMPACT_IDENTITY_KEYS = ["affectedUserSegment", "failedAction"];
33
+
34
+ /**
35
+ * @template {string} Field
36
+ * @param {Readonly<Record<string, unknown>>} source
37
+ * @param {readonly Field[]} fields
38
+ * @param {string} label
39
+ * @returns {{ [Key in Field]?: string }}
40
+ */
41
+ function identityFields(source, fields, label) {
42
+ /** @type {{ [Key in Field]?: string }} */
43
+ const validated = {};
44
+ for (const field of fields) {
45
+ const value = source[field];
46
+ if (value !== undefined) {
47
+ validated[field] = boundedIdentity(`${label} ${field}`, value, MAX_DIAGNOSTIC_IDENTITY_LENGTH);
48
+ }
49
+ }
50
+ return validated;
51
+ }
52
+
53
+ /**
54
+ * @param {unknown} value
55
+ * @returns {string}
56
+ */
57
+ function safeRelativeSourcePath(value) {
58
+ const path = boundedIdentity("issue evidence likelyFixArea file", value, MAX_DIAGNOSTIC_IDENTITY_LENGTH).replaceAll("\\", "/");
59
+ if (
60
+ path.startsWith("/")
61
+ || WINDOWS_DRIVE_PATTERN.test(path)
62
+ || path.includes("://")
63
+ || path.split("/").some((part) => part === "" || part === "." || part === "..")
64
+ ) {
65
+ throw new SdkError("validation_error", "issue evidence likelyFixArea file must be a safe relative path");
66
+ }
67
+ return path;
68
+ }
69
+
70
+ /**
71
+ * @param {"line" | "column"} key
72
+ * @param {unknown} value
73
+ * @returns {number | undefined}
74
+ */
75
+ function optionalCoordinate(key, value) {
76
+ if (value === undefined) {
77
+ return undefined;
78
+ }
79
+ if (typeof value !== "number" || !Number.isInteger(value) || value < MIN_COORDINATE || value > MAX_COORDINATE) {
80
+ throw new SdkError("validation_error", `issue evidence likelyFixArea ${key} must be a positive integer`);
81
+ }
82
+ return value;
83
+ }
84
+
85
+ /**
86
+ * @param {unknown} value
87
+ * @returns {boolean | undefined}
88
+ */
89
+ function optionalInApp(value) {
90
+ if (value !== undefined && typeof value !== "boolean") {
91
+ throw new SdkError("validation_error", "issue evidence likelyFixArea inApp must be a boolean");
92
+ }
93
+ return value;
94
+ }
95
+
96
+ /**
97
+ * @param {Readonly<Record<string, unknown>>} source
98
+ * @returns {Omit<IssueLikelyFixArea, "inApp">}
99
+ */
100
+ function fixAreaLocation(source) {
101
+ const identities = identityFields(source, FIX_AREA_IDENTITY_KEYS, "issue evidence likelyFixArea");
102
+ const file = source["file"] === undefined ? undefined : safeRelativeSourcePath(source["file"]);
103
+ const line = optionalCoordinate("line", source["line"]);
104
+ const column = optionalCoordinate("column", source["column"]);
105
+ return {
106
+ ...identities,
107
+ ...optionalEntry("file", file),
108
+ ...optionalEntry("line", line),
109
+ ...optionalEntry("column", column)
110
+ };
111
+ }
112
+
113
+ /**
114
+ * @param {unknown} area
115
+ * @returns {IssueLikelyFixArea | undefined}
116
+ */
117
+ function validateLikelyFixArea(area) {
118
+ if (area === undefined) {
119
+ return undefined;
120
+ }
121
+ const source = requireObject("issue evidence likelyFixArea", area);
122
+ rejectUnknownKeys("issue evidence likelyFixArea", source, FIX_AREA_KEYS);
123
+ const location = fixAreaLocation(source);
124
+ const inApp = optionalInApp(source["inApp"]);
125
+ if (Object.keys(location).length === NO_FIELDS) {
126
+ throw new SdkError("validation_error", "issue evidence likelyFixArea must identify a code location");
127
+ }
128
+ return { ...location, ...optionalEntry("inApp", inApp) };
129
+ }
130
+
131
+ /**
132
+ * @param {unknown} impact
133
+ * @returns {IssueImpactEvidence | undefined}
134
+ */
135
+ function validateImpactEvidence(impact) {
136
+ if (impact === undefined) {
137
+ return undefined;
138
+ }
139
+ const source = requireObject("issue evidence impact", impact);
140
+ rejectUnknownKeys("issue evidence impact", source, IMPACT_KEYS);
141
+ const validated = {
142
+ ...identityFields(source, IMPACT_IDENTITY_KEYS, "issue evidence impact"),
143
+ ...optionalEntry(
144
+ "userVisibleOutcome",
145
+ optionalText("issue evidence impact userVisibleOutcome", source["userVisibleOutcome"], MAX_DIAGNOSTIC_OUTCOME_LENGTH)
146
+ )
147
+ };
148
+ if (Object.keys(validated).length === NO_FIELDS) {
149
+ throw new SdkError("validation_error", "issue evidence impact must contain at least one field");
150
+ }
151
+ return validated;
152
+ }
153
+
154
+ /**
155
+ * @param {unknown} field
156
+ * @returns {field is string}
157
+ */
158
+ function isEvidenceField(field) {
159
+ return typeof field === "string" && EVIDENCE_FIELD_PATTERN.test(field);
160
+ }
161
+
162
+ /**
163
+ * @param {EvidenceFieldKey} key
164
+ * @param {unknown} fields
165
+ * @returns {string[] | undefined}
166
+ */
167
+ function validateEvidenceFields(key, fields) {
168
+ if (fields === undefined) {
169
+ return undefined;
170
+ }
171
+ if (!Array.isArray(fields) || fields.length < MIN_DIAGNOSTIC_FIELDS || fields.length > MAX_DIAGNOSTIC_FIELDS) {
172
+ throw new SdkError("validation_error", `issue evidence ${key} must contain 1-${MAX_DIAGNOSTIC_FIELDS} fields`);
173
+ }
174
+ /** @type {readonly unknown[]} */
175
+ const list = fields;
176
+ const identifiers = list.filter((field) => isEvidenceField(field));
177
+ if (new Set(list).size !== list.length || identifiers.length !== list.length) {
178
+ throw new SdkError("validation_error", `issue evidence ${key} fields must be unique bounded identifiers`);
179
+ }
180
+ return identifiers;
181
+ }
182
+
183
+ /**
184
+ * @param {DeepReadonly<EvidenceFieldLists>} lists
185
+ * @returns {void}
186
+ */
187
+ function rejectConflictingFields(lists) {
188
+ const present = new Set();
189
+ for (const field of Object.values(lists).flat()) {
190
+ if (present.has(field)) {
191
+ throw new SdkError("validation_error", `issue evidence field ${field} has conflicting states`);
192
+ }
193
+ present.add(field);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * @param {Readonly<Record<string, unknown>>} source
199
+ * @returns {EvidenceFieldLists}
200
+ */
201
+ function evidenceFieldLists(source) {
202
+ /** @type {EvidenceFieldLists} */
203
+ const lists = {};
204
+ for (const key of EVIDENCE_FIELD_KEYS) {
205
+ const fields = validateEvidenceFields(key, source[key]);
206
+ if (fields !== undefined) {
207
+ lists[key] = fields;
208
+ }
209
+ }
210
+ rejectConflictingFields(lists);
211
+ return lists;
212
+ }
213
+
214
+ /**
215
+ * @param {unknown} evidence
216
+ * @returns {IssueDiagnosticEvidence | undefined}
217
+ */
218
+ function validateIssueEvidence(evidence) {
219
+ if (evidence === undefined) {
220
+ return undefined;
221
+ }
222
+ const source = requireObject("issue evidence", evidence);
223
+ rejectUnknownKeys("issue evidence", source, EVIDENCE_KEYS);
224
+ const validated = {
225
+ ...optionalEntry(
226
+ "likelyRootCause",
227
+ optionalText("issue evidence likelyRootCause", source["likelyRootCause"], MAX_DIAGNOSTIC_CAUSE_LENGTH)
228
+ ),
229
+ ...optionalEntry("likelyFixArea", validateLikelyFixArea(source["likelyFixArea"])),
230
+ ...optionalEntry("impact", validateImpactEvidence(source["impact"])),
231
+ ...evidenceFieldLists(source)
232
+ };
233
+ if (Object.keys(validated).length === NO_FIELDS) {
234
+ throw new SdkError("validation_error", "issue evidence must contain at least one field");
235
+ }
236
+ return validated;
237
+ }
238
+
239
+ /**
240
+ * @param {DeepReadonly<IssueDiagnosticEvidence>} evidence
241
+ * @returns {DeepReadonly<IssueDiagnosticEvidence>}
242
+ */
243
+ function cloneIssueEvidence(evidence) {
244
+ const { capturedFields, impact, likelyFixArea, missingFields, redactedFields, truncatedFields } = evidence;
245
+ return {
246
+ ...evidence,
247
+ ...optionalEntry("likelyFixArea", likelyFixArea === undefined ? undefined : { ...likelyFixArea }),
248
+ ...optionalEntry("impact", impact === undefined ? undefined : { ...impact }),
249
+ ...optionalEntry("capturedFields", capturedFields === undefined ? undefined : [...capturedFields]),
250
+ ...optionalEntry("missingFields", missingFields === undefined ? undefined : [...missingFields]),
251
+ ...optionalEntry("redactedFields", redactedFields === undefined ? undefined : [...redactedFields]),
252
+ ...optionalEntry("truncatedFields", truncatedFields === undefined ? undefined : [...truncatedFields])
253
+ };
254
+ }
255
+
256
+ module.exports = { cloneIssueEvidence, validateIssueEvidence };
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { validateIssueStackFrames } = require("./issue-stack-frames.cjs");
5
+ const { boundedIdentity, optionalIdentity, optionalText, requireMachineName } = require("./issue-text.cjs");
6
+ const { isAllowedValue, optionalEntry, rejectUnknownKeys, requireObject } = require("./validation.cjs");
7
+
8
+ /**
9
+ * @import {
10
+ * IssueException,
11
+ * IssueExceptionChain,
12
+ * IssueExceptionChainEntry,
13
+ * IssueExceptionMechanism,
14
+ * IssueExceptionMessageState,
15
+ * IssueExceptionRelationship,
16
+ * IssueExceptionStackFramesState
17
+ * } from "./index.cjs"
18
+ * @import { DeepReadonly } from "./validation.cjs"
19
+ * @typedef {Pick<IssueExceptionChainEntry, "id" | "parentId" | "relationship">} EntryLineage
20
+ * @typedef {Pick<IssueExceptionChainEntry, "message" | "messageState">} EntryMessage
21
+ * @typedef {Pick<IssueExceptionChainEntry, "stackFrames" | "stackFramesState">} EntryStack
22
+ */
23
+
24
+ const MAX_ISSUE_EXCEPTIONS = 8;
25
+ const MAX_EXCEPTION_TYPE_LENGTH = 256;
26
+ const MAX_EXCEPTION_MESSAGE_LENGTH = 1024;
27
+ const MAX_EXCEPTION_MODULE_LENGTH = 512;
28
+ const ROOT_INDEX = 0;
29
+ const NEXT_INDEX = 1;
30
+ const EXCEPTION_KEYS = new Set(["type", "mechanism"]);
31
+ const MECHANISM_KEYS = new Set(["type", "handled"]);
32
+ const CHAIN_KEYS = new Set(["entries", "truncated"]);
33
+ const ENTRY_KEYS = new Set([
34
+ "id",
35
+ "parentId",
36
+ "relationship",
37
+ "type",
38
+ "message",
39
+ "messageState",
40
+ "module",
41
+ "mechanism",
42
+ "stackFrames",
43
+ "stackFramesState"
44
+ ]);
45
+ /** @type {ReadonlySet<IssueExceptionRelationship>} */
46
+ const CHILD_RELATIONSHIPS = new Set(["cause", "context", "aggregate_member", "suppressed"]);
47
+ /** @type {ReadonlySet<IssueExceptionMessageState>} */
48
+ const MESSAGE_STATES = new Set(["captured", "truncated", "redacted", "not_captured"]);
49
+ /** @type {ReadonlySet<IssueExceptionStackFramesState>} */
50
+ const STACK_FRAMES_STATES = new Set(["captured", "truncated", "not_captured"]);
51
+
52
+ /**
53
+ * @param {unknown} mechanism
54
+ * @returns {IssueExceptionMechanism | undefined}
55
+ */
56
+ function validateIssueExceptionMechanism(mechanism) {
57
+ if (mechanism === undefined) {
58
+ return undefined;
59
+ }
60
+ const source = requireObject("issue exception mechanism", mechanism);
61
+ rejectUnknownKeys("issue exception mechanism", source, MECHANISM_KEYS);
62
+ const type = requireMachineName("issue exception mechanism type", source["type"]);
63
+ const { handled } = source;
64
+ if (typeof handled !== "boolean") {
65
+ throw new SdkError("validation_error", "issue exception mechanism handled must be a boolean");
66
+ }
67
+ return {
68
+ type,
69
+
70
+ handled
71
+ };
72
+ }
73
+
74
+ /**
75
+ * @param {unknown} exception
76
+ * @returns {IssueException | undefined}
77
+ */
78
+ function validateIssueException(exception) {
79
+ if (exception === undefined) {
80
+ return undefined;
81
+ }
82
+ const source = requireObject("issue exception", exception);
83
+ rejectUnknownKeys("issue exception", source, EXCEPTION_KEYS);
84
+ const type = boundedIdentity("issue exception type", source["type"], MAX_EXCEPTION_TYPE_LENGTH);
85
+ const mechanism = validateIssueExceptionMechanism(source["mechanism"]);
86
+ return { type, ...optionalEntry("mechanism", mechanism) };
87
+ }
88
+
89
+ /**
90
+ * @param {string} type
91
+ * @param {string} mechanism
92
+ * @param {boolean} handled
93
+ * @returns {IssueException | undefined}
94
+ */
95
+ function createIssueException(type, mechanism, handled) {
96
+ return validateIssueException({ mechanism: { handled, type: mechanism }, type });
97
+ }
98
+
99
+ /**
100
+ * @param {unknown} parentId
101
+ * @param {number} index
102
+ * @returns {parentId is number}
103
+ */
104
+ function isParentIndex(parentId, index) {
105
+ return typeof parentId === "number" && Number.isInteger(parentId) && parentId >= ROOT_INDEX && parentId < index;
106
+ }
107
+
108
+ /**
109
+ * @param {Readonly<Record<string, unknown>>} source
110
+ * @param {number} index
111
+ * @returns {EntryLineage}
112
+ */
113
+ function entryLineage(source, index) {
114
+ const { id, parentId, relationship } = source;
115
+ if (typeof id !== "number" || !Number.isInteger(id) || id !== index) {
116
+ throw new SdkError("validation_error", `issue exceptionChain entry ${index} id must equal its array index`);
117
+ }
118
+ if (index === ROOT_INDEX) {
119
+ if (relationship !== "reported" || parentId !== undefined) {
120
+ throw new SdkError("validation_error", "issue exceptionChain entry 0 must be the parentless reported exception");
121
+ }
122
+ return { id, relationship };
123
+ }
124
+ if (typeof relationship !== "string" || !isAllowedValue(relationship, CHILD_RELATIONSHIPS) || !isParentIndex(parentId, index)) {
125
+ throw new SdkError("validation_error", `issue exceptionChain entry ${index} parent relationship is invalid`);
126
+ }
127
+ return { id, parentId, relationship };
128
+ }
129
+
130
+ /**
131
+ * @param {Readonly<Record<string, unknown>>} source
132
+ * @param {number} index
133
+ * @returns {EntryMessage}
134
+ */
135
+ function entryMessage(source, index) {
136
+ const { messageState } = source;
137
+ if (typeof messageState !== "string" || !isAllowedValue(messageState, MESSAGE_STATES)) {
138
+ throw new SdkError("validation_error", `issue exceptionChain entry ${index} messageState is invalid`);
139
+ }
140
+ const message = optionalText(`issue exceptionChain entry ${index} message`, source["message"], MAX_EXCEPTION_MESSAGE_LENGTH);
141
+ if ((messageState === "captured" || messageState === "truncated") !== (message !== undefined)) {
142
+ throw new SdkError("validation_error", `issue exceptionChain entry ${index} message does not match messageState`);
143
+ }
144
+ return { ...optionalEntry("message", message), messageState };
145
+ }
146
+
147
+ /**
148
+ * @param {Readonly<Record<string, unknown>>} source
149
+ * @param {number} index
150
+ * @returns {EntryStack}
151
+ */
152
+ function entryStack(source, index) {
153
+ const { stackFramesState } = source;
154
+ if (typeof stackFramesState !== "string" || !isAllowedValue(stackFramesState, STACK_FRAMES_STATES)) {
155
+ throw new SdkError("validation_error", `issue exceptionChain entry ${index} stackFramesState is invalid`);
156
+ }
157
+ const stackFrames = validateIssueStackFrames(source["stackFrames"]);
158
+ if ((stackFramesState === "captured" || stackFramesState === "truncated") !== (stackFrames !== undefined)) {
159
+ throw new SdkError("validation_error", `issue exceptionChain entry ${index} stackFrames do not match stackFramesState`);
160
+ }
161
+ return { ...optionalEntry("stackFrames", stackFrames), stackFramesState };
162
+ }
163
+
164
+ /**
165
+ * @param {unknown} entry
166
+ * @param {number} index
167
+ * @returns {IssueExceptionChainEntry}
168
+ */
169
+ function validateIssueExceptionChainEntry(entry, index) {
170
+ const label = `issue exceptionChain entry ${index}`;
171
+ const source = requireObject(label, entry);
172
+ rejectUnknownKeys(label, source, ENTRY_KEYS);
173
+ const lineage = entryLineage(source, index);
174
+ const type = boundedIdentity(`${label} type`, source["type"], MAX_EXCEPTION_TYPE_LENGTH);
175
+ const message = entryMessage(source, index);
176
+ const moduleName = optionalIdentity(`${label} module`, source["module"], MAX_EXCEPTION_MODULE_LENGTH);
177
+ const mechanism = validateIssueExceptionMechanism(source["mechanism"]);
178
+ const stack = entryStack(source, index);
179
+ return {
180
+ ...lineage,
181
+ type,
182
+ ...message,
183
+ ...optionalEntry("module", moduleName),
184
+ ...optionalEntry("mechanism", mechanism),
185
+ ...stack
186
+ };
187
+ }
188
+
189
+ /**
190
+ * @param {readonly unknown[]} entries
191
+ * @returns {entries is readonly [unknown, ...unknown[]]}
192
+ */
193
+ function hasChainEntries(entries) {
194
+ return entries.length >= NEXT_INDEX && entries.length <= MAX_ISSUE_EXCEPTIONS;
195
+ }
196
+
197
+ /**
198
+ * @param {unknown} entries
199
+ * @returns {readonly [unknown, ...unknown[]]}
200
+ */
201
+ function requireChainEntries(entries) {
202
+ if (Array.isArray(entries)) {
203
+ /** @type {readonly unknown[]} */
204
+ const list = entries;
205
+ if (hasChainEntries(list)) {
206
+ return list;
207
+ }
208
+ }
209
+ throw new SdkError("validation_error", `issue exceptionChain entries must contain 1-${MAX_ISSUE_EXCEPTIONS} exceptions`);
210
+ }
211
+
212
+ /**
213
+ * @param {unknown} truncated
214
+ * @returns {boolean}
215
+ */
216
+ function requireChainTruncated(truncated) {
217
+ if (typeof truncated !== "boolean") {
218
+ throw new SdkError("validation_error", "issue exceptionChain truncated must be a boolean");
219
+ }
220
+ return truncated;
221
+ }
222
+
223
+ /**
224
+ * @param {DeepReadonly<IssueExceptionChainEntry>} reported
225
+ * @param {DeepReadonly<IssueException> | undefined} legacyException
226
+ * @param {unknown} legacyStackFrames
227
+ * @returns {void}
228
+ */
229
+ function requireReportedMatch(reported, legacyException, legacyStackFrames) {
230
+ const reportedException = { type: reported.type, ...optionalEntry("mechanism", reported.mechanism) };
231
+ if (legacyException === undefined || JSON.stringify(reportedException) !== JSON.stringify(legacyException)) {
232
+ throw new SdkError("validation_error", "issue exceptionChain reported exception must match exception");
233
+ }
234
+ const canonicalLegacyFrames = validateIssueStackFrames(legacyStackFrames);
235
+ const framesMatch = reported.stackFramesState === "not_captured"
236
+ ? canonicalLegacyFrames === undefined
237
+ : JSON.stringify(reported.stackFrames) === JSON.stringify(canonicalLegacyFrames);
238
+ if (!framesMatch) {
239
+ throw new SdkError("validation_error", "issue exceptionChain reported stack must match stackFrames");
240
+ }
241
+ }
242
+
243
+ /**
244
+ * @param {unknown} chain
245
+ * @param {DeepReadonly<IssueException> | undefined} legacyException
246
+ * @param {unknown} legacyStackFrames
247
+ * @returns {IssueExceptionChain | undefined}
248
+ */
249
+ function validateIssueExceptionChain(chain, legacyException, legacyStackFrames) {
250
+ if (chain === undefined) {
251
+ return undefined;
252
+ }
253
+ const source = requireObject("issue exceptionChain", chain);
254
+ rejectUnknownKeys("issue exceptionChain", source, CHAIN_KEYS);
255
+ const [first, ...rest] = requireChainEntries(source["entries"]);
256
+ const truncated = requireChainTruncated(source["truncated"]);
257
+ const reported = validateIssueExceptionChainEntry(first, ROOT_INDEX);
258
+ const entries = [reported, ...rest.map((entry, offset) => validateIssueExceptionChainEntry(entry, offset + NEXT_INDEX))];
259
+ requireReportedMatch(reported, legacyException, legacyStackFrames);
260
+ return { entries, truncated };
261
+ }
262
+
263
+ module.exports = { createIssueException, validateIssueException, validateIssueExceptionChain };