meshai/work/dashboard-frontend/node_modules/object-hash/index.js
malice 2c46c9104d
feat(region-routing): unified per-family routing cards + region-scoped family→channel routing (#87)
* feat(region-routing): P1 tagging + region_routes primitive + read/write API + preview launcher

- config.py: add Coverage.region_tagging (bool=False); add RegionRouteMatrix
  dataclass (enabled, cells) above NotificationsConfig; add region_routes field
  to NotificationsConfig; add explicit hydration branch for region_routes in
  _dict_to_dataclass mirroring destinations pattern.

- coverage_area.py: add MonitoringArea.name (str|None=None, frozen); update
  areas_from_config to preserve name; refactor inline geom extraction from
  classify_event_areas into shared _event_geom_json helper; add
  matching_area_names(geom_json, areas)->list[str] (additive, all named
  matches, config-order, deduped; gate unchanged); add event_region_names
  convenience wrapper.

- coverage_filter.py: add region_tagging ctor kwarg; stamp event.region/
  regions before the gate when region_tagging=True and areas non-empty and
  not event.regions (never clobbers satpass preset).

- pipeline/__init__.py: wire region_tagging into CoverageFilter construction.

- notification_routes.py: add GET /notifications/regions (named coverage area
  names, config-order, deduped); GET /notifications/region-routing (matrix as
  JSON); POST /notifications/region-routing (explicit RMW — only region_routes
  changes, toggles/rules/destinations survive).

- scripts/preview_dashboard.py: mesh-free launcher — dashboard API only, no
  mesh connector, no broadcast loop; vite runs separately.

All 87 coverage tests pass; 300 total pass; 6 pre-existing failures unchanged
(adapter config count mismatch + MeshCore EventType.NEW_CONTACT).

* feat(region-routing): manual region x family matrix editor page

Adds RegionRoutingMatrix.tsx — a plain editor over the region_routes
config primitive. Rows = families (via useFamilies()), cols = regions
(from GET /api/notifications/regions). Each cell exposes MT channel
(ChannelPicker single + includeDisabled), MC channel name (text input),
min_severity select (routine/priority/critical/immediate), and an enabled
checkbox. Only cells where MT or MC is set are included in the sparse
POST payload. Master enable toggle maps to top-level enabled. MT budget
guard warns when more than 7 distinct MT indices are in use. Sticky
family column; horizontal scroll for wide region sets.

Registers route /region-routing in App.tsx and adds "Region Routing"
nav entry (Map icon) under the Meshtastic section in Layout.tsx,
immediately after Routing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(region-routing): regions endpoint reads saved (disk) coverage so routing columns are dynamic without a bot restart; preview reloads config after writes

* feat(routing): unify MT/MC routing into per-family cards; region routing as an in-card expand; remove rules/destinations UI + standalone page

* refactor(routing): move Meshtastic Routing from /notifications to /meshtastic/routing (mirror /meshcore/routing); redirect legacy path

* feat(region-routing): dispatcher honors region_routes matrix (authoritative-on-match, per-region cooldown, per-channel dedup); non-matrix path unchanged

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(region-routing): matrix dedup key must match boot-restore 2-tuple form (prevents restart re-broadcast flood); regression test

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 16:52:03 -06:00

453 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
var crypto = require('crypto');
/**
* Exported function
*
* Options:
*
* - `algorithm` hash algo to be used by this instance: *'sha1', 'md5'
* - `excludeValues` {true|*false} hash object keys, values ignored
* - `encoding` hash encoding, supports 'buffer', '*hex', 'binary', 'base64'
* - `ignoreUnknown` {true|*false} ignore unknown object types
* - `replacer` optional function that replaces values before hashing
* - `respectFunctionProperties` {*true|false} consider function properties when hashing
* - `respectFunctionNames` {*true|false} consider 'name' property of functions for hashing
* - `respectType` {*true|false} Respect special properties (prototype, constructor)
* when hashing to distinguish between types
* - `unorderedArrays` {true|*false} Sort all arrays before hashing
* - `unorderedSets` {*true|false} Sort `Set` and `Map` instances before hashing
* * = default
*
* @param {object} object value to hash
* @param {object} options hashing options
* @return {string} hash value
* @api public
*/
exports = module.exports = objectHash;
function objectHash(object, options){
options = applyDefaults(object, options);
return hash(object, options);
}
/**
* Exported sugar methods
*
* @param {object} object value to hash
* @return {string} hash value
* @api public
*/
exports.sha1 = function(object){
return objectHash(object);
};
exports.keys = function(object){
return objectHash(object, {excludeValues: true, algorithm: 'sha1', encoding: 'hex'});
};
exports.MD5 = function(object){
return objectHash(object, {algorithm: 'md5', encoding: 'hex'});
};
exports.keysMD5 = function(object){
return objectHash(object, {algorithm: 'md5', encoding: 'hex', excludeValues: true});
};
// Internals
var hashes = crypto.getHashes ? crypto.getHashes().slice() : ['sha1', 'md5'];
hashes.push('passthrough');
var encodings = ['buffer', 'hex', 'binary', 'base64'];
function applyDefaults(object, sourceOptions){
sourceOptions = sourceOptions || {};
// create a copy rather than mutating
var options = {};
options.algorithm = sourceOptions.algorithm || 'sha1';
options.encoding = sourceOptions.encoding || 'hex';
options.excludeValues = sourceOptions.excludeValues ? true : false;
options.algorithm = options.algorithm.toLowerCase();
options.encoding = options.encoding.toLowerCase();
options.ignoreUnknown = sourceOptions.ignoreUnknown !== true ? false : true; // default to false
options.respectType = sourceOptions.respectType === false ? false : true; // default to true
options.respectFunctionNames = sourceOptions.respectFunctionNames === false ? false : true;
options.respectFunctionProperties = sourceOptions.respectFunctionProperties === false ? false : true;
options.unorderedArrays = sourceOptions.unorderedArrays !== true ? false : true; // default to false
options.unorderedSets = sourceOptions.unorderedSets === false ? false : true; // default to false
options.unorderedObjects = sourceOptions.unorderedObjects === false ? false : true; // default to true
options.replacer = sourceOptions.replacer || undefined;
options.excludeKeys = sourceOptions.excludeKeys || undefined;
if(typeof object === 'undefined') {
throw new Error('Object argument required.');
}
// if there is a case-insensitive match in the hashes list, accept it
// (i.e. SHA256 for sha256)
for (var i = 0; i < hashes.length; ++i) {
if (hashes[i].toLowerCase() === options.algorithm.toLowerCase()) {
options.algorithm = hashes[i];
}
}
if(hashes.indexOf(options.algorithm) === -1){
throw new Error('Algorithm "' + options.algorithm + '" not supported. ' +
'supported values: ' + hashes.join(', '));
}
if(encodings.indexOf(options.encoding) === -1 &&
options.algorithm !== 'passthrough'){
throw new Error('Encoding "' + options.encoding + '" not supported. ' +
'supported values: ' + encodings.join(', '));
}
return options;
}
/** Check if the given function is a native function */
function isNativeFunction(f) {
if ((typeof f) !== 'function') {
return false;
}
var exp = /^function\s+\w*\s*\(\s*\)\s*{\s+\[native code\]\s+}$/i;
return exp.exec(Function.prototype.toString.call(f)) != null;
}
function hash(object, options) {
var hashingStream;
if (options.algorithm !== 'passthrough') {
hashingStream = crypto.createHash(options.algorithm);
} else {
hashingStream = new PassThrough();
}
if (typeof hashingStream.write === 'undefined') {
hashingStream.write = hashingStream.update;
hashingStream.end = hashingStream.update;
}
var hasher = typeHasher(options, hashingStream);
hasher.dispatch(object);
if (!hashingStream.update) {
hashingStream.end('');
}
if (hashingStream.digest) {
return hashingStream.digest(options.encoding === 'buffer' ? undefined : options.encoding);
}
var buf = hashingStream.read();
if (options.encoding === 'buffer') {
return buf;
}
return buf.toString(options.encoding);
}
/**
* Expose streaming API
*
* @param {object} object Value to serialize
* @param {object} options Options, as for hash()
* @param {object} stream A stream to write the serializiation to
* @api public
*/
exports.writeToStream = function(object, options, stream) {
if (typeof stream === 'undefined') {
stream = options;
options = {};
}
options = applyDefaults(object, options);
return typeHasher(options, stream).dispatch(object);
};
function typeHasher(options, writeTo, context){
context = context || [];
var write = function(str) {
if (writeTo.update) {
return writeTo.update(str, 'utf8');
} else {
return writeTo.write(str, 'utf8');
}
};
return {
dispatch: function(value){
if (options.replacer) {
value = options.replacer(value);
}
var type = typeof value;
if (value === null) {
type = 'null';
}
//console.log("[DEBUG] Dispatch: ", value, "->", type, " -> ", "_" + type);
return this['_' + type](value);
},
_object: function(object) {
var pattern = (/\[object (.*)\]/i);
var objString = Object.prototype.toString.call(object);
var objType = pattern.exec(objString);
if (!objType) { // object type did not match [object ...]
objType = 'unknown:[' + objString + ']';
} else {
objType = objType[1]; // take only the class name
}
objType = objType.toLowerCase();
var objectNumber = null;
if ((objectNumber = context.indexOf(object)) >= 0) {
return this.dispatch('[CIRCULAR:' + objectNumber + ']');
} else {
context.push(object);
}
if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(object)) {
write('buffer:');
return write(object);
}
if(objType !== 'object' && objType !== 'function' && objType !== 'asyncfunction') {
if(this['_' + objType]) {
this['_' + objType](object);
} else if (options.ignoreUnknown) {
return write('[' + objType + ']');
} else {
throw new Error('Unknown object type "' + objType + '"');
}
}else{
var keys = Object.keys(object);
if (options.unorderedObjects) {
keys = keys.sort();
}
// Make sure to incorporate special properties, so
// Types with different prototypes will produce
// a different hash and objects derived from
// different functions (`new Foo`, `new Bar`) will
// produce different hashes.
// We never do this for native functions since some
// seem to break because of that.
if (options.respectType !== false && !isNativeFunction(object)) {
keys.splice(0, 0, 'prototype', '__proto__', 'constructor');
}
if (options.excludeKeys) {
keys = keys.filter(function(key) { return !options.excludeKeys(key); });
}
write('object:' + keys.length + ':');
var self = this;
return keys.forEach(function(key){
self.dispatch(key);
write(':');
if(!options.excludeValues) {
self.dispatch(object[key]);
}
write(',');
});
}
},
_array: function(arr, unordered){
unordered = typeof unordered !== 'undefined' ? unordered :
options.unorderedArrays !== false; // default to options.unorderedArrays
var self = this;
write('array:' + arr.length + ':');
if (!unordered || arr.length <= 1) {
return arr.forEach(function(entry) {
return self.dispatch(entry);
});
}
// the unordered case is a little more complicated:
// since there is no canonical ordering on objects,
// i.e. {a:1} < {a:2} and {a:1} > {a:2} are both false,
// we first serialize each entry using a PassThrough stream
// before sorting.
// also: we cant use the same context array for all entries
// since the order of hashing should *not* matter. instead,
// we keep track of the additions to a copy of the context array
// and add all of them to the global context array when were done
var contextAdditions = [];
var entries = arr.map(function(entry) {
var strm = new PassThrough();
var localContext = context.slice(); // make copy
var hasher = typeHasher(options, strm, localContext);
hasher.dispatch(entry);
// take only what was added to localContext and append it to contextAdditions
contextAdditions = contextAdditions.concat(localContext.slice(context.length));
return strm.read().toString();
});
context = context.concat(contextAdditions);
entries.sort();
return this._array(entries, false);
},
_date: function(date){
return write('date:' + date.toJSON());
},
_symbol: function(sym){
return write('symbol:' + sym.toString());
},
_error: function(err){
return write('error:' + err.toString());
},
_boolean: function(bool){
return write('bool:' + bool.toString());
},
_string: function(string){
write('string:' + string.length + ':');
write(string.toString());
},
_function: function(fn){
write('fn:');
if (isNativeFunction(fn)) {
this.dispatch('[native]');
} else {
this.dispatch(fn.toString());
}
if (options.respectFunctionNames !== false) {
// Make sure we can still distinguish native functions
// by their name, otherwise String and Function will
// have the same hash
this.dispatch("function-name:" + String(fn.name));
}
if (options.respectFunctionProperties) {
this._object(fn);
}
},
_number: function(number){
return write('number:' + number.toString());
},
_xml: function(xml){
return write('xml:' + xml.toString());
},
_null: function() {
return write('Null');
},
_undefined: function() {
return write('Undefined');
},
_regexp: function(regex){
return write('regex:' + regex.toString());
},
_uint8array: function(arr){
write('uint8array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_uint8clampedarray: function(arr){
write('uint8clampedarray:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_int8array: function(arr){
write('int8array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_uint16array: function(arr){
write('uint16array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_int16array: function(arr){
write('int16array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_uint32array: function(arr){
write('uint32array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_int32array: function(arr){
write('int32array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_float32array: function(arr){
write('float32array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_float64array: function(arr){
write('float64array:');
return this.dispatch(Array.prototype.slice.call(arr));
},
_arraybuffer: function(arr){
write('arraybuffer:');
return this.dispatch(new Uint8Array(arr));
},
_url: function(url) {
return write('url:' + url.toString(), 'utf8');
},
_map: function(map) {
write('map:');
var arr = Array.from(map);
return this._array(arr, options.unorderedSets !== false);
},
_set: function(set) {
write('set:');
var arr = Array.from(set);
return this._array(arr, options.unorderedSets !== false);
},
_file: function(file) {
write('file:');
return this.dispatch([file.name, file.size, file.type, file.lastModfied]);
},
_blob: function() {
if (options.ignoreUnknown) {
return write('[blob]');
}
throw Error('Hashing Blob objects is currently not supported\n' +
'(see https://github.com/puleos/object-hash/issues/26)\n' +
'Use "options.replacer" or "options.ignoreUnknown"\n');
},
_domwindow: function() { return write('domwindow'); },
_bigint: function(number){
return write('bigint:' + number.toString());
},
/* Node.js standard native objects */
_process: function() { return write('process'); },
_timer: function() { return write('timer'); },
_pipe: function() { return write('pipe'); },
_tcp: function() { return write('tcp'); },
_udp: function() { return write('udp'); },
_tty: function() { return write('tty'); },
_statwatcher: function() { return write('statwatcher'); },
_securecontext: function() { return write('securecontext'); },
_connection: function() { return write('connection'); },
_zlib: function() { return write('zlib'); },
_context: function() { return write('context'); },
_nodescript: function() { return write('nodescript'); },
_httpparser: function() { return write('httpparser'); },
_dataview: function() { return write('dataview'); },
_signal: function() { return write('signal'); },
_fsevent: function() { return write('fsevent'); },
_tlswrap: function() { return write('tlswrap'); },
};
}
// Mini-implementation of stream.PassThrough
// We are far from having need for the full implementation, and we can
// make assumptions like "many writes, then only one final read"
// and we can ignore encoding specifics
function PassThrough() {
return {
buf: '',
write: function(b) {
this.buf += b;
},
end: function(b) {
this.buf += b;
},
read: function() {
return this.buf;
}
};
}