hippo-memory 1.32.0 → 1.33.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/dist/api.d.ts +24 -17
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +78 -142
- package/dist/api.js.map +1 -1
- package/dist/audit-prune.d.ts.map +1 -1
- package/dist/audit-prune.js +7 -1
- package/dist/audit-prune.js.map +1 -1
- package/dist/audit.d.ts +3 -2
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +13 -6
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/autolearn.js +4 -4
- package/dist/autolearn.js.map +1 -1
- package/dist/benchmarks/e1.3/incident-recall-eval.js +4 -0
- package/dist/benchmarks/e1.3/incident-recall-eval.js.map +1 -1
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +2 -2
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +1 -1
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +93 -66
- package/dist/capture.js.map +1 -1
- package/dist/client.d.ts +0 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -14
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -4
- package/dist/config.js.map +1 -1
- package/dist/connectors/github/backfill.d.ts.map +1 -1
- package/dist/connectors/github/backfill.js +25 -16
- package/dist/connectors/github/backfill.js.map +1 -1
- package/dist/connectors/github/cli-impl.d.ts +2 -1
- package/dist/connectors/github/cli-impl.d.ts.map +1 -1
- package/dist/connectors/github/cli-impl.js +20 -4
- package/dist/connectors/github/cli-impl.js.map +1 -1
- package/dist/connectors/github/deletion.d.ts.map +1 -1
- package/dist/connectors/github/deletion.js +2 -0
- package/dist/connectors/github/deletion.js.map +1 -1
- package/dist/connectors/github/dlq.d.ts.map +1 -1
- package/dist/connectors/github/dlq.js +9 -1
- package/dist/connectors/github/dlq.js.map +1 -1
- package/dist/connectors/github/idempotency.d.ts.map +1 -1
- package/dist/connectors/github/idempotency.js +2 -0
- package/dist/connectors/github/idempotency.js.map +1 -1
- package/dist/connectors/github/octokit-client.d.ts +2 -1
- package/dist/connectors/github/octokit-client.d.ts.map +1 -1
- package/dist/connectors/github/octokit-client.js +8 -3
- package/dist/connectors/github/octokit-client.js.map +1 -1
- package/dist/connectors/github/tenant-routing.d.ts.map +1 -1
- package/dist/connectors/github/tenant-routing.js +8 -0
- package/dist/connectors/github/tenant-routing.js.map +1 -1
- package/dist/connectors/github/types.d.ts +14 -5
- package/dist/connectors/github/types.d.ts.map +1 -1
- package/dist/connectors/github/types.js +35 -29
- package/dist/connectors/github/types.js.map +1 -1
- package/dist/connectors/slack/backfill.d.ts.map +1 -1
- package/dist/connectors/slack/backfill.js +2 -0
- package/dist/connectors/slack/backfill.js.map +1 -1
- package/dist/connectors/slack/deletion.d.ts.map +1 -1
- package/dist/connectors/slack/deletion.js +2 -0
- package/dist/connectors/slack/deletion.js.map +1 -1
- package/dist/connectors/slack/dlq.d.ts.map +1 -1
- package/dist/connectors/slack/dlq.js +3 -1
- package/dist/connectors/slack/dlq.js.map +1 -1
- package/dist/connectors/slack/idempotency.d.ts.map +1 -1
- package/dist/connectors/slack/idempotency.js +2 -0
- package/dist/connectors/slack/idempotency.js.map +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts.map +1 -1
- package/dist/connectors/slack/tenant-routing.js +4 -0
- package/dist/connectors/slack/tenant-routing.js.map +1 -1
- package/dist/connectors/slack/types.d.ts +8 -3
- package/dist/connectors/slack/types.d.ts.map +1 -1
- package/dist/connectors/slack/types.js +22 -10
- package/dist/connectors/slack/types.js.map +1 -1
- package/dist/connectors/slack/web-client.d.ts +0 -9
- package/dist/connectors/slack/web-client.d.ts.map +1 -1
- package/dist/connectors/slack/web-client.js +11 -11
- package/dist/connectors/slack/web-client.js.map +1 -1
- package/dist/connectors/slack/workspaces.d.ts.map +1 -1
- package/dist/connectors/slack/workspaces.js +2 -0
- package/dist/connectors/slack/workspaces.js.map +1 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +8 -3
- package/dist/consolidate.js.map +1 -1
- package/dist/customer-notes.d.ts.map +1 -1
- package/dist/customer-notes.js +17 -0
- package/dist/customer-notes.js.map +1 -1
- package/dist/dag.js +1 -1
- package/dist/dag.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +5 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +24 -0
- package/dist/db.js.map +1 -1
- package/dist/decisions.d.ts.map +1 -1
- package/dist/decisions.js +10 -0
- package/dist/decisions.js.map +1 -1
- package/dist/dedupe.d.ts +14 -6
- package/dist/dedupe.d.ts.map +1 -1
- package/dist/dedupe.js +61 -39
- package/dist/dedupe.js.map +1 -1
- package/dist/embedding-provider.d.ts.map +1 -1
- package/dist/embedding-provider.js +24 -15
- package/dist/embedding-provider.js.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +17 -1
- package/dist/embeddings.js.map +1 -1
- package/dist/eval-suite.d.ts.map +1 -1
- package/dist/eval-suite.js +5 -0
- package/dist/eval-suite.js.map +1 -1
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +10 -3
- package/dist/extract.js.map +1 -1
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +26 -12
- package/dist/goals.js.map +1 -1
- package/dist/graph-extract.d.ts.map +1 -1
- package/dist/graph-extract.js +9 -1
- package/dist/graph-extract.js.map +1 -1
- package/dist/graph-view.d.ts.map +1 -1
- package/dist/graph-view.js.map +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +38 -13
- package/dist/graph.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +42 -21
- package/dist/hooks.js.map +1 -1
- package/dist/importers.d.ts.map +1 -1
- package/dist/importers.js +23 -42
- package/dist/importers.js.map +1 -1
- package/dist/incidents.d.ts.map +1 -1
- package/dist/incidents.js +14 -0
- package/dist/incidents.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +58 -28
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory-value.d.ts +0 -10
- package/dist/memory-value.d.ts.map +1 -1
- package/dist/memory-value.js +8 -2
- package/dist/memory-value.js.map +1 -1
- package/dist/memory.js.map +1 -1
- package/dist/physics-state.d.ts.map +1 -1
- package/dist/physics-state.js +5 -0
- package/dist/physics-state.js.map +1 -1
- package/dist/physics.js.map +1 -1
- package/dist/policies.d.ts.map +1 -1
- package/dist/policies.js +22 -0
- package/dist/policies.js.map +1 -1
- package/dist/predictions.d.ts.map +1 -1
- package/dist/predictions.js +14 -0
- package/dist/predictions.js.map +1 -1
- package/dist/processes.d.ts +7 -1
- package/dist/processes.d.ts.map +1 -1
- package/dist/processes.js +21 -2
- package/dist/processes.js.map +1 -1
- package/dist/project-briefs.d.ts.map +1 -1
- package/dist/project-briefs.js +28 -2
- package/dist/project-briefs.js.map +1 -1
- package/dist/raw-archive-mirror-cleanup.d.ts.map +1 -1
- package/dist/raw-archive-mirror-cleanup.js +2 -0
- package/dist/raw-archive-mirror-cleanup.js.map +1 -1
- package/dist/raw-archive.d.ts +0 -9
- package/dist/raw-archive.d.ts.map +1 -1
- package/dist/raw-archive.js +3 -9
- package/dist/raw-archive.js.map +1 -1
- package/dist/recall-scope.d.ts.map +1 -1
- package/dist/recall-scope.js +11 -6
- package/dist/recall-scope.js.map +1 -1
- package/dist/recall-trace.d.ts.map +1 -1
- package/dist/recall-trace.js +7 -3
- package/dist/recall-trace.js.map +1 -1
- package/dist/refine-llm.d.ts.map +1 -1
- package/dist/refine-llm.js +3 -0
- package/dist/refine-llm.js.map +1 -1
- package/dist/reject-flow.d.ts.map +1 -1
- package/dist/reject-flow.js +2 -0
- package/dist/reject-flow.js.map +1 -1
- package/dist/rejection.d.ts.map +1 -1
- package/dist/rejection.js +3 -0
- package/dist/rejection.js.map +1 -1
- package/dist/replay.js.map +1 -1
- package/dist/rerankers/cross-encoder.d.ts.map +1 -1
- package/dist/rerankers/cross-encoder.js +11 -4
- package/dist/rerankers/cross-encoder.js.map +1 -1
- package/dist/rerankers/index.d.ts.map +1 -1
- package/dist/rerankers/index.js +5 -3
- package/dist/rerankers/index.js.map +1 -1
- package/dist/rerankers/llm.d.ts.map +1 -1
- package/dist/rerankers/llm.js +8 -5
- package/dist/rerankers/llm.js.map +1 -1
- package/dist/rerankers/types.d.ts +6 -1
- package/dist/rerankers/types.d.ts.map +1 -1
- package/dist/scheduler.d.ts +16 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +15 -5
- package/dist/scheduler.js.map +1 -1
- package/dist/search.d.ts +4 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/server-detect.d.ts.map +1 -1
- package/dist/server-detect.js +2 -0
- package/dist/server-detect.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +194 -135
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +10 -3
- package/dist/skills.js.map +1 -1
- package/dist/src/api.js +78 -142
- package/dist/src/api.js.map +1 -1
- package/dist/src/audit-prune.js +7 -1
- package/dist/src/audit-prune.js.map +1 -1
- package/dist/src/audit.js +13 -6
- package/dist/src/audit.js.map +1 -1
- package/dist/src/auth.js +6 -0
- package/dist/src/auth.js.map +1 -1
- package/dist/src/autolearn.js +4 -4
- package/dist/src/autolearn.js.map +1 -1
- package/dist/src/capture.js +93 -66
- package/dist/src/capture.js.map +1 -1
- package/dist/src/client.js +32 -14
- package/dist/src/client.js.map +1 -1
- package/dist/src/config.js +7 -4
- package/dist/src/config.js.map +1 -1
- package/dist/src/connectors/github/backfill.js +25 -16
- package/dist/src/connectors/github/backfill.js.map +1 -1
- package/dist/src/connectors/github/cli-impl.js +20 -4
- package/dist/src/connectors/github/cli-impl.js.map +1 -1
- package/dist/src/connectors/github/deletion.js +2 -0
- package/dist/src/connectors/github/deletion.js.map +1 -1
- package/dist/src/connectors/github/dlq.js +9 -1
- package/dist/src/connectors/github/dlq.js.map +1 -1
- package/dist/src/connectors/github/idempotency.js +2 -0
- package/dist/src/connectors/github/idempotency.js.map +1 -1
- package/dist/src/connectors/github/octokit-client.js +8 -3
- package/dist/src/connectors/github/octokit-client.js.map +1 -1
- package/dist/src/connectors/github/tenant-routing.js +8 -0
- package/dist/src/connectors/github/tenant-routing.js.map +1 -1
- package/dist/src/connectors/github/types.js +35 -29
- package/dist/src/connectors/github/types.js.map +1 -1
- package/dist/src/connectors/slack/backfill.js +2 -0
- package/dist/src/connectors/slack/backfill.js.map +1 -1
- package/dist/src/connectors/slack/deletion.js +2 -0
- package/dist/src/connectors/slack/deletion.js.map +1 -1
- package/dist/src/connectors/slack/dlq.js +3 -1
- package/dist/src/connectors/slack/dlq.js.map +1 -1
- package/dist/src/connectors/slack/idempotency.js +2 -0
- package/dist/src/connectors/slack/idempotency.js.map +1 -1
- package/dist/src/connectors/slack/tenant-routing.js +4 -0
- package/dist/src/connectors/slack/tenant-routing.js.map +1 -1
- package/dist/src/connectors/slack/types.js +22 -10
- package/dist/src/connectors/slack/types.js.map +1 -1
- package/dist/src/connectors/slack/web-client.js +11 -11
- package/dist/src/connectors/slack/web-client.js.map +1 -1
- package/dist/src/connectors/slack/workspaces.js +2 -0
- package/dist/src/connectors/slack/workspaces.js.map +1 -1
- package/dist/src/consolidate.js +8 -3
- package/dist/src/consolidate.js.map +1 -1
- package/dist/src/customer-notes.js +17 -0
- package/dist/src/customer-notes.js.map +1 -1
- package/dist/src/dag.js +1 -1
- package/dist/src/dag.js.map +1 -1
- package/dist/src/dashboard.js +5 -2
- package/dist/src/dashboard.js.map +1 -1
- package/dist/src/db.js +24 -0
- package/dist/src/db.js.map +1 -1
- package/dist/src/decisions.js +10 -0
- package/dist/src/decisions.js.map +1 -1
- package/dist/src/dedupe.js +61 -39
- package/dist/src/dedupe.js.map +1 -1
- package/dist/src/embedding-provider.js +24 -15
- package/dist/src/embedding-provider.js.map +1 -1
- package/dist/src/embeddings.js +17 -1
- package/dist/src/embeddings.js.map +1 -1
- package/dist/src/eval-suite.js +5 -0
- package/dist/src/eval-suite.js.map +1 -1
- package/dist/src/extract.js +10 -3
- package/dist/src/extract.js.map +1 -1
- package/dist/src/goals.js +26 -12
- package/dist/src/goals.js.map +1 -1
- package/dist/src/graph-extract.js +9 -1
- package/dist/src/graph-extract.js.map +1 -1
- package/dist/src/graph-view.js.map +1 -1
- package/dist/src/graph.js +38 -13
- package/dist/src/graph.js.map +1 -1
- package/dist/src/hooks.js +42 -21
- package/dist/src/hooks.js.map +1 -1
- package/dist/src/importers.js +23 -42
- package/dist/src/importers.js.map +1 -1
- package/dist/src/incidents.js +14 -0
- package/dist/src/incidents.js.map +1 -1
- package/dist/src/mcp/server.js +58 -28
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/memory-value.js +8 -2
- package/dist/src/memory-value.js.map +1 -1
- package/dist/src/memory.js.map +1 -1
- package/dist/src/physics-state.js +5 -0
- package/dist/src/physics-state.js.map +1 -1
- package/dist/src/physics.js.map +1 -1
- package/dist/src/policies.js +22 -0
- package/dist/src/policies.js.map +1 -1
- package/dist/src/predictions.js +14 -0
- package/dist/src/predictions.js.map +1 -1
- package/dist/src/processes.js +21 -2
- package/dist/src/processes.js.map +1 -1
- package/dist/src/project-briefs.js +28 -2
- package/dist/src/project-briefs.js.map +1 -1
- package/dist/src/raw-archive-mirror-cleanup.js +2 -0
- package/dist/src/raw-archive-mirror-cleanup.js.map +1 -1
- package/dist/src/raw-archive.js +3 -9
- package/dist/src/raw-archive.js.map +1 -1
- package/dist/src/recall-scope.js +11 -6
- package/dist/src/recall-scope.js.map +1 -1
- package/dist/src/recall-trace.js +7 -3
- package/dist/src/recall-trace.js.map +1 -1
- package/dist/src/refine-llm.js +3 -0
- package/dist/src/refine-llm.js.map +1 -1
- package/dist/src/reject-flow.js +2 -0
- package/dist/src/reject-flow.js.map +1 -1
- package/dist/src/rejection.js +3 -0
- package/dist/src/rejection.js.map +1 -1
- package/dist/src/replay.js.map +1 -1
- package/dist/src/rerankers/cross-encoder.js +11 -4
- package/dist/src/rerankers/cross-encoder.js.map +1 -1
- package/dist/src/rerankers/index.js +5 -3
- package/dist/src/rerankers/index.js.map +1 -1
- package/dist/src/rerankers/llm.js +8 -5
- package/dist/src/rerankers/llm.js.map +1 -1
- package/dist/src/scheduler.js +15 -5
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/search.js.map +1 -1
- package/dist/src/server-detect.js +2 -0
- package/dist/src/server-detect.js.map +1 -1
- package/dist/src/server.js +194 -135
- package/dist/src/server.js.map +1 -1
- package/dist/src/skills.js +10 -3
- package/dist/src/skills.js.map +1 -1
- package/dist/src/store.js +140 -14
- package/dist/src/store.js.map +1 -1
- package/dist/src/trace.js +15 -7
- package/dist/src/trace.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/working-memory.js +8 -0
- package/dist/src/working-memory.js.map +1 -1
- package/dist/src/yaml.js +12 -3
- package/dist/src/yaml.js.map +1 -1
- package/dist/store.d.ts +16 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +140 -14
- package/dist/store.js.map +1 -1
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js +15 -7
- package/dist/trace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +6 -2
- package/dist/working-memory.d.ts.map +1 -1
- package/dist/working-memory.js +8 -0
- package/dist/working-memory.js.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +12 -3
- package/dist/yaml.js.map +1 -1
- package/dist-ui/assets/{index-Cxa4RcAh.js → index-DOqenbV2.js} +11 -11
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/index.ts +50 -10
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -1
package/dist/src/server.js
CHANGED
|
@@ -118,6 +118,41 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
118
118
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
119
119
|
// small enough that a malicious client can't ask for the world.
|
|
120
120
|
const MAX_AUDIT_LIMIT = 10000;
|
|
121
|
+
function isJsonString(value) {
|
|
122
|
+
return typeof value === 'string';
|
|
123
|
+
}
|
|
124
|
+
function isJsonNumber(value) {
|
|
125
|
+
return typeof value === 'number';
|
|
126
|
+
}
|
|
127
|
+
function isJsonBoolean(value) {
|
|
128
|
+
return typeof value === 'boolean';
|
|
129
|
+
}
|
|
130
|
+
function isJsonObjectRecord(value) {
|
|
131
|
+
return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
// node:http header values are `string | string[] | undefined` (never a bare
|
|
134
|
+
// unknown), so this gets its own predicate rather than reusing isJsonString.
|
|
135
|
+
function isHeaderString(value) {
|
|
136
|
+
return typeof value === 'string';
|
|
137
|
+
}
|
|
138
|
+
// server.address() returns AddressInfo once a TCP socket is bound; null before
|
|
139
|
+
// listening, a string only for pipe/unix-socket listeners (never used here).
|
|
140
|
+
function isAddressInfo(a) {
|
|
141
|
+
return a !== null && typeof a !== 'string';
|
|
142
|
+
}
|
|
143
|
+
// Runtime membership check for a `ReadonlySet<T>` of string-literal union
|
|
144
|
+
// members, used at every `body` field validated against a VALID_* set below.
|
|
145
|
+
// Set<T>.has(value: T) itself gives no narrowing (its parameter type is T,
|
|
146
|
+
// not a type predicate) so callers previously needed a separate `as T` cast
|
|
147
|
+
// at both the check and the later usage; this helper is the one place that
|
|
148
|
+
// assertion lives, so downstream call sites narrow via the `value is T`
|
|
149
|
+
// return instead of re-asserting.
|
|
150
|
+
function isSetMember(set, value) {
|
|
151
|
+
// SAFETY: `value as T` is discarded unless `set.has` (the real runtime
|
|
152
|
+
// check) confirms membership; the `value is T` return type is what
|
|
153
|
+
// performs the actual narrowing for callers.
|
|
154
|
+
return set.has(value);
|
|
155
|
+
}
|
|
121
156
|
// HTTP-boundary validation for a process `steps` body (untrusted). Returns the
|
|
122
157
|
// step strings (saveProcess re-validates + trims, this is the fail-fast 400
|
|
123
158
|
// gate). Caps mirror src/processes.ts MAX_PROCESS_STEPS / MAX_PROCESS_STEP_LEN.
|
|
@@ -131,7 +166,7 @@ function validateProcessStepsBody(raw) {
|
|
|
131
166
|
throw new HttpError(400, 'steps exceeds 200-step cap');
|
|
132
167
|
}
|
|
133
168
|
for (const item of raw) {
|
|
134
|
-
if (
|
|
169
|
+
if (!isJsonString(item)) {
|
|
135
170
|
throw new HttpError(400, 'each step must be a string');
|
|
136
171
|
}
|
|
137
172
|
if (item.trim().length === 0) {
|
|
@@ -141,6 +176,7 @@ function validateProcessStepsBody(raw) {
|
|
|
141
176
|
throw new HttpError(400, 'a step exceeds the 2000-character cap');
|
|
142
177
|
}
|
|
143
178
|
}
|
|
179
|
+
// SAFETY: every item in raw was confirmed to be a string in the loop above.
|
|
144
180
|
return raw;
|
|
145
181
|
}
|
|
146
182
|
// HTTP-boundary check for an optional policy date field (validFrom/validTo).
|
|
@@ -150,7 +186,7 @@ function validateProcessStepsBody(raw) {
|
|
|
150
186
|
function optionalDateField(raw, label) {
|
|
151
187
|
if (raw === undefined || raw === null)
|
|
152
188
|
return undefined;
|
|
153
|
-
if (
|
|
189
|
+
if (!isJsonString(raw)) {
|
|
154
190
|
throw new HttpError(400, `${label} must be a string`);
|
|
155
191
|
}
|
|
156
192
|
if (raw.length > 64) {
|
|
@@ -216,6 +252,8 @@ async function readBody(req) {
|
|
|
216
252
|
const chunks = [];
|
|
217
253
|
let total = 0;
|
|
218
254
|
for await (const chunk of req) {
|
|
255
|
+
// SAFETY: IncomingMessage never runs setEncoding() here, so every
|
|
256
|
+
// streamed chunk is a Buffer, not a decoded string.
|
|
219
257
|
const buf = chunk;
|
|
220
258
|
total += buf.length;
|
|
221
259
|
if (total > MAX_BODY_BYTES) {
|
|
@@ -231,7 +269,7 @@ async function parseJsonBody(req) {
|
|
|
231
269
|
return {};
|
|
232
270
|
try {
|
|
233
271
|
const parsed = JSON.parse(raw);
|
|
234
|
-
if (
|
|
272
|
+
if (!isJsonObjectRecord(parsed)) {
|
|
235
273
|
throw new HttpError(400, 'request body must be a JSON object');
|
|
236
274
|
}
|
|
237
275
|
return parsed;
|
|
@@ -320,8 +358,9 @@ function readAuthHeader(req) {
|
|
|
320
358
|
if (raw === undefined)
|
|
321
359
|
return { kind: 'absent' };
|
|
322
360
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
323
|
-
if (
|
|
361
|
+
if (!isHeaderString(value) || value.length === 0) {
|
|
324
362
|
return { kind: 'malformed' };
|
|
363
|
+
}
|
|
325
364
|
const space = value.indexOf(' ');
|
|
326
365
|
if (space < 0)
|
|
327
366
|
return { kind: 'malformed' };
|
|
@@ -432,13 +471,13 @@ function requireAuth(req, hippoRoot) {
|
|
|
432
471
|
}
|
|
433
472
|
function getString(obj, key) {
|
|
434
473
|
const v = obj[key];
|
|
435
|
-
return
|
|
474
|
+
return isJsonString(v) ? v : undefined;
|
|
436
475
|
}
|
|
437
476
|
function getStringArray(obj, key) {
|
|
438
477
|
const v = obj[key];
|
|
439
478
|
if (!Array.isArray(v))
|
|
440
479
|
return undefined;
|
|
441
|
-
if (!v.every(
|
|
480
|
+
if (!v.every(isJsonString))
|
|
442
481
|
return undefined;
|
|
443
482
|
return v;
|
|
444
483
|
}
|
|
@@ -518,7 +557,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
518
557
|
throw new HttpError(400, 'content is required');
|
|
519
558
|
}
|
|
520
559
|
const kindRaw = getString(body, 'kind');
|
|
521
|
-
if (kindRaw !== undefined && !VALID_KINDS
|
|
560
|
+
if (kindRaw !== undefined && !isSetMember(VALID_KINDS, kindRaw)) {
|
|
522
561
|
throw new HttpError(400, `invalid kind: ${kindRaw}`);
|
|
523
562
|
}
|
|
524
563
|
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
@@ -667,19 +706,28 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
667
706
|
}
|
|
668
707
|
}
|
|
669
708
|
}
|
|
709
|
+
const recallExtra = {};
|
|
710
|
+
if (freshTailCount !== undefined)
|
|
711
|
+
recallExtra.freshTailCount = freshTailCount;
|
|
712
|
+
if (freshTailSessionId !== undefined)
|
|
713
|
+
recallExtra.freshTailSessionId = freshTailSessionId;
|
|
714
|
+
if (summarizeOverflow !== undefined)
|
|
715
|
+
recallExtra.summarizeOverflow = summarizeOverflow;
|
|
716
|
+
if (scorerWindow !== undefined)
|
|
717
|
+
recallExtra.scorerWindow = scorerWindow;
|
|
718
|
+
if (sessionId !== undefined)
|
|
719
|
+
recallExtra.sessionId = sessionId;
|
|
720
|
+
if (httpRecallHistory !== undefined)
|
|
721
|
+
recallExtra.recallHistory = httpRecallHistory;
|
|
722
|
+
if (explain)
|
|
723
|
+
recallExtra.explain = explain;
|
|
670
724
|
const result = recall(ctx, {
|
|
671
725
|
query: q,
|
|
672
726
|
limit,
|
|
673
|
-
mode:
|
|
727
|
+
mode: mode ?? undefined,
|
|
674
728
|
scope: scope ?? undefined,
|
|
675
729
|
includeContinuity,
|
|
676
|
-
...
|
|
677
|
-
...(freshTailSessionId !== undefined ? { freshTailSessionId } : {}),
|
|
678
|
-
...(summarizeOverflow !== undefined ? { summarizeOverflow } : {}),
|
|
679
|
-
...(scorerWindow !== undefined ? { scorerWindow } : {}),
|
|
680
|
-
...(sessionId !== undefined ? { sessionId } : {}),
|
|
681
|
-
...(httpRecallHistory !== undefined ? { recallHistory: httpRecallHistory } : {}),
|
|
682
|
-
...(explain ? { explain } : {}),
|
|
730
|
+
...recallExtra,
|
|
683
731
|
});
|
|
684
732
|
// v0.33 / J1 — append AFTER recall completes (snapshot was taken before
|
|
685
733
|
// recall() ran). anchoredOn carries the memoryId of any hint that fired
|
|
@@ -729,12 +777,16 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
729
777
|
const scopeQ = query.get('scope');
|
|
730
778
|
const scope = scopeQ !== null && scopeQ.length > 0 ? scopeQ : undefined;
|
|
731
779
|
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
780
|
+
const assembleExtra = {};
|
|
781
|
+
if (budget !== undefined)
|
|
782
|
+
assembleExtra.budget = budget;
|
|
783
|
+
if (freshTailCount !== undefined)
|
|
784
|
+
assembleExtra.freshTailCount = freshTailCount;
|
|
785
|
+
if (summarizeOlder !== undefined)
|
|
786
|
+
assembleExtra.summarizeOlder = summarizeOlder;
|
|
787
|
+
if (scope !== undefined)
|
|
788
|
+
assembleExtra.scope = scope;
|
|
789
|
+
const result = assemble(ctx, assembleMatch.id, assembleExtra);
|
|
738
790
|
sendJson(res, 200, result);
|
|
739
791
|
return;
|
|
740
792
|
}
|
|
@@ -769,11 +821,14 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
769
821
|
depth = parsed;
|
|
770
822
|
}
|
|
771
823
|
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
824
|
+
const drillExtra = {};
|
|
825
|
+
if (limit !== undefined)
|
|
826
|
+
drillExtra.limit = limit;
|
|
827
|
+
if (budget !== undefined)
|
|
828
|
+
drillExtra.budget = budget;
|
|
829
|
+
if (depth !== undefined)
|
|
830
|
+
drillExtra.depth = depth;
|
|
831
|
+
const result = drillDown(ctx, drillMatch.id, drillExtra);
|
|
777
832
|
if ('failure' in result) {
|
|
778
833
|
// v1.6.4: leaf id maps to 422 (caller-actionable). Other cases stay
|
|
779
834
|
// as 404 to avoid leaking cross-tenant existence or scope grants.
|
|
@@ -837,7 +892,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
837
892
|
if (method === 'POST' && path === '/v1/outcome') {
|
|
838
893
|
const body = await parseJsonBody(req);
|
|
839
894
|
const good = body['good'];
|
|
840
|
-
if (
|
|
895
|
+
if (!isJsonBoolean(good)) {
|
|
841
896
|
throw new HttpError(400, 'good is required (boolean)');
|
|
842
897
|
}
|
|
843
898
|
const idsRaw = body['ids'];
|
|
@@ -846,10 +901,9 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
846
901
|
if (!Array.isArray(idsRaw)) {
|
|
847
902
|
throw new HttpError(400, 'ids must be an array of non-empty strings');
|
|
848
903
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
904
|
+
const isNonEmptyId = (item) => isJsonString(item) && item.length > 0;
|
|
905
|
+
if (!idsRaw.every(isNonEmptyId)) {
|
|
906
|
+
throw new HttpError(400, 'ids must be an array of non-empty strings');
|
|
853
907
|
}
|
|
854
908
|
// v1.11.5: DoS cap on ids.length. Each id triggers ~3 DB ops (readEntry +
|
|
855
909
|
// writeEntry + appendAuditEvent). N=1000 keeps per-request work bounded
|
|
@@ -965,11 +1019,11 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
965
1019
|
}
|
|
966
1020
|
const body = await parseJsonBody(req);
|
|
967
1021
|
const dryRunRaw = body['dry_run'];
|
|
968
|
-
if (dryRunRaw !== undefined &&
|
|
1022
|
+
if (dryRunRaw !== undefined && !isJsonBoolean(dryRunRaw)) {
|
|
969
1023
|
throw new HttpError(400, 'dry_run must be a boolean');
|
|
970
1024
|
}
|
|
971
1025
|
const noShareRaw = body['no_share'];
|
|
972
|
-
if (noShareRaw !== undefined &&
|
|
1026
|
+
if (noShareRaw !== undefined && !isJsonBoolean(noShareRaw)) {
|
|
973
1027
|
throw new HttpError(400, 'no_share must be a boolean');
|
|
974
1028
|
}
|
|
975
1029
|
// v1.12.0: sleepCtx already built above for the admin-role gate; reuse.
|
|
@@ -986,7 +1040,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
986
1040
|
if (method === 'POST' && path === '/v1/auth/keys') {
|
|
987
1041
|
const body = await parseJsonBody(req);
|
|
988
1042
|
const labelRaw = body['label'];
|
|
989
|
-
if (labelRaw !== undefined &&
|
|
1043
|
+
if (labelRaw !== undefined && !isJsonString(labelRaw)) {
|
|
990
1044
|
throw new HttpError(400, 'label must be a string');
|
|
991
1045
|
}
|
|
992
1046
|
// v1.12.3: optional body.role mirrors the --role CLI flag. Validated
|
|
@@ -1054,7 +1108,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1054
1108
|
const opRaw = query.get('op');
|
|
1055
1109
|
let op;
|
|
1056
1110
|
if (opRaw !== null) {
|
|
1057
|
-
if (!VALID_AUDIT_OPS
|
|
1111
|
+
if (!isSetMember(VALID_AUDIT_OPS, opRaw)) {
|
|
1058
1112
|
throw new HttpError(400, `invalid op: ${opRaw}`);
|
|
1059
1113
|
}
|
|
1060
1114
|
op = opRaw;
|
|
@@ -1103,20 +1157,20 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1103
1157
|
if (method === 'POST' && path === '/v1/predictions') {
|
|
1104
1158
|
const body = await parseJsonBody(req);
|
|
1105
1159
|
const claim = body['claim'];
|
|
1106
|
-
if (
|
|
1160
|
+
if (!isJsonString(claim) || claim.length === 0) {
|
|
1107
1161
|
throw new HttpError(400, 'claim is required (non-empty string)');
|
|
1108
1162
|
}
|
|
1109
1163
|
if (claim.length > 4096) {
|
|
1110
1164
|
throw new HttpError(400, 'claim exceeds 4096-character cap');
|
|
1111
1165
|
}
|
|
1112
1166
|
const classTag = body['classTag'];
|
|
1113
|
-
if (
|
|
1167
|
+
if (!isJsonString(classTag) || classTag.length === 0) {
|
|
1114
1168
|
throw new HttpError(400, 'classTag is required (non-empty string)');
|
|
1115
1169
|
}
|
|
1116
1170
|
const estimate = body['estimate'];
|
|
1117
1171
|
let estimateValue;
|
|
1118
1172
|
if (estimate !== undefined && estimate !== null) {
|
|
1119
|
-
if (
|
|
1173
|
+
if (!isJsonNumber(estimate) || !Number.isFinite(estimate)) {
|
|
1120
1174
|
throw new HttpError(400, 'estimate must be a finite number');
|
|
1121
1175
|
}
|
|
1122
1176
|
estimateValue = estimate;
|
|
@@ -1124,7 +1178,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1124
1178
|
const unit = body['unit'];
|
|
1125
1179
|
let estimateUnit;
|
|
1126
1180
|
if (unit !== undefined && unit !== null) {
|
|
1127
|
-
if (
|
|
1181
|
+
if (!isJsonString(unit)) {
|
|
1128
1182
|
throw new HttpError(400, 'unit must be a string');
|
|
1129
1183
|
}
|
|
1130
1184
|
estimateUnit = unit;
|
|
@@ -1132,7 +1186,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1132
1186
|
const targetDate = body['targetDate'];
|
|
1133
1187
|
let targetDateValue;
|
|
1134
1188
|
if (targetDate !== undefined && targetDate !== null) {
|
|
1135
|
-
if (
|
|
1189
|
+
if (!isJsonString(targetDate)) {
|
|
1136
1190
|
throw new HttpError(400, 'targetDate must be an ISO date string');
|
|
1137
1191
|
}
|
|
1138
1192
|
targetDateValue = targetDate;
|
|
@@ -1169,7 +1223,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1169
1223
|
});
|
|
1170
1224
|
}
|
|
1171
1225
|
else {
|
|
1172
|
-
if (!VALID_CLOSURE_STATES
|
|
1226
|
+
if (!isSetMember(VALID_CLOSURE_STATES, status)) {
|
|
1173
1227
|
throw new HttpError(400, `status must be one of: open | closed | closed-unknown | all (got "${status}")`);
|
|
1174
1228
|
}
|
|
1175
1229
|
if (!classTag) {
|
|
@@ -1216,13 +1270,13 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1216
1270
|
const id = parseInt(predictionCloseMatch[1], 10);
|
|
1217
1271
|
const body = await parseJsonBody(req);
|
|
1218
1272
|
const state = body['state'];
|
|
1219
|
-
if (
|
|
1273
|
+
if (!isJsonString(state) || !isSetMember(VALID_CLOSURE_STATES, state) || state === 'open') {
|
|
1220
1274
|
throw new HttpError(400, 'state is required and must be one of: closed | closed-unknown');
|
|
1221
1275
|
}
|
|
1222
1276
|
const actual = body['actual'];
|
|
1223
1277
|
let actualValue;
|
|
1224
1278
|
if (actual !== undefined && actual !== null) {
|
|
1225
|
-
if (
|
|
1279
|
+
if (!isJsonNumber(actual) || !Number.isFinite(actual)) {
|
|
1226
1280
|
throw new HttpError(400, 'actual must be a finite number');
|
|
1227
1281
|
}
|
|
1228
1282
|
actualValue = actual;
|
|
@@ -1230,7 +1284,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1230
1284
|
const note = body['note'];
|
|
1231
1285
|
let closureNote;
|
|
1232
1286
|
if (note !== undefined && note !== null) {
|
|
1233
|
-
if (
|
|
1287
|
+
if (!isJsonString(note)) {
|
|
1234
1288
|
throw new HttpError(400, 'note must be a string');
|
|
1235
1289
|
}
|
|
1236
1290
|
if (note.length > 2048) {
|
|
@@ -1248,7 +1302,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1248
1302
|
sendJson(res, 200, { prediction });
|
|
1249
1303
|
}
|
|
1250
1304
|
catch (e) {
|
|
1251
|
-
const msg = e.message;
|
|
1305
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1252
1306
|
if (msg.includes('not found')) {
|
|
1253
1307
|
throw new HttpError(404, msg);
|
|
1254
1308
|
}
|
|
@@ -1269,7 +1323,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1269
1323
|
if (method === 'POST' && path === '/v1/decisions') {
|
|
1270
1324
|
const body = await parseJsonBody(req);
|
|
1271
1325
|
const text = body['text'];
|
|
1272
|
-
if (
|
|
1326
|
+
if (!isJsonString(text) || text.length === 0) {
|
|
1273
1327
|
throw new HttpError(400, 'text is required (non-empty string)');
|
|
1274
1328
|
}
|
|
1275
1329
|
if (text.length > 4096) {
|
|
@@ -1278,7 +1332,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1278
1332
|
const contextRaw = body['context'];
|
|
1279
1333
|
let context;
|
|
1280
1334
|
if (contextRaw !== undefined && contextRaw !== null) {
|
|
1281
|
-
if (
|
|
1335
|
+
if (!isJsonString(contextRaw)) {
|
|
1282
1336
|
throw new HttpError(400, 'context must be a string');
|
|
1283
1337
|
}
|
|
1284
1338
|
if (contextRaw.length > 4096) {
|
|
@@ -1289,7 +1343,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1289
1343
|
const supRaw = body['supersedesDecisionId'];
|
|
1290
1344
|
let supersedesDecisionId;
|
|
1291
1345
|
if (supRaw !== undefined && supRaw !== null) {
|
|
1292
|
-
if (
|
|
1346
|
+
if (!isJsonNumber(supRaw) || !Number.isInteger(supRaw) || supRaw <= 0) {
|
|
1293
1347
|
throw new HttpError(400, 'supersedesDecisionId must be a positive integer');
|
|
1294
1348
|
}
|
|
1295
1349
|
supersedesDecisionId = supRaw;
|
|
@@ -1304,7 +1358,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1304
1358
|
sendJson(res, 201, { decision });
|
|
1305
1359
|
}
|
|
1306
1360
|
catch (e) {
|
|
1307
|
-
const msg = e.message;
|
|
1361
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1308
1362
|
if (msg.includes('not found') || msg.includes('not active')) {
|
|
1309
1363
|
throw new HttpError(409, msg);
|
|
1310
1364
|
}
|
|
@@ -1321,11 +1375,11 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1321
1375
|
decisions = loadDecisions(opts.hippoRoot, ctx.tenantId, { limit });
|
|
1322
1376
|
}
|
|
1323
1377
|
else {
|
|
1324
|
-
if (!VALID_DECISION_STATES
|
|
1378
|
+
if (!isSetMember(VALID_DECISION_STATES, status)) {
|
|
1325
1379
|
throw new HttpError(400, `status must be one of: active | superseded | closed | all (got "${status}")`);
|
|
1326
1380
|
}
|
|
1327
1381
|
decisions = loadDecisions(opts.hippoRoot, ctx.tenantId, {
|
|
1328
|
-
status
|
|
1382
|
+
status,
|
|
1329
1383
|
limit,
|
|
1330
1384
|
});
|
|
1331
1385
|
}
|
|
@@ -1337,7 +1391,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1337
1391
|
const oldId = parseInt(decisionSupersedeMatch[1], 10);
|
|
1338
1392
|
const body = await parseJsonBody(req);
|
|
1339
1393
|
const text = body['text'];
|
|
1340
|
-
if (
|
|
1394
|
+
if (!isJsonString(text) || text.length === 0) {
|
|
1341
1395
|
throw new HttpError(400, 'text is required (non-empty string)');
|
|
1342
1396
|
}
|
|
1343
1397
|
if (text.length > 4096) {
|
|
@@ -1346,7 +1400,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1346
1400
|
const contextRaw = body['context'];
|
|
1347
1401
|
let context;
|
|
1348
1402
|
if (contextRaw !== undefined && contextRaw !== null) {
|
|
1349
|
-
if (
|
|
1403
|
+
if (!isJsonString(contextRaw)) {
|
|
1350
1404
|
throw new HttpError(400, 'context must be a string');
|
|
1351
1405
|
}
|
|
1352
1406
|
if (contextRaw.length > 4096) {
|
|
@@ -1364,7 +1418,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1364
1418
|
sendJson(res, 201, { decision });
|
|
1365
1419
|
}
|
|
1366
1420
|
catch (e) {
|
|
1367
|
-
const msg = e.message;
|
|
1421
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1368
1422
|
if (msg.includes('not found')) {
|
|
1369
1423
|
throw new HttpError(404, msg);
|
|
1370
1424
|
}
|
|
@@ -1384,7 +1438,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1384
1438
|
sendJson(res, 200, { decision });
|
|
1385
1439
|
}
|
|
1386
1440
|
catch (e) {
|
|
1387
|
-
const msg = e.message;
|
|
1441
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1388
1442
|
if (msg.includes('not found')) {
|
|
1389
1443
|
throw new HttpError(404, msg);
|
|
1390
1444
|
}
|
|
@@ -1420,7 +1474,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1420
1474
|
if (method === 'POST' && path === '/v1/incidents') {
|
|
1421
1475
|
const body = await parseJsonBody(req);
|
|
1422
1476
|
const text = body['text'];
|
|
1423
|
-
if (
|
|
1477
|
+
if (!isJsonString(text) || text.length === 0) {
|
|
1424
1478
|
throw new HttpError(400, 'text is required (non-empty string)');
|
|
1425
1479
|
}
|
|
1426
1480
|
if (text.length > 4096) {
|
|
@@ -1429,7 +1483,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1429
1483
|
const contextRaw = body['context'];
|
|
1430
1484
|
let context;
|
|
1431
1485
|
if (contextRaw !== undefined && contextRaw !== null) {
|
|
1432
|
-
if (
|
|
1486
|
+
if (!isJsonString(contextRaw)) {
|
|
1433
1487
|
throw new HttpError(400, 'context must be a string');
|
|
1434
1488
|
}
|
|
1435
1489
|
if (contextRaw.length > 4096) {
|
|
@@ -1446,10 +1500,9 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1446
1500
|
if (linkedRaw.length > 256) {
|
|
1447
1501
|
throw new HttpError(400, 'linkedMemoryIds exceeds 256-item cap');
|
|
1448
1502
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
}
|
|
1503
|
+
const isValidMemoryId = (item) => isJsonString(item) && item.length > 0 && item.length <= 4096;
|
|
1504
|
+
if (!linkedRaw.every(isValidMemoryId)) {
|
|
1505
|
+
throw new HttpError(400, 'each linkedMemoryIds entry must be a non-empty string <= 4096 chars');
|
|
1453
1506
|
}
|
|
1454
1507
|
linkedMemoryIds = linkedRaw;
|
|
1455
1508
|
}
|
|
@@ -1463,7 +1516,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1463
1516
|
sendJson(res, 201, { incident });
|
|
1464
1517
|
}
|
|
1465
1518
|
catch (e) {
|
|
1466
|
-
const msg = e.message;
|
|
1519
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1467
1520
|
if (msg.includes('not found')) {
|
|
1468
1521
|
throw new HttpError(409, msg);
|
|
1469
1522
|
}
|
|
@@ -1480,11 +1533,11 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1480
1533
|
incidents = loadIncidents(opts.hippoRoot, ctx.tenantId, { limit });
|
|
1481
1534
|
}
|
|
1482
1535
|
else {
|
|
1483
|
-
if (!VALID_INCIDENT_STATES
|
|
1536
|
+
if (!isSetMember(VALID_INCIDENT_STATES, status)) {
|
|
1484
1537
|
throw new HttpError(400, `status must be one of: open | resolved | closed | all (got "${status}")`);
|
|
1485
1538
|
}
|
|
1486
1539
|
incidents = loadIncidents(opts.hippoRoot, ctx.tenantId, {
|
|
1487
|
-
status
|
|
1540
|
+
status,
|
|
1488
1541
|
limit,
|
|
1489
1542
|
});
|
|
1490
1543
|
}
|
|
@@ -1496,7 +1549,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1496
1549
|
const id = parseInt(incidentResolveMatch[1], 10);
|
|
1497
1550
|
const body = await parseJsonBody(req);
|
|
1498
1551
|
const resolutionText = body['resolutionText'];
|
|
1499
|
-
if (
|
|
1552
|
+
if (!isJsonString(resolutionText) || resolutionText.trim().length === 0) {
|
|
1500
1553
|
throw new HttpError(400, 'resolutionText is required (non-empty string)');
|
|
1501
1554
|
}
|
|
1502
1555
|
if (resolutionText.length > 4096) {
|
|
@@ -1508,7 +1561,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1508
1561
|
sendJson(res, 200, { incident });
|
|
1509
1562
|
}
|
|
1510
1563
|
catch (e) {
|
|
1511
|
-
const msg = e.message;
|
|
1564
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1512
1565
|
if (msg.includes('not found')) {
|
|
1513
1566
|
throw new HttpError(404, msg);
|
|
1514
1567
|
}
|
|
@@ -1528,7 +1581,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1528
1581
|
sendJson(res, 200, { incident });
|
|
1529
1582
|
}
|
|
1530
1583
|
catch (e) {
|
|
1531
|
-
const msg = e.message;
|
|
1584
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1532
1585
|
if (msg.includes('not found')) {
|
|
1533
1586
|
throw new HttpError(404, msg);
|
|
1534
1587
|
}
|
|
@@ -1564,7 +1617,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1564
1617
|
if (method === 'POST' && path === '/v1/processes') {
|
|
1565
1618
|
const body = await parseJsonBody(req);
|
|
1566
1619
|
const processName = body['processName'];
|
|
1567
|
-
if (
|
|
1620
|
+
if (!isJsonString(processName) || processName.trim().length === 0) {
|
|
1568
1621
|
throw new HttpError(400, 'processName is required (non-empty string)');
|
|
1569
1622
|
}
|
|
1570
1623
|
if (processName.length > 4096) {
|
|
@@ -1574,7 +1627,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1574
1627
|
const descriptionRaw = body['description'];
|
|
1575
1628
|
let description;
|
|
1576
1629
|
if (descriptionRaw !== undefined && descriptionRaw !== null) {
|
|
1577
|
-
if (
|
|
1630
|
+
if (!isJsonString(descriptionRaw)) {
|
|
1578
1631
|
throw new HttpError(400, 'description must be a string');
|
|
1579
1632
|
}
|
|
1580
1633
|
if (descriptionRaw.length > 4096) {
|
|
@@ -1600,11 +1653,11 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1600
1653
|
processes = loadProcesses(opts.hippoRoot, ctx.tenantId, { limit });
|
|
1601
1654
|
}
|
|
1602
1655
|
else {
|
|
1603
|
-
if (!VALID_PROCESS_STATES
|
|
1656
|
+
if (!isSetMember(VALID_PROCESS_STATES, status)) {
|
|
1604
1657
|
throw new HttpError(400, `status must be one of: active | superseded | closed | all (got "${status}")`);
|
|
1605
1658
|
}
|
|
1606
1659
|
processes = loadProcesses(opts.hippoRoot, ctx.tenantId, {
|
|
1607
|
-
status
|
|
1660
|
+
status,
|
|
1608
1661
|
limit,
|
|
1609
1662
|
});
|
|
1610
1663
|
}
|
|
@@ -1622,7 +1675,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1622
1675
|
const changeRaw = body['changeSummary'];
|
|
1623
1676
|
let changeSummary;
|
|
1624
1677
|
if (changeRaw !== undefined && changeRaw !== null) {
|
|
1625
|
-
if (
|
|
1678
|
+
if (!isJsonString(changeRaw)) {
|
|
1626
1679
|
throw new HttpError(400, 'changeSummary must be a string');
|
|
1627
1680
|
}
|
|
1628
1681
|
if (changeRaw.length > 4096) {
|
|
@@ -1633,7 +1686,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1633
1686
|
const descRaw = body['description'];
|
|
1634
1687
|
let description;
|
|
1635
1688
|
if (descRaw !== undefined && descRaw !== null) {
|
|
1636
|
-
if (
|
|
1689
|
+
if (!isJsonString(descRaw)) {
|
|
1637
1690
|
throw new HttpError(400, 'description must be a string');
|
|
1638
1691
|
}
|
|
1639
1692
|
if (descRaw.length > 4096) {
|
|
@@ -1660,7 +1713,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1660
1713
|
sendJson(res, 200, { process });
|
|
1661
1714
|
}
|
|
1662
1715
|
catch (e) {
|
|
1663
|
-
const msg = e.message;
|
|
1716
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1664
1717
|
if (msg.includes('not found')) {
|
|
1665
1718
|
throw new HttpError(404, msg);
|
|
1666
1719
|
}
|
|
@@ -1680,7 +1733,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1680
1733
|
sendJson(res, 200, { process });
|
|
1681
1734
|
}
|
|
1682
1735
|
catch (e) {
|
|
1683
|
-
const msg = e.message;
|
|
1736
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1684
1737
|
if (msg.includes('not found')) {
|
|
1685
1738
|
throw new HttpError(404, msg);
|
|
1686
1739
|
}
|
|
@@ -1714,14 +1767,14 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1714
1767
|
if (method === 'POST' && path === '/v1/policies') {
|
|
1715
1768
|
const body = await parseJsonBody(req);
|
|
1716
1769
|
const policyName = body['policyName'];
|
|
1717
|
-
if (
|
|
1770
|
+
if (!isJsonString(policyName) || policyName.trim().length === 0) {
|
|
1718
1771
|
throw new HttpError(400, 'policyName is required (non-empty string)');
|
|
1719
1772
|
}
|
|
1720
1773
|
if (policyName.length > 4096) {
|
|
1721
1774
|
throw new HttpError(400, 'policyName exceeds 4096-character cap');
|
|
1722
1775
|
}
|
|
1723
1776
|
const policyText = body['policyText'];
|
|
1724
|
-
if (
|
|
1777
|
+
if (!isJsonString(policyText) || policyText.trim().length === 0) {
|
|
1725
1778
|
throw new HttpError(400, 'policyText is required (non-empty string)');
|
|
1726
1779
|
}
|
|
1727
1780
|
if (policyText.length > 4096) {
|
|
@@ -1741,7 +1794,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1741
1794
|
}
|
|
1742
1795
|
catch (e) {
|
|
1743
1796
|
// savePolicy throws on invalid/inverted dates (validation) -> 400.
|
|
1744
|
-
throw new HttpError(400, e.message);
|
|
1797
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
1745
1798
|
}
|
|
1746
1799
|
return;
|
|
1747
1800
|
}
|
|
@@ -1754,11 +1807,11 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1754
1807
|
policies = loadPolicies(opts.hippoRoot, ctx.tenantId, { limit });
|
|
1755
1808
|
}
|
|
1756
1809
|
else {
|
|
1757
|
-
if (!VALID_POLICY_STATES
|
|
1810
|
+
if (!isSetMember(VALID_POLICY_STATES, status)) {
|
|
1758
1811
|
throw new HttpError(400, `status must be one of: active | superseded | closed | all (got "${status}")`);
|
|
1759
1812
|
}
|
|
1760
1813
|
policies = loadPolicies(opts.hippoRoot, ctx.tenantId, {
|
|
1761
|
-
status
|
|
1814
|
+
status,
|
|
1762
1815
|
limit,
|
|
1763
1816
|
});
|
|
1764
1817
|
}
|
|
@@ -1779,7 +1832,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1779
1832
|
sendJson(res, 200, { policies });
|
|
1780
1833
|
}
|
|
1781
1834
|
catch (e) {
|
|
1782
|
-
throw new HttpError(400, e.message);
|
|
1835
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
1783
1836
|
}
|
|
1784
1837
|
return;
|
|
1785
1838
|
}
|
|
@@ -1788,7 +1841,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1788
1841
|
const id = parseInt(policySupersedeMatch[1], 10);
|
|
1789
1842
|
const body = await parseJsonBody(req);
|
|
1790
1843
|
const policyText = body['policyText'];
|
|
1791
|
-
if (
|
|
1844
|
+
if (!isJsonString(policyText) || policyText.trim().length === 0) {
|
|
1792
1845
|
throw new HttpError(400, 'policyText is required (non-empty string)');
|
|
1793
1846
|
}
|
|
1794
1847
|
if (policyText.length > 4096) {
|
|
@@ -1799,7 +1852,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1799
1852
|
const changeRaw = body['changeSummary'];
|
|
1800
1853
|
let changeSummary;
|
|
1801
1854
|
if (changeRaw !== undefined && changeRaw !== null) {
|
|
1802
|
-
if (
|
|
1855
|
+
if (!isJsonString(changeRaw)) {
|
|
1803
1856
|
throw new HttpError(400, 'changeSummary must be a string');
|
|
1804
1857
|
}
|
|
1805
1858
|
if (changeRaw.length > 4096) {
|
|
@@ -1824,7 +1877,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1824
1877
|
sendJson(res, 200, { policy });
|
|
1825
1878
|
}
|
|
1826
1879
|
catch (e) {
|
|
1827
|
-
const msg = e.message;
|
|
1880
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1828
1881
|
if (msg.includes('not found')) {
|
|
1829
1882
|
throw new HttpError(404, msg);
|
|
1830
1883
|
}
|
|
@@ -1845,7 +1898,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1845
1898
|
sendJson(res, 200, { policy });
|
|
1846
1899
|
}
|
|
1847
1900
|
catch (e) {
|
|
1848
|
-
const msg = e.message;
|
|
1901
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1849
1902
|
if (msg.includes('not found')) {
|
|
1850
1903
|
throw new HttpError(404, msg);
|
|
1851
1904
|
}
|
|
@@ -1882,14 +1935,14 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1882
1935
|
if (method === 'POST' && path === '/v1/skills') {
|
|
1883
1936
|
const body = await parseJsonBody(req);
|
|
1884
1937
|
const skillName = body['skillName'];
|
|
1885
|
-
if (
|
|
1938
|
+
if (!isJsonString(skillName) || skillName.trim().length === 0) {
|
|
1886
1939
|
throw new HttpError(400, 'skillName is required (non-empty string)');
|
|
1887
1940
|
}
|
|
1888
1941
|
if (skillName.length > 256) {
|
|
1889
1942
|
throw new HttpError(400, 'skillName exceeds 256-character cap');
|
|
1890
1943
|
}
|
|
1891
1944
|
const instructions = body['instructions'];
|
|
1892
|
-
if (
|
|
1945
|
+
if (!isJsonString(instructions) || instructions.trim().length === 0) {
|
|
1893
1946
|
throw new HttpError(400, 'instructions are required (non-empty string)');
|
|
1894
1947
|
}
|
|
1895
1948
|
if (instructions.length > 8192) {
|
|
@@ -1898,7 +1951,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1898
1951
|
const triggerRaw = body['trigger'];
|
|
1899
1952
|
let trigger;
|
|
1900
1953
|
if (triggerRaw !== undefined && triggerRaw !== null) {
|
|
1901
|
-
if (
|
|
1954
|
+
if (!isJsonString(triggerRaw)) {
|
|
1902
1955
|
throw new HttpError(400, 'trigger must be a string');
|
|
1903
1956
|
}
|
|
1904
1957
|
if (triggerRaw.length > 1024) {
|
|
@@ -1917,7 +1970,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1917
1970
|
}
|
|
1918
1971
|
catch (e) {
|
|
1919
1972
|
// saveSkill throws on validation (single-line name etc.) -> 400.
|
|
1920
|
-
throw new HttpError(400, e.message);
|
|
1973
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
1921
1974
|
}
|
|
1922
1975
|
return;
|
|
1923
1976
|
}
|
|
@@ -1930,11 +1983,11 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1930
1983
|
skills = loadSkills(opts.hippoRoot, ctx.tenantId, { limit });
|
|
1931
1984
|
}
|
|
1932
1985
|
else {
|
|
1933
|
-
if (!VALID_SKILL_STATES
|
|
1986
|
+
if (!isSetMember(VALID_SKILL_STATES, status)) {
|
|
1934
1987
|
throw new HttpError(400, `status must be one of: active | superseded | closed | all (got "${status}")`);
|
|
1935
1988
|
}
|
|
1936
1989
|
skills = loadSkills(opts.hippoRoot, ctx.tenantId, {
|
|
1937
|
-
status
|
|
1990
|
+
status,
|
|
1938
1991
|
limit,
|
|
1939
1992
|
});
|
|
1940
1993
|
}
|
|
@@ -1954,7 +2007,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1954
2007
|
const id = parseInt(skillSupersedeMatch[1], 10);
|
|
1955
2008
|
const body = await parseJsonBody(req);
|
|
1956
2009
|
const instructions = body['instructions'];
|
|
1957
|
-
if (
|
|
2010
|
+
if (!isJsonString(instructions) || instructions.trim().length === 0) {
|
|
1958
2011
|
throw new HttpError(400, 'instructions are required (non-empty string)');
|
|
1959
2012
|
}
|
|
1960
2013
|
if (instructions.length > 8192) {
|
|
@@ -1963,7 +2016,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1963
2016
|
const triggerRaw = body['trigger'];
|
|
1964
2017
|
let trigger;
|
|
1965
2018
|
if (triggerRaw !== undefined && triggerRaw !== null) {
|
|
1966
|
-
if (
|
|
2019
|
+
if (!isJsonString(triggerRaw)) {
|
|
1967
2020
|
throw new HttpError(400, 'trigger must be a string');
|
|
1968
2021
|
}
|
|
1969
2022
|
if (triggerRaw.length > 1024) {
|
|
@@ -1974,7 +2027,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1974
2027
|
const changeRaw = body['changeSummary'];
|
|
1975
2028
|
let changeSummary;
|
|
1976
2029
|
if (changeRaw !== undefined && changeRaw !== null) {
|
|
1977
|
-
if (
|
|
2030
|
+
if (!isJsonString(changeRaw)) {
|
|
1978
2031
|
throw new HttpError(400, 'changeSummary must be a string');
|
|
1979
2032
|
}
|
|
1980
2033
|
if (changeRaw.length > 4096) {
|
|
@@ -1998,7 +2051,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1998
2051
|
sendJson(res, 200, { skill });
|
|
1999
2052
|
}
|
|
2000
2053
|
catch (e) {
|
|
2001
|
-
const msg = e.message;
|
|
2054
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2002
2055
|
if (msg.includes('not found')) {
|
|
2003
2056
|
throw new HttpError(404, msg);
|
|
2004
2057
|
}
|
|
@@ -2018,7 +2071,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2018
2071
|
sendJson(res, 200, { skill });
|
|
2019
2072
|
}
|
|
2020
2073
|
catch (e) {
|
|
2021
|
-
const msg = e.message;
|
|
2074
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2022
2075
|
if (msg.includes('not found')) {
|
|
2023
2076
|
throw new HttpError(404, msg);
|
|
2024
2077
|
}
|
|
@@ -2053,14 +2106,14 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2053
2106
|
if (method === 'POST' && path === '/v1/project-briefs') {
|
|
2054
2107
|
const body = await parseJsonBody(req);
|
|
2055
2108
|
const repo = body['repo'];
|
|
2056
|
-
if (
|
|
2109
|
+
if (!isJsonString(repo) || repo.trim().length === 0) {
|
|
2057
2110
|
throw new HttpError(400, 'repo is required (non-empty string)');
|
|
2058
2111
|
}
|
|
2059
2112
|
if (repo.length > 256) {
|
|
2060
2113
|
throw new HttpError(400, 'repo exceeds 256-character cap');
|
|
2061
2114
|
}
|
|
2062
2115
|
const summary = body['summary'];
|
|
2063
|
-
if (
|
|
2116
|
+
if (!isJsonString(summary) || summary.trim().length === 0) {
|
|
2064
2117
|
throw new HttpError(400, 'summary is required (non-empty string)');
|
|
2065
2118
|
}
|
|
2066
2119
|
if (summary.length > 8192) {
|
|
@@ -2076,7 +2129,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2076
2129
|
}
|
|
2077
2130
|
catch (e) {
|
|
2078
2131
|
// saveProjectBrief throws on validation (single-line repo etc.) -> 400.
|
|
2079
|
-
throw new HttpError(400, e.message);
|
|
2132
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
2080
2133
|
}
|
|
2081
2134
|
return;
|
|
2082
2135
|
}
|
|
@@ -2090,7 +2143,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2090
2143
|
listOpts.repo = repoFilter.trim();
|
|
2091
2144
|
}
|
|
2092
2145
|
if (status !== 'all') {
|
|
2093
|
-
if (!VALID_BRIEF_STATES
|
|
2146
|
+
if (!isSetMember(VALID_BRIEF_STATES, status)) {
|
|
2094
2147
|
throw new HttpError(400, `status must be one of: active | superseded | closed | all (got "${status}")`);
|
|
2095
2148
|
}
|
|
2096
2149
|
listOpts.status = status;
|
|
@@ -2104,7 +2157,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2104
2157
|
if (method === 'POST' && path === '/v1/project-briefs/refresh') {
|
|
2105
2158
|
const body = await parseJsonBody(req);
|
|
2106
2159
|
const repo = body['repo'];
|
|
2107
|
-
if (
|
|
2160
|
+
if (!isJsonString(repo) || repo.trim().length === 0) {
|
|
2108
2161
|
throw new HttpError(400, 'repo is required (non-empty string)');
|
|
2109
2162
|
}
|
|
2110
2163
|
if (repo.length > 256) {
|
|
@@ -2126,7 +2179,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2126
2179
|
// loadActiveBriefForRepo and the supersede CAS) is a state conflict, not a
|
|
2127
2180
|
// validation error — map it to 409 like the explicit supersede route
|
|
2128
2181
|
// (codex-review 2026-05-30, P3).
|
|
2129
|
-
const msg = e.message;
|
|
2182
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2130
2183
|
if (msg.includes('not found')) {
|
|
2131
2184
|
throw new HttpError(404, msg);
|
|
2132
2185
|
}
|
|
@@ -2142,7 +2195,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2142
2195
|
const id = parseInt(briefSupersedeMatch[1], 10);
|
|
2143
2196
|
const body = await parseJsonBody(req);
|
|
2144
2197
|
const summary = body['summary'];
|
|
2145
|
-
if (
|
|
2198
|
+
if (!isJsonString(summary) || summary.trim().length === 0) {
|
|
2146
2199
|
throw new HttpError(400, 'summary is required (non-empty string)');
|
|
2147
2200
|
}
|
|
2148
2201
|
if (summary.length > 8192) {
|
|
@@ -2151,7 +2204,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2151
2204
|
const changeRaw = body['changeSummary'];
|
|
2152
2205
|
let changeSummary;
|
|
2153
2206
|
if (changeRaw !== undefined && changeRaw !== null) {
|
|
2154
|
-
if (
|
|
2207
|
+
if (!isJsonString(changeRaw)) {
|
|
2155
2208
|
throw new HttpError(400, 'changeSummary must be a string');
|
|
2156
2209
|
}
|
|
2157
2210
|
if (changeRaw.length > 4096) {
|
|
@@ -2174,7 +2227,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2174
2227
|
sendJson(res, 200, { brief });
|
|
2175
2228
|
}
|
|
2176
2229
|
catch (e) {
|
|
2177
|
-
const msg = e.message;
|
|
2230
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2178
2231
|
if (msg.includes('not found')) {
|
|
2179
2232
|
throw new HttpError(404, msg);
|
|
2180
2233
|
}
|
|
@@ -2194,7 +2247,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2194
2247
|
sendJson(res, 200, { brief });
|
|
2195
2248
|
}
|
|
2196
2249
|
catch (e) {
|
|
2197
|
-
const msg = e.message;
|
|
2250
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2198
2251
|
if (msg.includes('not found')) {
|
|
2199
2252
|
throw new HttpError(404, msg);
|
|
2200
2253
|
}
|
|
@@ -2227,14 +2280,14 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2227
2280
|
if (method === 'POST' && path === '/v1/customer-notes') {
|
|
2228
2281
|
const body = await parseJsonBody(req);
|
|
2229
2282
|
const customer = body['customer'];
|
|
2230
|
-
if (
|
|
2283
|
+
if (!isJsonString(customer) || customer.trim().length === 0) {
|
|
2231
2284
|
throw new HttpError(400, 'customer is required (non-empty string)');
|
|
2232
2285
|
}
|
|
2233
2286
|
if (customer.length > 256) {
|
|
2234
2287
|
throw new HttpError(400, 'customer exceeds 256-character cap');
|
|
2235
2288
|
}
|
|
2236
2289
|
const note = body['note'];
|
|
2237
|
-
if (
|
|
2290
|
+
if (!isJsonString(note) || note.trim().length === 0) {
|
|
2238
2291
|
throw new HttpError(400, 'note is required (non-empty string)');
|
|
2239
2292
|
}
|
|
2240
2293
|
if (note.length > 8192) {
|
|
@@ -2250,7 +2303,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2250
2303
|
}
|
|
2251
2304
|
catch (e) {
|
|
2252
2305
|
// saveCustomerNote throws on validation (single-line customer etc.) -> 400.
|
|
2253
|
-
throw new HttpError(400, e.message);
|
|
2306
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
2254
2307
|
}
|
|
2255
2308
|
return;
|
|
2256
2309
|
}
|
|
@@ -2264,7 +2317,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2264
2317
|
listOpts.customer = customerFilter.trim();
|
|
2265
2318
|
}
|
|
2266
2319
|
if (status !== 'all') {
|
|
2267
|
-
if (!VALID_NOTE_STATES
|
|
2320
|
+
if (!isSetMember(VALID_NOTE_STATES, status)) {
|
|
2268
2321
|
throw new HttpError(400, `status must be one of: active | superseded | closed | all (got "${status}")`);
|
|
2269
2322
|
}
|
|
2270
2323
|
listOpts.status = status;
|
|
@@ -2278,7 +2331,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2278
2331
|
const id = parseInt(noteSupersedeMatch[1], 10);
|
|
2279
2332
|
const body = await parseJsonBody(req);
|
|
2280
2333
|
const note = body['note'];
|
|
2281
|
-
if (
|
|
2334
|
+
if (!isJsonString(note) || note.trim().length === 0) {
|
|
2282
2335
|
throw new HttpError(400, 'note is required (non-empty string)');
|
|
2283
2336
|
}
|
|
2284
2337
|
if (note.length > 8192) {
|
|
@@ -2287,7 +2340,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2287
2340
|
const changeRaw = body['changeSummary'];
|
|
2288
2341
|
let changeSummary;
|
|
2289
2342
|
if (changeRaw !== undefined && changeRaw !== null) {
|
|
2290
|
-
if (
|
|
2343
|
+
if (!isJsonString(changeRaw)) {
|
|
2291
2344
|
throw new HttpError(400, 'changeSummary must be a string');
|
|
2292
2345
|
}
|
|
2293
2346
|
if (changeRaw.length > 4096) {
|
|
@@ -2310,7 +2363,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2310
2363
|
sendJson(res, 200, { note: customerNote });
|
|
2311
2364
|
}
|
|
2312
2365
|
catch (e) {
|
|
2313
|
-
const msg = e.message;
|
|
2366
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2314
2367
|
if (msg.includes('not found')) {
|
|
2315
2368
|
throw new HttpError(404, msg);
|
|
2316
2369
|
}
|
|
@@ -2330,7 +2383,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2330
2383
|
sendJson(res, 200, { note: customerNote });
|
|
2331
2384
|
}
|
|
2332
2385
|
catch (e) {
|
|
2333
|
-
const msg = e.message;
|
|
2386
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2334
2387
|
if (msg.includes('not found')) {
|
|
2335
2388
|
throw new HttpError(404, msg);
|
|
2336
2389
|
}
|
|
@@ -2386,8 +2439,8 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2386
2439
|
const previousSecret = process.env.SLACK_SIGNING_SECRET_PREVIOUS;
|
|
2387
2440
|
const sig = req.headers['x-slack-signature'];
|
|
2388
2441
|
const tsHdr = req.headers['x-slack-request-timestamp'];
|
|
2389
|
-
const sigStr =
|
|
2390
|
-
const tsStr =
|
|
2442
|
+
const sigStr = isHeaderString(sig) ? sig : null;
|
|
2443
|
+
const tsStr = isHeaderString(tsHdr) ? tsHdr : null;
|
|
2391
2444
|
if (sigStr === null ||
|
|
2392
2445
|
tsStr === null ||
|
|
2393
2446
|
!verifySlackSignature({
|
|
@@ -2438,20 +2491,21 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2438
2491
|
sendJson(res, 200, { ok: true, status: 'dlq' });
|
|
2439
2492
|
return;
|
|
2440
2493
|
}
|
|
2441
|
-
if (body
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2494
|
+
if (isJsonObjectRecord(body)) {
|
|
2495
|
+
const bodyRecord = body;
|
|
2496
|
+
if (bodyRecord.type === 'url_verification') {
|
|
2497
|
+
sendJson(res, 200, {
|
|
2498
|
+
challenge: String(bodyRecord.challenge ?? ''),
|
|
2499
|
+
});
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2448
2502
|
}
|
|
2449
2503
|
// Resolve tenant. v0.39 fail-closed: when slack_workspaces is non-empty
|
|
2450
2504
|
// and the team_id is unknown, resolveTenantForTeam returns null and we
|
|
2451
2505
|
// park the envelope in slack_dlq with bucket='unroutable'. Mandatory ACK
|
|
2452
2506
|
// 200 so Slack stops retrying; do NOT call ingest.
|
|
2453
2507
|
let resolvedTenant = null;
|
|
2454
|
-
if (isSlackEventEnvelope(body)) {
|
|
2508
|
+
if (body !== undefined && isSlackEventEnvelope(body)) {
|
|
2455
2509
|
const db = openHippoDb(opts.hippoRoot);
|
|
2456
2510
|
try {
|
|
2457
2511
|
resolvedTenant = resolveTenantForTeam(db, body.team_id);
|
|
@@ -2488,7 +2542,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2488
2542
|
tenantId: resolvedTenant,
|
|
2489
2543
|
actor: adminActor('connector:slack'),
|
|
2490
2544
|
};
|
|
2491
|
-
if (!isSlackEventEnvelope(body)) {
|
|
2545
|
+
if (body === undefined || !isSlackEventEnvelope(body)) {
|
|
2492
2546
|
const db = openHippoDb(ctx.hippoRoot);
|
|
2493
2547
|
try {
|
|
2494
2548
|
writeToDlq(db, {
|
|
@@ -2542,7 +2596,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2542
2596
|
tenantId: ctx.tenantId,
|
|
2543
2597
|
teamId: body.team_id,
|
|
2544
2598
|
rawPayload: rawBody,
|
|
2545
|
-
error: `unhandled event type: ${inner
|
|
2599
|
+
error: `unhandled event type: ${inner.type ?? 'unknown'}`,
|
|
2546
2600
|
bucket: 'parse_error',
|
|
2547
2601
|
signature: sigStr,
|
|
2548
2602
|
slackTimestamp: tsStr,
|
|
@@ -2583,9 +2637,9 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2583
2637
|
const sigHdr = req.headers['x-hub-signature-256'];
|
|
2584
2638
|
const eventHdr = req.headers['x-github-event'];
|
|
2585
2639
|
const deliveryHdr = req.headers['x-github-delivery'];
|
|
2586
|
-
const sigStr =
|
|
2587
|
-
const eventName =
|
|
2588
|
-
const deliveryId =
|
|
2640
|
+
const sigStr = isHeaderString(sigHdr) ? sigHdr : null;
|
|
2641
|
+
const eventName = isHeaderString(eventHdr) ? eventHdr : null;
|
|
2642
|
+
const deliveryId = isHeaderString(deliveryHdr) ? deliveryHdr : null;
|
|
2589
2643
|
if (sigStr === null ||
|
|
2590
2644
|
!verifyGitHubSignature({
|
|
2591
2645
|
rawBody,
|
|
@@ -2686,7 +2740,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2686
2740
|
sendJson(res, 200, { ok: true, status: 'dlq' });
|
|
2687
2741
|
return;
|
|
2688
2742
|
}
|
|
2689
|
-
if (!isGitHubWebhookEnvelope(body)) {
|
|
2743
|
+
if (body === undefined || !isGitHubWebhookEnvelope(body)) {
|
|
2690
2744
|
const db = openHippoDb(opts.hippoRoot);
|
|
2691
2745
|
try {
|
|
2692
2746
|
writeToGitHubDlq(db, {
|
|
@@ -2892,12 +2946,16 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2892
2946
|
catch {
|
|
2893
2947
|
throw new HttpError(400, 'invalid JSON-RPC body');
|
|
2894
2948
|
}
|
|
2895
|
-
if (!mcpReq ||
|
|
2949
|
+
if (!isJsonObjectRecord(mcpReq) || !isJsonString(mcpReq.method)) {
|
|
2896
2950
|
throw new HttpError(400, 'JSON-RPC body must include a method string');
|
|
2897
2951
|
}
|
|
2952
|
+
// SAFETY: validated above as a plain JSON object carrying a string method;
|
|
2953
|
+
// the remaining McpRequest wire fields (jsonrpc, id, params) are checked or
|
|
2954
|
+
// safely defaulted inside handleMcpRequest's JSON-RPC dispatch.
|
|
2955
|
+
const rpcReq = mcpReq;
|
|
2898
2956
|
let mcpRes;
|
|
2899
2957
|
try {
|
|
2900
|
-
mcpRes = await handleMcpRequest(
|
|
2958
|
+
mcpRes = await handleMcpRequest(rpcReq, {
|
|
2901
2959
|
hippoRoot: ctx.hippoRoot,
|
|
2902
2960
|
tenantId: ctx.tenantId,
|
|
2903
2961
|
// v1.12.0: McpContext.actor stays string; extract subject at the boundary.
|
|
@@ -2982,7 +3040,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
2982
3040
|
}, heartbeatMs);
|
|
2983
3041
|
// Don't keep the event loop alive just for this timer — the server's
|
|
2984
3042
|
// listener already does that, and tests want the process to exit cleanly.
|
|
2985
|
-
if (
|
|
3043
|
+
if (ping.unref instanceof Function)
|
|
2986
3044
|
ping.unref();
|
|
2987
3045
|
req.on('close', () => clearInterval(ping));
|
|
2988
3046
|
return;
|
|
@@ -3098,10 +3156,11 @@ export async function serve(opts) {
|
|
|
3098
3156
|
server.listen(requestedPort, host);
|
|
3099
3157
|
});
|
|
3100
3158
|
const address = server.address();
|
|
3101
|
-
if (!address
|
|
3159
|
+
if (!isAddressInfo(address)) {
|
|
3102
3160
|
throw new Error('server.address() returned unexpected shape');
|
|
3103
3161
|
}
|
|
3104
|
-
const
|
|
3162
|
+
const addressInfo = address;
|
|
3163
|
+
const actualPort = addressInfo.port;
|
|
3105
3164
|
const url = `http://${host}:${actualPort}`;
|
|
3106
3165
|
writePidfile(opts.hippoRoot, { port: actualPort, url, startedAt });
|
|
3107
3166
|
let stopping = false;
|