x-openapi-flow 1.4.3 → 1.5.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.
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // x-openapi-flow Stable Error & Warning Code Registry
5
+ //
6
+ // Codes are organized by category:
7
+ // XFLOW_E0xx – Schema validation errors
8
+ // XFLOW_E1xx – Graph validation errors
9
+ // XFLOW_W2xx – Quality check warnings
10
+ // XFLOW_L3xx – Lint rule violations
11
+ // XFLOW_RT4xx – Runtime guard errors
12
+ // XFLOW_CLI5xx – CLI / argument errors
13
+ //
14
+ // All codes are stable across releases. New codes are only appended.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const CODES = {
18
+ // ── Schema ──────────────────────────────────────────────────────────────
19
+ /** x-openapi-flow payload failed JSON Schema validation. */
20
+ SCHEMA_VALIDATION_FAILED: {
21
+ code: "XFLOW_E001",
22
+ category: "schema",
23
+ severity: "error",
24
+ title: "Schema validation failed",
25
+ },
26
+ /** Required field is missing from the x-openapi-flow object. */
27
+ SCHEMA_MISSING_REQUIRED: {
28
+ code: "XFLOW_E002",
29
+ category: "schema",
30
+ severity: "error",
31
+ title: "Missing required field",
32
+ },
33
+ /** Unknown property found in x-openapi-flow payload. */
34
+ SCHEMA_ADDITIONAL_PROPERTY: {
35
+ code: "XFLOW_E003",
36
+ category: "schema",
37
+ severity: "error",
38
+ title: "Additional property not allowed",
39
+ },
40
+ /** Field value is not in the allowed enum set. */
41
+ SCHEMA_INVALID_ENUM: {
42
+ code: "XFLOW_E004",
43
+ category: "schema",
44
+ severity: "error",
45
+ title: "Invalid enum value",
46
+ },
47
+
48
+ // ── Graph ────────────────────────────────────────────────────────────────
49
+ /** No initial state (indegree = 0) detected in the flow graph. */
50
+ GRAPH_NO_INITIAL_STATE: {
51
+ code: "XFLOW_E101",
52
+ category: "graph",
53
+ severity: "error",
54
+ title: "No initial state detected",
55
+ },
56
+ /** No terminal state (outdegree = 0) detected in the flow graph. */
57
+ GRAPH_NO_TERMINAL_STATE: {
58
+ code: "XFLOW_E102",
59
+ category: "graph",
60
+ severity: "error",
61
+ title: "No terminal state detected",
62
+ },
63
+ /** One or more states are unreachable from any initial state. */
64
+ GRAPH_UNREACHABLE_STATES: {
65
+ code: "XFLOW_E103",
66
+ category: "graph",
67
+ severity: "error",
68
+ title: "Unreachable state(s) detected",
69
+ },
70
+ /** A cycle was detected in the flow graph (strict profile fails on cycles). */
71
+ GRAPH_CYCLE_DETECTED: {
72
+ code: "XFLOW_E104",
73
+ category: "graph",
74
+ severity: "error",
75
+ title: "Cycle detected in flow graph",
76
+ },
77
+ /** States referenced in transitions but not defined as current_state ancestors. */
78
+ GRAPH_ORPHAN_STATES: {
79
+ code: "XFLOW_E105",
80
+ category: "graph",
81
+ severity: "error",
82
+ title: "Orphan states detected",
83
+ },
84
+
85
+ // ── Quality ──────────────────────────────────────────────────────────────
86
+ /** More than one flow has no incoming transitions (multiple starting points). */
87
+ QUALITY_MULTIPLE_INITIAL_STATES: {
88
+ code: "XFLOW_W201",
89
+ category: "quality",
90
+ severity: "warning",
91
+ title: "Multiple initial states",
92
+ },
93
+ /** Two or more transitions from the same state to the same target are identical. */
94
+ QUALITY_DUPLICATE_TRANSITIONS: {
95
+ code: "XFLOW_W202",
96
+ category: "quality",
97
+ severity: "warning",
98
+ title: "Duplicate transitions",
99
+ },
100
+ /** A state exists that has no outgoing path to any terminal state. */
101
+ QUALITY_NON_TERMINATING_STATES: {
102
+ code: "XFLOW_W203",
103
+ category: "quality",
104
+ severity: "warning",
105
+ title: "Non-terminating states",
106
+ },
107
+ /** A transition references an operationId that is not defined in the API spec. */
108
+ QUALITY_INVALID_OPERATION_REF: {
109
+ code: "XFLOW_W204",
110
+ category: "quality",
111
+ severity: "warning",
112
+ title: "Invalid operation reference in transition",
113
+ },
114
+ /** A field reference in a transition cannot be resolved against the API schema. */
115
+ QUALITY_INVALID_FIELD_REF: {
116
+ code: "XFLOW_W205",
117
+ category: "quality",
118
+ severity: "warning",
119
+ title: "Invalid field reference in transition",
120
+ },
121
+ /** State names use inconsistent naming conventions (e.g. mix of snake_case and camelCase). */
122
+ QUALITY_SEMANTIC_INCONSISTENT_NAMING: {
123
+ code: "XFLOW_W206",
124
+ category: "quality",
125
+ severity: "warning",
126
+ title: "Inconsistent state naming style",
127
+ },
128
+ /** Different capitalisation variants resolve to the same canonical state name. */
129
+ QUALITY_SEMANTIC_AMBIGUOUS_VARIANTS: {
130
+ code: "XFLOW_W207",
131
+ category: "quality",
132
+ severity: "warning",
133
+ title: "Ambiguous state name variants",
134
+ },
135
+
136
+ // ── Lint ─────────────────────────────────────────────────────────────────
137
+ /** next_operation_id references an operationId that does not exist. */
138
+ LINT_NEXT_OPERATION_ID_EXISTS: {
139
+ code: "XFLOW_L301",
140
+ category: "lint",
141
+ severity: "error",
142
+ title: "next_operation_id references unknown operation",
143
+ },
144
+ /** prerequisite_operation_ids references one or more operationIds that do not exist. */
145
+ LINT_PREREQUISITE_OPERATION_IDS_EXIST: {
146
+ code: "XFLOW_L302",
147
+ category: "lint",
148
+ severity: "error",
149
+ title: "prerequisite_operation_ids references unknown operation",
150
+ },
151
+ /** Duplicate transitions violate lint rule. */
152
+ LINT_DUPLICATE_TRANSITIONS: {
153
+ code: "XFLOW_L303",
154
+ category: "lint",
155
+ severity: "error",
156
+ title: "Duplicate transitions (lint)",
157
+ },
158
+ /** State(s) cannot reach any terminal state, violating the terminal_path lint rule. */
159
+ LINT_TERMINAL_PATH: {
160
+ code: "XFLOW_L304",
161
+ category: "lint",
162
+ severity: "error",
163
+ title: "No terminal path from state",
164
+ },
165
+ /** Semantic naming consistency issues detected in flow state names. */
166
+ LINT_SEMANTIC_CONSISTENCY: {
167
+ code: "XFLOW_L305",
168
+ category: "lint",
169
+ severity: "error",
170
+ title: "Semantic modeling inconsistency",
171
+ },
172
+
173
+ // ── Runtime ──────────────────────────────────────────────────────────────
174
+ /** Request blocked because the resource is not in a state that allows this operation. */
175
+ RUNTIME_INVALID_STATE_TRANSITION: {
176
+ code: "XFLOW_RT401",
177
+ category: "runtime",
178
+ severity: "error",
179
+ title: "Invalid state transition blocked",
180
+ httpStatus: 409,
181
+ legacyCode: "INVALID_STATE_TRANSITION",
182
+ },
183
+ /** Runtime guard cannot resolve an operation for the incoming request. */
184
+ RUNTIME_UNKNOWN_OPERATION: {
185
+ code: "XFLOW_RT402",
186
+ category: "runtime",
187
+ severity: "error",
188
+ title: "Unknown operation",
189
+ httpStatus: 500,
190
+ legacyCode: "UNKNOWN_OPERATION",
191
+ },
192
+ /** Runtime guard is configured without a getCurrentState callback. */
193
+ RUNTIME_MISSING_STATE_RESOLVER: {
194
+ code: "XFLOW_RT403",
195
+ category: "runtime",
196
+ severity: "error",
197
+ title: "Missing state resolver",
198
+ httpStatus: 500,
199
+ legacyCode: "MISSING_STATE_RESOLVER",
200
+ },
201
+ /** Runtime guard cannot determine the resource id for the incoming request. */
202
+ RUNTIME_MISSING_RESOURCE_ID: {
203
+ code: "XFLOW_RT404",
204
+ category: "runtime",
205
+ severity: "error",
206
+ title: "Missing resource id",
207
+ httpStatus: 400,
208
+ legacyCode: "MISSING_RESOURCE_ID",
209
+ },
210
+
211
+ // ── CLI ──────────────────────────────────────────────────────────────────
212
+ /** OpenAPI or sidecar file could not be found at the specified path. */
213
+ CLI_FILE_NOT_FOUND: {
214
+ code: "XFLOW_CLI501",
215
+ category: "cli",
216
+ severity: "error",
217
+ title: "File not found",
218
+ },
219
+ /** OpenAPI or sidecar file could not be parsed (invalid YAML/JSON). */
220
+ CLI_PARSE_ERROR: {
221
+ code: "XFLOW_CLI502",
222
+ category: "cli",
223
+ severity: "error",
224
+ title: "File parse error",
225
+ },
226
+ /** Invalid or missing CLI argument. */
227
+ CLI_INVALID_ARGS: {
228
+ code: "XFLOW_CLI503",
229
+ category: "cli",
230
+ severity: "error",
231
+ title: "Invalid arguments",
232
+ },
233
+ };
234
+
235
+ /**
236
+ * Build a structured diagnostic issue object suitable for JSON output.
237
+ *
238
+ * @param {object} def - A CODES entry.
239
+ * @param {string} [message] - Human-readable detail message.
240
+ * @param {object} [opts]
241
+ * @param {string} [opts.location] - Operation endpoint or state label.
242
+ * @param {string} [opts.suggestion] - Actionable fix text.
243
+ * @param {object} [opts.details] - Extra machine-readable fields.
244
+ * @returns {{ code: string, category: string, severity: string, title: string, message: string, location?: string, suggestion?: string }}
245
+ */
246
+ function buildIssue(def, message, { location, suggestion, details } = {}) {
247
+ const issue = {
248
+ code: def.code,
249
+ category: def.category,
250
+ severity: def.severity,
251
+ title: def.title,
252
+ message: message || def.title,
253
+ };
254
+
255
+ if (location != null) {
256
+ issue.location = location;
257
+ }
258
+
259
+ if (suggestion != null) {
260
+ issue.suggestion = suggestion;
261
+ }
262
+
263
+ if (details && typeof details === "object") {
264
+ Object.assign(issue, details);
265
+ }
266
+
267
+ return issue;
268
+ }
269
+
270
+ /**
271
+ * Look up a CODES entry by its short key (e.g. "SCHEMA_VALIDATION_FAILED") or
272
+ * by stable code string (e.g. "XFLOW_E001").
273
+ *
274
+ * @param {string} keyOrCode
275
+ * @returns {object|null}
276
+ */
277
+ function lookup(keyOrCode) {
278
+ if (CODES[keyOrCode]) {
279
+ return CODES[keyOrCode];
280
+ }
281
+
282
+ return Object.values(CODES).find((entry) => entry.code === keyOrCode) || null;
283
+ }
284
+
285
+ module.exports = {
286
+ CODES,
287
+ buildIssue,
288
+ lookup,
289
+ };
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const { loadApi } = require("./validator");
5
+
6
+ const HTTP_METHODS = [
7
+ "get",
8
+ "put",
9
+ "post",
10
+ "delete",
11
+ "options",
12
+ "head",
13
+ "patch",
14
+ "trace",
15
+ ];
16
+
17
+ function normalizePathTemplate(inputPath) {
18
+ if (!inputPath) {
19
+ return "/";
20
+ }
21
+
22
+ const withSlashes = String(inputPath).startsWith("/")
23
+ ? String(inputPath)
24
+ : `/${String(inputPath)}`;
25
+
26
+ return withSlashes
27
+ .replace(/:([A-Za-z0-9_]+)/g, "{$1}")
28
+ .replace(/\/+/g, "/")
29
+ .replace(/\/$/, "") || "/";
30
+ }
31
+
32
+ function buildPathRegex(pathTemplate) {
33
+ const escaped = normalizePathTemplate(pathTemplate)
34
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
35
+ .replace(/\\\{[^}]+\\\}/g, "[^/]+");
36
+
37
+ return new RegExp(`^${escaped}$`);
38
+ }
39
+
40
+ function routeKey(method, pathTemplate) {
41
+ return `${String(method || "").toUpperCase()} ${normalizePathTemplate(pathTemplate)}`;
42
+ }
43
+
44
+ function extractFlowOperationsFromOpenApi(api, options = {}) {
45
+ const methods = Array.isArray(options.httpMethods) && options.httpMethods.length > 0
46
+ ? options.httpMethods
47
+ : HTTP_METHODS;
48
+
49
+ const operations = [];
50
+ const paths = (api && api.paths) || {};
51
+
52
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
53
+ for (const method of methods) {
54
+ const operation = pathItem[method];
55
+ if (!operation || !operation["x-openapi-flow"]) {
56
+ continue;
57
+ }
58
+
59
+ const flow = operation["x-openapi-flow"];
60
+ const operationId = operation.operationId || `${method}_${pathKey}`;
61
+
62
+ operations.push({
63
+ operationId,
64
+ method: method.toUpperCase(),
65
+ pathTemplate: normalizePathTemplate(pathKey),
66
+ routeKey: routeKey(method, pathKey),
67
+ routeRegex: buildPathRegex(pathKey),
68
+ currentState: flow.current_state,
69
+ transitions: Array.isArray(flow.transitions) ? flow.transitions : [],
70
+ });
71
+ }
72
+ }
73
+
74
+ return operations;
75
+ }
76
+
77
+ function buildStateMachineDefinitionFromOperations(operations) {
78
+ const byOperationId = new Map(operations.map((operation) => [operation.operationId, operation]));
79
+ const transitions = [];
80
+
81
+ for (const source of operations) {
82
+ for (const transition of source.transitions) {
83
+ if (transition.next_operation_id && byOperationId.has(transition.next_operation_id)) {
84
+ const target = byOperationId.get(transition.next_operation_id);
85
+ transitions.push({
86
+ from: source.currentState,
87
+ action: transition.next_operation_id,
88
+ to: target.currentState,
89
+ });
90
+ continue;
91
+ }
92
+
93
+ if (!transition.target_state) {
94
+ continue;
95
+ }
96
+
97
+ for (const target of operations) {
98
+ if (target.currentState === transition.target_state) {
99
+ transitions.push({
100
+ from: source.currentState,
101
+ action: target.operationId,
102
+ to: target.currentState,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ return {
110
+ transitions,
111
+ };
112
+ }
113
+
114
+ function buildStateMachineDefinitionFromOpenApi(api, options = {}) {
115
+ const operations = extractFlowOperationsFromOpenApi(api, options);
116
+ return buildStateMachineDefinitionFromOperations(operations);
117
+ }
118
+
119
+ function buildStateMachineDefinitionFromOpenApiFile(openapiPath, options = {}) {
120
+ const api = loadApi(path.resolve(openapiPath));
121
+ return buildStateMachineDefinitionFromOpenApi(api, options);
122
+ }
123
+
124
+ function createStateMachineAdapterModel(options = {}) {
125
+ if (!options.openapi && !options.openapiPath) {
126
+ throw new Error("State machine adapter requires 'openapi' object or 'openapiPath'.");
127
+ }
128
+
129
+ const api = options.openapiPath
130
+ ? loadApi(path.resolve(options.openapiPath))
131
+ : options.openapi;
132
+
133
+ const operations = extractFlowOperationsFromOpenApi(api, options);
134
+ const definition = buildStateMachineDefinitionFromOperations(operations);
135
+
136
+ return {
137
+ api,
138
+ operations,
139
+ definition,
140
+ };
141
+ }
142
+
143
+ module.exports = {
144
+ normalizePathTemplate,
145
+ routeKey,
146
+ extractFlowOperationsFromOpenApi,
147
+ buildStateMachineDefinitionFromOperations,
148
+ buildStateMachineDefinitionFromOpenApi,
149
+ buildStateMachineDefinitionFromOpenApiFile,
150
+ createStateMachineAdapterModel,
151
+ };
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+
3
+ const {
4
+ FlowGuardError,
5
+ invalidTransitionError,
6
+ unknownOperationError,
7
+ missingStateResolverError,
8
+ missingResourceIdError,
9
+ } = require("./errors");
10
+ const { loadRuntimeModel, normalizePathTemplate, routeKey } = require("./model");
11
+
12
+ function defaultResolveResourceId(context) {
13
+ const params = context && context.params;
14
+ if (!params || typeof params !== "object") {
15
+ return null;
16
+ }
17
+
18
+ if (params.id != null) {
19
+ return String(params.id);
20
+ }
21
+
22
+ if (params.resourceId != null) {
23
+ return String(params.resourceId);
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ class RuntimeFlowGuard {
30
+ constructor(options = {}) {
31
+ const runtimeModel = loadRuntimeModel(options);
32
+
33
+ this.operations = runtimeModel.operations;
34
+ this.byOperationId = runtimeModel.byOperationId;
35
+ this.byRouteKey = runtimeModel.byRouteKey;
36
+ this.stateMachine = runtimeModel.stateMachine;
37
+
38
+ this.getCurrentState = options.getCurrentState;
39
+ this.resolveResourceId = options.resolveResourceId || defaultResolveResourceId;
40
+ this.resolveOperationId = options.resolveOperationId || null;
41
+
42
+ this.allowUnknownOperations = options.allowUnknownOperations === true;
43
+ this.allowIdempotentState = options.allowIdempotentState !== false;
44
+ this.allowMissingStateForInitial = options.allowMissingStateForInitial !== false;
45
+ this.requireResourceIdForTransitions = options.requireResourceIdForTransitions !== false;
46
+ }
47
+
48
+ resolveOperation({ operationId, method, path }) {
49
+ if (operationId && this.byOperationId.has(operationId)) {
50
+ return this.byOperationId.get(operationId);
51
+ }
52
+
53
+ const normalizedPath = normalizePathTemplate(path || "/");
54
+ const byKey = this.byRouteKey.get(routeKey(method, normalizedPath));
55
+ if (byKey) {
56
+ return byKey;
57
+ }
58
+
59
+ const methodUpper = String(method || "").toUpperCase();
60
+ for (const operation of this.operations) {
61
+ if (operation.method !== methodUpper) {
62
+ continue;
63
+ }
64
+
65
+ if (operation.routeRegex.test(normalizedPath)) {
66
+ return operation;
67
+ }
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ async enforce(context = {}) {
74
+ if (typeof this.getCurrentState !== "function") {
75
+ throw missingStateResolverError();
76
+ }
77
+
78
+ const operationFromResolver = this.resolveOperationId
79
+ ? await this.resolveOperationId(context)
80
+ : null;
81
+
82
+ const operation = this.resolveOperation({
83
+ operationId: context.operationId || operationFromResolver,
84
+ method: context.method,
85
+ path: context.path,
86
+ });
87
+
88
+ if (!operation) {
89
+ if (this.allowUnknownOperations) {
90
+ return { ok: true, skipped: true, reason: "unknown_operation" };
91
+ }
92
+
93
+ throw unknownOperationError({
94
+ operationId: context.operationId || operationFromResolver,
95
+ method: context.method,
96
+ path: context.path,
97
+ });
98
+ }
99
+
100
+ const resourceId = this.resolveResourceId(context);
101
+ if (!resourceId && operation.incomingFromStates.size > 0 && this.requireResourceIdForTransitions) {
102
+ throw missingResourceIdError({
103
+ operationId: operation.operationId,
104
+ method: context.method,
105
+ path: context.path,
106
+ });
107
+ }
108
+
109
+ const currentState = await this.getCurrentState({
110
+ operation,
111
+ operationId: operation.operationId,
112
+ resourceId,
113
+ context,
114
+ });
115
+
116
+ if (currentState == null) {
117
+ if (operation.incomingFromStates.size === 0 && this.allowMissingStateForInitial) {
118
+ return {
119
+ ok: true,
120
+ operationId: operation.operationId,
121
+ resourceId,
122
+ currentState: null,
123
+ };
124
+ }
125
+
126
+ throw invalidTransitionError({
127
+ operationId: operation.operationId,
128
+ currentState: null,
129
+ allowedFromStates: operation.incomingFromStates,
130
+ resourceId,
131
+ });
132
+ }
133
+
134
+ const normalizedState = String(currentState);
135
+ const isAllowedFrom = this.stateMachine.canTransition(normalizedState, operation.operationId);
136
+ const isSameState = this.allowIdempotentState && normalizedState === String(operation.currentState);
137
+
138
+ if (!isAllowedFrom && !isSameState) {
139
+ throw invalidTransitionError({
140
+ operationId: operation.operationId,
141
+ currentState: normalizedState,
142
+ allowedFromStates: operation.incomingFromStates,
143
+ resourceId,
144
+ });
145
+ }
146
+
147
+ return {
148
+ ok: true,
149
+ operationId: operation.operationId,
150
+ resourceId,
151
+ currentState: normalizedState,
152
+ nextState: this.stateMachine.getNextState(normalizedState, operation.operationId),
153
+ };
154
+ }
155
+ }
156
+
157
+ function createRuntimeFlowGuard(options) {
158
+ return new RuntimeFlowGuard(options);
159
+ }
160
+
161
+ function toErrorPayload(error) {
162
+ if (error instanceof FlowGuardError) {
163
+ return error.toJSON();
164
+ }
165
+
166
+ return {
167
+ code: "INTERNAL_RUNTIME_GUARD_ERROR",
168
+ message: error && error.message ? error.message : "Unknown runtime guard error.",
169
+ };
170
+ }
171
+
172
+ module.exports = {
173
+ RuntimeFlowGuard,
174
+ createRuntimeFlowGuard,
175
+ toErrorPayload,
176
+ };
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ class FlowGuardError extends Error {
4
+ constructor(message, details = {}, statusCode = 409) {
5
+ super(message);
6
+ this.name = "FlowGuardError";
7
+ this.code = details.code || "FLOW_GUARD_ERROR";
8
+ this.statusCode = statusCode;
9
+ this.details = details;
10
+ }
11
+
12
+ toJSON() {
13
+ return {
14
+ code: this.code,
15
+ message: this.message,
16
+ ...this.details,
17
+ };
18
+ }
19
+ }
20
+
21
+ function invalidTransitionError({
22
+ operationId,
23
+ currentState,
24
+ allowedFromStates,
25
+ resourceId,
26
+ }) {
27
+ const allowed = [...allowedFromStates].sort();
28
+ const current = currentState == null ? null : String(currentState);
29
+
30
+ return new FlowGuardError(
31
+ `Blocked invalid transition for operation '${operationId}'. Current state '${current}' cannot transition to this operation.`,
32
+ {
33
+ code: "INVALID_STATE_TRANSITION",
34
+ operation_id: operationId,
35
+ current_state: current,
36
+ allowed_from_states: allowed,
37
+ resource_id: resourceId || null,
38
+ },
39
+ 409
40
+ );
41
+ }
42
+
43
+ function unknownOperationError({ operationId, method, path }) {
44
+ return new FlowGuardError(
45
+ `Runtime guard could not resolve operation for '${method} ${path}'.`,
46
+ {
47
+ code: "UNKNOWN_OPERATION",
48
+ operation_id: operationId || null,
49
+ method: method || null,
50
+ path: path || null,
51
+ },
52
+ 500
53
+ );
54
+ }
55
+
56
+ function missingStateResolverError() {
57
+ return new FlowGuardError(
58
+ "Runtime guard requires 'getCurrentState' callback to enforce transitions.",
59
+ {
60
+ code: "MISSING_STATE_RESOLVER",
61
+ },
62
+ 500
63
+ );
64
+ }
65
+
66
+ function missingResourceIdError({ operationId, method, path }) {
67
+ return new FlowGuardError(
68
+ `Runtime guard requires a resource id to enforce operation '${operationId}'.`,
69
+ {
70
+ code: "MISSING_RESOURCE_ID",
71
+ operation_id: operationId || null,
72
+ method: method || null,
73
+ path: path || null,
74
+ },
75
+ 400
76
+ );
77
+ }
78
+
79
+ module.exports = {
80
+ FlowGuardError,
81
+ invalidTransitionError,
82
+ unknownOperationError,
83
+ missingStateResolverError,
84
+ missingResourceIdError,
85
+ };