@ankhorage/flow 0.0.0-stage → 0.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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +19 -2
- package/dist/ai.d.ts +7 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +7 -0
- package/dist/ai.js.map +1 -0
- package/dist/execution.d.ts +5 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +5 -0
- package/dist/execution.js.map +1 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.d.ts +4 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.d.ts.map +1 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.js +219 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.js.map +1 -0
- package/dist/features/execution/application/cancelDurableRunAsync.d.ts +8 -0
- package/dist/features/execution/application/cancelDurableRunAsync.d.ts.map +1 -0
- package/dist/features/execution/application/cancelDurableRunAsync.js +22 -0
- package/dist/features/execution/application/cancelDurableRunAsync.js.map +1 -0
- package/dist/features/execution/application/createDurableRunAsync.d.ts +12 -0
- package/dist/features/execution/application/createDurableRunAsync.d.ts.map +1 -0
- package/dist/features/execution/application/createDurableRunAsync.js +8 -0
- package/dist/features/execution/application/createDurableRunAsync.js.map +1 -0
- package/dist/features/execution/application/executeFlowAsync.d.ts +4 -0
- package/dist/features/execution/application/executeFlowAsync.d.ts.map +1 -0
- package/dist/features/execution/application/executeFlowAsync.js +211 -0
- package/dist/features/execution/application/executeFlowAsync.js.map +1 -0
- package/dist/features/execution/application/runDurableWorkerAsync.d.ts +52 -0
- package/dist/features/execution/application/runDurableWorkerAsync.d.ts.map +1 -0
- package/dist/features/execution/application/runDurableWorkerAsync.js +195 -0
- package/dist/features/execution/application/runDurableWorkerAsync.js.map +1 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts +5 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts.map +1 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js +11 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js.map +1 -0
- package/dist/features/execution/domain/calculateRetryDueAt.d.ts +4 -0
- package/dist/features/execution/domain/calculateRetryDueAt.d.ts.map +1 -0
- package/dist/features/execution/domain/calculateRetryDueAt.js +14 -0
- package/dist/features/execution/domain/calculateRetryDueAt.js.map +1 -0
- package/dist/features/execution/domain/compileFlowGraph.d.ts +5 -0
- package/dist/features/execution/domain/compileFlowGraph.d.ts.map +1 -0
- package/dist/features/execution/domain/compileFlowGraph.js +76 -0
- package/dist/features/execution/domain/compileFlowGraph.js.map +1 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts +4 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts.map +1 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.js +15 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.js.map +1 -0
- package/dist/features/execution/domain/createDurableRun.d.ts +7 -0
- package/dist/features/execution/domain/createDurableRun.d.ts.map +1 -0
- package/dist/features/execution/domain/createDurableRun.js +10 -0
- package/dist/features/execution/domain/createDurableRun.js.map +1 -0
- package/dist/features/execution/domain/createRetryWork.d.ts +4 -0
- package/dist/features/execution/domain/createRetryWork.d.ts.map +1 -0
- package/dist/features/execution/domain/createRetryWork.js +17 -0
- package/dist/features/execution/domain/createRetryWork.js.map +1 -0
- package/dist/features/execution/domain/isCronDueAt.d.ts +7 -0
- package/dist/features/execution/domain/isCronDueAt.d.ts.map +1 -0
- package/dist/features/execution/domain/isCronDueAt.js +114 -0
- package/dist/features/execution/domain/isCronDueAt.js.map +1 -0
- package/dist/features/execution/domain/validateFlowDataSchema.d.ts +6 -0
- package/dist/features/execution/domain/validateFlowDataSchema.d.ts.map +1 -0
- package/dist/features/execution/domain/validateFlowDataSchema.js +144 -0
- package/dist/features/execution/domain/validateFlowDataSchema.js.map +1 -0
- package/dist/features/execution/ports/FlowPersistencePort.d.ts +34 -0
- package/dist/features/execution/ports/FlowPersistencePort.d.ts.map +1 -0
- package/dist/features/execution/ports/FlowPersistencePort.js +2 -0
- package/dist/features/execution/ports/FlowPersistencePort.js.map +1 -0
- package/dist/features/mcp/McpToolError.d.ts +6 -0
- package/dist/features/mcp/McpToolError.d.ts.map +1 -0
- package/dist/features/mcp/McpToolError.js +10 -0
- package/dist/features/mcp/McpToolError.js.map +1 -0
- package/dist/features/mcp/createMcpRemoteTransport.d.ts +8 -0
- package/dist/features/mcp/createMcpRemoteTransport.d.ts.map +1 -0
- package/dist/features/mcp/createMcpRemoteTransport.js +23 -0
- package/dist/features/mcp/createMcpRemoteTransport.js.map +1 -0
- package/dist/features/mcp/createMcpToolRegistry.d.ts +10 -0
- package/dist/features/mcp/createMcpToolRegistry.d.ts.map +1 -0
- package/dist/features/mcp/createMcpToolRegistry.js +185 -0
- package/dist/features/mcp/createMcpToolRegistry.js.map +1 -0
- package/dist/features/mcp/loadMcpSdkAsync.d.ts +12 -0
- package/dist/features/mcp/loadMcpSdkAsync.d.ts.map +1 -0
- package/dist/features/mcp/loadMcpSdkAsync.js +20 -0
- package/dist/features/mcp/loadMcpSdkAsync.js.map +1 -0
- package/dist/features/mcp/prepareMcpProviders.d.ts +4 -0
- package/dist/features/mcp/prepareMcpProviders.d.ts.map +1 -0
- package/dist/features/mcp/prepareMcpProviders.js +97 -0
- package/dist/features/mcp/prepareMcpProviders.js.map +1 -0
- package/dist/features/mcp/validateMcpSchema.d.ts +6 -0
- package/dist/features/mcp/validateMcpSchema.d.ts.map +1 -0
- package/dist/features/mcp/validateMcpSchema.js +220 -0
- package/dist/features/mcp/validateMcpSchema.js.map +1 -0
- package/dist/features/security/FlowSecurityError.d.ts +6 -0
- package/dist/features/security/FlowSecurityError.d.ts.map +1 -0
- package/dist/features/security/FlowSecurityError.js +10 -0
- package/dist/features/security/FlowSecurityError.js.map +1 -0
- package/dist/features/security/application/createFlowSecurityPort.d.ts +12 -0
- package/dist/features/security/application/createFlowSecurityPort.d.ts.map +1 -0
- package/dist/features/security/application/createFlowSecurityPort.js +208 -0
- package/dist/features/security/application/createFlowSecurityPort.js.map +1 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.d.ts +4 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.d.ts.map +1 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.js +57 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +3 -0
- package/dist/mcp.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/types/durableExecution.d.ts +55 -0
- package/dist/types/durableExecution.d.ts.map +1 -0
- package/dist/types/durableExecution.js +2 -0
- package/dist/types/durableExecution.js.map +1 -0
- package/dist/types/execution.d.ts +33 -0
- package/dist/types/execution.d.ts.map +1 -0
- package/dist/types/execution.js +2 -0
- package/dist/types/execution.js.map +1 -0
- package/dist/types/mcp.d.ts +61 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +2 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/security.d.ts +123 -0
- package/dist/types/security.d.ts.map +1 -0
- package/dist/types/security.js +2 -0
- package/dist/types/security.js.map +1 -0
- package/package.json +90 -4
- package/src/ai.ts +6 -0
- package/src/execution.ts +9 -0
- package/src/features/execution/adapters/FileFlowPersistenceAdapter.ts +333 -0
- package/src/features/execution/application/cancelDurableRunAsync.ts +28 -0
- package/src/features/execution/application/createDurableRunAsync.ts +14 -0
- package/src/features/execution/application/executeFlowAsync.ts +338 -0
- package/src/features/execution/application/runDurableWorkerAsync.ts +337 -0
- package/src/features/execution/domain/assertFlowGraphSchemasSupported.ts +17 -0
- package/src/features/execution/domain/calculateRetryDueAt.ts +21 -0
- package/src/features/execution/domain/compileFlowGraph.ts +97 -0
- package/src/features/execution/domain/createDefaultNodeRegistry.ts +26 -0
- package/src/features/execution/domain/createDurableRun.ts +14 -0
- package/src/features/execution/domain/createRetryWork.ts +22 -0
- package/src/features/execution/domain/isCronDueAt.ts +131 -0
- package/src/features/execution/domain/validateFlowDataSchema.ts +218 -0
- package/src/features/execution/ports/FlowPersistencePort.ts +38 -0
- package/src/features/mcp/McpToolError.ts +10 -0
- package/src/features/mcp/createMcpRemoteTransport.ts +34 -0
- package/src/features/mcp/createMcpToolRegistry.ts +324 -0
- package/src/features/mcp/loadMcpSdkAsync.ts +32 -0
- package/src/features/mcp/prepareMcpProviders.ts +135 -0
- package/src/features/mcp/validateMcpSchema.ts +269 -0
- package/src/features/security/FlowSecurityError.ts +17 -0
- package/src/features/security/application/createFlowSecurityPort.ts +339 -0
- package/src/features/security/domain/validateFlowSecurityTarget.ts +73 -0
- package/src/index.ts +49 -0
- package/src/mcp.ts +11 -0
- package/src/social.ts +6 -0
- package/src/types/durableExecution.ts +61 -0
- package/src/types/execution.ts +41 -0
- package/src/types/mcp.ts +70 -0
- package/src/types/security.ts +148 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { McpToolError } from './McpToolError.js';
|
|
4
|
+
|
|
5
|
+
const MAX_DEPTH = 16;
|
|
6
|
+
const TYPES = new Set(['null', 'boolean', 'number', 'integer', 'string', 'array', 'object']);
|
|
7
|
+
const KEYS = new Set([
|
|
8
|
+
'$schema',
|
|
9
|
+
'title',
|
|
10
|
+
'description',
|
|
11
|
+
'type',
|
|
12
|
+
'const',
|
|
13
|
+
'enum',
|
|
14
|
+
'properties',
|
|
15
|
+
'required',
|
|
16
|
+
'additionalProperties',
|
|
17
|
+
'minProperties',
|
|
18
|
+
'maxProperties',
|
|
19
|
+
'items',
|
|
20
|
+
'minItems',
|
|
21
|
+
'maxItems',
|
|
22
|
+
'uniqueItems',
|
|
23
|
+
'minLength',
|
|
24
|
+
'maxLength',
|
|
25
|
+
'pattern',
|
|
26
|
+
'minimum',
|
|
27
|
+
'maximum',
|
|
28
|
+
'exclusiveMinimum',
|
|
29
|
+
'exclusiveMaximum',
|
|
30
|
+
'multipleOf',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/*** Validates a closed supported JSON Schema and returns its stable argument matcher. */
|
|
34
|
+
export const validateMcpSchema = (schema: Readonly<Record<string, unknown>>) => {
|
|
35
|
+
validateSchema(schema, 0);
|
|
36
|
+
return {
|
|
37
|
+
fingerprint: createHash('sha256').update(stableStringify(schema)).digest('hex'),
|
|
38
|
+
matches: (value: unknown): boolean => matches(schema, value),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/*** Rejects unsupported JSON Schema features instead of weakening validation. */
|
|
43
|
+
const validateSchema = (schema: Readonly<Record<string, unknown>>, depth: number): void => {
|
|
44
|
+
if (depth > MAX_DEPTH)
|
|
45
|
+
throw new McpToolError('invalid_schema', 'MCP schema exceeds depth limit.');
|
|
46
|
+
if (Object.keys(schema).some((key) => !KEYS.has(key)))
|
|
47
|
+
throw new McpToolError(
|
|
48
|
+
'unsupported_schema',
|
|
49
|
+
'The MCP tool schema uses unsupported constraints.',
|
|
50
|
+
);
|
|
51
|
+
const { type } = schema;
|
|
52
|
+
if (typeof type !== 'string' || !TYPES.has(type))
|
|
53
|
+
throw new McpToolError('invalid_schema', 'MCP schemas require one supported type.');
|
|
54
|
+
validateJsonConstraints(schema);
|
|
55
|
+
if (type === 'object') validateObject(schema, depth);
|
|
56
|
+
if (type === 'array') validateArray(schema, depth);
|
|
57
|
+
if (type === 'string') validateString(schema);
|
|
58
|
+
if (type === 'number' || type === 'integer') validateNumber(schema);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/*** Validates portable const and enum declarations. */
|
|
62
|
+
const validateJsonConstraints = (schema: Readonly<Record<string, unknown>>): void => {
|
|
63
|
+
if ('const' in schema && !isJson(schema.const))
|
|
64
|
+
throw new McpToolError('invalid_schema', 'MCP schema const must be JSON data.');
|
|
65
|
+
if (
|
|
66
|
+
'enum' in schema &&
|
|
67
|
+
(!Array.isArray(schema.enum) || !schema.enum.length || !schema.enum.every(isJson))
|
|
68
|
+
)
|
|
69
|
+
throw new McpToolError('invalid_schema', 'MCP schema enum must contain JSON data.');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/*** Requires a closed object schema with validated declared properties. */
|
|
73
|
+
const validateObject = (schema: Readonly<Record<string, unknown>>, depth: number): void => {
|
|
74
|
+
const { properties, required, additionalProperties } = schema;
|
|
75
|
+
if (additionalProperties !== false)
|
|
76
|
+
throw new McpToolError(
|
|
77
|
+
'unsupported_schema',
|
|
78
|
+
'MCP object schemas must set additionalProperties to false.',
|
|
79
|
+
);
|
|
80
|
+
if (!isRecord(properties))
|
|
81
|
+
throw new McpToolError('invalid_schema', 'MCP object schemas need property definitions.');
|
|
82
|
+
Object.values(properties).forEach((property) => {
|
|
83
|
+
if (!isRecord(property))
|
|
84
|
+
throw new McpToolError('invalid_schema', 'MCP property schemas must be objects.');
|
|
85
|
+
validateSchema(property, depth + 1);
|
|
86
|
+
});
|
|
87
|
+
if (
|
|
88
|
+
required !== undefined &&
|
|
89
|
+
(!isUniqueStrings(required) || required.some((key) => !(key in properties)))
|
|
90
|
+
)
|
|
91
|
+
throw new McpToolError(
|
|
92
|
+
'invalid_schema',
|
|
93
|
+
'MCP required properties must be declared exactly once.',
|
|
94
|
+
);
|
|
95
|
+
validateBounds(
|
|
96
|
+
schema.minProperties,
|
|
97
|
+
schema.maxProperties,
|
|
98
|
+
'minProperties',
|
|
99
|
+
'maxProperties',
|
|
100
|
+
true,
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/*** Requires an item schema so every array value is validated. */
|
|
105
|
+
const validateArray = (schema: Readonly<Record<string, unknown>>, depth: number): void => {
|
|
106
|
+
if (!isRecord(schema.items))
|
|
107
|
+
throw new McpToolError('unsupported_schema', 'MCP array schemas require an item schema.');
|
|
108
|
+
validateSchema(schema.items, depth + 1);
|
|
109
|
+
if (schema.uniqueItems !== undefined && typeof schema.uniqueItems !== 'boolean')
|
|
110
|
+
throw new McpToolError('invalid_schema', 'MCP uniqueItems must be boolean.');
|
|
111
|
+
validateBounds(schema.minItems, schema.maxItems, 'minItems', 'maxItems', true);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/*** Validates string bounds and regular-expression patterns. */
|
|
115
|
+
const validateString = (schema: Readonly<Record<string, unknown>>): void => {
|
|
116
|
+
validateBounds(schema.minLength, schema.maxLength, 'minLength', 'maxLength', true);
|
|
117
|
+
if (schema.pattern !== undefined) {
|
|
118
|
+
if (typeof schema.pattern !== 'string')
|
|
119
|
+
throw new McpToolError('invalid_schema', 'MCP pattern must be a string.');
|
|
120
|
+
try {
|
|
121
|
+
new RegExp(schema.pattern, 'u');
|
|
122
|
+
} catch {
|
|
123
|
+
throw new McpToolError('invalid_schema', 'MCP pattern must compile.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/*** Validates finite numeric constraints. */
|
|
129
|
+
const validateNumber = (schema: Readonly<Record<string, unknown>>): void => {
|
|
130
|
+
const values = [
|
|
131
|
+
schema.minimum,
|
|
132
|
+
schema.maximum,
|
|
133
|
+
schema.exclusiveMinimum,
|
|
134
|
+
schema.exclusiveMaximum,
|
|
135
|
+
schema.multipleOf,
|
|
136
|
+
];
|
|
137
|
+
if (
|
|
138
|
+
values.some(
|
|
139
|
+
(value) => value !== undefined && (typeof value !== 'number' || !Number.isFinite(value)),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
throw new McpToolError('invalid_schema', 'MCP numeric constraints must be finite numbers.');
|
|
143
|
+
if (typeof schema.multipleOf === 'number' && schema.multipleOf <= 0)
|
|
144
|
+
throw new McpToolError('invalid_schema', 'MCP multipleOf must be positive.');
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/*** Validates ordered numeric or integer bounds. */
|
|
148
|
+
const validateBounds = (
|
|
149
|
+
minimum: unknown,
|
|
150
|
+
maximum: unknown,
|
|
151
|
+
minimumKey: string,
|
|
152
|
+
maximumKey: string,
|
|
153
|
+
integers: boolean,
|
|
154
|
+
): void => {
|
|
155
|
+
const valid = (value: unknown): value is number =>
|
|
156
|
+
typeof value === 'number' &&
|
|
157
|
+
Number.isFinite(value) &&
|
|
158
|
+
(!integers || (Number.isSafeInteger(value) && value >= 0));
|
|
159
|
+
if (minimum !== undefined && !valid(minimum))
|
|
160
|
+
throw new McpToolError('invalid_schema', `MCP ${minimumKey} is invalid.`);
|
|
161
|
+
if (maximum !== undefined && !valid(maximum))
|
|
162
|
+
throw new McpToolError('invalid_schema', `MCP ${maximumKey} is invalid.`);
|
|
163
|
+
if (typeof minimum === 'number' && typeof maximum === 'number' && minimum > maximum)
|
|
164
|
+
throw new McpToolError('invalid_schema', `MCP ${minimumKey} cannot exceed ${maximumKey}.`);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/*** Matches supported JSON Schema constraints without coercion. */
|
|
168
|
+
const matches = (schema: Readonly<Record<string, unknown>>, value: unknown): boolean => {
|
|
169
|
+
const { type } = schema;
|
|
170
|
+
if (typeof type !== 'string' || !matchesType(type, value)) return false;
|
|
171
|
+
if ('const' in schema && stableStringify(schema.const) !== stableStringify(value)) return false;
|
|
172
|
+
if (
|
|
173
|
+
Array.isArray(schema.enum) &&
|
|
174
|
+
!schema.enum.some((item) => stableStringify(item) === stableStringify(value))
|
|
175
|
+
)
|
|
176
|
+
return false;
|
|
177
|
+
if (type === 'object') return matchesObject(schema, value);
|
|
178
|
+
if (type === 'array') return matchesArray(schema, value);
|
|
179
|
+
if (type === 'string') return matchesString(schema, value);
|
|
180
|
+
return (type !== 'number' && type !== 'integer') || matchesNumber(schema, value as number);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/*** Matches a closed object and each declared property schema. */
|
|
184
|
+
const matchesObject = (schema: Readonly<Record<string, unknown>>, value: unknown): boolean => {
|
|
185
|
+
const { properties, required } = schema;
|
|
186
|
+
const minProperties = numberOr(schema.minProperties, 0);
|
|
187
|
+
const maxProperties = numberOr(schema.maxProperties, Infinity);
|
|
188
|
+
if (!isRecord(value) || !isRecord(properties)) return false;
|
|
189
|
+
const entries = Object.entries(value);
|
|
190
|
+
return (
|
|
191
|
+
entries.length >= minProperties &&
|
|
192
|
+
entries.length <= maxProperties &&
|
|
193
|
+
(!Array.isArray(required) ||
|
|
194
|
+
required.every((key) => typeof key === 'string' && key in value)) &&
|
|
195
|
+
entries.every(([key, item]) => {
|
|
196
|
+
const property = Object.entries(properties).find(([name]) => name === key)?.[1];
|
|
197
|
+
return isRecord(property) && matches(property, item);
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/*** Matches array items, bounds, and deterministic uniqueness. */
|
|
203
|
+
const matchesArray = (schema: Readonly<Record<string, unknown>>, value: unknown): boolean => {
|
|
204
|
+
const { items, uniqueItems = false } = schema;
|
|
205
|
+
const minItems = numberOr(schema.minItems, 0);
|
|
206
|
+
const maxItems = numberOr(schema.maxItems, Infinity);
|
|
207
|
+
return (
|
|
208
|
+
Array.isArray(value) &&
|
|
209
|
+
isRecord(items) &&
|
|
210
|
+
value.length >= minItems &&
|
|
211
|
+
value.length <= maxItems &&
|
|
212
|
+
(!uniqueItems || new Set(value.map(stableStringify)).size === value.length) &&
|
|
213
|
+
value.every((item) => matches(items, item))
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/*** Matches string constraints. */
|
|
218
|
+
const matchesString = (schema: Readonly<Record<string, unknown>>, value: unknown): boolean =>
|
|
219
|
+
typeof value === 'string' &&
|
|
220
|
+
[...value].length >= numberOr(schema.minLength, 0) &&
|
|
221
|
+
[...value].length <= numberOr(schema.maxLength, Infinity) &&
|
|
222
|
+
(typeof schema.pattern !== 'string' || new RegExp(schema.pattern, 'u').test(value));
|
|
223
|
+
|
|
224
|
+
/*** Matches finite number constraints. */
|
|
225
|
+
const matchesNumber = (schema: Readonly<Record<string, unknown>>, value: number): boolean =>
|
|
226
|
+
(typeof schema.minimum !== 'number' || value >= schema.minimum) &&
|
|
227
|
+
(typeof schema.maximum !== 'number' || value <= schema.maximum) &&
|
|
228
|
+
(typeof schema.exclusiveMinimum !== 'number' || value > schema.exclusiveMinimum) &&
|
|
229
|
+
(typeof schema.exclusiveMaximum !== 'number' || value < schema.exclusiveMaximum) &&
|
|
230
|
+
(typeof schema.multipleOf !== 'number' || Number.isInteger(value / schema.multipleOf));
|
|
231
|
+
|
|
232
|
+
/*** Matches a JSON Schema primitive type. */
|
|
233
|
+
const matchesType = (type: string, value: unknown): boolean =>
|
|
234
|
+
(type === 'null' && value === null) ||
|
|
235
|
+
(type === 'array' && Array.isArray(value)) ||
|
|
236
|
+
(type === 'object' && isRecord(value)) ||
|
|
237
|
+
(type === 'integer' && Number.isSafeInteger(value)) ||
|
|
238
|
+
(type === 'number' && typeof value === 'number' && Number.isFinite(value)) ||
|
|
239
|
+
(type === 'string' && typeof value === 'string') ||
|
|
240
|
+
(type === 'boolean' && typeof value === 'boolean');
|
|
241
|
+
|
|
242
|
+
/*** Narrows unknown data to a non-array object. */
|
|
243
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
244
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
245
|
+
/*** Confirms a value is portable JSON data. */
|
|
246
|
+
const isJson = (value: unknown): boolean =>
|
|
247
|
+
value === null ||
|
|
248
|
+
['string', 'boolean'].includes(typeof value) ||
|
|
249
|
+
(typeof value === 'number' && Number.isFinite(value)) ||
|
|
250
|
+
(Array.isArray(value) && value.every(isJson)) ||
|
|
251
|
+
(isRecord(value) && Object.values(value).every(isJson));
|
|
252
|
+
/*** Confirms a value is an array of unique non-empty strings. */
|
|
253
|
+
const isUniqueStrings = (value: unknown): value is readonly string[] =>
|
|
254
|
+
Array.isArray(value) &&
|
|
255
|
+
value.every((item) => typeof item === 'string' && item.length > 0) &&
|
|
256
|
+
new Set(value).size === value.length;
|
|
257
|
+
/*** Returns a numeric schema value or its validated default. */
|
|
258
|
+
const numberOr = (value: unknown, fallback: number): number =>
|
|
259
|
+
typeof value === 'number' ? value : fallback;
|
|
260
|
+
/*** Serializes JSON data deterministically. */
|
|
261
|
+
const stableStringify = (value: unknown): string =>
|
|
262
|
+
Array.isArray(value)
|
|
263
|
+
? `[${value.map(stableStringify).join(',')}]`
|
|
264
|
+
: isRecord(value)
|
|
265
|
+
? `{${Object.entries(value)
|
|
266
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
267
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
268
|
+
.join(',')}}`
|
|
269
|
+
: JSON.stringify(value);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*** Represents a stable, payload-redacted Flow security denial. */
|
|
2
|
+
export class FlowSecurityError extends Error {
|
|
3
|
+
public constructor(
|
|
4
|
+
public readonly code:
|
|
5
|
+
| 'approval_pending'
|
|
6
|
+
| 'approval_rejected'
|
|
7
|
+
| 'idempotency_conflict'
|
|
8
|
+
| 'invalid_target'
|
|
9
|
+
| 'permission_denied'
|
|
10
|
+
| 'rate_limited'
|
|
11
|
+
| 'secret_unavailable',
|
|
12
|
+
message: string,
|
|
13
|
+
) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'FlowSecurityError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type { SecretRef, SecretStoreAdapter } from '@ankhorage/contracts/secrets';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FlowApprovalStorePort,
|
|
5
|
+
FlowOperationStorePort,
|
|
6
|
+
FlowRateLimitPort,
|
|
7
|
+
FlowResolvedSecrets,
|
|
8
|
+
FlowSecretCreateInput,
|
|
9
|
+
FlowSecretMetadata,
|
|
10
|
+
FlowSecretRevokeInput,
|
|
11
|
+
FlowSecurityActor,
|
|
12
|
+
FlowSecurityAuditEvent,
|
|
13
|
+
FlowSecurityAuditPort,
|
|
14
|
+
FlowSecurityGrant,
|
|
15
|
+
FlowSecurityOperation,
|
|
16
|
+
FlowSecurityOperationIdentity,
|
|
17
|
+
FlowSecurityPermit,
|
|
18
|
+
FlowSecurityPort,
|
|
19
|
+
FlowSecurityTarget,
|
|
20
|
+
} from '../../../types/security.js';
|
|
21
|
+
import { validateFlowSecurityTarget } from '../domain/validateFlowSecurityTarget.js';
|
|
22
|
+
import { FlowSecurityError } from '../FlowSecurityError.js';
|
|
23
|
+
|
|
24
|
+
const METADATA_CAPABILITY = 'secrets:metadata';
|
|
25
|
+
const CREATE_CAPABILITY = 'secrets:create';
|
|
26
|
+
const REVOKE_CAPABILITY = 'secrets:revoke';
|
|
27
|
+
type FlowSecurityDenialReason = Exclude<
|
|
28
|
+
FlowSecurityAuditEvent['reason'],
|
|
29
|
+
'credential_create' | 'credential_revoke'
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
/*** Creates the provider-neutral server-side authorization, approval, audit, and secret boundary. */
|
|
33
|
+
export const createFlowSecurityPort = (input: {
|
|
34
|
+
readonly approvalStore: FlowApprovalStorePort;
|
|
35
|
+
readonly audit: FlowSecurityAuditPort;
|
|
36
|
+
readonly grants: readonly FlowSecurityGrant[];
|
|
37
|
+
readonly operationStore: FlowOperationStorePort;
|
|
38
|
+
readonly rateLimit: FlowRateLimitPort;
|
|
39
|
+
readonly secretStore: SecretStoreAdapter;
|
|
40
|
+
}): FlowSecurityPort => ({
|
|
41
|
+
authorizeAsync: async (operation) => authorizeAsync(input, operation),
|
|
42
|
+
createSecretAsync: async (request) => createSecretAsync(input, request),
|
|
43
|
+
getSecretMetadataAsync: async ({ actor, ref }) => getSecretMetadataAsync(input, actor, ref),
|
|
44
|
+
revokeSecretAsync: async (request) => revokeSecretAsync(input, request),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/*** Authorizes and durably claims one exact operation before resolving any secret material. */
|
|
48
|
+
const authorizeAsync = async (
|
|
49
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
50
|
+
operation: FlowSecurityOperation,
|
|
51
|
+
): Promise<FlowSecurityPermit> => {
|
|
52
|
+
const identity = identityOf(operation);
|
|
53
|
+
const targetHost = validateFlowSecurityTarget(identity.target);
|
|
54
|
+
if (!isGranted(input.grants, identity.actor, identity.capability))
|
|
55
|
+
return denyAsync(input.audit, identity, targetHost, 'permission_denied');
|
|
56
|
+
const approval = await input.approvalStore.getAsync(identity);
|
|
57
|
+
if (approval?.status === 'pending')
|
|
58
|
+
return denyAsync(input.audit, identity, targetHost, 'approval_pending');
|
|
59
|
+
if (approval?.status !== 'approved' || !sameIdentity(approval.operation, identity))
|
|
60
|
+
return denyAsync(input.audit, identity, targetHost, 'approval_rejected');
|
|
61
|
+
if (!(await input.operationStore.claimAsync(identity)))
|
|
62
|
+
return denyAsync(input.audit, identity, targetHost, 'idempotency_conflict');
|
|
63
|
+
try {
|
|
64
|
+
if (
|
|
65
|
+
!(await input.rateLimit.acquireAsync({
|
|
66
|
+
actor: identity.actor,
|
|
67
|
+
capability: identity.capability,
|
|
68
|
+
operationId: identity.operationId,
|
|
69
|
+
}))
|
|
70
|
+
)
|
|
71
|
+
return denyAfterReleaseAsync(input, identity, targetHost, 'rate_limited');
|
|
72
|
+
await writeAuditAsync(input.audit, identity, targetHost, 'permitted', 'permission_denied');
|
|
73
|
+
return createPermit(input, identity);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await input.operationStore.releaseAsync(identity);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/*** Creates a one-use permit that atomically consumes the durable operation claim before side effects. */
|
|
81
|
+
const createPermit = (
|
|
82
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
83
|
+
identity: FlowSecurityOperationIdentity,
|
|
84
|
+
): FlowSecurityPermit => ({
|
|
85
|
+
operation: identity,
|
|
86
|
+
executeAsync: async (callback) => {
|
|
87
|
+
if (!(await input.operationStore.consumeAsync(identity)))
|
|
88
|
+
throw new FlowSecurityError(
|
|
89
|
+
'idempotency_conflict',
|
|
90
|
+
'The operation permit has already been consumed.',
|
|
91
|
+
);
|
|
92
|
+
return resolveAndExecuteAsync(input, identity.actor, identity.secretRefs, callback);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/*** Releases an unconsumed claim before emitting a retryable denial. */
|
|
97
|
+
const denyAfterReleaseAsync = async (
|
|
98
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
99
|
+
identity: FlowSecurityOperationIdentity,
|
|
100
|
+
targetHost: string | undefined,
|
|
101
|
+
reason: FlowSecurityDenialReason,
|
|
102
|
+
): Promise<never> => {
|
|
103
|
+
await input.operationStore.releaseAsync(identity);
|
|
104
|
+
return denyAsync(input.audit, identity, targetHost, reason);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/*** Creates a credential only after an explicit server-side scoped grant and redacted audit. */
|
|
108
|
+
const createSecretAsync = async (
|
|
109
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
110
|
+
request: FlowSecretCreateInput,
|
|
111
|
+
): Promise<FlowSecretMetadata> => {
|
|
112
|
+
const actor = snapshotActor(request.actor);
|
|
113
|
+
if (!isGranted(input.grants, actor, CREATE_CAPABILITY))
|
|
114
|
+
return denyCredentialAsync(input.audit, actor, request.ref, CREATE_CAPABILITY);
|
|
115
|
+
await writeCredentialAuditAsync(input.audit, actor, request.ref, CREATE_CAPABILITY);
|
|
116
|
+
const result = await input.secretStore.create({
|
|
117
|
+
kind: request.kind,
|
|
118
|
+
payload: request.payload,
|
|
119
|
+
...(request.provider === undefined ? {} : { provider: request.provider }),
|
|
120
|
+
ref: request.ref,
|
|
121
|
+
scope: scopeOf(actor),
|
|
122
|
+
});
|
|
123
|
+
if (!result.ok)
|
|
124
|
+
throw new FlowSecurityError('secret_unavailable', 'The requested secret is unavailable.');
|
|
125
|
+
return result.data;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/*** Returns metadata only after an explicit actor, tenant, project, and environment grant. */
|
|
129
|
+
const getSecretMetadataAsync = async (
|
|
130
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
131
|
+
actor: FlowSecurityActor,
|
|
132
|
+
ref: SecretRef,
|
|
133
|
+
) => {
|
|
134
|
+
const identity = snapshotActor(actor);
|
|
135
|
+
if (!isGranted(input.grants, identity, METADATA_CAPABILITY))
|
|
136
|
+
throw new FlowSecurityError(
|
|
137
|
+
'permission_denied',
|
|
138
|
+
'The actor is not permitted to inspect secrets.',
|
|
139
|
+
);
|
|
140
|
+
const result = await input.secretStore.getMetadata({ scope: scopeOf(identity), ref });
|
|
141
|
+
if (!result.ok)
|
|
142
|
+
throw new FlowSecurityError('secret_unavailable', 'The requested secret is unavailable.');
|
|
143
|
+
return result.data;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/*** Revokes a credential only after an explicit server-side scoped grant and redacted audit. */
|
|
147
|
+
const revokeSecretAsync = async (
|
|
148
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
149
|
+
request: FlowSecretRevokeInput,
|
|
150
|
+
): Promise<void> => {
|
|
151
|
+
const actor = snapshotActor(request.actor);
|
|
152
|
+
if (!isGranted(input.grants, actor, REVOKE_CAPABILITY))
|
|
153
|
+
return denyCredentialAsync(input.audit, actor, request.ref, REVOKE_CAPABILITY);
|
|
154
|
+
await writeCredentialAuditAsync(input.audit, actor, request.ref, REVOKE_CAPABILITY);
|
|
155
|
+
const result = await input.secretStore.remove({ ref: request.ref, scope: scopeOf(actor) });
|
|
156
|
+
if (!result.ok)
|
|
157
|
+
throw new FlowSecurityError('secret_unavailable', 'The requested secret is unavailable.');
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/*** Resolves secrets only in the server-side callback after durable consumption succeeds. */
|
|
161
|
+
const resolveAndExecuteAsync = async <T>(
|
|
162
|
+
input: Parameters<typeof createFlowSecurityPort>[0],
|
|
163
|
+
actor: FlowSecurityActor,
|
|
164
|
+
secretRefs: readonly SecretRef[],
|
|
165
|
+
callback: (secrets: FlowResolvedSecrets) => Promise<T>,
|
|
166
|
+
): Promise<T> => {
|
|
167
|
+
const resolved = await Promise.all(
|
|
168
|
+
secretRefs.map(
|
|
169
|
+
async (ref) => [ref, await resolveSecretAsync(input.secretStore, actor, ref)] as const,
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
return callback(Object.fromEntries(resolved));
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/*** Resolves one published secret reference without exposing a provider error or payload in failures. */
|
|
176
|
+
const resolveSecretAsync = async (
|
|
177
|
+
secretStore: SecretStoreAdapter,
|
|
178
|
+
actor: FlowSecurityActor,
|
|
179
|
+
ref: SecretRef,
|
|
180
|
+
) => {
|
|
181
|
+
const result = await secretStore.resolve({ scope: scopeOf(actor), ref });
|
|
182
|
+
if (!result.ok)
|
|
183
|
+
throw new FlowSecurityError('secret_unavailable', 'A required secret is unavailable.');
|
|
184
|
+
return result.data;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/*** Creates a deterministic complete identity from untrusted operation input. */
|
|
188
|
+
const identityOf = (operation: FlowSecurityOperation): FlowSecurityOperationIdentity =>
|
|
189
|
+
Object.freeze({
|
|
190
|
+
actor: snapshotActor(operation.actor),
|
|
191
|
+
capability: operation.capability,
|
|
192
|
+
idempotencyKey: operation.idempotencyKey,
|
|
193
|
+
operationId: operation.operationId,
|
|
194
|
+
secretRefs: Object.freeze(unique(operation.secretRefs ?? [])),
|
|
195
|
+
...(operation.target
|
|
196
|
+
? {
|
|
197
|
+
target: Object.freeze({
|
|
198
|
+
allowedHosts: Object.freeze([...operation.target.allowedHosts]),
|
|
199
|
+
url: operation.target.url,
|
|
200
|
+
}),
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/*** Captures a runtime-immutable actor scope before any authorization boundary can await. */
|
|
206
|
+
const snapshotActor = (actor: FlowSecurityActor): FlowSecurityActor => Object.freeze({ ...actor });
|
|
207
|
+
|
|
208
|
+
/*** Compares every authorization-relevant operation field without relying on object identity. */
|
|
209
|
+
const sameIdentity = (
|
|
210
|
+
left: FlowSecurityOperationIdentity,
|
|
211
|
+
right: FlowSecurityOperationIdentity,
|
|
212
|
+
): boolean =>
|
|
213
|
+
left.actor.id === right.actor.id &&
|
|
214
|
+
left.actor.tenantId === right.actor.tenantId &&
|
|
215
|
+
left.actor.projectId === right.actor.projectId &&
|
|
216
|
+
left.actor.environment === right.actor.environment &&
|
|
217
|
+
left.capability === right.capability &&
|
|
218
|
+
left.idempotencyKey === right.idempotencyKey &&
|
|
219
|
+
left.operationId === right.operationId &&
|
|
220
|
+
sameStrings(left.secretRefs, right.secretRefs) &&
|
|
221
|
+
sameTarget(left.target, right.target);
|
|
222
|
+
|
|
223
|
+
/*** Compares target authority exactly so approvals cannot move an operation to another host. */
|
|
224
|
+
const sameTarget = (
|
|
225
|
+
left: FlowSecurityTarget | undefined,
|
|
226
|
+
right: FlowSecurityTarget | undefined,
|
|
227
|
+
): boolean =>
|
|
228
|
+
left === undefined || right === undefined
|
|
229
|
+
? left === right
|
|
230
|
+
: left.url === right.url && sameStrings(left.allowedHosts, right.allowedHosts);
|
|
231
|
+
|
|
232
|
+
/*** Matches two ordered identity arrays exactly. */
|
|
233
|
+
const sameStrings = (left: readonly string[], right: readonly string[]): boolean =>
|
|
234
|
+
JSON.stringify(left) === JSON.stringify(right);
|
|
235
|
+
|
|
236
|
+
/*** Matches a grant only when all actor isolation dimensions are equal. */
|
|
237
|
+
const isGranted = (
|
|
238
|
+
grants: readonly FlowSecurityGrant[],
|
|
239
|
+
actor: FlowSecurityActor,
|
|
240
|
+
capability: string,
|
|
241
|
+
): boolean =>
|
|
242
|
+
grants.some(
|
|
243
|
+
(grant) =>
|
|
244
|
+
grant.actorId === actor.id &&
|
|
245
|
+
grant.capability === capability &&
|
|
246
|
+
grant.tenantId === actor.tenantId &&
|
|
247
|
+
grant.projectId === actor.projectId &&
|
|
248
|
+
grant.environment === actor.environment,
|
|
249
|
+
);
|
|
250
|
+
/*** Converts a trusted actor boundary to the published secret-store scope. */
|
|
251
|
+
const scopeOf = (actor: FlowSecurityActor) => ({
|
|
252
|
+
projectId: actor.projectId,
|
|
253
|
+
environment: actor.environment,
|
|
254
|
+
});
|
|
255
|
+
/*** Deduplicates secret references without changing their deterministic order. */
|
|
256
|
+
const unique = (refs: readonly SecretRef[]): readonly SecretRef[] => [...new Set(refs)];
|
|
257
|
+
/*** Audits a redacted decision before returning a stable denial. */
|
|
258
|
+
const denyAsync = async (
|
|
259
|
+
audit: FlowSecurityAuditPort,
|
|
260
|
+
identity: FlowSecurityOperationIdentity,
|
|
261
|
+
targetHost: string | undefined,
|
|
262
|
+
reason: FlowSecurityDenialReason,
|
|
263
|
+
): Promise<never> => {
|
|
264
|
+
await writeAuditAsync(audit, identity, targetHost, 'denied', reason);
|
|
265
|
+
throw new FlowSecurityError(reason, denialMessage(reason));
|
|
266
|
+
};
|
|
267
|
+
/*** Emits only identifiers and target host metadata to the audit edge. */
|
|
268
|
+
const writeAuditAsync = (
|
|
269
|
+
audit: FlowSecurityAuditPort,
|
|
270
|
+
identity: FlowSecurityOperationIdentity,
|
|
271
|
+
targetHost: string | undefined,
|
|
272
|
+
decision: FlowSecurityAuditEvent['decision'],
|
|
273
|
+
reason: FlowSecurityAuditEvent['reason'],
|
|
274
|
+
): Promise<void> =>
|
|
275
|
+
audit.writeAsync({
|
|
276
|
+
actorId: identity.actor.id,
|
|
277
|
+
capability: identity.capability,
|
|
278
|
+
decision,
|
|
279
|
+
operationId: identity.operationId,
|
|
280
|
+
reason,
|
|
281
|
+
secretRefs: identity.secretRefs,
|
|
282
|
+
...(targetHost ? { targetHost } : {}),
|
|
283
|
+
});
|
|
284
|
+
/*** Audits a credential-management decision without accepting its payload at the audit edge. */
|
|
285
|
+
const writeCredentialAuditAsync = (
|
|
286
|
+
audit: FlowSecurityAuditPort,
|
|
287
|
+
actor: FlowSecurityActor,
|
|
288
|
+
ref: SecretRef,
|
|
289
|
+
capability: typeof CREATE_CAPABILITY | typeof REVOKE_CAPABILITY,
|
|
290
|
+
): Promise<void> =>
|
|
291
|
+
audit.writeAsync({
|
|
292
|
+
actorId: actor.id,
|
|
293
|
+
capability,
|
|
294
|
+
decision: 'permitted',
|
|
295
|
+
operationId: ref,
|
|
296
|
+
reason: capability === CREATE_CAPABILITY ? 'credential_create' : 'credential_revoke',
|
|
297
|
+
secretRefs: [ref],
|
|
298
|
+
});
|
|
299
|
+
/*** Audits and returns a stable denial for unauthorized credential management. */
|
|
300
|
+
const denyCredentialAsync = async (
|
|
301
|
+
audit: FlowSecurityAuditPort,
|
|
302
|
+
actor: FlowSecurityActor,
|
|
303
|
+
ref: SecretRef,
|
|
304
|
+
capability: typeof CREATE_CAPABILITY | typeof REVOKE_CAPABILITY,
|
|
305
|
+
): Promise<never> => {
|
|
306
|
+
await audit.writeAsync({
|
|
307
|
+
actorId: actor.id,
|
|
308
|
+
capability,
|
|
309
|
+
decision: 'denied',
|
|
310
|
+
operationId: ref,
|
|
311
|
+
reason: 'permission_denied',
|
|
312
|
+
secretRefs: [ref],
|
|
313
|
+
});
|
|
314
|
+
throw new FlowSecurityError(
|
|
315
|
+
'permission_denied',
|
|
316
|
+
'The actor is not permitted to perform this operation.',
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
/*** Maps policy reasons to public redaction-safe error text. */
|
|
320
|
+
const denialMessage = (reason: FlowSecurityDenialReason): string => {
|
|
321
|
+
switch (reason) {
|
|
322
|
+
case 'approval_pending':
|
|
323
|
+
return 'The operation requires an approved Flow approval gate.';
|
|
324
|
+
case 'approval_rejected':
|
|
325
|
+
return 'The Flow approval gate rejected this operation.';
|
|
326
|
+
case 'idempotency_conflict':
|
|
327
|
+
return 'The idempotency key is already bound to another operation.';
|
|
328
|
+
case 'invalid_target':
|
|
329
|
+
return 'The external target is not permitted.';
|
|
330
|
+
case 'permission_denied':
|
|
331
|
+
return 'The actor is not permitted to perform this operation.';
|
|
332
|
+
case 'rate_limited':
|
|
333
|
+
return 'The operation is rate limited.';
|
|
334
|
+
case 'secret_unavailable':
|
|
335
|
+
return 'A required secret is unavailable.';
|
|
336
|
+
}
|
|
337
|
+
const impossible: never = reason;
|
|
338
|
+
return impossible;
|
|
339
|
+
};
|