feat(data-plane): add bounded Gelios telemetry contract
This commit is contained in:
@@ -7,6 +7,11 @@ export {
|
||||
isBoundedGeoJsonGeometry,
|
||||
validateGeoJsonGeometry,
|
||||
} from "./geometry.mjs";
|
||||
export {
|
||||
TELEMETRY_READINGS_MAX_BYTES,
|
||||
TELEMETRY_READINGS_MAX_ITEMS,
|
||||
isBoundedTelemetryReadings,
|
||||
} from "./telemetry-readings.mjs";
|
||||
export {
|
||||
DATA_PRODUCT_HISTORY_SCHEMA_VERSION,
|
||||
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||
|
||||
@@ -9,6 +9,11 @@ export {
|
||||
isBoundedGeoJsonGeometry,
|
||||
validateGeoJsonGeometry,
|
||||
} from "./geometry.mjs";
|
||||
export {
|
||||
TELEMETRY_READINGS_MAX_BYTES,
|
||||
TELEMETRY_READINGS_MAX_ITEMS,
|
||||
isBoundedTelemetryReadings,
|
||||
} from "./telemetry-readings.mjs";
|
||||
export {
|
||||
ZONE_SOURCE_ADAPTERS,
|
||||
ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
|
||||
|
||||
@@ -4,6 +4,7 @@ export const L2_CONNECTION_INSTANCE_SCHEMA_VERSION = "nodedc.l2-connection-insta
|
||||
export const SEMANTIC_MAPPING_SCHEMA_VERSION = "nodedc.semantic-mapping/v1";
|
||||
|
||||
import { SECRET_LIKE_VALUE as SECRET_VALUE } from "./sensitive-field-policy.mjs";
|
||||
import { isBoundedTelemetryReadings } from "./telemetry-readings.mjs";
|
||||
|
||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||
@@ -20,7 +21,7 @@ const TOKEN_REFRESH_MODES = new Set(["not_applicable", "operator_managed", "runt
|
||||
const COLLECTION_MODES = new Set(["realtime", "manual", "history", "weekly"]);
|
||||
const DELIVERY_MODES = new Set(["snapshot", "snapshot+patch", "query"]);
|
||||
const HISTORY_MODES = new Set(["none", "all", "sampled"]);
|
||||
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "point", "geometry"]);
|
||||
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "telemetry_readings", "point", "geometry"]);
|
||||
const L2_STEP_KINDS = new Set([
|
||||
"collection_trigger",
|
||||
"provider_request",
|
||||
@@ -871,7 +872,7 @@ function validateFieldContract(value, path, errors) {
|
||||
if (value.enum !== undefined) {
|
||||
if (!Array.isArray(value.enum) || value.enum.length === 0 || new Set(value.enum.map(stableLiteral)).size !== value.enum.length) {
|
||||
errors.push(`${path}.enum_must_be_nonempty_unique_array`);
|
||||
} else if (new Set(["point", "geometry", "string_array"]).has(value.type)
|
||||
} else if (new Set(["point", "geometry", "string_array", "telemetry_readings"]).has(value.type)
|
||||
|| value.enum.some((item) => !fieldContractValueMatchesType(item, value.type))) {
|
||||
errors.push(`${path}.enum_value_type_invalid`);
|
||||
}
|
||||
@@ -939,6 +940,7 @@ function validateMappedValueAgainstFieldContract(value, contract, path, errors)
|
||||
|
||||
function fieldContractValueMatchesType(value, type) {
|
||||
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
if (type === "telemetry_readings") return isBoundedTelemetryReadings(value);
|
||||
if (type === "point") {
|
||||
return isPlainObject(value)
|
||||
&& value.type === "Point"
|
||||
@@ -997,7 +999,7 @@ function validateExpression(value, path, errors) {
|
||||
function validateDerivation(value, path, errors) {
|
||||
if (!isPlainObject(value)) return errors.push(`${path}_must_be_object`);
|
||||
rejectUnknownKeys(value, DERIVATION_KEYS, path, errors);
|
||||
if (!new Set(["boolean_rule", "ordered_rules", "flag_set"]).has(value.kind)) errors.push(`${path}.kind_invalid`);
|
||||
if (!new Set(["boolean_rule", "ordered_rules", "flag_set", "bounded_readings"]).has(value.kind)) errors.push(`${path}.kind_invalid`);
|
||||
requiredUniqueIdentifierArray(value.rules, `${path}.rules`, errors);
|
||||
if (value.default === undefined) {
|
||||
errors.push(`${path}.default_required`);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
export const TELEMETRY_READINGS_MAX_ITEMS = 128;
|
||||
export const TELEMETRY_READINGS_MAX_BYTES = 128 * 1024;
|
||||
|
||||
const READING_KEYS = new Set(["id", "label", "value", "unit", "observedAt"]);
|
||||
const READING_ID = /^[a-z][a-z0-9._:-]{1,127}$/;
|
||||
|
||||
/**
|
||||
* Validate the provider-neutral bounded telemetry collection accepted inside
|
||||
* one current-position fact. Provider-specific payloads, raw sensor objects
|
||||
* and arbitrary recursive JSON are deliberately excluded.
|
||||
*/
|
||||
export function isBoundedTelemetryReadings(value) {
|
||||
if (!Array.isArray(value) || value.length > TELEMETRY_READINGS_MAX_ITEMS) return false;
|
||||
let encoded;
|
||||
try {
|
||||
encoded = new TextEncoder().encode(JSON.stringify(value));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (encoded.byteLength > TELEMETRY_READINGS_MAX_BYTES) return false;
|
||||
|
||||
const ids = new Set();
|
||||
for (const reading of value) {
|
||||
if (!isPlainObject(reading) || Object.keys(reading).some((key) => !READING_KEYS.has(key))) return false;
|
||||
if (typeof reading.id !== "string" || !READING_ID.test(reading.id) || ids.has(reading.id)) return false;
|
||||
ids.add(reading.id);
|
||||
if (typeof reading.label !== "string" || !reading.label.trim() || reading.label.length > 160) return false;
|
||||
if (!isScalar(reading.value)) return false;
|
||||
if (typeof reading.value === "string" && reading.value.length > 512) return false;
|
||||
if (reading.unit !== undefined && (typeof reading.unit !== "string" || reading.unit.length > 32)) return false;
|
||||
if (reading.observedAt !== undefined && !isIsoTimestamp(reading.observedAt)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isScalar(value) {
|
||||
return typeof value === "string"
|
||||
|| typeof value === "boolean"
|
||||
|| (typeof value === "number" && Number.isFinite(value));
|
||||
}
|
||||
|
||||
function isIsoTimestamp(value) {
|
||||
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user