@yemi33/minions 0.1.2424 → 0.1.2426

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.
Files changed (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -0,0 +1,543 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_REJECTED_STRING_MAX = 120;
4
+ const DEFAULT_IDENTIFIER_MAX = 128;
5
+ const MAX_ALLOWED_VALUES = 50;
6
+ const API_INPUT_ERROR_DETAILS_MAX = 50;
7
+ const API_INPUT_ERROR_MARKER = Symbol.for('minions.apiInputError');
8
+ const SECRET_DETAIL_MARKER = Symbol('minions.secretDetail');
9
+ // Credential-shaped field/path tokens. Aligned with the repository's env-key
10
+ // deny patterns (engine/shared.js#ENGINE_DEFAULTS.managedSpawn.envKeyDenyPatterns:
11
+ // _SECRET, _TOKEN, _API_KEY, _PASSWORD, _PASSWD, _CREDENTIALS, _AUTH, _PAT, …) so
12
+ // short credential names such as `pat`, `auth`, and `passwd` are redacted. Matched
13
+ // against camelCase/underscore/kebab TOKENS (not raw substrings) so ordinary
14
+ // fields that merely contain a token as a substring — `path` (contains "pat"),
15
+ // `author` (contains "auth") — are NOT redacted.
16
+ const SECRET_FIELD_TOKENS = new Set([
17
+ 'password', 'passwd', 'passphrase', 'pwd',
18
+ 'secret', 'secrets', 'clientsecret',
19
+ 'token', 'tokens', 'accesstoken', 'refreshtoken', 'idtoken', 'bearer',
20
+ 'apikey', 'apikeys', 'accesskey', 'privatekey', 'secretkey',
21
+ 'authorization', 'auth', 'oauth',
22
+ 'credential', 'credentials',
23
+ 'cookie', 'session', 'sessionid',
24
+ 'pat', 'signature', 'sig',
25
+ ]);
26
+ // Adjacent-token pairs that only read as credential-shaped once joined
27
+ // (`api key` → apikey, `access key` → accesskey, `private key` → privatekey,
28
+ // `client secret` → clientsecret, `refresh token` → refreshtoken).
29
+ const SECRET_TOKEN_PAIRS = new Set([
30
+ 'apikey', 'accesskey', 'privatekey', 'secretkey', 'clientsecret',
31
+ 'accesstoken', 'refreshtoken', 'idtoken', 'sessionid',
32
+ ]);
33
+ const DEFAULT_IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
34
+
35
+ function _splitFieldTokens(text) {
36
+ return String(text == null ? '' : text)
37
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase boundary
38
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // ACRONYMWord boundary
39
+ .split(/[^A-Za-z0-9]+/) // separators (._-/ space …)
40
+ .map(token => token.toLowerCase())
41
+ .filter(Boolean);
42
+ }
43
+
44
+ function hasOwn(value, key) {
45
+ return Object.prototype.hasOwnProperty.call(value, key);
46
+ }
47
+
48
+ function normalizeCode(value, fallback) {
49
+ const code = typeof value === 'string' ? value.trim() : '';
50
+ return /^[a-z][a-z0-9-]*$/.test(code) ? code : fallback;
51
+ }
52
+
53
+ function normalizePath(path) {
54
+ if (Array.isArray(path)) {
55
+ let result = '';
56
+ for (const segment of path) {
57
+ if (Number.isInteger(segment) && segment >= 0) {
58
+ result += `[${segment}]`;
59
+ continue;
60
+ }
61
+ const part = String(segment == null ? '' : segment).trim();
62
+ if (!part) continue;
63
+ result += result ? `.${part}` : part;
64
+ }
65
+ return result.slice(0, 256);
66
+ }
67
+ return typeof path === 'string' ? path.trim().slice(0, 256) : '';
68
+ }
69
+
70
+ function fieldFromPath(path) {
71
+ if (Array.isArray(path)) {
72
+ for (let i = path.length - 1; i >= 0; i -= 1) {
73
+ if (typeof path[i] === 'string' && path[i].trim()) return path[i].trim().slice(0, 128);
74
+ }
75
+ }
76
+ const normalized = normalizePath(path);
77
+ if (!normalized) return '';
78
+ const match = normalized.match(/(?:^|\.)([^.[\]]+)(?:\[\d+\])?$/);
79
+ return match ? match[1].slice(0, 128) : '';
80
+ }
81
+
82
+ function isSecretLikePath(path, field) {
83
+ const parts = [];
84
+ if (Array.isArray(path)) {
85
+ for (const segment of path) parts.push(String(segment == null ? '' : segment));
86
+ } else {
87
+ parts.push(String(path == null ? '' : path));
88
+ }
89
+ parts.push(String(field == null ? '' : field));
90
+ for (const part of parts) {
91
+ const tokens = _splitFieldTokens(part);
92
+ for (let i = 0; i < tokens.length; i += 1) {
93
+ if (SECRET_FIELD_TOKENS.has(tokens[i])) return true;
94
+ if (i + 1 < tokens.length && SECRET_TOKEN_PAIRS.has(tokens[i] + tokens[i + 1])) return true;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+
100
+ function boundedString(value, maxLength) {
101
+ const text = String(value);
102
+ const max = Number.isInteger(maxLength) && maxLength > 0
103
+ ? maxLength
104
+ : DEFAULT_REJECTED_STRING_MAX;
105
+ return text.length <= max ? text : `${text.slice(0, max)}...`;
106
+ }
107
+
108
+ function existingContainerSummary(value) {
109
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
110
+ const keys = Object.keys(value);
111
+ if ((value.type === 'array' || value.type === 'buffer')
112
+ && keys.length === 2
113
+ && keys.includes('length')
114
+ && Number.isInteger(value.length)
115
+ && value.length >= 0) {
116
+ return { type: value.type, length: value.length };
117
+ }
118
+ if (value.type === 'object'
119
+ && keys.length === 2
120
+ && keys.includes('keyCount')
121
+ && Number.isInteger(value.keyCount)
122
+ && value.keyCount >= 0) {
123
+ return { type: 'object', keyCount: value.keyCount };
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function summarizeRejectedValue(value, options = {}) {
129
+ if (options.secret === true || isSecretLikePath(options.path, options.field)) return '[REDACTED]';
130
+ if (value === null) return null;
131
+ if (value === undefined) return '[undefined]';
132
+ if (typeof value === 'string') return boundedString(value, options.maxStringLength);
133
+ if (typeof value === 'boolean') return value;
134
+ if (typeof value === 'number') return Number.isFinite(value) ? value : `[${String(value)}]`;
135
+ if (typeof value === 'bigint') return boundedString(`${value}n`, options.maxStringLength);
136
+ if (typeof value === 'symbol') return '[symbol]';
137
+ if (typeof value === 'function') return '[function]';
138
+ const priorSummary = existingContainerSummary(value);
139
+ if (priorSummary) return priorSummary;
140
+ if (Buffer.isBuffer(value)) return { type: 'buffer', length: value.length };
141
+ if (Array.isArray(value)) return { type: 'array', length: value.length };
142
+ if (typeof value === 'object') {
143
+ let keyCount = 0;
144
+ try { keyCount = Object.keys(value).length; } catch {}
145
+ return { type: 'object', keyCount };
146
+ }
147
+ return boundedString(value, options.maxStringLength);
148
+ }
149
+
150
+ function summarizeAllowedValues(values, options = {}) {
151
+ if (!Array.isArray(values)
152
+ || options.secret === true
153
+ || isSecretLikePath(options.path, options.field)) return undefined;
154
+ return values.slice(0, MAX_ALLOWED_VALUES).map(value => (
155
+ summarizeRejectedValue(value, { maxStringLength: 80 })
156
+ ));
157
+ }
158
+
159
+ function normalizeDetail(detail, defaults = {}) {
160
+ const source = detail && typeof detail === 'object' && !Array.isArray(detail)
161
+ ? detail
162
+ : {};
163
+ const rawPath = source.path !== undefined ? source.path : defaults.path;
164
+ const rawField = source.field !== undefined
165
+ ? source.field
166
+ : (defaults.field || fieldFromPath(rawPath));
167
+ const secret = source[SECRET_DETAIL_MARKER] === true
168
+ || source.secret === true
169
+ || isSecretLikePath(rawPath, rawField);
170
+ const path = normalizePath(rawPath);
171
+ const field = String(rawField).trim().slice(0, 128);
172
+ const code = normalizeCode(source.code, normalizeCode(defaults.code, 'invalid-input'));
173
+ const message = boundedString(
174
+ source.message !== undefined ? source.message : (defaults.message || 'Invalid input'),
175
+ 500,
176
+ );
177
+ const normalized = { code, message };
178
+ if (secret) Object.defineProperty(normalized, SECRET_DETAIL_MARKER, { value: true });
179
+ if (field) normalized.field = field;
180
+ if (path) normalized.path = path;
181
+ if (typeof source.expected === 'string' && source.expected.trim()) {
182
+ normalized.expected = source.expected.trim().slice(0, 120);
183
+ }
184
+ if (hasOwn(source, 'rejectedValue')) {
185
+ normalized.rejectedValue = summarizeRejectedValue(source.rejectedValue, {
186
+ path,
187
+ field,
188
+ secret,
189
+ maxStringLength: source.maxRejectedStringLength,
190
+ });
191
+ }
192
+ const allowedValues = summarizeAllowedValues(source.allowedValues, { path, field, secret });
193
+ if (allowedValues) normalized.allowedValues = allowedValues;
194
+ for (const key of ['min', 'max', 'minLength', 'maxLength']) {
195
+ if (Number.isFinite(source[key])) normalized[key] = source[key];
196
+ }
197
+ if (typeof source.inclusiveMin === 'boolean') normalized.inclusiveMin = source.inclusiveMin;
198
+ if (typeof source.inclusiveMax === 'boolean') normalized.inclusiveMax = source.inclusiveMax;
199
+ return normalized;
200
+ }
201
+
202
+ class ApiInputError extends Error {
203
+ constructor(message, options = {}) {
204
+ const safeMessage = boundedString(message || 'Invalid input', 500);
205
+ super(safeMessage);
206
+ this.name = 'ApiInputError';
207
+ this.code = normalizeCode(options.code, 'invalid-input');
208
+ this.statusCode = Number.isInteger(options.statusCode)
209
+ && options.statusCode >= 400
210
+ && options.statusCode < 500
211
+ ? options.statusCode
212
+ : 400;
213
+ Object.defineProperty(this, API_INPUT_ERROR_MARKER, { value: true });
214
+
215
+ const suppliedDetails = Array.isArray(options.details) ? options.details : [];
216
+ const hasInlineDetail = ['field', 'path', 'expected', 'rejectedValue', 'allowedValues', 'min', 'max', 'minLength', 'maxLength']
217
+ .some(key => hasOwn(options, key));
218
+ const rawDetails = suppliedDetails.length
219
+ ? suppliedDetails
220
+ : (hasInlineDetail ? [options] : []);
221
+ this.details = rawDetails.slice(0, API_INPUT_ERROR_DETAILS_MAX).map(detail => normalizeDetail(detail, {
222
+ code: this.code,
223
+ message: safeMessage,
224
+ field: options.field,
225
+ path: options.path,
226
+ }));
227
+
228
+ const first = this.details[0];
229
+ if (first) {
230
+ for (const key of ['field', 'path', 'rejectedValue', 'allowedValues', 'expected', 'min', 'max', 'minLength', 'maxLength']) {
231
+ if (hasOwn(first, key)) this[key] = first[key];
232
+ }
233
+ }
234
+ if (options.cause !== undefined) this.cause = options.cause;
235
+ }
236
+ }
237
+
238
+ function isApiInputError(error) {
239
+ return !!(error && (error instanceof ApiInputError || error[API_INPUT_ERROR_MARKER] === true));
240
+ }
241
+
242
+ function statusBearingCode(error) {
243
+ return error instanceof Error
244
+ && Number.isInteger(error.statusCode)
245
+ && error.statusCode >= 400
246
+ && error.statusCode <= 599
247
+ ? error.statusCode
248
+ : null;
249
+ }
250
+
251
+ function exposedErrorDetails(error) {
252
+ if (!Array.isArray(error && error.details)) return [];
253
+ return error.details
254
+ .filter(detail => detail && typeof detail === 'object' && !Array.isArray(detail))
255
+ .slice(0, API_INPUT_ERROR_DETAILS_MAX)
256
+ .map(detail => normalizeDetail(detail, {
257
+ code: error.code,
258
+ message: error.message,
259
+ field: error.field,
260
+ path: error.path,
261
+ }));
262
+ }
263
+
264
+ function exposedEnvelope(error, fallbackCode) {
265
+ const message = boundedString(error && error.message ? error.message : 'Request failed', 500);
266
+ const code = normalizeCode(error && error.code, fallbackCode);
267
+ const body = { error: message, code };
268
+ const details = exposedErrorDetails(error);
269
+ if (details.length) {
270
+ body.details = details;
271
+ const first = details[0];
272
+ for (const key of ['field', 'path', 'rejectedValue', 'allowedValues', 'expected', 'min', 'max', 'minLength', 'maxLength']) {
273
+ if (hasOwn(first, key)) body[key] = first[key];
274
+ }
275
+ }
276
+ return body;
277
+ }
278
+
279
+ function toApiErrorResponse(error) {
280
+ if (isApiInputError(error)) {
281
+ return {
282
+ statusCode: error.statusCode,
283
+ body: exposedEnvelope(error, 'invalid-input'),
284
+ };
285
+ }
286
+ const statusCode = statusBearingCode(error);
287
+ if (statusCode !== null) {
288
+ if (statusCode >= 500) {
289
+ return {
290
+ statusCode,
291
+ body: {
292
+ error: 'Internal server error',
293
+ code: 'internal-error',
294
+ },
295
+ };
296
+ }
297
+ return {
298
+ statusCode,
299
+ body: exposedEnvelope(error, 'request-failed'),
300
+ };
301
+ }
302
+ return {
303
+ statusCode: 500,
304
+ body: {
305
+ error: 'Internal server error',
306
+ code: 'internal-error',
307
+ },
308
+ };
309
+ }
310
+
311
+ function buildApiErrorEnvelope(error) {
312
+ return toApiErrorResponse(error).body;
313
+ }
314
+
315
+ function optionalValue(value, options) {
316
+ if (value === undefined && options.optional === true) return true;
317
+ if (value === null && options.nullable === true) return true;
318
+ return false;
319
+ }
320
+
321
+ function validationLabel(options) {
322
+ return normalizePath(options.path) || options.field || 'value';
323
+ }
324
+
325
+ function validationError(value, options, defaults, extra = {}) {
326
+ const message = options.message || `${validationLabel(options)} ${defaults.expectation}`;
327
+ throw new ApiInputError(message, {
328
+ code: options.code || defaults.code,
329
+ statusCode: options.statusCode,
330
+ field: options.field,
331
+ path: options.path !== undefined ? options.path : options.field,
332
+ expected: defaults.expected,
333
+ rejectedValue: value,
334
+ ...(options.secret === true ? { secret: true } : {}),
335
+ ...extra,
336
+ });
337
+ }
338
+
339
+ function validatePlainObject(value, options = {}) {
340
+ if (optionalValue(value, options)) return value;
341
+ const prototype = value && typeof value === 'object' ? Object.getPrototypeOf(value) : null;
342
+ if (!value || typeof value !== 'object' || Array.isArray(value)
343
+ || (prototype !== Object.prototype && prototype !== null)) {
344
+ validationError(value, options, {
345
+ code: 'invalid-object',
346
+ expectation: 'must be a plain object',
347
+ expected: 'plain object',
348
+ });
349
+ }
350
+ return value;
351
+ }
352
+
353
+ function validateString(value, options = {}) {
354
+ if (optionalValue(value, options)) return value;
355
+ if (typeof value !== 'string') {
356
+ validationError(value, options, {
357
+ code: 'invalid-string',
358
+ expectation: 'must be a string',
359
+ expected: 'string',
360
+ });
361
+ }
362
+ const normalized = options.trim === true ? value.trim() : value;
363
+ const minLength = Number.isInteger(options.minLength) && options.minLength >= 0
364
+ ? options.minLength
365
+ : (options.allowEmpty === false ? 1 : null);
366
+ const maxLength = Number.isInteger(options.maxLength) && options.maxLength >= 0
367
+ ? options.maxLength
368
+ : null;
369
+ if ((minLength !== null && normalized.length < minLength)
370
+ || (maxLength !== null && normalized.length > maxLength)) {
371
+ validationError(value, options, {
372
+ code: 'invalid-string',
373
+ expectation: 'has an invalid length',
374
+ expected: 'string',
375
+ }, {
376
+ ...(minLength !== null ? { minLength } : {}),
377
+ ...(maxLength !== null ? { maxLength } : {}),
378
+ });
379
+ }
380
+ return normalized;
381
+ }
382
+
383
+ function validateBoolean(value, options = {}) {
384
+ if (optionalValue(value, options)) return value;
385
+ if (typeof value !== 'boolean') {
386
+ validationError(value, options, {
387
+ code: 'invalid-boolean',
388
+ expectation: 'must be a boolean',
389
+ expected: 'boolean',
390
+ });
391
+ }
392
+ return value;
393
+ }
394
+
395
+ function validateFiniteNumber(value, options = {}) {
396
+ if (optionalValue(value, options)) return value;
397
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
398
+ validationError(value, options, {
399
+ code: 'invalid-number',
400
+ expectation: 'must be a finite number',
401
+ expected: 'finite number',
402
+ });
403
+ }
404
+ return value;
405
+ }
406
+
407
+ function validateInteger(value, options = {}) {
408
+ if (optionalValue(value, options)) return value;
409
+ if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
410
+ validationError(value, options, {
411
+ code: 'invalid-integer',
412
+ expectation: 'must be an integer',
413
+ expected: 'integer',
414
+ });
415
+ }
416
+ return value;
417
+ }
418
+
419
+ function validateArray(value, options = {}) {
420
+ if (optionalValue(value, options)) return value;
421
+ if (!Array.isArray(value)) {
422
+ validationError(value, options, {
423
+ code: 'invalid-array',
424
+ expectation: 'must be an array',
425
+ expected: 'array',
426
+ });
427
+ }
428
+ const minLength = Number.isInteger(options.minLength) && options.minLength >= 0
429
+ ? options.minLength
430
+ : null;
431
+ const maxLength = Number.isInteger(options.maxLength) && options.maxLength >= 0
432
+ ? options.maxLength
433
+ : null;
434
+ if ((minLength !== null && value.length < minLength)
435
+ || (maxLength !== null && value.length > maxLength)) {
436
+ validationError(value, options, {
437
+ code: 'invalid-array',
438
+ expectation: 'has an invalid length',
439
+ expected: 'array',
440
+ }, {
441
+ ...(minLength !== null ? { minLength } : {}),
442
+ ...(maxLength !== null ? { maxLength } : {}),
443
+ });
444
+ }
445
+ if (typeof options.itemValidator === 'function') {
446
+ value.forEach((entry, index) => {
447
+ options.itemValidator(entry, {
448
+ index,
449
+ path: [...(Array.isArray(options.path) ? options.path : [options.path || options.field || 'value']), index],
450
+ });
451
+ });
452
+ }
453
+ return value;
454
+ }
455
+
456
+ function validateEnum(value, options = {}) {
457
+ if (optionalValue(value, options)) return value;
458
+ if (!Array.isArray(options.values) || options.values.length === 0) {
459
+ throw new TypeError('validateEnum requires a non-empty values array');
460
+ }
461
+ if (!options.values.some(candidate => Object.is(candidate, value))) {
462
+ validationError(value, options, {
463
+ code: 'invalid-enum',
464
+ expectation: 'must be one of the allowed values',
465
+ expected: 'enum',
466
+ }, { allowedValues: options.values });
467
+ }
468
+ return value;
469
+ }
470
+
471
+ function validateRange(value, options = {}) {
472
+ if (optionalValue(value, options)) return value;
473
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
474
+ validationError(value, options, {
475
+ code: 'invalid-number',
476
+ expectation: 'must be a finite number',
477
+ expected: 'finite number',
478
+ });
479
+ }
480
+ const hasMin = Number.isFinite(options.min);
481
+ const hasMax = Number.isFinite(options.max);
482
+ if (!hasMin && !hasMax) throw new TypeError('validateRange requires a finite min or max');
483
+ const inclusiveMin = options.inclusiveMin !== false;
484
+ const inclusiveMax = options.inclusiveMax !== false;
485
+ const below = hasMin && (inclusiveMin ? value < options.min : value <= options.min);
486
+ const above = hasMax && (inclusiveMax ? value > options.max : value >= options.max);
487
+ if (below || above) {
488
+ validationError(value, options, {
489
+ code: 'out-of-range',
490
+ expectation: 'is outside the allowed range',
491
+ expected: 'number in range',
492
+ }, {
493
+ ...(hasMin ? { min: options.min, inclusiveMin } : {}),
494
+ ...(hasMax ? { max: options.max, inclusiveMax } : {}),
495
+ });
496
+ }
497
+ return value;
498
+ }
499
+
500
+ function validateIdentifier(value, options = {}) {
501
+ if (optionalValue(value, options)) return value;
502
+ if (typeof value !== 'string') {
503
+ validationError(value, options, {
504
+ code: 'invalid-identifier',
505
+ expectation: 'must be a valid identifier',
506
+ expected: 'identifier',
507
+ });
508
+ }
509
+ const normalized = options.trim === true ? value.trim() : value;
510
+ const maxLength = Number.isInteger(options.maxLength) && options.maxLength > 0
511
+ ? options.maxLength
512
+ : DEFAULT_IDENTIFIER_MAX;
513
+ const pattern = options.pattern instanceof RegExp ? options.pattern : DEFAULT_IDENTIFIER_RE;
514
+ pattern.lastIndex = 0;
515
+ const validPattern = pattern.test(normalized);
516
+ pattern.lastIndex = 0;
517
+ if (!normalized || normalized.length > maxLength || !validPattern) {
518
+ validationError(value, options, {
519
+ code: 'invalid-identifier',
520
+ expectation: 'must be a valid identifier',
521
+ expected: 'identifier',
522
+ }, { minLength: 1, maxLength });
523
+ }
524
+ return normalized;
525
+ }
526
+
527
+ module.exports = {
528
+ API_INPUT_ERROR_DETAILS_MAX,
529
+ ApiInputError,
530
+ buildApiErrorEnvelope,
531
+ toApiErrorResponse,
532
+ summarizeRejectedValue,
533
+ isApiInputError,
534
+ validatePlainObject,
535
+ validateString,
536
+ validateBoolean,
537
+ validateFiniteNumber,
538
+ validateInteger,
539
+ validateArray,
540
+ validateEnum,
541
+ validateRange,
542
+ validateIdentifier,
543
+ };