@yemi33/minions 0.1.2423 → 0.1.2425
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/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +6 -4
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/internal-install.md +190 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +12 -7
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-auto-fix-dispatch.md +3 -3
- package/docs/pr-review-fix-loop.md +6 -6
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- 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
|
+
};
|