feat(connectors): version provider packages and ontology contracts
This commit is contained in:
parent
567f1550ab
commit
3415674e76
|
|
@ -8,15 +8,14 @@ export const geliosPositionsCurrentExample = Object.freeze({
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
ontology: {
|
ontology: {
|
||||||
packageId: "gelios",
|
packageId: "gelios",
|
||||||
revision: "gelios.positions.v1",
|
revision: "ontology.gelios.v1",
|
||||||
},
|
},
|
||||||
l2Template: {
|
l2Template: {
|
||||||
id: "gelios.positions.current",
|
id: "gelios.positions.current.l2.v1",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
},
|
},
|
||||||
capabilities: [
|
capabilities: [
|
||||||
{ id: "gelios.units.current.read", classification: "read" },
|
{ id: "gelios.units.current.read", classification: "read" },
|
||||||
{ id: "gelios.units.command.write", classification: "write" },
|
|
||||||
],
|
],
|
||||||
dataProductIds: ["fleet.positions.current.v1"],
|
dataProductIds: ["fleet.positions.current.v1"],
|
||||||
},
|
},
|
||||||
|
|
@ -25,10 +24,10 @@ export const geliosPositionsCurrentExample = Object.freeze({
|
||||||
id: "gelios.sample-fleet",
|
id: "gelios.sample-fleet",
|
||||||
providerId: "gelios",
|
providerId: "gelios",
|
||||||
tenantId: "sample-tenant",
|
tenantId: "sample-tenant",
|
||||||
credentialRef: { owner: "engine", reference: "engine-credential-reference" },
|
credentialRef: { owner: "ndc_l2_credentials", reference: "ndc-credref:provider-example-0001" },
|
||||||
scope: {
|
scope: {
|
||||||
capabilityIds: ["gelios.units.current.read"],
|
capabilityIds: ["gelios.units.current.read"],
|
||||||
fieldPolicyId: "fleet.position.display.v1",
|
fieldPolicyId: "gelios.positions.current.fields.v1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collectionProfile: {
|
collectionProfile: {
|
||||||
|
|
@ -36,7 +35,7 @@ export const geliosPositionsCurrentExample = Object.freeze({
|
||||||
id: "gelios.positions.realtime",
|
id: "gelios.positions.realtime",
|
||||||
connectionId: "gelios.sample-fleet",
|
connectionId: "gelios.sample-fleet",
|
||||||
mode: "realtime",
|
mode: "realtime",
|
||||||
schedule: { intervalMs: 15000 },
|
schedule: { intervalMs: 10000 },
|
||||||
capabilityIds: ["gelios.units.current.read"],
|
capabilityIds: ["gelios.units.current.read"],
|
||||||
dataProductId: "fleet.positions.current.v1",
|
dataProductId: "fleet.positions.current.v1",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./src/index.mjs",
|
"exports": {
|
||||||
|
".": "./src/index.mjs",
|
||||||
|
"./data-plane": "./src/data-plane.mjs",
|
||||||
|
"./providers/*": "./providers/*/index.mjs"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "node test/contract.test.mjs && node test/data-product.test.mjs && node test/engine-credential-sink.test.mjs && node test/engine-private-extension.test.mjs"
|
"check": "node test/contract.test.mjs && node test/data-product.test.mjs && node test/provider-package.test.mjs && node test/engine-private-extension.test.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { DATA_PRODUCT_PUBLISH_SCHEMA_VERSION } from "../../../../src/data-product.mjs";
|
||||||
|
|
||||||
|
export const geliosUnitsCurrentFixtureV1 = Object.freeze({
|
||||||
|
sourceResponse: {
|
||||||
|
units: [{
|
||||||
|
id: 1001,
|
||||||
|
name: "Synthetic unit 1001",
|
||||||
|
hw_id: "restricted-fixture-value",
|
||||||
|
phone: "restricted-fixture-value",
|
||||||
|
lastMsg: {
|
||||||
|
time: 1784203200,
|
||||||
|
lat: "55.7558",
|
||||||
|
lon: "37.6173",
|
||||||
|
speed: 24.5,
|
||||||
|
course: 91,
|
||||||
|
height: 164,
|
||||||
|
gps_valid: true,
|
||||||
|
sats: 11,
|
||||||
|
hdop: 0.8,
|
||||||
|
accuracy: 4.2,
|
||||||
|
params: "restricted-fixture-value",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
id: "001002",
|
||||||
|
name: "Synthetic unit 1002",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
collectionContext: {
|
||||||
|
receivedAt: "2026-07-16T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
expectedPublish: {
|
||||||
|
schemaVersion: DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||||
|
batch: {
|
||||||
|
runId: "gelios-fixture-run-20260716",
|
||||||
|
sequence: 0,
|
||||||
|
idempotencyKey: "gelios-fixture-run-20260716.batch-0",
|
||||||
|
},
|
||||||
|
facts: [{
|
||||||
|
sourceId: "gelios-unit-1001",
|
||||||
|
semanticType: "map.moving_object",
|
||||||
|
observedAt: "2026-07-16T12:00:00.000Z",
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [37.6173, 55.7558],
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
course_degrees: 91,
|
||||||
|
display_name: "Synthetic unit 1001",
|
||||||
|
elevation_meters: 164,
|
||||||
|
hdop: 0.8,
|
||||||
|
horizontal_accuracy_meters: 4.2,
|
||||||
|
object_kind: "tracked_unit",
|
||||||
|
operational_status: "active",
|
||||||
|
position_source: "gelios",
|
||||||
|
position_valid: true,
|
||||||
|
quality_flags: [],
|
||||||
|
satellite_count: 11,
|
||||||
|
speed_kph: 24.5,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
sourceId: "gelios-unit-001002",
|
||||||
|
semanticType: "map.moving_object",
|
||||||
|
observedAt: "2026-07-16T12:00:00.000Z",
|
||||||
|
attributes: {
|
||||||
|
display_name: "Synthetic unit 1002",
|
||||||
|
object_kind: "tracked_unit",
|
||||||
|
operational_status: "no_position",
|
||||||
|
position_source: "gelios",
|
||||||
|
position_valid: false,
|
||||||
|
quality_flags: ["missing_geometry"],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export {
|
||||||
|
GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
GELIOS_POSITIONS_DATA_PRODUCT_VERSION,
|
||||||
|
GELIOS_POSITIONS_ONTOLOGY_REVISION,
|
||||||
|
GELIOS_PROVIDER_PACKAGE_ID,
|
||||||
|
GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
GELIOS_UNIT_SOURCE_ID_PREFIX,
|
||||||
|
geliosProviderPackageV1,
|
||||||
|
} from "./package.mjs";
|
||||||
|
|
||||||
|
export { geliosUnitsCurrentFixtureV1 } from "./fixtures/units-current.fixture.mjs";
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
import {
|
||||||
|
L2_TEMPLATE_SCHEMA_VERSION,
|
||||||
|
PROVIDER_PACKAGE_SCHEMA_VERSION,
|
||||||
|
SEMANTIC_MAPPING_SCHEMA_VERSION,
|
||||||
|
} from "../../../src/provider-package.mjs";
|
||||||
|
|
||||||
|
export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v1";
|
||||||
|
export const GELIOS_PROVIDER_PACKAGE_VERSION = "1.0.0";
|
||||||
|
export const GELIOS_POSITIONS_DATA_PRODUCT_ID = "fleet.positions.current.v1";
|
||||||
|
export const GELIOS_POSITIONS_DATA_PRODUCT_VERSION = "1.0.0";
|
||||||
|
export const GELIOS_POSITIONS_ONTOLOGY_REVISION = "ontology.map.moving_object.v1";
|
||||||
|
export const GELIOS_UNIT_SOURCE_ID_PREFIX = "gelios-unit-";
|
||||||
|
|
||||||
|
const targetFields = Object.freeze([
|
||||||
|
"course_degrees",
|
||||||
|
"display_name",
|
||||||
|
"elevation_meters",
|
||||||
|
"geometry",
|
||||||
|
"hdop",
|
||||||
|
"horizontal_accuracy_meters",
|
||||||
|
"object_kind",
|
||||||
|
"operational_status",
|
||||||
|
"position_source",
|
||||||
|
"position_valid",
|
||||||
|
"quality_flags",
|
||||||
|
"satellite_count",
|
||||||
|
"speed_kph",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const geliosProviderPackageV1 = deepFreeze({
|
||||||
|
schemaVersion: PROVIDER_PACKAGE_SCHEMA_VERSION,
|
||||||
|
id: GELIOS_PROVIDER_PACKAGE_ID,
|
||||||
|
providerId: "gelios",
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
manifest: {
|
||||||
|
id: "gelios.provider.manifest.v1",
|
||||||
|
providerId: "gelios",
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
ontology: {
|
||||||
|
packageId: "gelios",
|
||||||
|
revision: "ontology.gelios.v1",
|
||||||
|
},
|
||||||
|
authModeIds: ["gelios.rest-bearer.v1"],
|
||||||
|
capabilityIds: ["gelios.units.current.read"],
|
||||||
|
fieldPolicyIds: ["gelios.positions.current.fields.v1"],
|
||||||
|
collectionProfileIds: [
|
||||||
|
"gelios.positions.current.realtime.v1",
|
||||||
|
"gelios.positions.current.manual.v1",
|
||||||
|
],
|
||||||
|
dataProductIds: [GELIOS_POSITIONS_DATA_PRODUCT_ID],
|
||||||
|
mappingContractIds: ["gelios.units.to.fleet.positions.current.v1"],
|
||||||
|
l2TemplateIds: ["gelios.positions.current.l2.v1"],
|
||||||
|
},
|
||||||
|
authModes: [{
|
||||||
|
id: "gelios.rest-bearer.v1",
|
||||||
|
kind: "api_token",
|
||||||
|
credentialOwner: "ndc_l2_credentials",
|
||||||
|
bindingCardinality: "one_per_connection",
|
||||||
|
transport: {
|
||||||
|
placement: "header",
|
||||||
|
name: "Authorization",
|
||||||
|
},
|
||||||
|
tokenLifecycle: {
|
||||||
|
artifacts: ["access", "refresh"],
|
||||||
|
requestArtifact: "access",
|
||||||
|
refreshMode: "operator_managed",
|
||||||
|
},
|
||||||
|
secretHandling: {
|
||||||
|
store: "ndc_l2_credentials",
|
||||||
|
exposeToGraph: false,
|
||||||
|
persistInPackage: false,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
capabilities: [{
|
||||||
|
id: "gelios.units.current.read",
|
||||||
|
classification: "read",
|
||||||
|
status: "implemented",
|
||||||
|
authModeId: "gelios.rest-bearer.v1",
|
||||||
|
request: {
|
||||||
|
method: "GET",
|
||||||
|
baseUrl: "https://api.geliospro.com",
|
||||||
|
path: "/api/v1/units",
|
||||||
|
query: {},
|
||||||
|
response: {
|
||||||
|
collectionPaths: ["units", "data", "data.units", "result", "$"],
|
||||||
|
pagination: "single_bounded_response",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entityScope: {
|
||||||
|
mode: "all_visible_to_credential",
|
||||||
|
refresh: "each_collection_run",
|
||||||
|
businessEntityFilter: "forbidden",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
fieldPolicies: [{
|
||||||
|
id: "gelios.positions.current.fields.v1",
|
||||||
|
version: "1.0.0",
|
||||||
|
dataProductId: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
targetFields: [...targetFields],
|
||||||
|
unknownSourceFields: "drop",
|
||||||
|
dynamicSourceFields: "drop_until_classified",
|
||||||
|
restrictedSourcePaths: [
|
||||||
|
"decrypt_key",
|
||||||
|
"hw_id",
|
||||||
|
"imei",
|
||||||
|
"info",
|
||||||
|
"lmsg.address",
|
||||||
|
"lmsg.params",
|
||||||
|
"lmsg.sensors",
|
||||||
|
"lastMsg.address",
|
||||||
|
"lastMsg.params",
|
||||||
|
"lastMsg.sensors",
|
||||||
|
"lastMessage.address",
|
||||||
|
"lastMessage.params",
|
||||||
|
"lastMessage.sensors",
|
||||||
|
"phone",
|
||||||
|
"phone2",
|
||||||
|
"sensors",
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
collectionProfiles: [{
|
||||||
|
id: "gelios.positions.current.realtime.v1",
|
||||||
|
version: "1.0.0",
|
||||||
|
mode: "realtime",
|
||||||
|
schedule: { intervalMs: 10000 },
|
||||||
|
capabilityIds: ["gelios.units.current.read"],
|
||||||
|
dataProductId: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
mappingContractId: "gelios.units.to.fleet.positions.current.v1",
|
||||||
|
fieldPolicyId: "gelios.positions.current.fields.v1",
|
||||||
|
l2TemplateId: "gelios.positions.current.l2.v1",
|
||||||
|
entityScope: {
|
||||||
|
mode: "all_visible_to_credential",
|
||||||
|
refresh: "each_collection_run",
|
||||||
|
businessEntityFilter: "forbidden",
|
||||||
|
},
|
||||||
|
batching: { maxFacts: 5000 },
|
||||||
|
cardinality: {
|
||||||
|
maxCurrentEntities: 5000,
|
||||||
|
onExceed: "require_partitioned_data_product",
|
||||||
|
},
|
||||||
|
retry: {
|
||||||
|
maxAttempts: 4,
|
||||||
|
backoff: "exponential_with_jitter",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
id: "gelios.positions.current.manual.v1",
|
||||||
|
version: "1.0.0",
|
||||||
|
mode: "manual",
|
||||||
|
capabilityIds: ["gelios.units.current.read"],
|
||||||
|
dataProductId: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
mappingContractId: "gelios.units.to.fleet.positions.current.v1",
|
||||||
|
fieldPolicyId: "gelios.positions.current.fields.v1",
|
||||||
|
l2TemplateId: "gelios.positions.current.l2.v1",
|
||||||
|
entityScope: {
|
||||||
|
mode: "all_visible_to_credential",
|
||||||
|
refresh: "each_collection_run",
|
||||||
|
businessEntityFilter: "forbidden",
|
||||||
|
},
|
||||||
|
batching: { maxFacts: 5000 },
|
||||||
|
cardinality: {
|
||||||
|
maxCurrentEntities: 5000,
|
||||||
|
onExceed: "require_partitioned_data_product",
|
||||||
|
},
|
||||||
|
retry: {
|
||||||
|
maxAttempts: 4,
|
||||||
|
backoff: "exponential_with_jitter",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
dataProducts: [{
|
||||||
|
id: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
version: GELIOS_POSITIONS_DATA_PRODUCT_VERSION,
|
||||||
|
ontologyRevision: GELIOS_POSITIONS_ONTOLOGY_REVISION,
|
||||||
|
deliveryMode: "snapshot+patch",
|
||||||
|
semanticTypes: ["map.moving_object"],
|
||||||
|
fields: [...targetFields],
|
||||||
|
history: {
|
||||||
|
mode: "sampled",
|
||||||
|
intervalMs: 60000,
|
||||||
|
strategy: "latest-per-entity-per-bucket",
|
||||||
|
retentionDays: 90,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
mappingContracts: [{
|
||||||
|
schemaVersion: SEMANTIC_MAPPING_SCHEMA_VERSION,
|
||||||
|
id: "gelios.units.to.fleet.positions.current.v1",
|
||||||
|
version: "1.0.0",
|
||||||
|
sourceCapabilityId: "gelios.units.current.read",
|
||||||
|
fieldPolicyId: "gelios.positions.current.fields.v1",
|
||||||
|
target: {
|
||||||
|
dataProductId: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
version: GELIOS_POSITIONS_DATA_PRODUCT_VERSION,
|
||||||
|
ontologyRevision: GELIOS_POSITIONS_ONTOLOGY_REVISION,
|
||||||
|
semanticType: "map.moving_object",
|
||||||
|
},
|
||||||
|
derivations: {
|
||||||
|
position_valid: {
|
||||||
|
kind: "boolean_rule",
|
||||||
|
rules: [
|
||||||
|
"geometry.coordinates_finite_wgs84",
|
||||||
|
"provider.gps_valid_not_false",
|
||||||
|
],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
operational_status: {
|
||||||
|
kind: "ordered_rules",
|
||||||
|
rules: [
|
||||||
|
"position_invalid.no_position",
|
||||||
|
"observed_age_gt_limit.stale",
|
||||||
|
"otherwise.active",
|
||||||
|
],
|
||||||
|
default: "active",
|
||||||
|
parameters: { staleAfterMs: 60000 },
|
||||||
|
},
|
||||||
|
quality_flags: {
|
||||||
|
kind: "flag_set",
|
||||||
|
rules: [
|
||||||
|
"geometry_missing.missing_geometry",
|
||||||
|
"provider_gps_false.gps_invalid",
|
||||||
|
"satellite_count_zero.no_satellites",
|
||||||
|
"observed_age_gt_limit.stale",
|
||||||
|
],
|
||||||
|
default: [],
|
||||||
|
parameters: { staleAfterMs: 60000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fact: {
|
||||||
|
sourceId: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["id", "unit_id", "unitId"],
|
||||||
|
coerce: "string",
|
||||||
|
prefix: GELIOS_UNIT_SOURCE_ID_PREFIX,
|
||||||
|
},
|
||||||
|
semanticType: { constant: "map.moving_object" },
|
||||||
|
observedAt: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: [
|
||||||
|
"lmsg.time",
|
||||||
|
"lmsg.ts",
|
||||||
|
"lmsg.timestamp",
|
||||||
|
"last_msg.time",
|
||||||
|
"last_msg.timestamp",
|
||||||
|
"lastMsg.time",
|
||||||
|
"lastMessage.time",
|
||||||
|
"last_msg_time",
|
||||||
|
"lmsg_time",
|
||||||
|
],
|
||||||
|
coerce: "unix_or_iso_timestamp",
|
||||||
|
fallback: "collection_received_at",
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
longitude: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.lon", "lmsg.lng", "lmsg.longitude", "last_msg.lon", "lastMsg.lon", "lastMsg.lng", "lastMsg.longitude", "lastMessage.lon", "lastMessage.lng", "lastMessage.longitude", "lon", "lng", "longitude"],
|
||||||
|
coerce: "number",
|
||||||
|
},
|
||||||
|
latitude: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.lat", "lmsg.latitude", "last_msg.lat", "lastMsg.lat", "lastMsg.latitude", "lastMessage.lat", "lastMessage.latitude", "lat", "latitude"],
|
||||||
|
coerce: "number",
|
||||||
|
},
|
||||||
|
omitIfInvalid: true,
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
course_degrees: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.course", "lmsg.angle", "last_msg.course", "lastMsg.course", "lastMsg.angle", "lastMessage.course", "lastMessage.angle", "course"],
|
||||||
|
coerce: "number",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
display_name: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["name", "unit_name", "title", "label"],
|
||||||
|
coerce: "string",
|
||||||
|
},
|
||||||
|
elevation_meters: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.height", "lmsg.altitude", "last_msg.height", "lastMsg.height", "lastMsg.altitude", "lastMessage.height", "lastMessage.altitude", "height", "altitude"],
|
||||||
|
coerce: "number",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
hdop: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.hdop", "last_msg.hdop", "lastMsg.hdop", "lastMessage.hdop", "gps_hdop", "hdop"],
|
||||||
|
coerce: "number",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
horizontal_accuracy_meters: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.accuracy", "last_msg.accuracy", "lastMsg.accuracy", "lastMessage.accuracy", "gps_accuracy_m", "accuracy_m", "accuracy"],
|
||||||
|
coerce: "number",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
object_kind: { constant: "tracked_unit" },
|
||||||
|
operational_status: { derive: "operational_status" },
|
||||||
|
position_source: { constant: "gelios" },
|
||||||
|
position_valid: { derive: "position_valid" },
|
||||||
|
quality_flags: { derive: "quality_flags" },
|
||||||
|
satellite_count: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.sats", "lmsg.satellites", "last_msg.sats", "lastMsg.sats", "lastMsg.satellites", "lastMessage.sats", "lastMessage.satellites", "gps_sats", "sats", "satellites"],
|
||||||
|
coerce: "number",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
speed_kph: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["lmsg.speed", "last_msg.speed", "lastMsg.speed", "lastMessage.speed", "speed"],
|
||||||
|
coerce: "number",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
l2Templates: [{
|
||||||
|
schemaVersion: L2_TEMPLATE_SCHEMA_VERSION,
|
||||||
|
id: "gelios.positions.current.l2.v1",
|
||||||
|
version: "1.0.0",
|
||||||
|
runtime: "ndc_l2",
|
||||||
|
instanceMode: "one_connection_per_workflow",
|
||||||
|
connectionParameters: [
|
||||||
|
"tenant_id",
|
||||||
|
"connection_id",
|
||||||
|
"collection_profile_id",
|
||||||
|
"provider_credential_ref",
|
||||||
|
],
|
||||||
|
credentialBindings: [{
|
||||||
|
role: "provider",
|
||||||
|
owner: "ndc_l2_credentials",
|
||||||
|
management: "connection_input",
|
||||||
|
authModeId: "gelios.rest-bearer.v1",
|
||||||
|
}, {
|
||||||
|
role: "publisher",
|
||||||
|
owner: "ndc_l2_credentials",
|
||||||
|
management: "control_plane_managed",
|
||||||
|
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
}],
|
||||||
|
steps: [{
|
||||||
|
id: "collection.trigger",
|
||||||
|
kind: "collection_trigger",
|
||||||
|
collectionProfileDriven: true,
|
||||||
|
}, {
|
||||||
|
id: "provider.fetch",
|
||||||
|
kind: "provider_request",
|
||||||
|
capabilityId: "gelios.units.current.read",
|
||||||
|
}, {
|
||||||
|
id: "provider.extract-items",
|
||||||
|
kind: "extract_items",
|
||||||
|
capabilityId: "gelios.units.current.read",
|
||||||
|
}, {
|
||||||
|
id: "ontology.map",
|
||||||
|
kind: "semantic_mapping",
|
||||||
|
mappingContractId: "gelios.units.to.fleet.positions.current.v1",
|
||||||
|
}, {
|
||||||
|
id: "data-product.publish",
|
||||||
|
kind: "data_product_publish",
|
||||||
|
dataProductId: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
}],
|
||||||
|
invariants: {
|
||||||
|
oneConnectionPerInstance: true,
|
||||||
|
providerSecretByReferenceOnly: true,
|
||||||
|
publisherBindingControlPlaneManaged: true,
|
||||||
|
callerScopeForbidden: true,
|
||||||
|
providerSpecificServiceForbidden: true,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
function deepFreeze(value) {
|
||||||
|
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
||||||
|
Object.freeze(value);
|
||||||
|
for (const child of Object.values(value)) deepFreeze(child);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const EXTERNAL_PROVIDER_CONTRACT_VERSION = "nodedc.external-provider-contract/v1";
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
|
||||||
|
export { validateIntakeBatch } from "./intake-batch.mjs";
|
||||||
|
export {
|
||||||
|
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||||
|
DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||||
|
DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION,
|
||||||
|
validateDataProductPatch,
|
||||||
|
validateDataProductPublish,
|
||||||
|
validateDataProductSnapshot,
|
||||||
|
} from "./data-product.mjs";
|
||||||
|
|
@ -2,11 +2,11 @@ export const DATA_PRODUCT_PUBLISH_SCHEMA_VERSION = "nodedc.data-product.publish/
|
||||||
export const DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION = "nodedc.data-product.snapshot/v1";
|
export const DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION = "nodedc.data-product.snapshot/v1";
|
||||||
export const DATA_PRODUCT_PATCH_SCHEMA_VERSION = "nodedc.data-product.patch/v1";
|
export const DATA_PRODUCT_PATCH_SCHEMA_VERSION = "nodedc.data-product.patch/v1";
|
||||||
|
|
||||||
|
import { SECRET_LIKE_KEY, SECRET_LIKE_VALUE } from "./sensitive-field-policy.mjs";
|
||||||
|
|
||||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||||
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||||
const CURSOR = /^(?:0|[1-9]\d*)$/;
|
const CURSOR = /^(?:0|[1-9]\d*)$/;
|
||||||
const SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
|
|
||||||
const SECRET_LIKE_VALUE = /(?:ndc_edp(?:wb|rb)_[A-Za-z0-9_-]*|[?&](?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)=|(?:bearer|basic)\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i;
|
|
||||||
const MAX_BATCH_SEQUENCE = 2_147_483_647;
|
const MAX_BATCH_SEQUENCE = 2_147_483_647;
|
||||||
const MAX_FACT_ATTRIBUTES_BYTES = 64 * 1024;
|
const MAX_FACT_ATTRIBUTES_BYTES = 64 * 1024;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,662 +0,0 @@
|
||||||
import { createHash, verify as verifySignature } from "node:crypto";
|
|
||||||
|
|
||||||
export const ENGINE_CREDENTIAL_SINK_PROVISION_SCHEMA_VERSION = "nodedc.engine.credential-sink.provision/v1";
|
|
||||||
export const ENGINE_CREDENTIAL_SINK_RECEIPT_SCHEMA_VERSION = "nodedc.engine.credential-sink.receipt/v1";
|
|
||||||
export const ENGINE_CREDENTIAL_SINK_ROLLBACK_SCHEMA_VERSION = "nodedc.engine.credential-sink.rollback/v1";
|
|
||||||
export const ENGINE_CREDENTIAL_SINK_ROLLBACK_RECEIPT_SCHEMA_VERSION = "nodedc.engine.credential-sink.rollback-receipt/v1";
|
|
||||||
export const ENGINE_CREDENTIAL_SINK_AUDIT_SCHEMA_VERSION = "nodedc.engine.credential-sink.audit/v1";
|
|
||||||
|
|
||||||
const HASH = /^sha256:[a-f0-9]{64}$/;
|
|
||||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
|
||||||
const OPAQUE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,159}$/;
|
|
||||||
const NODE_TYPE = /^[a-z][A-Za-z0-9.-]{2,159}$/;
|
|
||||||
const CREDENTIAL_TYPE = /^[a-z][A-Za-z0-9]{2,127}$/;
|
|
||||||
const CREDENTIAL_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9_-]{5,159}$/;
|
|
||||||
const REASON_CODE = /^[a-z][a-z0-9_.:-]{2,127}$/;
|
|
||||||
const ED25519_SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
|
|
||||||
const SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key|material|value)/i;
|
|
||||||
const SECRET_LIKE_VALUE = /(?:ndc_(?:edp(?:wb|rb)|fndbg)_[A-Za-z0-9_-]+|(?:bearer|basic)\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i;
|
|
||||||
const MAX_BINDINGS = 32;
|
|
||||||
const MAX_REQUEST_LIFETIME_MS = 15 * 60 * 1000;
|
|
||||||
const MAX_REQUEST_CLOCK_SKEW_MS = 60 * 1000;
|
|
||||||
|
|
||||||
const CAPABILITY_SPECS = Object.freeze({
|
|
||||||
"external-data-plane.writer": Object.freeze({
|
|
||||||
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
|
||||||
credentialType: "ndcDataProductWriterApi",
|
|
||||||
materialPattern: /^ndc_edpwb_[A-Za-z0-9_-]{43}$/,
|
|
||||||
}),
|
|
||||||
"external-data-plane.reader": Object.freeze({
|
|
||||||
nodeType: "n8n-nodes-ndc.ndcDataProductRead",
|
|
||||||
credentialType: "ndcDataProductReaderApi",
|
|
||||||
materialPattern: /^ndc_edprb_[A-Za-z0-9_-]{43}$/,
|
|
||||||
}),
|
|
||||||
"foundry.binding": Object.freeze({
|
|
||||||
nodeType: "n8n-nodes-ndc.ndcFoundryBinding",
|
|
||||||
credentialType: "ndcFoundryBindingApi",
|
|
||||||
materialPattern: /^ndc_fndbg_[A-Za-z0-9_-]{43}$/,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ENGINE_CREDENTIAL_SINK_CAPABILITY_TYPES = Object.freeze(Object.keys(CAPABILITY_SPECS));
|
|
||||||
|
|
||||||
const PROVISION_KEYS = new Set(["schemaVersion", "transaction", "bindings"]);
|
|
||||||
const TRANSACTION_KEYS = new Set([
|
|
||||||
"id",
|
|
||||||
"idempotencyKey",
|
|
||||||
"requestedAt",
|
|
||||||
"requestExpiresAt",
|
|
||||||
"policyHash",
|
|
||||||
"failureMode",
|
|
||||||
"issuer",
|
|
||||||
"attestation",
|
|
||||||
]);
|
|
||||||
const ISSUER_KEYS = new Set(["serviceId", "keyId"]);
|
|
||||||
const ATTESTATION_KEYS = new Set(["algorithm", "signature"]);
|
|
||||||
const BINDING_KEYS = new Set([
|
|
||||||
"bindingId",
|
|
||||||
"capabilityType",
|
|
||||||
"grantId",
|
|
||||||
"target",
|
|
||||||
"expiresAt",
|
|
||||||
"policyHash",
|
|
||||||
"capabilityDigest",
|
|
||||||
"material",
|
|
||||||
]);
|
|
||||||
const TARGET_KEYS = new Set([
|
|
||||||
"workflowId",
|
|
||||||
"workflowRevision",
|
|
||||||
"nodeId",
|
|
||||||
"nodeType",
|
|
||||||
"credentialType",
|
|
||||||
]);
|
|
||||||
const MATERIAL_KEYS = new Set(["format", "value"]);
|
|
||||||
const RECEIPT_KEYS = new Set([
|
|
||||||
"schemaVersion",
|
|
||||||
"transactionId",
|
|
||||||
"idempotencyKey",
|
|
||||||
"outcome",
|
|
||||||
"policyHash",
|
|
||||||
"processedAt",
|
|
||||||
"credentials",
|
|
||||||
"rollback",
|
|
||||||
"errorCode",
|
|
||||||
]);
|
|
||||||
const RECEIPT_CREDENTIAL_KEYS = new Set([
|
|
||||||
"bindingId",
|
|
||||||
"capabilityType",
|
|
||||||
"grantId",
|
|
||||||
"target",
|
|
||||||
"credentialRef",
|
|
||||||
"expiresAt",
|
|
||||||
"policyHash",
|
|
||||||
"capabilityDigest",
|
|
||||||
"disposition",
|
|
||||||
]);
|
|
||||||
const RECEIPT_ROLLBACK_KEYS = new Set(["status", "completedAt"]);
|
|
||||||
const ROLLBACK_REQUEST_KEYS = new Set(["schemaVersion", "rollback"]);
|
|
||||||
const ROLLBACK_KEYS = new Set([
|
|
||||||
"id",
|
|
||||||
"idempotencyKey",
|
|
||||||
"transactionId",
|
|
||||||
"requestedAt",
|
|
||||||
"requestExpiresAt",
|
|
||||||
"policyHash",
|
|
||||||
"committedReceiptHash",
|
|
||||||
"reasonCode",
|
|
||||||
]);
|
|
||||||
const ROLLBACK_RECEIPT_KEYS = new Set([
|
|
||||||
"schemaVersion",
|
|
||||||
"rollbackId",
|
|
||||||
"transactionId",
|
|
||||||
"outcome",
|
|
||||||
"policyHash",
|
|
||||||
"committedReceiptHash",
|
|
||||||
"processedAt",
|
|
||||||
"errorCode",
|
|
||||||
]);
|
|
||||||
const AUDIT_KEYS = new Set([
|
|
||||||
"schemaVersion",
|
|
||||||
"eventId",
|
|
||||||
"transactionId",
|
|
||||||
"operationId",
|
|
||||||
"operation",
|
|
||||||
"outcome",
|
|
||||||
"occurredAt",
|
|
||||||
"policyHash",
|
|
||||||
"principal",
|
|
||||||
"targets",
|
|
||||||
"reasonCode",
|
|
||||||
]);
|
|
||||||
const PRINCIPAL_KEYS = new Set(["serviceId", "fingerprint"]);
|
|
||||||
const AUDIT_TARGET_KEYS = new Set([
|
|
||||||
"bindingId",
|
|
||||||
"capabilityType",
|
|
||||||
"grantId",
|
|
||||||
"target",
|
|
||||||
"expiresAt",
|
|
||||||
"policyHash",
|
|
||||||
"capabilityDigest",
|
|
||||||
"credentialRefHash",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the only request allowed to carry plaintext workload capability
|
|
||||||
* material across the trusted Platform -> Engine server boundary. Callers and
|
|
||||||
* receivers must never log, trace, persist or return this request body.
|
|
||||||
*/
|
|
||||||
export function validateEngineCredentialSinkProvision(value, { now = Date.now(), issuerPublicKeys } = {}) {
|
|
||||||
const errors = [];
|
|
||||||
const nowMs = normalizeNow(now, errors);
|
|
||||||
if (!isPlainObject(value)) return result(["credentialSinkProvision_must_be_object"]);
|
|
||||||
if (value.schemaVersion !== ENGINE_CREDENTIAL_SINK_PROVISION_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
|
|
||||||
rejectUnknownKeys(value, PROVISION_KEYS, "credentialSinkProvision", errors);
|
|
||||||
|
|
||||||
if (!isPlainObject(value.transaction)) {
|
|
||||||
errors.push("transaction_must_be_object");
|
|
||||||
} else {
|
|
||||||
rejectUnknownKeys(value.transaction, TRANSACTION_KEYS, "transaction", errors);
|
|
||||||
requiredOpaqueId(value.transaction.id, "transaction.id", errors);
|
|
||||||
requiredOpaqueId(value.transaction.idempotencyKey, "transaction.idempotencyKey", errors);
|
|
||||||
requiredTimestamp(value.transaction.requestedAt, "transaction.requestedAt", errors);
|
|
||||||
requiredTimestamp(value.transaction.requestExpiresAt, "transaction.requestExpiresAt", errors);
|
|
||||||
requiredHash(value.transaction.policyHash, "transaction.policyHash", errors);
|
|
||||||
if (value.transaction.failureMode !== "rollback-all") errors.push("transaction.failureMode_must_be_rollback-all");
|
|
||||||
validateIssuer(value.transaction.issuer, "transaction.issuer", errors);
|
|
||||||
validateAttestation(value.transaction.attestation, "transaction.attestation", errors);
|
|
||||||
validateRequestWindow(value.transaction.requestedAt, value.transaction.requestExpiresAt, nowMs, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(value.bindings) || value.bindings.length === 0) {
|
|
||||||
errors.push("bindings_must_be_nonempty_array");
|
|
||||||
} else if (value.bindings.length > MAX_BINDINGS) {
|
|
||||||
errors.push("bindings_limit_exceeded");
|
|
||||||
} else {
|
|
||||||
const bindingIds = new Set();
|
|
||||||
const targets = new Set();
|
|
||||||
value.bindings.forEach((binding, index) => {
|
|
||||||
validateProvisionBinding(binding, index, value.transaction?.requestedAt, errors);
|
|
||||||
if (!isPlainObject(binding)) return;
|
|
||||||
if (bindingIds.has(binding.bindingId)) errors.push("bindings_bindingId_must_be_unique");
|
|
||||||
bindingIds.add(binding.bindingId);
|
|
||||||
const targetKey = targetIdentity(binding.target);
|
|
||||||
if (targetKey && targets.has(targetKey)) errors.push("bindings_target_credential_must_be_unique");
|
|
||||||
if (targetKey) targets.add(targetKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(value.transaction) && HASH.test(String(value.transaction.policyHash || ""))) {
|
|
||||||
const expectedHash = computeEngineCredentialSinkPolicyHash(value);
|
|
||||||
if (value.transaction.policyHash !== expectedHash) errors.push("transaction.policyHash_mismatch");
|
|
||||||
verifyProvisionAttestation(value.transaction, issuerPublicKeys, errors);
|
|
||||||
}
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the aggregate policy digest over the complete secret-free request
|
|
||||||
* descriptor. Plaintext `material` is excluded by construction, while its
|
|
||||||
* high-entropy capability digest is included; changing a target, grant,
|
|
||||||
* capability, expiry, individual policy hash or transaction envelope changes
|
|
||||||
* the aggregate digest and invalidates the issuer attestation.
|
|
||||||
*/
|
|
||||||
export function computeEngineCredentialSinkPolicyHash(value) {
|
|
||||||
const descriptor = {
|
|
||||||
schemaVersion: value?.schemaVersion,
|
|
||||||
transaction: isPlainObject(value?.transaction) ? {
|
|
||||||
id: value.transaction.id,
|
|
||||||
idempotencyKey: value.transaction.idempotencyKey,
|
|
||||||
requestedAt: value.transaction.requestedAt,
|
|
||||||
requestExpiresAt: value.transaction.requestExpiresAt,
|
|
||||||
failureMode: value.transaction.failureMode,
|
|
||||||
issuer: value.transaction.issuer,
|
|
||||||
} : value?.transaction,
|
|
||||||
bindings: Array.isArray(value?.bindings)
|
|
||||||
? value.bindings.map(secretFreeBindingDescriptor)
|
|
||||||
: value?.bindings,
|
|
||||||
};
|
|
||||||
return `sha256:${createHash("sha256").update(stableJson(descriptor), "utf8").digest("hex")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receipt is intentionally incapable of carrying credential material. When a
|
|
||||||
* request is supplied, the validator also proves the sink committed the exact
|
|
||||||
* requested workflow/node/type set without target substitution.
|
|
||||||
*/
|
|
||||||
export function validateEngineCredentialSinkReceipt(value, { request, issuerPublicKeys } = {}) {
|
|
||||||
const errors = [];
|
|
||||||
if (!isPlainObject(value)) return result(["credentialSinkReceipt_must_be_object"]);
|
|
||||||
if (value.schemaVersion !== ENGINE_CREDENTIAL_SINK_RECEIPT_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
|
|
||||||
rejectUnknownKeys(value, RECEIPT_KEYS, "credentialSinkReceipt", errors);
|
|
||||||
requiredOpaqueId(value.transactionId, "transactionId", errors);
|
|
||||||
requiredOpaqueId(value.idempotencyKey, "idempotencyKey", errors);
|
|
||||||
requiredHash(value.policyHash, "policyHash", errors);
|
|
||||||
requiredTimestamp(value.processedAt, "processedAt", errors);
|
|
||||||
const outcomes = new Set(["committed", "rolled-back", "rejected", "rollback-failed"]);
|
|
||||||
if (!outcomes.has(value.outcome)) errors.push("outcome_invalid");
|
|
||||||
|
|
||||||
if (!Array.isArray(value.credentials)) {
|
|
||||||
errors.push("credentials_must_be_array");
|
|
||||||
} else {
|
|
||||||
const bindingIds = new Set();
|
|
||||||
const refs = new Set();
|
|
||||||
value.credentials.forEach((credential, index) => {
|
|
||||||
validateReceiptCredential(credential, index, errors);
|
|
||||||
if (!isPlainObject(credential)) return;
|
|
||||||
if (bindingIds.has(credential.bindingId)) errors.push("credentials_bindingId_must_be_unique");
|
|
||||||
bindingIds.add(credential.bindingId);
|
|
||||||
if (refs.has(credential.credentialRef)) errors.push("credentials_credentialRef_must_be_unique");
|
|
||||||
refs.add(credential.credentialRef);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
validateReceiptOutcome(value, errors);
|
|
||||||
if (containsSecretLikeMaterial(value)) errors.push("receipt_must_not_contain_secret_material");
|
|
||||||
if (request !== undefined) compareReceiptToProvisionRequest(value, request, issuerPublicKeys, errors);
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeEngineCredentialSinkReceiptHash(value) {
|
|
||||||
return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateEngineCredentialSinkRollback(value, { now = Date.now() } = {}) {
|
|
||||||
const errors = [];
|
|
||||||
const nowMs = normalizeNow(now, errors);
|
|
||||||
if (!isPlainObject(value)) return result(["credentialSinkRollback_must_be_object"]);
|
|
||||||
if (value.schemaVersion !== ENGINE_CREDENTIAL_SINK_ROLLBACK_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
|
|
||||||
rejectUnknownKeys(value, ROLLBACK_REQUEST_KEYS, "credentialSinkRollback", errors);
|
|
||||||
if (!isPlainObject(value.rollback)) {
|
|
||||||
errors.push("rollback_must_be_object");
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value.rollback, ROLLBACK_KEYS, "rollback", errors);
|
|
||||||
requiredOpaqueId(value.rollback.id, "rollback.id", errors);
|
|
||||||
requiredOpaqueId(value.rollback.idempotencyKey, "rollback.idempotencyKey", errors);
|
|
||||||
requiredOpaqueId(value.rollback.transactionId, "rollback.transactionId", errors);
|
|
||||||
requiredTimestamp(value.rollback.requestedAt, "rollback.requestedAt", errors);
|
|
||||||
requiredTimestamp(value.rollback.requestExpiresAt, "rollback.requestExpiresAt", errors);
|
|
||||||
requiredHash(value.rollback.policyHash, "rollback.policyHash", errors);
|
|
||||||
requiredHash(value.rollback.committedReceiptHash, "rollback.committedReceiptHash", errors);
|
|
||||||
requiredReason(value.rollback.reasonCode, "rollback.reasonCode", errors);
|
|
||||||
validateRequestWindow(value.rollback.requestedAt, value.rollback.requestExpiresAt, nowMs, errors);
|
|
||||||
if (containsSecretLikeMaterial(value)) errors.push("rollback_must_not_contain_secret_material");
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateEngineCredentialSinkRollbackReceipt(value, { request } = {}) {
|
|
||||||
const errors = [];
|
|
||||||
if (!isPlainObject(value)) return result(["credentialSinkRollbackReceipt_must_be_object"]);
|
|
||||||
if (value.schemaVersion !== ENGINE_CREDENTIAL_SINK_ROLLBACK_RECEIPT_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
|
|
||||||
rejectUnknownKeys(value, ROLLBACK_RECEIPT_KEYS, "credentialSinkRollbackReceipt", errors);
|
|
||||||
requiredOpaqueId(value.rollbackId, "rollbackId", errors);
|
|
||||||
requiredOpaqueId(value.transactionId, "transactionId", errors);
|
|
||||||
requiredHash(value.policyHash, "policyHash", errors);
|
|
||||||
requiredHash(value.committedReceiptHash, "committedReceiptHash", errors);
|
|
||||||
requiredTimestamp(value.processedAt, "processedAt", errors);
|
|
||||||
if (!new Set(["rolled-back", "rejected", "rollback-failed"]).has(value.outcome)) errors.push("outcome_invalid");
|
|
||||||
if (value.outcome === "rolled-back") {
|
|
||||||
if (value.errorCode !== undefined) errors.push("errorCode_forbidden_for_success");
|
|
||||||
} else {
|
|
||||||
requiredReason(value.errorCode, "errorCode", errors);
|
|
||||||
}
|
|
||||||
if (containsSecretLikeMaterial(value)) errors.push("rollbackReceipt_must_not_contain_secret_material");
|
|
||||||
if (request !== undefined && isPlainObject(request?.rollback)) {
|
|
||||||
if (!validateEngineCredentialSinkRollback(request, { now: value.processedAt }).ok) {
|
|
||||||
errors.push("request_invalid_for_rollback_receipt_comparison");
|
|
||||||
}
|
|
||||||
if (value.rollbackId !== request.rollback.id) errors.push("rollbackId_request_mismatch");
|
|
||||||
if (value.transactionId !== request.rollback.transactionId) errors.push("transactionId_request_mismatch");
|
|
||||||
if (value.policyHash !== request.rollback.policyHash) errors.push("policyHash_request_mismatch");
|
|
||||||
if (value.committedReceiptHash !== request.rollback.committedReceiptHash) {
|
|
||||||
errors.push("committedReceiptHash_request_mismatch");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateEngineCredentialSinkAudit(value) {
|
|
||||||
const errors = [];
|
|
||||||
if (!isPlainObject(value)) return result(["credentialSinkAudit_must_be_object"]);
|
|
||||||
if (value.schemaVersion !== ENGINE_CREDENTIAL_SINK_AUDIT_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
|
|
||||||
rejectUnknownKeys(value, AUDIT_KEYS, "credentialSinkAudit", errors);
|
|
||||||
requiredOpaqueId(value.eventId, "eventId", errors);
|
|
||||||
requiredOpaqueId(value.transactionId, "transactionId", errors);
|
|
||||||
requiredOpaqueId(value.operationId, "operationId", errors);
|
|
||||||
if (!new Set(["provision", "rollback"]).has(value.operation)) errors.push("operation_invalid");
|
|
||||||
if (!new Set(["committed", "rolled-back", "rejected", "rollback-failed"]).has(value.outcome)) errors.push("outcome_invalid");
|
|
||||||
if (value.operation === "provision" && value.outcome === "rolled-back" && !value.reasonCode) {
|
|
||||||
errors.push("reasonCode_required");
|
|
||||||
}
|
|
||||||
if (value.operation === "rollback" && value.outcome === "committed") errors.push("rollback_outcome_invalid");
|
|
||||||
requiredTimestamp(value.occurredAt, "occurredAt", errors);
|
|
||||||
requiredHash(value.policyHash, "policyHash", errors);
|
|
||||||
if (!isPlainObject(value.principal)) {
|
|
||||||
errors.push("principal_must_be_object");
|
|
||||||
} else {
|
|
||||||
rejectUnknownKeys(value.principal, PRINCIPAL_KEYS, "principal", errors);
|
|
||||||
requiredIdentifier(value.principal.serviceId, "principal.serviceId", errors);
|
|
||||||
requiredHash(value.principal.fingerprint, "principal.fingerprint", errors);
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value.targets)) {
|
|
||||||
errors.push("targets_must_be_array");
|
|
||||||
} else {
|
|
||||||
value.targets.forEach((target, index) => validateAuditTarget(target, index, errors));
|
|
||||||
}
|
|
||||||
if (value.reasonCode !== undefined) requiredReason(value.reasonCode, "reasonCode", errors);
|
|
||||||
if (new Set(["rejected", "rollback-failed"]).has(value.outcome) && value.reasonCode === undefined) {
|
|
||||||
errors.push("reasonCode_required");
|
|
||||||
}
|
|
||||||
if (containsSecretLikeMaterial(value)) errors.push("audit_must_not_contain_secret_material");
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns audit-safe target descriptors; material and credential refs cannot escape. */
|
|
||||||
export function engineCredentialSinkAuditTargets(request, receipt) {
|
|
||||||
const refByBinding = new Map(
|
|
||||||
Array.isArray(receipt?.credentials)
|
|
||||||
? receipt.credentials.map((item) => [item.bindingId, item.credentialRef])
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
return Array.isArray(request?.bindings) ? request.bindings.map((binding) => {
|
|
||||||
const descriptor = secretFreeBindingDescriptor(binding);
|
|
||||||
const credentialRef = refByBinding.get(binding.bindingId);
|
|
||||||
return credentialRef
|
|
||||||
? { ...descriptor, credentialRefHash: sha256Value(credentialRef) }
|
|
||||||
: descriptor;
|
|
||||||
}) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateProvisionBinding(value, index, requestedAt, errors) {
|
|
||||||
const path = `bindings[${index}]`;
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, BINDING_KEYS, path, errors);
|
|
||||||
requiredIdentifier(value.bindingId, `${path}.bindingId`, errors);
|
|
||||||
const spec = CAPABILITY_SPECS[value.capabilityType];
|
|
||||||
if (!spec) errors.push(`${path}.capabilityType_invalid`);
|
|
||||||
requiredOpaqueId(value.grantId, `${path}.grantId`, errors);
|
|
||||||
validateTarget(value.target, path, errors);
|
|
||||||
requiredTimestamp(value.expiresAt, `${path}.expiresAt`, errors);
|
|
||||||
requiredHash(value.policyHash, `${path}.policyHash`, errors);
|
|
||||||
requiredHash(value.capabilityDigest, `${path}.capabilityDigest`, errors);
|
|
||||||
if (isTimestamp(requestedAt) && isTimestamp(value.expiresAt) && Date.parse(value.expiresAt) <= Date.parse(requestedAt)) {
|
|
||||||
errors.push(`${path}.expiresAt_must_be_after_requestedAt`);
|
|
||||||
}
|
|
||||||
if (spec && isPlainObject(value.target)) {
|
|
||||||
if (value.target.nodeType !== spec.nodeType) errors.push(`${path}.target.nodeType_capability_mismatch`);
|
|
||||||
if (value.target.credentialType !== spec.credentialType) errors.push(`${path}.target.credentialType_capability_mismatch`);
|
|
||||||
}
|
|
||||||
if (!isPlainObject(value.material)) {
|
|
||||||
errors.push(`${path}.material_must_be_object`);
|
|
||||||
} else {
|
|
||||||
rejectUnknownKeys(value.material, MATERIAL_KEYS, `${path}.material`, errors);
|
|
||||||
if (value.material.format !== "opaque-bearer") errors.push(`${path}.material.format_must_be_opaque-bearer`);
|
|
||||||
if (!spec || typeof value.material.value !== "string" || !spec.materialPattern.test(value.material.value)) {
|
|
||||||
errors.push(`${path}.material.value_invalid_for_capability`);
|
|
||||||
} else if (value.capabilityDigest !== computeEngineCredentialCapabilityDigest(value.material.value)) {
|
|
||||||
errors.push(`${path}.capabilityDigest_material_mismatch`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateTarget(value, path, errors) {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}.target_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, TARGET_KEYS, `${path}.target`, errors);
|
|
||||||
requiredOpaqueId(value.workflowId, `${path}.target.workflowId`, errors);
|
|
||||||
requiredOpaqueId(value.workflowRevision, `${path}.target.workflowRevision`, errors);
|
|
||||||
requiredOpaqueId(value.nodeId, `${path}.target.nodeId`, errors);
|
|
||||||
if (typeof value.nodeType !== "string" || !NODE_TYPE.test(value.nodeType)) errors.push(`${path}.target.nodeType_invalid`);
|
|
||||||
if (typeof value.credentialType !== "string" || !CREDENTIAL_TYPE.test(value.credentialType)) {
|
|
||||||
errors.push(`${path}.target.credentialType_invalid`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateReceiptCredential(value, index, errors) {
|
|
||||||
const path = `credentials[${index}]`;
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, RECEIPT_CREDENTIAL_KEYS, path, errors);
|
|
||||||
requiredIdentifier(value.bindingId, `${path}.bindingId`, errors);
|
|
||||||
if (!CAPABILITY_SPECS[value.capabilityType]) errors.push(`${path}.capabilityType_invalid`);
|
|
||||||
requiredOpaqueId(value.grantId, `${path}.grantId`, errors);
|
|
||||||
validateTarget(value.target, path, errors);
|
|
||||||
if (typeof value.credentialRef !== "string" || !CREDENTIAL_REFERENCE.test(value.credentialRef)) {
|
|
||||||
errors.push(`${path}.credentialRef_invalid`);
|
|
||||||
}
|
|
||||||
requiredTimestamp(value.expiresAt, `${path}.expiresAt`, errors);
|
|
||||||
requiredHash(value.policyHash, `${path}.policyHash`, errors);
|
|
||||||
requiredHash(value.capabilityDigest, `${path}.capabilityDigest`, errors);
|
|
||||||
if (!new Set(["created", "reused", "rotated"]).has(value.disposition)) errors.push(`${path}.disposition_invalid`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateReceiptOutcome(value, errors) {
|
|
||||||
if (!isPlainObject(value.rollback)) {
|
|
||||||
errors.push("rollback_must_be_object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value.rollback, RECEIPT_ROLLBACK_KEYS, "rollback", errors);
|
|
||||||
const expectedRollback = {
|
|
||||||
committed: "not-required",
|
|
||||||
"rolled-back": "complete",
|
|
||||||
rejected: "not-started",
|
|
||||||
"rollback-failed": "incomplete",
|
|
||||||
}[value.outcome];
|
|
||||||
if (expectedRollback && value.rollback.status !== expectedRollback) errors.push("rollback.status_outcome_mismatch");
|
|
||||||
if (new Set(["complete", "incomplete"]).has(value.rollback.status)) {
|
|
||||||
requiredTimestamp(value.rollback.completedAt, "rollback.completedAt", errors);
|
|
||||||
} else if (value.rollback.completedAt !== undefined) {
|
|
||||||
errors.push("rollback.completedAt_not_allowed");
|
|
||||||
}
|
|
||||||
if (value.outcome === "committed") {
|
|
||||||
if (!Array.isArray(value.credentials) || value.credentials.length === 0) errors.push("committed_credentials_required");
|
|
||||||
if (value.errorCode !== undefined) errors.push("errorCode_forbidden_for_success");
|
|
||||||
} else {
|
|
||||||
if (Array.isArray(value.credentials) && value.credentials.length !== 0) errors.push("noncommitted_credentials_must_be_empty");
|
|
||||||
requiredReason(value.errorCode, "errorCode", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareReceiptToProvisionRequest(receipt, request, issuerPublicKeys, errors) {
|
|
||||||
const requestValidation = validateEngineCredentialSinkProvision(request, {
|
|
||||||
now: receipt.processedAt,
|
|
||||||
issuerPublicKeys,
|
|
||||||
});
|
|
||||||
if (!requestValidation.ok) {
|
|
||||||
errors.push("request_invalid_for_receipt_comparison");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (receipt.transactionId !== request.transaction.id) errors.push("transactionId_request_mismatch");
|
|
||||||
if (receipt.idempotencyKey !== request.transaction.idempotencyKey) errors.push("idempotencyKey_request_mismatch");
|
|
||||||
if (receipt.policyHash !== request.transaction.policyHash) errors.push("policyHash_request_mismatch");
|
|
||||||
if (receipt.outcome !== "committed") return;
|
|
||||||
if (receipt.credentials.length !== request.bindings.length) errors.push("credentials_request_count_mismatch");
|
|
||||||
const requested = new Map(request.bindings.map((binding) => [binding.bindingId, secretFreeBindingDescriptor(binding)]));
|
|
||||||
for (const credential of receipt.credentials) {
|
|
||||||
const expected = requested.get(credential.bindingId);
|
|
||||||
if (!expected || stableJson({
|
|
||||||
bindingId: credential.bindingId,
|
|
||||||
capabilityType: credential.capabilityType,
|
|
||||||
grantId: credential.grantId,
|
|
||||||
target: credential.target,
|
|
||||||
expiresAt: credential.expiresAt,
|
|
||||||
policyHash: credential.policyHash,
|
|
||||||
capabilityDigest: credential.capabilityDigest,
|
|
||||||
}) !== stableJson(expected)) {
|
|
||||||
errors.push("credentials_request_target_mismatch");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateAuditTarget(value, index, errors) {
|
|
||||||
const path = `targets[${index}]`;
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, AUDIT_TARGET_KEYS, path, errors);
|
|
||||||
requiredIdentifier(value.bindingId, `${path}.bindingId`, errors);
|
|
||||||
if (!CAPABILITY_SPECS[value.capabilityType]) errors.push(`${path}.capabilityType_invalid`);
|
|
||||||
requiredOpaqueId(value.grantId, `${path}.grantId`, errors);
|
|
||||||
validateTarget(value.target, path, errors);
|
|
||||||
requiredTimestamp(value.expiresAt, `${path}.expiresAt`, errors);
|
|
||||||
requiredHash(value.policyHash, `${path}.policyHash`, errors);
|
|
||||||
requiredHash(value.capabilityDigest, `${path}.capabilityDigest`, errors);
|
|
||||||
if (value.credentialRefHash !== undefined) requiredHash(value.credentialRefHash, `${path}.credentialRefHash`, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
function secretFreeBindingDescriptor(value) {
|
|
||||||
return {
|
|
||||||
bindingId: value?.bindingId,
|
|
||||||
capabilityType: value?.capabilityType,
|
|
||||||
grantId: value?.grantId,
|
|
||||||
target: value?.target,
|
|
||||||
expiresAt: value?.expiresAt,
|
|
||||||
policyHash: value?.policyHash,
|
|
||||||
capabilityDigest: value?.capabilityDigest,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeEngineCredentialCapabilityDigest(value) {
|
|
||||||
return sha256Value(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateIssuer(value, path, errors) {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, ISSUER_KEYS, path, errors);
|
|
||||||
requiredIdentifier(value.serviceId, `${path}.serviceId`, errors);
|
|
||||||
requiredOpaqueId(value.keyId, `${path}.keyId`, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateAttestation(value, path, errors) {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, ATTESTATION_KEYS, path, errors);
|
|
||||||
if (value.algorithm !== "Ed25519") errors.push(`${path}.algorithm_must_be_Ed25519`);
|
|
||||||
if (typeof value.signature !== "string" || !ED25519_SIGNATURE.test(value.signature)) {
|
|
||||||
errors.push(`${path}.signature_invalid`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyProvisionAttestation(transaction, issuerPublicKeys, errors) {
|
|
||||||
if (!isPlainObject(transaction?.issuer) || !isPlainObject(transaction?.attestation)) return;
|
|
||||||
if (transaction.attestation.algorithm !== "Ed25519" || !ED25519_SIGNATURE.test(String(transaction.attestation.signature || ""))) return;
|
|
||||||
const keyIdentity = `${transaction.issuer.serviceId}:${transaction.issuer.keyId}`;
|
|
||||||
const publicKey = isPlainObject(issuerPublicKeys) && Object.hasOwn(issuerPublicKeys, keyIdentity)
|
|
||||||
? issuerPublicKeys[keyIdentity]
|
|
||||||
: undefined;
|
|
||||||
if (!publicKey) {
|
|
||||||
errors.push("transaction.issuer_public_key_required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let valid = false;
|
|
||||||
try {
|
|
||||||
valid = verifySignature(
|
|
||||||
null,
|
|
||||||
Buffer.from(String(transaction.policyHash), "utf8"),
|
|
||||||
publicKey,
|
|
||||||
Buffer.from(transaction.attestation.signature, "base64url"),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
if (!valid) errors.push("transaction.attestation_invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRequestWindow(requestedAt, requestExpiresAt, nowMs, errors) {
|
|
||||||
if (!isTimestamp(requestedAt) || !isTimestamp(requestExpiresAt)) return;
|
|
||||||
const requested = Date.parse(requestedAt);
|
|
||||||
const expires = Date.parse(requestExpiresAt);
|
|
||||||
if (expires <= requested) errors.push("requestExpiresAt_must_be_after_requestedAt");
|
|
||||||
if (expires - requested > MAX_REQUEST_LIFETIME_MS) errors.push("request_lifetime_exceeds_15_minutes");
|
|
||||||
if (Number.isFinite(nowMs)) {
|
|
||||||
if (requested > nowMs + MAX_REQUEST_CLOCK_SKEW_MS) errors.push("requestedAt_exceeds_clock_skew");
|
|
||||||
if (expires <= nowMs) errors.push("request_expired");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNow(value, errors) {
|
|
||||||
const normalized = value instanceof Date ? value.getTime() : typeof value === "string" ? Date.parse(value) : Number(value);
|
|
||||||
if (!Number.isFinite(normalized)) {
|
|
||||||
errors.push("validation_now_invalid");
|
|
||||||
return Number.NaN;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function targetIdentity(value) {
|
|
||||||
if (!isPlainObject(value)) return "";
|
|
||||||
return [value.workflowId, value.workflowRevision, value.nodeId, value.nodeType, value.credentialType].join("\u0000");
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredIdentifier(value, path, errors) {
|
|
||||||
if (typeof value !== "string" || !IDENTIFIER.test(value)) errors.push(`${path}_invalid`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredOpaqueId(value, path, errors) {
|
|
||||||
if (typeof value !== "string" || !OPAQUE_ID.test(value)) errors.push(`${path}_invalid`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredHash(value, path, errors) {
|
|
||||||
if (typeof value !== "string" || !HASH.test(value)) errors.push(`${path}_invalid`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredReason(value, path, errors) {
|
|
||||||
if (typeof value !== "string" || !REASON_CODE.test(value)) errors.push(`${path}_invalid`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredTimestamp(value, path, errors) {
|
|
||||||
if (!isTimestamp(value)) errors.push(`${path}_invalid_timestamp`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTimestamp(value) {
|
|
||||||
return typeof value === "string" && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sha256Value(value) {
|
|
||||||
return `sha256:${createHash("sha256").update(String(value), "utf8").digest("hex")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stableJson(value) {
|
|
||||||
return JSON.stringify(sortValue(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortValue(value) {
|
|
||||||
if (Array.isArray(value)) return value.map(sortValue);
|
|
||||||
if (!isPlainObject(value)) return value;
|
|
||||||
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortValue(value[key])]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function containsSecretLikeMaterial(value) {
|
|
||||||
if (typeof value === "string") return SECRET_LIKE_VALUE.test(value);
|
|
||||||
if (Array.isArray(value)) return value.some(containsSecretLikeMaterial);
|
|
||||||
if (!isPlainObject(value)) return false;
|
|
||||||
return Object.entries(value).some(([key, child]) => SECRET_LIKE_KEY.test(key) || containsSecretLikeMaterial(child));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value) {
|
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
||||||
if (!isPlainObject(value)) return;
|
|
||||||
for (const key of Object.keys(value)) {
|
|
||||||
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function result(errors) {
|
|
||||||
const uniqueErrors = [...new Set(errors)];
|
|
||||||
return Object.freeze({ ok: uniqueErrors.length === 0, errors: Object.freeze(uniqueErrors) });
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,23 @@
|
||||||
export const EXTERNAL_PROVIDER_CONTRACT_VERSION = "nodedc.external-provider-contract/v1";
|
import { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
|
||||||
|
|
||||||
|
export { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
|
||||||
|
export { validateIntakeBatch } from "./intake-batch.mjs";
|
||||||
export const FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION = "nodedc.foundry.binding-upsert/v1";
|
export const FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION = "nodedc.foundry.binding-upsert/v1";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SECRET_LIKE_KEY,
|
||||||
|
SECRET_LIKE_VALUE,
|
||||||
|
} from "./sensitive-field-policy.mjs";
|
||||||
|
|
||||||
|
export {
|
||||||
|
L2_CONNECTION_INSTANCE_SCHEMA_VERSION,
|
||||||
|
L2_TEMPLATE_SCHEMA_VERSION,
|
||||||
|
PROVIDER_PACKAGE_SCHEMA_VERSION,
|
||||||
|
SEMANTIC_MAPPING_SCHEMA_VERSION,
|
||||||
|
instantiateL2Connection,
|
||||||
|
validateProviderPackage,
|
||||||
|
} from "./provider-package.mjs";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||||
DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||||
|
|
@ -10,24 +27,6 @@ export {
|
||||||
validateDataProductSnapshot,
|
validateDataProductSnapshot,
|
||||||
} from "./data-product.mjs";
|
} from "./data-product.mjs";
|
||||||
|
|
||||||
export {
|
|
||||||
ENGINE_CREDENTIAL_SINK_AUDIT_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_CAPABILITY_TYPES,
|
|
||||||
ENGINE_CREDENTIAL_SINK_PROVISION_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_RECEIPT_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_ROLLBACK_RECEIPT_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_ROLLBACK_SCHEMA_VERSION,
|
|
||||||
computeEngineCredentialCapabilityDigest,
|
|
||||||
computeEngineCredentialSinkPolicyHash,
|
|
||||||
computeEngineCredentialSinkReceiptHash,
|
|
||||||
engineCredentialSinkAuditTargets,
|
|
||||||
validateEngineCredentialSinkAudit,
|
|
||||||
validateEngineCredentialSinkProvision,
|
|
||||||
validateEngineCredentialSinkReceipt,
|
|
||||||
validateEngineCredentialSinkRollback,
|
|
||||||
validateEngineCredentialSinkRollbackReceipt,
|
|
||||||
} from "./engine-credential-sink.mjs";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ENGINE_PRIVATE_EXTENSION_APPLY_RECEIPT_SCHEMA_VERSION,
|
ENGINE_PRIVATE_EXTENSION_APPLY_RECEIPT_SCHEMA_VERSION,
|
||||||
ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION,
|
ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION,
|
||||||
|
|
@ -55,16 +54,11 @@ export {
|
||||||
const COLLECTION_MODES = new Set(["realtime", "manual", "weekly", "history"]);
|
const COLLECTION_MODES = new Set(["realtime", "manual", "weekly", "history"]);
|
||||||
const DELIVERY_MODES = new Set(["snapshot", "snapshot+patch", "query"]);
|
const DELIVERY_MODES = new Set(["snapshot", "snapshot+patch", "query"]);
|
||||||
const CAPABILITY_CLASSIFICATIONS = new Set(["read", "metadata", "write", "destructive", "unknown"]);
|
const CAPABILITY_CLASSIFICATIONS = new Set(["read", "metadata", "write", "destructive", "unknown"]);
|
||||||
const SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
|
|
||||||
const SECRET_LIKE_REFERENCE = /(?:[?&](?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)=|(?:bearer|basic)\s+)/i;
|
|
||||||
const SECRET_LIKE_VALUE = /(?:ndc_edp(?:wb|rb)_[A-Za-z0-9_-]*|[?&](?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)=|(?:bearer|basic)\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i;
|
|
||||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||||
const FOUNDRY_APPLICATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
const FOUNDRY_APPLICATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
const FOUNDRY_PAGE_ID = /^[a-z0-9][a-z0-9-]{0,79}$/;
|
const FOUNDRY_PAGE_ID = /^[a-z0-9][a-z0-9-]{0,79}$/;
|
||||||
const FOUNDRY_SLOT_ID = /^[A-Za-z0-9][A-Za-z0-9-]{0,79}$/;
|
const FOUNDRY_SLOT_ID = /^[A-Za-z0-9][A-Za-z0-9-]{0,79}$/;
|
||||||
const CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
const CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||||
const MAX_BATCH_SEQUENCE = 2_147_483_647;
|
|
||||||
const MAX_FACT_ATTRIBUTES_BYTES = 64 * 1024;
|
|
||||||
|
|
||||||
const PROVIDER_MANIFEST_KEYS = new Set(["schemaVersion", "id", "providerId", "version", "ontology", "l2Template", "capabilities", "dataProductIds"]);
|
const PROVIDER_MANIFEST_KEYS = new Set(["schemaVersion", "id", "providerId", "version", "ontology", "l2Template", "capabilities", "dataProductIds"]);
|
||||||
const CONNECTION_PROFILE_KEYS = new Set(["schemaVersion", "id", "providerId", "tenantId", "credentialRef", "scope"]);
|
const CONNECTION_PROFILE_KEYS = new Set(["schemaVersion", "id", "providerId", "tenantId", "credentialRef", "scope"]);
|
||||||
|
|
@ -159,8 +153,8 @@ export function validateConnectionProfile(value) {
|
||||||
requiredString(value?.credentialRef?.owner, "credentialRef.owner", errors);
|
requiredString(value?.credentialRef?.owner, "credentialRef.owner", errors);
|
||||||
requiredString(value?.credentialRef?.reference, "credentialRef.reference", errors);
|
requiredString(value?.credentialRef?.reference, "credentialRef.reference", errors);
|
||||||
|
|
||||||
if (value?.credentialRef?.owner && value.credentialRef.owner !== "engine") {
|
if (value?.credentialRef?.owner && value.credentialRef.owner !== "ndc_l2_credentials") {
|
||||||
errors.push("credentialRef.owner_must_be_engine");
|
errors.push("credentialRef.owner_must_be_ndc_l2_credentials");
|
||||||
}
|
}
|
||||||
if (containsSecretLikeMaterial(value)) errors.push("profile_must_not_contain_secret_material");
|
if (containsSecretLikeMaterial(value)) errors.push("profile_must_not_contain_secret_material");
|
||||||
if (value?.scope !== undefined) {
|
if (value?.scope !== undefined) {
|
||||||
|
|
@ -301,45 +295,6 @@ export function validateFoundryBindingUpsert(value) {
|
||||||
return result(errors);
|
return result(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provider-neutral batch written by an L2 connector to External Data Plane.
|
|
||||||
* The payload deliberately describes source facts rather than any provider
|
|
||||||
* field names, customer entities or renderer representation.
|
|
||||||
*/
|
|
||||||
export function validateIntakeBatch(value) {
|
|
||||||
const errors = baseErrors(value, "intakeBatch");
|
|
||||||
rejectUnknownKeys(value, new Set(["schemaVersion", "source", "contract", "batch", "raw", "facts"]), "intakeBatch", errors);
|
|
||||||
rejectUnknownKeys(value?.source, new Set(["providerId", "tenantId", "connectionId"]), "source", errors);
|
|
||||||
rejectUnknownKeys(value?.contract, new Set(["dataProductId", "ontologyRevision", "version"]), "contract", errors);
|
|
||||||
rejectUnknownKeys(value?.batch, new Set(["runId", "sequence", "idempotencyKey", "receivedAt"]), "batch", errors);
|
|
||||||
requiredIdentifier(value?.source?.providerId, "source.providerId", errors);
|
|
||||||
requiredIdentifier(value?.source?.tenantId, "source.tenantId", errors);
|
|
||||||
requiredIdentifier(value?.source?.connectionId, "source.connectionId", errors);
|
|
||||||
requiredIdentifier(value?.contract?.dataProductId, "contract.dataProductId", errors);
|
|
||||||
requiredIdentifier(value?.contract?.ontologyRevision, "contract.ontologyRevision", errors);
|
|
||||||
requiredString(value?.contract?.version, "contract.version", errors);
|
|
||||||
if (value?.contract?.version && !CONTRACT_VERSION.test(value.contract.version)) {
|
|
||||||
errors.push("contract.version_must_be_semver");
|
|
||||||
}
|
|
||||||
requiredIdentifier(value?.batch?.runId, "batch.runId", errors);
|
|
||||||
requiredIdentifier(value?.batch?.idempotencyKey, "batch.idempotencyKey", errors);
|
|
||||||
if (!Number.isInteger(value?.batch?.sequence) || value.batch.sequence < 0 || value.batch.sequence > MAX_BATCH_SEQUENCE) {
|
|
||||||
errors.push("batch.sequence_must_be_integer_0_to_2147483647");
|
|
||||||
}
|
|
||||||
requiredIsoTimestamp(value?.batch?.receivedAt, "batch.receivedAt", errors);
|
|
||||||
|
|
||||||
if (!Array.isArray(value?.facts) || value.facts.length === 0) {
|
|
||||||
errors.push("facts_must_be_nonempty_array");
|
|
||||||
} else {
|
|
||||||
value.facts.forEach((fact, index) => validateFact(fact, `facts[${index}]`, errors));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value?.raw !== undefined) validateRawEnvelope(value.raw, errors);
|
|
||||||
if (containsSecretLikeMaterial(value)) errors.push("intake_must_not_contain_secret_material");
|
|
||||||
|
|
||||||
return result(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertValid(validator, value) {
|
export function assertValid(validator, value) {
|
||||||
const validation = validator(value);
|
const validation = validator(value);
|
||||||
if (!validation.ok) throw new Error(`external_provider_contract_invalid:${validation.errors.join(",")}`);
|
if (!validation.ok) throw new Error(`external_provider_contract_invalid:${validation.errors.join(",")}`);
|
||||||
|
|
@ -394,66 +349,6 @@ function validateUniqueIdentifierArray(value, path, errors) {
|
||||||
if (new Set(value).size !== value.length) errors.push(`${path}_must_not_contain_duplicates`);
|
if (new Set(value).size !== value.length) errors.push(`${path}_must_not_contain_duplicates`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requiredIsoTimestamp(value, path, errors) {
|
|
||||||
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) errors.push(`${path}_invalid_timestamp`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateFact(value, path, errors) {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push(`${path}_must_be_object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, new Set(["sourceId", "semanticType", "observedAt", "attributes", "geometry"]), path, errors);
|
|
||||||
requiredIdentifier(value.sourceId, `${path}.sourceId`, errors);
|
|
||||||
requiredIdentifier(value.semanticType, `${path}.semanticType`, errors);
|
|
||||||
requiredIsoTimestamp(value.observedAt, `${path}.observedAt`, errors);
|
|
||||||
if (value.attributes !== undefined) {
|
|
||||||
if (!isPlainObject(value.attributes)) {
|
|
||||||
errors.push(`${path}.attributes_must_be_object`);
|
|
||||||
} else if (serializedByteLength(value.attributes) > MAX_FACT_ATTRIBUTES_BYTES) {
|
|
||||||
errors.push(`${path}.attributes_size_exceeded`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value.geometry !== undefined) validatePointGeometry(value.geometry, `${path}.geometry`, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validatePointGeometry(value, path, errors) {
|
|
||||||
if (!isPlainObject(value) || value.type !== "Point" || !Array.isArray(value.coordinates) || value.coordinates.length !== 2) {
|
|
||||||
errors.push(`${path}_must_be_geojson_point`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, new Set(["type", "coordinates"]), path, errors);
|
|
||||||
if (!value.coordinates.every((coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate))) {
|
|
||||||
errors.push(`${path}_coordinates_must_be_finite_numbers`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [longitude, latitude] = value.coordinates;
|
|
||||||
if (longitude < -180 || longitude > 180) errors.push(`${path}.longitude_out_of_range`);
|
|
||||||
if (latitude < -90 || latitude > 90) errors.push(`${path}.latitude_out_of_range`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRawEnvelope(value, errors) {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
errors.push("raw_must_be_object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rejectUnknownKeys(value, new Set(["contentType", "payload", "hash", "ref", "retentionDays"]), "raw", errors);
|
|
||||||
requiredString(value.contentType, "raw.contentType", errors);
|
|
||||||
if (value.payload === undefined && value.ref === undefined) errors.push("raw_requires_payload_or_ref");
|
|
||||||
if (value.payload !== undefined) errors.push("raw.inline_payload_not_supported");
|
|
||||||
if (value.payload === undefined) requiredString(value.hash, "raw.hash", errors);
|
|
||||||
if (value.hash !== undefined) requiredString(value.hash, "raw.hash", errors);
|
|
||||||
if (value.ref !== undefined) {
|
|
||||||
requiredString(value.ref, "raw.ref", errors);
|
|
||||||
if (typeof value.ref === "string" && SECRET_LIKE_REFERENCE.test(value.ref)) {
|
|
||||||
errors.push("raw.ref_must_not_contain_secret_material");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value.retentionDays !== undefined && (!Number.isInteger(value.retentionDays) || value.retentionDays < 1)) {
|
|
||||||
errors.push("raw.retentionDays_must_be_positive_integer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value) {
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
@ -465,14 +360,6 @@ function rejectUnknownKeys(value, allowedKeys, path, errors) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializedByteLength(value) {
|
|
||||||
try {
|
|
||||||
return Buffer.byteLength(JSON.stringify(value));
|
|
||||||
} catch {
|
|
||||||
return Number.POSITIVE_INFINITY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function containsSecretLikeMaterial(value) {
|
function containsSecretLikeMaterial(value) {
|
||||||
if (typeof value === "string") return SECRET_LIKE_VALUE.test(value);
|
if (typeof value === "string") return SECRET_LIKE_VALUE.test(value);
|
||||||
if (Array.isArray(value)) return value.some(containsSecretLikeMaterial);
|
if (Array.isArray(value)) return value.some(containsSecretLikeMaterial);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
|
||||||
|
import {
|
||||||
|
SECRET_LIKE_KEY,
|
||||||
|
SECRET_LIKE_REFERENCE,
|
||||||
|
SECRET_LIKE_VALUE,
|
||||||
|
} from "./sensitive-field-policy.mjs";
|
||||||
|
|
||||||
|
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||||
|
const CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||||
|
const MAX_BATCH_SEQUENCE = 2_147_483_647;
|
||||||
|
const MAX_FACT_ATTRIBUTES_BYTES = 64 * 1024;
|
||||||
|
|
||||||
|
/** Provider-neutral batch written by an L2 connector to External Data Plane. */
|
||||||
|
export function validateIntakeBatch(value) {
|
||||||
|
const errors = baseErrors(value, "intakeBatch");
|
||||||
|
rejectUnknownKeys(value, new Set(["schemaVersion", "source", "contract", "batch", "raw", "facts"]), "intakeBatch", errors);
|
||||||
|
rejectUnknownKeys(value?.source, new Set(["providerId", "tenantId", "connectionId"]), "source", errors);
|
||||||
|
rejectUnknownKeys(value?.contract, new Set(["dataProductId", "ontologyRevision", "version"]), "contract", errors);
|
||||||
|
rejectUnknownKeys(value?.batch, new Set(["runId", "sequence", "idempotencyKey", "receivedAt"]), "batch", errors);
|
||||||
|
requiredIdentifier(value?.source?.providerId, "source.providerId", errors);
|
||||||
|
requiredIdentifier(value?.source?.tenantId, "source.tenantId", errors);
|
||||||
|
requiredIdentifier(value?.source?.connectionId, "source.connectionId", errors);
|
||||||
|
requiredIdentifier(value?.contract?.dataProductId, "contract.dataProductId", errors);
|
||||||
|
requiredIdentifier(value?.contract?.ontologyRevision, "contract.ontologyRevision", errors);
|
||||||
|
requiredString(value?.contract?.version, "contract.version", errors);
|
||||||
|
if (value?.contract?.version && !CONTRACT_VERSION.test(value.contract.version)) {
|
||||||
|
errors.push("contract.version_must_be_semver");
|
||||||
|
}
|
||||||
|
requiredIdentifier(value?.batch?.runId, "batch.runId", errors);
|
||||||
|
requiredIdentifier(value?.batch?.idempotencyKey, "batch.idempotencyKey", errors);
|
||||||
|
if (!Number.isInteger(value?.batch?.sequence) || value.batch.sequence < 0 || value.batch.sequence > MAX_BATCH_SEQUENCE) {
|
||||||
|
errors.push("batch.sequence_must_be_integer_0_to_2147483647");
|
||||||
|
}
|
||||||
|
requiredIsoTimestamp(value?.batch?.receivedAt, "batch.receivedAt", errors);
|
||||||
|
|
||||||
|
if (!Array.isArray(value?.facts) || value.facts.length === 0) {
|
||||||
|
errors.push("facts_must_be_nonempty_array");
|
||||||
|
} else {
|
||||||
|
value.facts.forEach((fact, index) => validateFact(fact, `facts[${index}]`, errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value?.raw !== undefined) validateRawEnvelope(value.raw, errors);
|
||||||
|
if (containsSecretLikeMaterial(value)) errors.push("intake_must_not_contain_secret_material");
|
||||||
|
|
||||||
|
return result(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseErrors(value, label) {
|
||||||
|
const errors = [];
|
||||||
|
if (!isPlainObject(value)) return [`${label}_must_be_object`];
|
||||||
|
if (value.schemaVersion !== EXTERNAL_PROVIDER_CONTRACT_VERSION) errors.push("schemaVersion_mismatch");
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredString(value, path, errors) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) errors.push(`${path}_required`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredIdentifier(value, path, errors) {
|
||||||
|
if (typeof value !== "string" || !IDENTIFIER.test(value)) errors.push(`${path}_invalid`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredIsoTimestamp(value, path, errors) {
|
||||||
|
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) errors.push(`${path}_invalid_timestamp`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFact(value, path, errors) {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
errors.push(`${path}_must_be_object`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectUnknownKeys(value, new Set(["sourceId", "semanticType", "observedAt", "attributes", "geometry"]), path, errors);
|
||||||
|
requiredIdentifier(value.sourceId, `${path}.sourceId`, errors);
|
||||||
|
requiredIdentifier(value.semanticType, `${path}.semanticType`, errors);
|
||||||
|
requiredIsoTimestamp(value.observedAt, `${path}.observedAt`, errors);
|
||||||
|
if (value.attributes !== undefined) {
|
||||||
|
if (!isPlainObject(value.attributes)) {
|
||||||
|
errors.push(`${path}.attributes_must_be_object`);
|
||||||
|
} else if (serializedByteLength(value.attributes) > MAX_FACT_ATTRIBUTES_BYTES) {
|
||||||
|
errors.push(`${path}.attributes_size_exceeded`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.geometry !== undefined) validatePointGeometry(value.geometry, `${path}.geometry`, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePointGeometry(value, path, errors) {
|
||||||
|
if (!isPlainObject(value) || value.type !== "Point" || !Array.isArray(value.coordinates) || value.coordinates.length !== 2) {
|
||||||
|
errors.push(`${path}_must_be_geojson_point`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectUnknownKeys(value, new Set(["type", "coordinates"]), path, errors);
|
||||||
|
if (!value.coordinates.every((coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate))) {
|
||||||
|
errors.push(`${path}_coordinates_must_be_finite_numbers`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [longitude, latitude] = value.coordinates;
|
||||||
|
if (longitude < -180 || longitude > 180) errors.push(`${path}.longitude_out_of_range`);
|
||||||
|
if (latitude < -90 || latitude > 90) errors.push(`${path}.latitude_out_of_range`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRawEnvelope(value, errors) {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
errors.push("raw_must_be_object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectUnknownKeys(value, new Set(["contentType", "payload", "hash", "ref", "retentionDays"]), "raw", errors);
|
||||||
|
requiredString(value.contentType, "raw.contentType", errors);
|
||||||
|
if (value.payload === undefined && value.ref === undefined) errors.push("raw_requires_payload_or_ref");
|
||||||
|
if (value.payload !== undefined) errors.push("raw.inline_payload_not_supported");
|
||||||
|
if (value.payload === undefined) requiredString(value.hash, "raw.hash", errors);
|
||||||
|
if (value.hash !== undefined) requiredString(value.hash, "raw.hash", errors);
|
||||||
|
if (value.ref !== undefined) {
|
||||||
|
requiredString(value.ref, "raw.ref", errors);
|
||||||
|
if (typeof value.ref === "string" && SECRET_LIKE_REFERENCE.test(value.ref)) {
|
||||||
|
errors.push("raw.ref_must_not_contain_secret_material");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.retentionDays !== undefined && (!Number.isInteger(value.retentionDays) || value.retentionDays < 1)) {
|
||||||
|
errors.push("raw.retentionDays_must_be_positive_integer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectUnknownKeys(value, allowedKeys, path, errors) {
|
||||||
|
if (!isPlainObject(value)) return;
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (!allowedKeys.has(key)) errors.push(`${path}.${key}_not_allowed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializedByteLength(value) {
|
||||||
|
try {
|
||||||
|
return Buffer.byteLength(JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsSecretLikeMaterial(value) {
|
||||||
|
if (typeof value === "string") return SECRET_LIKE_VALUE.test(value);
|
||||||
|
if (Array.isArray(value)) return value.some(containsSecretLikeMaterial);
|
||||||
|
if (!isPlainObject(value)) return false;
|
||||||
|
return Object.entries(value).some(([key, child]) => SECRET_LIKE_KEY.test(key) || containsSecretLikeMaterial(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
function result(errors) {
|
||||||
|
const uniqueErrors = [...new Set(errors)];
|
||||||
|
return Object.freeze({ ok: uniqueErrors.length === 0, errors: Object.freeze(uniqueErrors) });
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
||||||
|
export const NDC_SECRET_CAPABILITY_SOURCE =
|
||||||
|
String.raw`ndc_(?:edp(?:wb|rb|pr)|fndbg)_[A-Za-z0-9_-]+`;
|
||||||
|
|
||||||
|
export const SECRET_LIKE_KEY =
|
||||||
|
/(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
|
||||||
|
|
||||||
|
export const SECRET_LIKE_MATERIAL_KEY =
|
||||||
|
/(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key|material|value)/i;
|
||||||
|
|
||||||
|
export const SECRET_LIKE_REFERENCE =
|
||||||
|
/(?:[?&](?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)=|(?:bearer|basic)\s+)/i;
|
||||||
|
|
||||||
|
export const SECRET_LIKE_VALUE = new RegExp(
|
||||||
|
`(?:${NDC_SECRET_CAPABILITY_SOURCE}|[?&](?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)=|(?:bearer|basic)\\s+\\S+|eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+)`,
|
||||||
|
"i",
|
||||||
|
);
|
||||||
|
|
@ -15,6 +15,10 @@ import { geliosPositionsCurrentExample } from "../examples/gelios-positions-curr
|
||||||
|
|
||||||
assert.equal(validateProviderManifest(geliosPositionsCurrentExample.providerManifest).ok, true);
|
assert.equal(validateProviderManifest(geliosPositionsCurrentExample.providerManifest).ok, true);
|
||||||
assert.equal(validateConnectionProfile(geliosPositionsCurrentExample.connection).ok, true);
|
assert.equal(validateConnectionProfile(geliosPositionsCurrentExample.connection).ok, true);
|
||||||
|
assert.equal(validateConnectionProfile({
|
||||||
|
...geliosPositionsCurrentExample.connection,
|
||||||
|
credentialRef: { ...geliosPositionsCurrentExample.connection.credentialRef, owner: "engine" },
|
||||||
|
}).errors.includes("credentialRef.owner_must_be_ndc_l2_credentials"), true);
|
||||||
assert.equal(validateCollectionProfile(geliosPositionsCurrentExample.collectionProfile).ok, true);
|
assert.equal(validateCollectionProfile(geliosPositionsCurrentExample.collectionProfile).ok, true);
|
||||||
assert.equal(validateDataProduct(geliosPositionsCurrentExample.dataProduct).ok, true);
|
assert.equal(validateDataProduct(geliosPositionsCurrentExample.dataProduct).ok, true);
|
||||||
assert.equal(validateFoundryBinding(geliosPositionsCurrentExample.foundryBinding).ok, true);
|
assert.equal(validateFoundryBinding(geliosPositionsCurrentExample.foundryBinding).ok, true);
|
||||||
|
|
@ -108,6 +112,10 @@ assert.equal(validateIntakeBatch({
|
||||||
...neutralIntakeBatch,
|
...neutralIntakeBatch,
|
||||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { metadata: "ndc_edprb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE" } }],
|
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { metadata: "ndc_edprb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE" } }],
|
||||||
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||||
|
assert.equal(validateIntakeBatch({
|
||||||
|
...neutralIntakeBatch,
|
||||||
|
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { metadata: `ndc_edppr_${"P".repeat(43)}` } }],
|
||||||
|
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||||
assert.equal(validateIntakeBatch({
|
assert.equal(validateIntakeBatch({
|
||||||
...neutralIntakeBatch,
|
...neutralIntakeBatch,
|
||||||
raw: { contentType: "application/json", hash: "prefix ndc_edpwb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE suffix", ref: "restricted://raw/fixture" },
|
raw: { contentType: "application/json", hash: "prefix ndc_edpwb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE suffix", ref: "restricted://raw/fixture" },
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ assert.equal(validateDataProductPublish({
|
||||||
...publish,
|
...publish,
|
||||||
facts: [{ ...fact, attributes: { accessToken: "must-never-cross-the-boundary" } }],
|
facts: [{ ...fact, attributes: { accessToken: "must-never-cross-the-boundary" } }],
|
||||||
}).errors.includes("publish_must_not_contain_secret_material"), true);
|
}).errors.includes("publish_must_not_contain_secret_material"), true);
|
||||||
|
assert.equal(validateDataProductPublish({
|
||||||
|
...publish,
|
||||||
|
facts: [{ ...fact, attributes: { status: `ndc_edppr_${"P".repeat(43)}` } }],
|
||||||
|
}).errors.includes("publish_must_not_contain_secret_material"), true);
|
||||||
assert.equal(validateDataProductPublish({
|
assert.equal(validateDataProductPublish({
|
||||||
...publish,
|
...publish,
|
||||||
facts: [fact, { ...fact, observedAt: "2026-07-15T10:00:01.000Z" }],
|
facts: [fact, { ...fact, observedAt: "2026-07-15T10:00:01.000Z" }],
|
||||||
|
|
@ -85,6 +89,10 @@ assert.equal(validateDataProductSnapshot({
|
||||||
...snapshot,
|
...snapshot,
|
||||||
facts: [{ ...canonicalFact, attributes: { status: "ndc_edprb_forbidden-reader-token" } }],
|
facts: [{ ...canonicalFact, attributes: { status: "ndc_edprb_forbidden-reader-token" } }],
|
||||||
}).errors.includes("snapshot_must_not_contain_secret_material"), true);
|
}).errors.includes("snapshot_must_not_contain_secret_material"), true);
|
||||||
|
assert.equal(validateDataProductSnapshot({
|
||||||
|
...snapshot,
|
||||||
|
facts: [{ ...canonicalFact, attributes: { status: `ndc_edppr_${"P".repeat(43)}` } }],
|
||||||
|
}).errors.includes("snapshot_must_not_contain_secret_material"), true);
|
||||||
|
|
||||||
const patch = {
|
const patch = {
|
||||||
schemaVersion: DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
schemaVersion: DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||||
|
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { generateKeyPairSync, sign } from "node:crypto";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ENGINE_CREDENTIAL_SINK_AUDIT_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_PROVISION_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_RECEIPT_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_ROLLBACK_RECEIPT_SCHEMA_VERSION,
|
|
||||||
ENGINE_CREDENTIAL_SINK_ROLLBACK_SCHEMA_VERSION,
|
|
||||||
computeEngineCredentialCapabilityDigest,
|
|
||||||
computeEngineCredentialSinkPolicyHash,
|
|
||||||
computeEngineCredentialSinkReceiptHash,
|
|
||||||
engineCredentialSinkAuditTargets,
|
|
||||||
validateEngineCredentialSinkAudit,
|
|
||||||
validateEngineCredentialSinkProvision,
|
|
||||||
validateEngineCredentialSinkReceipt,
|
|
||||||
validateEngineCredentialSinkRollback,
|
|
||||||
validateEngineCredentialSinkRollbackReceipt,
|
|
||||||
} from "../src/index.mjs";
|
|
||||||
|
|
||||||
const hash = (character) => `sha256:${character.repeat(64)}`;
|
|
||||||
const issuer = { serviceId: "platform.external-data-plane", keyId: "edp-issuer-20260715-001" };
|
|
||||||
const { publicKey: issuerPublicKey, privateKey: issuerPrivateKey } = generateKeyPairSync("ed25519");
|
|
||||||
const issuerPublicKeys = { [`${issuer.serviceId}:${issuer.keyId}`]: issuerPublicKey };
|
|
||||||
const target = (nodeId, nodeType, credentialType) => ({
|
|
||||||
workflowId: "WCb62yGL8v",
|
|
||||||
workflowRevision: "revision-20260715-001",
|
|
||||||
nodeId,
|
|
||||||
nodeType,
|
|
||||||
credentialType,
|
|
||||||
});
|
|
||||||
|
|
||||||
const request = {
|
|
||||||
schemaVersion: ENGINE_CREDENTIAL_SINK_PROVISION_SCHEMA_VERSION,
|
|
||||||
transaction: {
|
|
||||||
id: "credential-transaction-20260715-001",
|
|
||||||
idempotencyKey: "credential-transaction-20260715-001",
|
|
||||||
requestedAt: "2026-07-15T18:00:00.000Z",
|
|
||||||
requestExpiresAt: "2026-07-15T18:10:00.000Z",
|
|
||||||
policyHash: hash("0"),
|
|
||||||
failureMode: "rollback-all",
|
|
||||||
issuer,
|
|
||||||
attestation: { algorithm: "Ed25519", signature: "A".repeat(86) },
|
|
||||||
},
|
|
||||||
bindings: [
|
|
||||||
{
|
|
||||||
bindingId: "positions-writer",
|
|
||||||
capabilityType: "external-data-plane.writer",
|
|
||||||
grantId: "writer-grant-001",
|
|
||||||
target: target(
|
|
||||||
"publish-node-001",
|
|
||||||
"n8n-nodes-ndc.ndcDataProductPublish",
|
|
||||||
"ndcDataProductWriterApi",
|
|
||||||
),
|
|
||||||
expiresAt: "2026-08-15T18:00:00.000Z",
|
|
||||||
policyHash: hash("1"),
|
|
||||||
material: { format: "opaque-bearer", value: `ndc_edpwb_${"A".repeat(43)}` },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bindingId: "positions-reader",
|
|
||||||
capabilityType: "external-data-plane.reader",
|
|
||||||
grantId: "reader-grant-001",
|
|
||||||
target: target(
|
|
||||||
"read-node-001",
|
|
||||||
"n8n-nodes-ndc.ndcDataProductRead",
|
|
||||||
"ndcDataProductReaderApi",
|
|
||||||
),
|
|
||||||
expiresAt: "2026-08-15T18:00:00.000Z",
|
|
||||||
policyHash: hash("2"),
|
|
||||||
material: { format: "opaque-bearer", value: `ndc_edprb_${"B".repeat(43)}` },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bindingId: "positions-foundry",
|
|
||||||
capabilityType: "foundry.binding",
|
|
||||||
grantId: "foundry-grant-001",
|
|
||||||
target: target(
|
|
||||||
"foundry-node-001",
|
|
||||||
"n8n-nodes-ndc.ndcFoundryBinding",
|
|
||||||
"ndcFoundryBindingApi",
|
|
||||||
),
|
|
||||||
expiresAt: "2026-08-15T18:00:00.000Z",
|
|
||||||
policyHash: hash("3"),
|
|
||||||
material: { format: "opaque-bearer", value: `ndc_fndbg_${"C".repeat(43)}` },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
for (const binding of request.bindings) {
|
|
||||||
binding.capabilityDigest = computeEngineCredentialCapabilityDigest(binding.material.value);
|
|
||||||
}
|
|
||||||
attest(request);
|
|
||||||
const provisionValidationNow = "2026-07-15T18:00:01.000Z";
|
|
||||||
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision(request, { now: provisionValidationNow, issuerPublicKeys }).ok, true);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision(request, {
|
|
||||||
now: provisionValidationNow,
|
|
||||||
}).errors.includes("transaction.issuer_public_key_required"), true);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision({
|
|
||||||
...request,
|
|
||||||
transaction: { ...request.transaction, policyHash: hash("f") },
|
|
||||||
}, { now: provisionValidationNow, issuerPublicKeys }).errors.includes("transaction.policyHash_mismatch"), true);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision({
|
|
||||||
...request,
|
|
||||||
bindings: [{
|
|
||||||
...request.bindings[0],
|
|
||||||
target: { ...request.bindings[0].target, nodeType: "n8n-nodes-ndc.ndcDataProductRead" },
|
|
||||||
}],
|
|
||||||
}, { now: provisionValidationNow, issuerPublicKeys }).errors.includes("bindings[0].target.nodeType_capability_mismatch"), true);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision({
|
|
||||||
...request,
|
|
||||||
bindings: [{
|
|
||||||
...request.bindings[0],
|
|
||||||
material: { format: "opaque-bearer", value: request.bindings[1].material.value },
|
|
||||||
}],
|
|
||||||
}, { now: provisionValidationNow, issuerPublicKeys }).errors.includes("bindings[0].material.value_invalid_for_capability"), true);
|
|
||||||
|
|
||||||
const swappedCapabilityRequest = structuredClone(request);
|
|
||||||
swappedCapabilityRequest.bindings[0].material.value = `ndc_edpwb_${"D".repeat(43)}`;
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision(swappedCapabilityRequest, {
|
|
||||||
now: provisionValidationNow,
|
|
||||||
issuerPublicKeys,
|
|
||||||
}).errors.includes("bindings[0].capabilityDigest_material_mismatch"), true);
|
|
||||||
|
|
||||||
const resignedByAttackerRequest = structuredClone(swappedCapabilityRequest);
|
|
||||||
resignedByAttackerRequest.bindings[0].capabilityDigest = computeEngineCredentialCapabilityDigest(
|
|
||||||
resignedByAttackerRequest.bindings[0].material.value,
|
|
||||||
);
|
|
||||||
resignedByAttackerRequest.transaction.policyHash = computeEngineCredentialSinkPolicyHash(resignedByAttackerRequest);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision(resignedByAttackerRequest, {
|
|
||||||
now: provisionValidationNow,
|
|
||||||
issuerPublicKeys,
|
|
||||||
}).errors.includes("transaction.attestation_invalid"), true);
|
|
||||||
|
|
||||||
const staleRequest = structuredClone(request);
|
|
||||||
staleRequest.transaction.requestedAt = "2020-01-01T00:00:00.000Z";
|
|
||||||
staleRequest.transaction.requestExpiresAt = "2020-01-01T00:10:00.000Z";
|
|
||||||
attest(staleRequest);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision(staleRequest, { now: provisionValidationNow, issuerPublicKeys }).errors.includes("request_expired"), true);
|
|
||||||
|
|
||||||
const futureRequest = structuredClone(request);
|
|
||||||
futureRequest.transaction.requestedAt = "2026-07-15T18:02:00.000Z";
|
|
||||||
futureRequest.transaction.requestExpiresAt = "2026-07-15T18:12:00.000Z";
|
|
||||||
attest(futureRequest);
|
|
||||||
assert.equal(validateEngineCredentialSinkProvision(futureRequest, { now: provisionValidationNow, issuerPublicKeys }).errors.includes("requestedAt_exceeds_clock_skew"), true);
|
|
||||||
|
|
||||||
const credentials = request.bindings.map((binding, index) => ({
|
|
||||||
bindingId: binding.bindingId,
|
|
||||||
capabilityType: binding.capabilityType,
|
|
||||||
grantId: binding.grantId,
|
|
||||||
target: binding.target,
|
|
||||||
credentialRef: `engcred_${index + 1}_opaque_reference`,
|
|
||||||
expiresAt: binding.expiresAt,
|
|
||||||
policyHash: binding.policyHash,
|
|
||||||
capabilityDigest: binding.capabilityDigest,
|
|
||||||
disposition: "created",
|
|
||||||
}));
|
|
||||||
const receipt = {
|
|
||||||
schemaVersion: ENGINE_CREDENTIAL_SINK_RECEIPT_SCHEMA_VERSION,
|
|
||||||
transactionId: request.transaction.id,
|
|
||||||
idempotencyKey: request.transaction.idempotencyKey,
|
|
||||||
outcome: "committed",
|
|
||||||
policyHash: request.transaction.policyHash,
|
|
||||||
processedAt: "2026-07-15T18:00:02.000Z",
|
|
||||||
credentials,
|
|
||||||
rollback: { status: "not-required" },
|
|
||||||
};
|
|
||||||
assert.equal(validateEngineCredentialSinkReceipt(receipt, { request, issuerPublicKeys }).ok, true);
|
|
||||||
assert.equal(validateEngineCredentialSinkReceipt({
|
|
||||||
...receipt,
|
|
||||||
credentials: [{ ...credentials[0], target: { ...credentials[0].target, nodeId: "wrong-node" } }, ...credentials.slice(1)],
|
|
||||||
}, { request, issuerPublicKeys }).errors.includes("credentials_request_target_mismatch"), true);
|
|
||||||
assert.equal(validateEngineCredentialSinkReceipt({
|
|
||||||
...receipt,
|
|
||||||
capability: request.bindings[0].material.value,
|
|
||||||
}).errors.includes("credentialSinkReceipt.capability_not_allowed"), true);
|
|
||||||
assert.equal(validateEngineCredentialSinkReceipt({
|
|
||||||
...receipt,
|
|
||||||
outcome: "rolled-back",
|
|
||||||
credentials: credentials.slice(0, 1),
|
|
||||||
rollback: { status: "complete", completedAt: "2026-07-15T18:00:02.000Z" },
|
|
||||||
errorCode: "engine_binding_failed",
|
|
||||||
}).errors.includes("noncommitted_credentials_must_be_empty"), true);
|
|
||||||
|
|
||||||
const committedReceiptHash = computeEngineCredentialSinkReceiptHash(receipt);
|
|
||||||
const rollbackRequest = {
|
|
||||||
schemaVersion: ENGINE_CREDENTIAL_SINK_ROLLBACK_SCHEMA_VERSION,
|
|
||||||
rollback: {
|
|
||||||
id: "credential-rollback-20260715-001",
|
|
||||||
idempotencyKey: "credential-rollback-20260715-001",
|
|
||||||
transactionId: request.transaction.id,
|
|
||||||
requestedAt: "2026-07-15T18:05:00.000Z",
|
|
||||||
requestExpiresAt: "2026-07-15T18:10:00.000Z",
|
|
||||||
policyHash: request.transaction.policyHash,
|
|
||||||
committedReceiptHash,
|
|
||||||
reasonCode: "operator_requested",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
assert.equal(validateEngineCredentialSinkRollback(rollbackRequest, { now: "2026-07-15T18:05:01.000Z" }).ok, true);
|
|
||||||
const staleRollbackRequest = structuredClone(rollbackRequest);
|
|
||||||
staleRollbackRequest.rollback.requestedAt = "2020-01-01T00:00:00.000Z";
|
|
||||||
staleRollbackRequest.rollback.requestExpiresAt = "2020-01-01T00:10:00.000Z";
|
|
||||||
assert.equal(validateEngineCredentialSinkRollback(staleRollbackRequest, { now: "2026-07-15T18:05:01.000Z" }).errors.includes("request_expired"), true);
|
|
||||||
const rollbackReceipt = {
|
|
||||||
schemaVersion: ENGINE_CREDENTIAL_SINK_ROLLBACK_RECEIPT_SCHEMA_VERSION,
|
|
||||||
rollbackId: rollbackRequest.rollback.id,
|
|
||||||
transactionId: rollbackRequest.rollback.transactionId,
|
|
||||||
outcome: "rolled-back",
|
|
||||||
policyHash: rollbackRequest.rollback.policyHash,
|
|
||||||
committedReceiptHash,
|
|
||||||
processedAt: "2026-07-15T18:05:01.000Z",
|
|
||||||
};
|
|
||||||
assert.equal(validateEngineCredentialSinkRollbackReceipt(rollbackReceipt, { request: rollbackRequest }).ok, true);
|
|
||||||
|
|
||||||
const audit = {
|
|
||||||
schemaVersion: ENGINE_CREDENTIAL_SINK_AUDIT_SCHEMA_VERSION,
|
|
||||||
eventId: "credential-audit-20260715-001",
|
|
||||||
transactionId: request.transaction.id,
|
|
||||||
operationId: request.transaction.id,
|
|
||||||
operation: "provision",
|
|
||||||
outcome: "committed",
|
|
||||||
occurredAt: receipt.processedAt,
|
|
||||||
policyHash: request.transaction.policyHash,
|
|
||||||
principal: { serviceId: "platform.credential-provisioner", fingerprint: hash("a") },
|
|
||||||
targets: engineCredentialSinkAuditTargets(request, receipt),
|
|
||||||
};
|
|
||||||
assert.equal(validateEngineCredentialSinkAudit(audit).ok, true);
|
|
||||||
assert.equal(JSON.stringify(audit).includes("ndc_edpwb_"), false);
|
|
||||||
assert.equal(JSON.stringify(audit).includes("engcred_1_opaque_reference"), false);
|
|
||||||
assert.equal(validateEngineCredentialSinkAudit({
|
|
||||||
...audit,
|
|
||||||
authorization: `Bearer ${request.bindings[0].material.value}`,
|
|
||||||
}).errors.includes("audit_must_not_contain_secret_material"), true);
|
|
||||||
|
|
||||||
function attest(value) {
|
|
||||||
value.transaction.policyHash = computeEngineCredentialSinkPolicyHash(value);
|
|
||||||
value.transaction.attestation.signature = sign(
|
|
||||||
null,
|
|
||||||
Buffer.from(value.transaction.policyHash, "utf8"),
|
|
||||||
issuerPrivateKey,
|
|
||||||
).toString("base64url");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("external-provider credential sink contract: ok");
|
|
||||||
|
|
@ -132,6 +132,16 @@ assert.equal(
|
||||||
}).errors.includes("transition_policy_mismatch"),
|
}).errors.includes("transition_policy_mismatch"),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
validateEnginePrivateExtensionPlan({
|
||||||
|
...activatePlan,
|
||||||
|
transition: {
|
||||||
|
...transition(),
|
||||||
|
loaderEnvironment: { ...transition().loaderEnvironment, NODE_PATH: "/tmp/untrusted-node-modules" },
|
||||||
|
},
|
||||||
|
}).errors.includes("transition.loaderEnvironment.NODE_PATH_not_allowed"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const applyRequest = {
|
const applyRequest = {
|
||||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION,
|
schemaVersion: ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,464 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import {
|
||||||
|
L2_CONNECTION_INSTANCE_SCHEMA_VERSION,
|
||||||
|
instantiateL2Connection,
|
||||||
|
validateDataProductPublish,
|
||||||
|
validateProviderPackage,
|
||||||
|
} from "../src/index.mjs";
|
||||||
|
import {
|
||||||
|
GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
GELIOS_POSITIONS_DATA_PRODUCT_VERSION,
|
||||||
|
GELIOS_POSITIONS_ONTOLOGY_REVISION,
|
||||||
|
GELIOS_UNIT_SOURCE_ID_PREFIX,
|
||||||
|
geliosProviderPackageV1,
|
||||||
|
geliosUnitsCurrentFixtureV1,
|
||||||
|
} from "../providers/gelios/v1/index.mjs";
|
||||||
|
import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs";
|
||||||
|
|
||||||
|
const expectedFields = [
|
||||||
|
"course_degrees",
|
||||||
|
"display_name",
|
||||||
|
"elevation_meters",
|
||||||
|
"geometry",
|
||||||
|
"hdop",
|
||||||
|
"horizontal_accuracy_meters",
|
||||||
|
"object_kind",
|
||||||
|
"operational_status",
|
||||||
|
"position_source",
|
||||||
|
"position_valid",
|
||||||
|
"quality_flags",
|
||||||
|
"satellite_count",
|
||||||
|
"speed_kph",
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(validateProviderPackage(geliosProviderPackageV1), { ok: true, errors: [] });
|
||||||
|
|
||||||
|
const geliosAuth = geliosProviderPackageV1.authModes[0];
|
||||||
|
const geliosUnitsRead = geliosProviderPackageV1.capabilities[0];
|
||||||
|
assert.equal(geliosAuth.id, "gelios.rest-bearer.v1");
|
||||||
|
assert.deepEqual(geliosAuth.transport, { placement: "header", name: "Authorization" });
|
||||||
|
assert.deepEqual(geliosAuth.tokenLifecycle, {
|
||||||
|
artifacts: ["access", "refresh"],
|
||||||
|
requestArtifact: "access",
|
||||||
|
refreshMode: "operator_managed",
|
||||||
|
});
|
||||||
|
assert.equal(geliosAuth.tokenLifecycle.artifacts.includes("writer"), false);
|
||||||
|
assert.equal(geliosUnitsRead.request.baseUrl, "https://api.geliospro.com");
|
||||||
|
assert.equal(geliosUnitsRead.request.path, "/api/v1/units");
|
||||||
|
assert.deepEqual(geliosUnitsRead.request.query, {});
|
||||||
|
|
||||||
|
const product = geliosProviderPackageV1.dataProducts[0];
|
||||||
|
const registeredProduct = JSON.parse(await readFile(new URL(
|
||||||
|
"../../../services/external-data-plane/definitions/fleet.positions.current.v1.json",
|
||||||
|
import.meta.url,
|
||||||
|
), "utf8"));
|
||||||
|
assert.equal(product.id, GELIOS_POSITIONS_DATA_PRODUCT_ID);
|
||||||
|
assert.equal(product.version, GELIOS_POSITIONS_DATA_PRODUCT_VERSION);
|
||||||
|
assert.equal(product.ontologyRevision, GELIOS_POSITIONS_ONTOLOGY_REVISION);
|
||||||
|
assert.equal(product.id, "fleet.positions.current.v1");
|
||||||
|
assert.equal(product.version, "1.0.0");
|
||||||
|
assert.equal(product.ontologyRevision, "ontology.map.moving_object.v1");
|
||||||
|
assert.deepEqual(product.fields, expectedFields);
|
||||||
|
assert.deepEqual(product, registeredProduct);
|
||||||
|
assert.doesNotThrow(() => normalizeDataProductDefinition(product));
|
||||||
|
assert.equal(product.fields.every((field) => /^[a-z][a-z0-9_]*$/.test(field)), true);
|
||||||
|
|
||||||
|
const mapping = geliosProviderPackageV1.mappingContracts[0];
|
||||||
|
assert.equal(mapping.target.dataProductId, product.id);
|
||||||
|
assert.equal(mapping.target.version, product.version);
|
||||||
|
assert.equal(mapping.target.ontologyRevision, product.ontologyRevision);
|
||||||
|
assert.deepEqual(
|
||||||
|
[...Object.keys(mapping.fact.attributes), "geometry"].sort(),
|
||||||
|
[...product.fields].sort(),
|
||||||
|
);
|
||||||
|
assert.equal(mapping.derivations.operational_status.parameters.staleAfterMs, 60000);
|
||||||
|
assert.deepEqual(mapping.derivations.quality_flags.default, []);
|
||||||
|
assert.equal(mapping.fact.observedAt.fallback, "collection_received_at");
|
||||||
|
assert.equal(GELIOS_UNIT_SOURCE_ID_PREFIX, "gelios-unit-");
|
||||||
|
assert.equal(mapping.fact.sourceId.prefix, GELIOS_UNIT_SOURCE_ID_PREFIX);
|
||||||
|
assert.equal(mapping.fact.sourceId.coerce, "string");
|
||||||
|
assert.equal(mapping.fact.geometry.longitude.paths.includes("lastMsg.lon"), true);
|
||||||
|
assert.equal(mapping.fact.geometry.latitude.paths.includes("lastMsg.lat"), true);
|
||||||
|
assert.equal(mapping.fact.attributes.speed_kph.paths.includes("lastMsg.speed"), true);
|
||||||
|
|
||||||
|
assert.equal(validateDataProductPublish(geliosUnitsCurrentFixtureV1.expectedPublish).ok, true);
|
||||||
|
assert.equal(
|
||||||
|
geliosUnitsCurrentFixtureV1.sourceResponse.units.length,
|
||||||
|
geliosUnitsCurrentFixtureV1.expectedPublish.facts.length,
|
||||||
|
"every entity returned for the bound credential remains in the publish fixture",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
geliosUnitsCurrentFixtureV1.expectedPublish.facts.map((fact) => fact.sourceId),
|
||||||
|
geliosUnitsCurrentFixtureV1.sourceResponse.units.map(
|
||||||
|
(unit) => `${GELIOS_UNIT_SOURCE_ID_PREFIX}${String(unit.id)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].sourceId,
|
||||||
|
"gelios-unit-001002",
|
||||||
|
"provider ID strings must remain byte-for-byte stable after the canonical prefix",
|
||||||
|
);
|
||||||
|
assert.equal(geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].geometry, undefined);
|
||||||
|
assert.equal(geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].attributes.operational_status, "no_position");
|
||||||
|
assert.equal(geliosUnitsCurrentFixtureV1.sourceResponse.units[1].lastMsg, undefined);
|
||||||
|
assert.equal(
|
||||||
|
geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].observedAt,
|
||||||
|
geliosUnitsCurrentFixtureV1.collectionContext.receivedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
const realtimeProfileId = "gelios.positions.current.realtime.v1";
|
||||||
|
assert.equal(
|
||||||
|
geliosProviderPackageV1.collectionProfiles[0].cardinality.onExceed,
|
||||||
|
"require_partitioned_data_product",
|
||||||
|
);
|
||||||
|
const accountA = instantiateL2Connection(geliosProviderPackageV1, {
|
||||||
|
tenantId: "tenant-alpha",
|
||||||
|
connectionId: "gelios-account-alpha",
|
||||||
|
collectionProfileId: realtimeProfileId,
|
||||||
|
providerCredentialRef: "ndc-credref:provider-alpha-0001",
|
||||||
|
});
|
||||||
|
const accountB = instantiateL2Connection(geliosProviderPackageV1, {
|
||||||
|
tenantId: "tenant-beta",
|
||||||
|
connectionId: "gelios-account-beta",
|
||||||
|
collectionProfileId: realtimeProfileId,
|
||||||
|
providerCredentialRef: "ndc-credref:provider-beta-0001",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(accountA.schemaVersion, L2_CONNECTION_INSTANCE_SCHEMA_VERSION);
|
||||||
|
assert.equal(accountA.package.id, accountB.package.id);
|
||||||
|
assert.equal(accountA.package.version, accountB.package.version);
|
||||||
|
assert.equal(accountA.l2TemplateId, accountB.l2TemplateId);
|
||||||
|
assert.notEqual(accountA.connectionId, accountB.connectionId);
|
||||||
|
assert.notEqual(accountA.credentialRefs.provider.reference, accountB.credentialRefs.provider.reference);
|
||||||
|
assert.equal(accountA.credentialRefs.provider.owner, "ndc_l2_credentials");
|
||||||
|
assert.deepEqual(Object.keys(accountA.credentialRefs), ["provider"]);
|
||||||
|
assert.deepEqual(accountA.systemBindings.publisher, {
|
||||||
|
role: "publisher",
|
||||||
|
owner: "ndc_l2_credentials",
|
||||||
|
management: "control_plane_managed",
|
||||||
|
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
desiredState: "bound",
|
||||||
|
status: "unresolved",
|
||||||
|
});
|
||||||
|
assert.equal(accountA.scope.mode, "all_visible_to_credential");
|
||||||
|
assert.equal(accountA.scope.refresh, "each_collection_run");
|
||||||
|
assert.equal(Object.isFrozen(accountA), true);
|
||||||
|
assert.equal(Object.isFrozen(accountA.credentialRefs), true);
|
||||||
|
assert.equal(Object.isFrozen(accountA.systemBindings), true);
|
||||||
|
|
||||||
|
const withEntityAllowlist = structuredClone(geliosProviderPackageV1);
|
||||||
|
withEntityAllowlist.capabilities[0].entityScope.unitIds = ["gelios-unit-1001"];
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withEntityAllowlist).errors.includes("capabilities[0].entityScope.unitIds_not_allowed"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withGroupFilter = structuredClone(geliosProviderPackageV1);
|
||||||
|
withGroupFilter.collectionProfiles[0].entityScope.businessEntityFilter = "provider_group";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withGroupFilter).errors.includes("collectionProfiles[0].entityScope.businessEntityFilter_must_be_forbidden"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withWrongRevision = structuredClone(geliosProviderPackageV1);
|
||||||
|
withWrongRevision.mappingContracts[0].target.ontologyRevision = "gelios.positions.v1";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withWrongRevision).errors.includes("mappingContract.gelios.units.to.fleet.positions.current.v1.target.ontologyRevision_must_match_data_product"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withMissingDerivation = structuredClone(geliosProviderPackageV1);
|
||||||
|
delete withMissingDerivation.mappingContracts[0].derivations.position_valid;
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withMissingDerivation).errors.includes("mappingContracts[0].derive_position_valid_missing_definition"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withWrongFactSemanticType = structuredClone(geliosProviderPackageV1);
|
||||||
|
withWrongFactSemanticType.mappingContracts[0].fact.semanticType.constant = "map.other";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withWrongFactSemanticType).errors.includes(
|
||||||
|
"mappingContract.gelios.units.to.fleet.positions.current.v1.fact.semanticType_must_equal_target_constant",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nonGeospatialPackage = structuredClone(geliosProviderPackageV1);
|
||||||
|
nonGeospatialPackage.dataProducts[0].fields = ["display_name"];
|
||||||
|
nonGeospatialPackage.fieldPolicies[0].targetFields = ["display_name"];
|
||||||
|
delete nonGeospatialPackage.mappingContracts[0].fact.geometry;
|
||||||
|
nonGeospatialPackage.mappingContracts[0].fact.attributes = {
|
||||||
|
display_name: { strategy: "first_non_empty", paths: ["name"], coerce: "string" },
|
||||||
|
};
|
||||||
|
delete nonGeospatialPackage.mappingContracts[0].derivations;
|
||||||
|
assert.deepEqual(validateProviderPackage(nonGeospatialPackage), { ok: true, errors: [] });
|
||||||
|
|
||||||
|
for (const restrictedPath of ["phone", "phone.number", "lmsg"]) {
|
||||||
|
const withRestrictedMappingPath = structuredClone(geliosProviderPackageV1);
|
||||||
|
withRestrictedMappingPath.mappingContracts[0].fact.attributes.display_name.paths = [restrictedPath];
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withRestrictedMappingPath).errors.includes(
|
||||||
|
"mappingContract.gelios.units.to.fleet.positions.current.v1.fact_source_path_restricted",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withFieldPolicyProductMismatch = structuredClone(geliosProviderPackageV1);
|
||||||
|
const secondProduct = structuredClone(withFieldPolicyProductMismatch.dataProducts[0]);
|
||||||
|
secondProduct.id = "fleet.positions.alternate.v1";
|
||||||
|
withFieldPolicyProductMismatch.dataProducts.push(secondProduct);
|
||||||
|
withFieldPolicyProductMismatch.manifest.dataProductIds.push(secondProduct.id);
|
||||||
|
withFieldPolicyProductMismatch.fieldPolicies[0].dataProductId = secondProduct.id;
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withFieldPolicyProductMismatch).errors.includes(
|
||||||
|
"mappingContract.gelios.units.to.fleet.positions.current.v1.field_policy_data_product_mismatch",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withCamelCaseField = structuredClone(geliosProviderPackageV1);
|
||||||
|
withCamelCaseField.dataProducts[0].fields[12] = "speedKph";
|
||||||
|
assert.equal(validateProviderPackage(withCamelCaseField).ok, false);
|
||||||
|
|
||||||
|
const withSecretField = structuredClone(geliosProviderPackageV1);
|
||||||
|
withSecretField.dataProducts[0].fields[12] = "api_token";
|
||||||
|
withSecretField.fieldPolicies[0].targetFields[12] = "api_token";
|
||||||
|
withSecretField.mappingContracts[0].fact.attributes.api_token = { constant: "metadata-only" };
|
||||||
|
delete withSecretField.mappingContracts[0].fact.attributes.speed_kph;
|
||||||
|
const secretFieldValidation = validateProviderPackage(withSecretField);
|
||||||
|
assert.equal(secretFieldValidation.errors.includes("dataProducts[0].fields_must_not_contain_secret_fields"), true);
|
||||||
|
assert.equal(secretFieldValidation.errors.includes("fieldPolicies[0].targetFields_must_not_contain_secret_fields"), true);
|
||||||
|
assert.equal(secretFieldValidation.errors.includes("mappingContracts[0].fact.attributes.api_token_secret_field_not_allowed"), true);
|
||||||
|
|
||||||
|
const withRuntimeAccountState = structuredClone(geliosProviderPackageV1);
|
||||||
|
withRuntimeAccountState.tenantId = "must-not-live-in-package";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withRuntimeAccountState).errors.includes("providerPackage_must_not_contain_connection_instance_state"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withPrimitiveCapability = structuredClone(geliosProviderPackageV1);
|
||||||
|
withPrimitiveCapability.capabilities.push(null);
|
||||||
|
assert.doesNotThrow(() => validateProviderPackage(withPrimitiveCapability));
|
||||||
|
assert.equal(validateProviderPackage(withPrimitiveCapability).ok, false);
|
||||||
|
|
||||||
|
const withMalformedNestedArrays = structuredClone(geliosProviderPackageV1);
|
||||||
|
withMalformedNestedArrays.collectionProfiles[0].capabilityIds = {};
|
||||||
|
withMalformedNestedArrays.l2Templates[0].credentialBindings = {};
|
||||||
|
withMalformedNestedArrays.l2Templates[0].steps = {};
|
||||||
|
assert.doesNotThrow(() => validateProviderPackage(withMalformedNestedArrays));
|
||||||
|
assert.equal(validateProviderPackage(withMalformedNestedArrays).ok, false);
|
||||||
|
|
||||||
|
const withCredentialInStaticQuery = structuredClone(geliosProviderPackageV1);
|
||||||
|
withCredentialInStaticQuery.authModes[0].transport = { placement: "query", name: "token" };
|
||||||
|
withCredentialInStaticQuery.capabilities[0].request.query.token = "plain-provider-key";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withCredentialInStaticQuery).errors.includes("capabilities[0].request.query.token_credential_parameter_not_allowed"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withCredentialInStaticQuery).errors.includes("capability.gelios.units.current.read.request.query_must_not_embed_credential_parameter"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withCredentialInBaseUrl = structuredClone(geliosProviderPackageV1);
|
||||||
|
withCredentialInBaseUrl.capabilities[0].request.baseUrl = "https://user:pass@api.geliospro.com/?token=forbidden";
|
||||||
|
assert.equal(validateProviderPackage(withCredentialInBaseUrl).ok, false);
|
||||||
|
|
||||||
|
for (const unsafePath of ["/ok\nX-Evil: yes", "/back\\slash", "/query?x=1"]) {
|
||||||
|
const withUnsafeRequestPath = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnsafeRequestPath.capabilities[0].request.path = unsafePath;
|
||||||
|
assert.equal(validateProviderPackage(withUnsafeRequestPath).ok, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const unsafeCollectionPath of ["__proto__.polluted", "data.items[0]", "constructor.prototype"]) {
|
||||||
|
const withUnsafeCollectionPath = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnsafeCollectionPath.capabilities[0].request.response.collectionPaths = [unsafeCollectionPath];
|
||||||
|
assert.equal(validateProviderPackage(withUnsafeCollectionPath).ok, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withUnsafeMappingPath = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnsafeMappingPath.mappingContracts[0].fact.attributes.display_name.paths = ["__proto__.polluted"];
|
||||||
|
assert.equal(validateProviderPackage(withUnsafeMappingPath).ok, false);
|
||||||
|
|
||||||
|
const withUnsafeCollectionCapability = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnsafeCollectionCapability.capabilities[0].classification = "write";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withUnsafeCollectionCapability).errors.includes("collectionProfile.gelios.positions.current.realtime.v1.capabilityIds_must_reference_implemented_safe_read"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withUnexecutedSecondCapability = structuredClone(geliosProviderPackageV1);
|
||||||
|
const secondCapability = structuredClone(withUnexecutedSecondCapability.capabilities[0]);
|
||||||
|
secondCapability.id = "gelios.units.metadata.read";
|
||||||
|
withUnexecutedSecondCapability.capabilities.push(secondCapability);
|
||||||
|
withUnexecutedSecondCapability.manifest.capabilityIds.push(secondCapability.id);
|
||||||
|
for (const profile of withUnexecutedSecondCapability.collectionProfiles) {
|
||||||
|
profile.capabilityIds.push(secondCapability.id);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withUnexecutedSecondCapability).errors.includes(
|
||||||
|
"collectionProfile.gelios.positions.current.realtime.v1.capabilityIds_must_select_one_v1_read",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withReorderedSteps = structuredClone(geliosProviderPackageV1);
|
||||||
|
[withReorderedSteps.l2Templates[0].steps[0], withReorderedSteps.l2Templates[0].steps[1]] = [
|
||||||
|
withReorderedSteps.l2Templates[0].steps[1],
|
||||||
|
withReorderedSteps.l2Templates[0].steps[0],
|
||||||
|
];
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withReorderedSteps).errors.includes("l2Templates[0].steps_sequence_invalid"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withUnknownConnectionParameters = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnknownConnectionParameters.l2Templates[0].connectionParameters = ["foo"];
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withUnknownConnectionParameters).errors.includes(
|
||||||
|
"l2Templates[0].connectionParameters_must_match_v1_contract",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withCredentialAuthMismatch = structuredClone(geliosProviderPackageV1);
|
||||||
|
const secondAuthMode = structuredClone(withCredentialAuthMismatch.authModes[0]);
|
||||||
|
secondAuthMode.id = "gelios.rest-bearer.alternate.v1";
|
||||||
|
withCredentialAuthMismatch.authModes.push(secondAuthMode);
|
||||||
|
withCredentialAuthMismatch.manifest.authModeIds.push(secondAuthMode.id);
|
||||||
|
withCredentialAuthMismatch.l2Templates[0].credentialBindings[0].authModeId = secondAuthMode.id;
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withCredentialAuthMismatch).errors.includes(
|
||||||
|
"l2Template.gelios.positions.current.l2.v1.provider_credential_auth_mode_mismatch",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withCallerManagedPublisher = structuredClone(geliosProviderPackageV1);
|
||||||
|
withCallerManagedPublisher.l2Templates[0].credentialBindings[1].management = "connection_input";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withCallerManagedPublisher).errors.includes(
|
||||||
|
"l2Templates[0].credentialBindings[1].publisher_binding_shape_invalid",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withUnknownTokenArtifact = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnknownTokenArtifact.authModes[0].tokenLifecycle.artifacts.push("writer");
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withUnknownTokenArtifact).errors.includes(
|
||||||
|
"authModes[0].tokenLifecycle.artifacts_invalid",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withRefreshUsedForRequest = structuredClone(geliosProviderPackageV1);
|
||||||
|
withRefreshUsedForRequest.authModes[0].tokenLifecycle.requestArtifact = "refresh";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withRefreshUsedForRequest).errors.includes(
|
||||||
|
"authModes[0].tokenLifecycle.requestArtifact_must_be_access",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withUnmanagedRefreshArtifact = structuredClone(geliosProviderPackageV1);
|
||||||
|
withUnmanagedRefreshArtifact.authModes[0].tokenLifecycle.refreshMode = "not_applicable";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withUnmanagedRefreshArtifact).errors.includes(
|
||||||
|
"authModes[0].tokenLifecycle.refreshMode_required_for_refresh_artifact",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const history of [{
|
||||||
|
mode: "none",
|
||||||
|
intervalMs: 60000,
|
||||||
|
strategy: "latest-per-entity-per-bucket",
|
||||||
|
retentionDays: 90,
|
||||||
|
}, {
|
||||||
|
mode: "sampled",
|
||||||
|
intervalMs: 24 * 60 * 60 * 1000 + 1,
|
||||||
|
strategy: "latest-per-entity-per-bucket",
|
||||||
|
retentionDays: 90,
|
||||||
|
}, {
|
||||||
|
mode: "all",
|
||||||
|
retentionDays: 3651,
|
||||||
|
}]) {
|
||||||
|
const withInvalidHistory = structuredClone(geliosProviderPackageV1);
|
||||||
|
withInvalidHistory.dataProducts[0].history = history;
|
||||||
|
assert.equal(validateProviderPackage(withInvalidHistory).ok, false);
|
||||||
|
assert.throws(() => normalizeDataProductDefinition(withInvalidHistory.dataProducts[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const withBrokenArtifactChain = structuredClone(geliosProviderPackageV1);
|
||||||
|
withBrokenArtifactChain.l2Templates[0].steps[4].dataProductId = "unrelated.product.v1";
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withBrokenArtifactChain).errors.includes("collectionProfile.gelios.positions.current.realtime.v1.template_data_product_mismatch"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withSecretNamedDerivationParameter = structuredClone(geliosProviderPackageV1);
|
||||||
|
withSecretNamedDerivationParameter.mappingContracts[0].derivations.position_valid.parameters = {
|
||||||
|
accessToken: "opaque-provider-value",
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withSecretNamedDerivationParameter).errors.includes(
|
||||||
|
"mappingContracts[0].derivations.position_valid.parameters.accessToken_secret_field_not_allowed",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withObjectConstant = structuredClone(geliosProviderPackageV1);
|
||||||
|
withObjectConstant.mappingContracts[0].fact.attributes.object_kind.constant = {
|
||||||
|
accessToken: "opaque-provider-value",
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
validateProviderPackage(withObjectConstant).errors.includes(
|
||||||
|
"mappingContracts[0].fact.attributes.object_kind.constant_must_be_scalar_or_scalar_array",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(() => instantiateL2Connection(geliosProviderPackageV1, {
|
||||||
|
tenantId: "tenant-alpha",
|
||||||
|
connectionId: "gelios-account-alpha",
|
||||||
|
collectionProfileId: realtimeProfileId,
|
||||||
|
providerCredentialRef: "Bearer plaintext-must-not-cross-boundary",
|
||||||
|
}), /l2_connection_invalid/);
|
||||||
|
|
||||||
|
assert.throws(() => instantiateL2Connection(geliosProviderPackageV1, {
|
||||||
|
tenantId: "tenant-alpha",
|
||||||
|
connectionId: "gelios-account-alpha",
|
||||||
|
collectionProfileId: realtimeProfileId,
|
||||||
|
providerCredentialRef: "ndc-credref:provider-alpha-0001",
|
||||||
|
writerCredentialRef: "ndc-credref:caller-writer-forbidden",
|
||||||
|
}), /options\.writerCredentialRef_not_allowed/);
|
||||||
|
|
||||||
|
const serializedPackage = JSON.stringify(geliosProviderPackageV1);
|
||||||
|
assert.equal(serializedPackage.includes('"prefix":"unit-"'), false);
|
||||||
|
assert.equal(serializedPackage.includes("unitIds"), false);
|
||||||
|
assert.equal(serializedPackage.includes("tenant-alpha"), false);
|
||||||
|
assert.equal(serializedPackage.includes("gelios-gateway"), false);
|
||||||
|
assert.equal(serializedPackage.includes("writer_credential_ref"), false);
|
||||||
|
assert.equal(serializedPackage.includes("writerCapabilityByReferenceOnly"), false);
|
||||||
|
assert.equal(serializedPackage.includes("publisherBindingControlPlaneManaged\":true"), true);
|
||||||
|
assert.equal(serializedPackage.includes("providerSpecificServiceForbidden\":true"), true);
|
||||||
|
assert.deepEqual(
|
||||||
|
geliosProviderPackageV1.l2Templates[0].steps.map((step) => step.kind),
|
||||||
|
[
|
||||||
|
"collection_trigger",
|
||||||
|
"provider_request",
|
||||||
|
"extract_items",
|
||||||
|
"semantic_mapping",
|
||||||
|
"data_product_publish",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
geliosProviderPackageV1.l2Templates[0].steps.at(-1).nodeType,
|
||||||
|
"n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("external-provider-contract provider-package: ok");
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"aliases": [
|
"aliases": [
|
||||||
{ "alias": "gelios", "canonicalId": "gelios.integration" },
|
{ "alias": "gelios", "canonicalId": "gelios.integration" },
|
||||||
{ "alias": "gelius", "canonicalId": "gelios.integration" },
|
{ "alias": "gelius", "canonicalId": "gelios.integration" },
|
||||||
{ "alias": "helius", "canonicalId": "gelios.integration" },
|
{ "alias": "helius", "canonicalId": "gelios.integration" },
|
||||||
{ "alias": "hel i os", "canonicalId": "gelios.integration" },
|
{ "alias": "hel i os", "canonicalId": "gelios.integration" },
|
||||||
{ "alias": "гелиос", "canonicalId": "gelios.integration" },
|
{ "alias": "гелиос", "canonicalId": "gelios.integration" },
|
||||||
{ "alias": "трайк robot2b", "canonicalId": "gelios.unit" },
|
{ "alias": "отслеживаемый объект гелиос", "canonicalId": "gelios.unit" },
|
||||||
{ "alias": "юнит гелиос", "canonicalId": "gelios.unit" },
|
{ "alias": "юнит гелиос", "canonicalId": "gelios.unit" },
|
||||||
{ "alias": "группа юнитов гелиос", "canonicalId": "gelios.unit_group" },
|
{ "alias": "группа юнитов гелиос", "canonicalId": "gelios.unit_group" },
|
||||||
{ "alias": "последняя телеметрия гелиос", "canonicalId": "gelios.telemetry_snapshot" },
|
{ "alias": "последняя телеметрия гелиос", "canonicalId": "gelios.telemetry_snapshot" },
|
||||||
{ "alias": "сырая телеметрия гелиос", "canonicalId": "gelios.raw_telemetry_message" },
|
{ "alias": "сырая телеметрия гелиос", "canonicalId": "gelios.raw_telemetry_message" },
|
||||||
{ "alias": "позиция трайка", "canonicalId": "gelios.position_fix" },
|
{ "alias": "позиция объекта гелиос", "canonicalId": "gelios.position_fix" },
|
||||||
{ "alias": "статус трайка", "canonicalId": "gelios.operational_status" },
|
{ "alias": "статус объекта гелиос", "canonicalId": "gelios.operational_status" },
|
||||||
{ "alias": "датчик гелиос", "canonicalId": "gelios.sensor_definition" },
|
{ "alias": "датчик гелиос", "canonicalId": "gelios.sensor_definition" },
|
||||||
{ "alias": "показание датчика", "canonicalId": "gelios.sensor_reading" },
|
{ "alias": "показание датчика", "canonicalId": "gelios.sensor_reading" },
|
||||||
{ "alias": "калибровка датчика", "canonicalId": "gelios.sensor_conversion" },
|
{ "alias": "калибровка датчика", "canonicalId": "gelios.sensor_conversion" },
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
{ "alias": "шаблон команды гелиос", "canonicalId": "gelios.command_template" },
|
{ "alias": "шаблон команды гелиос", "canonicalId": "gelios.command_template" },
|
||||||
{ "alias": "диспетчеризация команды гелиос", "canonicalId": "gelios.command_dispatch" },
|
{ "alias": "диспетчеризация команды гелиос", "canonicalId": "gelios.command_dispatch" },
|
||||||
{ "alias": "аудит команды гелиос", "canonicalId": "gelios.command_audit" },
|
{ "alias": "аудит команды гелиос", "canonicalId": "gelios.command_audit" },
|
||||||
{ "alias": "контур robot2b", "canonicalId": "gelios.access_scope" },
|
{ "alias": "область доступа credential гелиос", "canonicalId": "gelios.access_scope" },
|
||||||
{ "alias": "курсор загрузки гелиос", "canonicalId": "gelios.ingestion_cursor" }
|
{ "alias": "курсор загрузки гелиос", "canonicalId": "gelios.ingestion_cursor" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"entities": [
|
"entities": [
|
||||||
{ "id": "gelios.integration", "name": "Gelios Integration", "surface": "platform", "status": ["product-required", "source-evidenced"], "authority": "Gelios Gateway", "summary": "Server-side provider integration boundary for Gelios REST OAuth and legacy compatibility. It owns no UI, map renderer or workflow secret." },
|
{ "id": "gelios.integration", "name": "Gelios Integration", "surface": "platform", "status": ["product-required", "source-evidenced"], "authority": "NDC L2 connection", "summary": "One tenant-scoped Gelios provider connection backed by one opaque NDC L2 credential reference. It owns no UI, renderer, secret value or provider-specific Platform service." },
|
||||||
{ "id": "gelios.access_scope", "name": "Gelios Access Scope", "surface": "platform", "status": ["product-required", "source-evidenced"], "authority": "Robot2B owner + Gelios Gateway", "summary": "Approved collection boundary expressed as an allowlist or owner rule for units, groups and allowed capabilities; distinct from broad provider-account visibility." },
|
{ "id": "gelios.access_scope", "name": "Gelios Access Scope", "surface": "platform", "status": ["product-required", "source-evidenced"], "authority": "Gelios credential visibility + NDC L2 connection policy", "summary": "Dynamic collection boundary: selected safe-read capabilities include every entity visible to the bound credential on each run. Entity allowlists and provider-group business filters are not part of collection scope." },
|
||||||
{ "id": "gelios.unit", "name": "Gelios Unit", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Tracked operational object, including a Robot2B trike when its stable source unit ID is inside the approved scope." },
|
{ "id": "gelios.unit", "name": "Gelios Unit", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Tracked operational object with a stable provider source ID. Every unit returned for the bound credential is eligible for canonical mapping." },
|
||||||
{ "id": "gelios.unit_group", "name": "Gelios Unit Group", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider-managed grouping of units. It is evidence for navigation and access, not an automatic definition of the Robot2B business scope." },
|
{ "id": "gelios.unit_group", "name": "Gelios Unit Group", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider-managed grouping of units used as domain data and optional consumer metadata, never as an implicit collection filter." },
|
||||||
{ "id": "gelios.tracker_device", "name": "Tracker Device", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Hardware tracker associated with a unit, carrying device type/manufacturer and restricted hardware identity fields." },
|
{ "id": "gelios.tracker_device", "name": "Tracker Device", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Hardware tracker associated with a unit, carrying device type/manufacturer and restricted hardware identity fields." },
|
||||||
{ "id": "gelios.telemetry_snapshot", "name": "Telemetry Snapshot", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Normalized current telemetry state for one unit at observed and received time; derived from the Gelios pure last message contract." },
|
{ "id": "gelios.telemetry_snapshot", "name": "Telemetry Snapshot", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "NDC L2 semantic mapping", "summary": "Normalized current telemetry state for one unit at observed and received time; derived from the versioned Gelios last-message mapping contract." },
|
||||||
{ "id": "gelios.raw_telemetry_message", "name": "Raw Telemetry Message", "surface": "telemetry", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider hardware message preserved only behind restricted raw-data policy; it is not the default Studio or analytics contract." },
|
{ "id": "gelios.raw_telemetry_message", "name": "Raw Telemetry Message", "surface": "telemetry", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider hardware message preserved only behind restricted raw-data policy; it is not the default Studio or analytics contract." },
|
||||||
{ "id": "gelios.position_fix", "name": "Position Fix", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Time-qualified geographic position with latitude, longitude, height, course, speed, satellite and quality attributes for one unit." },
|
{ "id": "gelios.position_fix", "name": "Position Fix", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "NDC L2 semantic mapping", "summary": "Time-qualified geographic position with latitude, longitude, height, course, speed, satellite and quality attributes for one unit." },
|
||||||
{ "id": "gelios.telemetry_parameter", "name": "Telemetry Parameter", "surface": "telemetry", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Name/value parameter received with a message. Parameters are dynamic and require a namespaced registry and whitelist before product exposure." },
|
{ "id": "gelios.telemetry_parameter", "name": "Telemetry Parameter", "surface": "telemetry", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Name/value parameter received with a message. Parameters are dynamic and require a namespaced registry and whitelist before product exposure." },
|
||||||
{ "id": "gelios.operational_status", "name": "Operational Status", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Derived unit state such as online, offline, moving, parked, no-position or low-GPS; distinct from raw provider message fields." },
|
{ "id": "gelios.operational_status", "name": "Operational Status", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "NDC L2 semantic mapping", "summary": "Derived unit state such as active, stale, moving, parked, no-position or low-GPS; distinct from raw provider message fields." },
|
||||||
{ "id": "gelios.sensor_definition", "name": "Sensor Definition", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Declared sensor on a unit with message parameter, type, unit of measure, visibility and optional fuel semantics." },
|
{ "id": "gelios.sensor_definition", "name": "Sensor Definition", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Declared sensor on a unit with message parameter, type, unit of measure, visibility and optional fuel semantics." },
|
||||||
{ "id": "gelios.sensor_reading", "name": "Sensor Reading", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Time-qualified normalized reading of a defined sensor, retaining both numeric value and human-readable representation when permitted." },
|
{ "id": "gelios.sensor_reading", "name": "Sensor Reading", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "NDC L2 semantic mapping", "summary": "Time-qualified normalized reading of a defined sensor, retaining both numeric value and human-readable representation only when permitted by a versioned field policy." },
|
||||||
{ "id": "gelios.sensor_conversion", "name": "Sensor Conversion", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Calibration/conversion rule or row for translating raw sensor values into operational measures." },
|
{ "id": "gelios.sensor_conversion", "name": "Sensor Conversion", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Calibration/conversion rule or row for translating raw sensor values into operational measures." },
|
||||||
{ "id": "gelios.fuel_profile", "name": "Fuel Profile", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Fuel and consumption configuration associated with a unit or fuel sensor; use only after sensor semantics are approved." },
|
{ "id": "gelios.fuel_profile", "name": "Fuel Profile", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Fuel and consumption configuration associated with a unit or fuel sensor; use only after sensor semantics are approved." },
|
||||||
{ "id": "gelios.maintenance_plan", "name": "Maintenance Plan", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Planned maintenance configuration for a unit, separate from historical maintenance events." },
|
{ "id": "gelios.maintenance_plan", "name": "Maintenance Plan", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Planned maintenance configuration for a unit, separate from historical maintenance events." },
|
||||||
|
|
@ -22,13 +22,13 @@
|
||||||
{ "id": "gelios.geozone_group", "name": "Gelios Geozone Group", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider grouping of geozones." },
|
{ "id": "gelios.geozone_group", "name": "Gelios Geozone Group", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider grouping of geozones." },
|
||||||
{ "id": "gelios.geopoint", "name": "Gelios Geopoint", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Named provider spatial point; it must remain distinct from a live unit position." },
|
{ "id": "gelios.geopoint", "name": "Gelios Geopoint", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Named provider spatial point; it must remain distinct from a live unit position." },
|
||||||
{ "id": "gelios.report_template", "name": "Gelios Report Template", "surface": "analytics", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Reusable provider report definition available to the account or scope." },
|
{ "id": "gelios.report_template", "name": "Gelios Report Template", "surface": "analytics", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Reusable provider report definition available to the account or scope." },
|
||||||
{ "id": "gelios.report_result", "name": "Gelios Report Result", "surface": "analytics", "status": ["source-evidenced"], "authority": "Gelios Pro + Gelios Gateway", "summary": "Bounded result, table, graphic or map output produced from an approved report request; not a default realtime stream." },
|
{ "id": "gelios.report_result", "name": "Gelios Report Result", "surface": "analytics", "status": ["source-evidenced"], "authority": "Gelios Pro + NDC L2 semantic mapping", "summary": "Bounded result, table, graphic or map output produced from an approved report request; not a default realtime stream." },
|
||||||
{ "id": "gelios.collection_run", "name": "Gelios Collection Run", "surface": "platform", "status": ["product-required"], "authority": "Gelios Gateway", "summary": "Auditable read-only collection attempt with scope, endpoint family, volume controls and outcome; it contains no token or raw payload." },
|
{ "id": "gelios.collection_run", "name": "Gelios Collection Run", "surface": "platform", "status": ["product-required"], "authority": "NDC L2 workflow", "summary": "Auditable safe-read collection attempt with credential-visible entity scope, endpoint family, volume controls and outcome; it contains no credential value or raw payload." },
|
||||||
{ "id": "gelios.ingestion_cursor", "name": "Gelios Ingestion Cursor", "surface": "platform", "status": ["product-required"], "authority": "Gelios Gateway + telemetry storage", "summary": "Checkpoint and watermark controlling resumable historical or incremental ingestion for an approved unit and data family." },
|
{ "id": "gelios.ingestion_cursor", "name": "Gelios Ingestion Cursor", "surface": "platform", "status": ["product-required"], "authority": "NDC L2 workflow state + External Data Plane", "summary": "Checkpoint and watermark controlling resumable historical or incremental ingestion for a connection and data family." },
|
||||||
{ "id": "gelios.command_template", "name": "Gelios Command Template", "surface": "command", "status": ["source-evidenced", "product-required"], "authority": "Gelios Pro", "summary": "Read-only catalogued command template assigned to a unit. Template content is red-domain configuration, not a telemetry field." },
|
{ "id": "gelios.command_template", "name": "Gelios Command Template", "surface": "command", "status": ["source-evidenced", "product-required"], "authority": "Gelios Pro", "summary": "Read-only catalogued command template assigned to a unit. Template content is red-domain configuration, not a telemetry field." },
|
||||||
{ "id": "gelios.command_group", "name": "Gelios Command Group", "surface": "command", "status": ["source-evidenced", "product-required"], "authority": "Gelios Pro", "summary": "Provider grouping of command templates, managed separately from operational unit groups." },
|
{ "id": "gelios.command_group", "name": "Gelios Command Group", "surface": "command", "status": ["source-evidenced", "product-required"], "authority": "Gelios Pro", "summary": "Provider grouping of command templates, managed separately from operational unit groups." },
|
||||||
{ "id": "gelios.command_dispatch", "name": "Gelios Command Dispatch", "surface": "command", "status": ["product-required"], "authority": "Future Command Gateway", "summary": "Explicit, user-confirmed request to send a command to one approved unit or approved group. It must never be created by background ingestion or map rendering." },
|
{ "id": "gelios.command_dispatch", "name": "Gelios Command Dispatch", "surface": "command", "status": ["future-concept"], "authority": "Unimplemented NDC red-domain command boundary", "summary": "Future explicit, user-confirmed request to send a command to one approved unit or approved group. It must never be created by background ingestion or map rendering." },
|
||||||
{ "id": "gelios.command_delivery", "name": "Gelios Command Delivery", "surface": "command", "status": ["product-required", "source-evidenced"], "authority": "Gelios Pro + Future Command Gateway", "summary": "Provider delivery/task status associated with a command dispatch." },
|
{ "id": "gelios.command_delivery", "name": "Gelios Command Delivery", "surface": "command", "status": ["future-concept", "source-evidenced"], "authority": "Gelios Pro; future NDC red-domain reconciliation", "summary": "Provider delivery/task status that may be reconciled with a future governed command dispatch; no command transport exists in the current package." },
|
||||||
{ "id": "gelios.command_audit", "name": "Gelios Command Audit", "surface": "command", "status": ["product-required"], "authority": "Future Command Gateway", "summary": "Immutable governance record of an explicit command decision, confirmation, target, outcome and actor; separate from provider command history." }
|
{ "id": "gelios.command_audit", "name": "Gelios Command Audit", "surface": "command", "status": ["future-concept"], "authority": "Unimplemented NDC red-domain command boundary", "summary": "Future immutable governance record of an explicit command decision, confirmation, target, outcome and actor; separate from provider command history." }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"sourceRoots": [
|
"sourceRoots": [
|
||||||
{
|
{
|
||||||
"surface": "gelios-rest",
|
"surface": "gelios-rest",
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
"mode": "OpenAPI inspection and safe read-only runtime audit; no write routes executed"
|
"mode": "OpenAPI inspection and safe read-only runtime audit; no write routes executed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"surface": "engine",
|
"surface": "ndc-l2-legacy-evidence",
|
||||||
"path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_ENGINE_INFRA/nodedc-source",
|
"path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_ENGINE_INFRA/nodedc-source",
|
||||||
"mode": "Read-only MMAP workflow, Gelios node and historical snapshot donor inspection"
|
"mode": "Read-only historical NDC L2 donor, Gelios mapping and synthetic-safe field-shape inspection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"surface": "module-studio",
|
"surface": "module-studio",
|
||||||
|
|
@ -41,9 +41,9 @@
|
||||||
"baselineDocs": ["docs/GELIOS_DOMAIN_ONTOLOGY.md"],
|
"baselineDocs": ["docs/GELIOS_DOMAIN_ONTOLOGY.md"],
|
||||||
"restrictions": [
|
"restrictions": [
|
||||||
"Do not copy access/refresh tokens, passwords, hardware decrypt keys or runtime snapshots into Ontology Core.",
|
"Do not copy access/refresh tokens, passwords, hardware decrypt keys or runtime snapshots into Ontology Core.",
|
||||||
"Do not make the current Engine Gelios node, Cesium entities or provider group names canonical domain identities.",
|
"Do not make a current NDC L2 workflow node, renderer entities or provider group names canonical domain identities.",
|
||||||
"Do not invoke command send, create, update, delete or purge routes as ontology evidence.",
|
"Do not invoke command send, create, update, delete or purge routes as ontology evidence.",
|
||||||
"Do not treat the 107 currently visible provider-account units or the 95 legacy snapshot units as the final Robot2B scope without owner approval.",
|
"Robot2B pilot counts (107 credential-visible units and 95 legacy snapshot units) are historical evidence only; canonical collection scope is dynamically all entities visible to the bound credential.",
|
||||||
"Do not bulk-download history, media or full geozone geometry before collection policy and storage architecture are approved."
|
"Do not bulk-download history, media or full geozone geometry before collection policy and storage architecture are approved."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"rules": [
|
"rules": [
|
||||||
{
|
{
|
||||||
"id": "guardrail.gelios.ontology_not_runtime_store",
|
"id": "guardrail.gelios.ontology_not_runtime_store",
|
||||||
|
|
@ -8,10 +8,16 @@
|
||||||
"summary": "Ontology Core describes Gelios meanings and contracts only. It must not contain access/refresh tokens, raw messages, live positions, provider command text, hardware decrypt keys or bulk geometry.",
|
"summary": "Ontology Core describes Gelios meanings and contracts only. It must not contain access/refresh tokens, raw messages, live positions, provider command text, hardware decrypt keys or bulk geometry.",
|
||||||
"entityIds": ["gelios.integration", "gelios.telemetry_snapshot", "gelios.raw_telemetry_message", "gelios.position_fix", "gelios.command_template", "gelios.geozone"]
|
"entityIds": ["gelios.integration", "gelios.telemetry_snapshot", "gelios.raw_telemetry_message", "gelios.position_fix", "gelios.command_template", "gelios.geozone"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "guardrail.gelios.provider_auth_pair_not_capability_scope",
|
||||||
|
"severity": "error",
|
||||||
|
"summary": "Gelios provider authentication consists of access and refresh tokens only. Credential labels and safe-read classifications are workflow policy, not provider token scope; internal External Data Plane capabilities are not Gelios tokens.",
|
||||||
|
"entityIds": ["gelios.integration", "gelios.access_scope", "gelios.collection_run"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "guardrail.gelios.scope_precedes_collection",
|
"id": "guardrail.gelios.scope_precedes_collection",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"summary": "A Gelios collection run must resolve an approved Robot2B access scope before addressing units. Provider-account visibility and provider group membership are not sufficient product scope on their own.",
|
"summary": "A Gelios safe-read collection run must process every valid entity returned for the bound credential and selected capability. Static unit allowlists and provider-group business filters are forbidden at collection boundary; consumer visibility is handled downstream.",
|
||||||
"entityIds": ["gelios.integration", "gelios.access_scope", "gelios.collection_run", "gelios.unit", "gelios.unit_group"]
|
"entityIds": ["gelios.integration", "gelios.access_scope", "gelios.collection_run", "gelios.unit", "gelios.unit_group"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -41,7 +47,7 @@
|
||||||
{
|
{
|
||||||
"id": "guardrail.gelios.history_requires_cursor",
|
"id": "guardrail.gelios.history_requires_cursor",
|
||||||
"severity": "warning",
|
"severity": "warning",
|
||||||
"summary": "Any future history ingestion must be bounded by unit scope, time window, field whitelist and durable cursor. It must not turn a historical API family into an uncontrolled account dump.",
|
"summary": "Any future history ingestion must be bounded by connection, credential-visible entity scope, time window, field policy and durable cursor. It must not turn a historical API family into an uncontrolled account dump.",
|
||||||
"entityIds": ["gelios.collection_run", "gelios.ingestion_cursor", "gelios.raw_telemetry_message", "gelios.report_result"]
|
"entityIds": ["gelios.collection_run", "gelios.ingestion_cursor", "gelios.raw_telemetry_message", "gelios.report_result"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "gelios",
|
"id": "gelios",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"status": "product-required/source-evidenced",
|
"status": "product-required/source-evidenced",
|
||||||
"summary": "Gelios Pro telemetry, fleet, spatial, reporting and command-domain ontology for the Robot2B client context. The package describes canonical meanings and contracts; it does not store credentials or runtime telemetry."
|
"summary": "Provider-neutral Gelios Pro telemetry, fleet, spatial, reporting and command-domain ontology. It describes canonical meanings and contracts; it does not store credentials, account scope or runtime telemetry."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"relations": [
|
"relations": [
|
||||||
{ "id": "gelios.integration.is_provider_connection", "from": ["gelios.integration"], "to": ["integration.connection"], "status": "product-required", "summary": "A Gelios integration is a tenant-scoped instance of the common external provider connection contract." },
|
{ "id": "gelios.integration.is_provider_connection", "from": ["gelios.integration"], "to": ["integration.connection"], "status": "product-required", "summary": "A Gelios integration is a tenant-scoped instance of the common external provider connection contract." },
|
||||||
{ "id": "gelios.access_scope.specializes_integration_scope", "from": ["gelios.access_scope"], "to": ["integration.access_scope"], "status": "product-required", "summary": "Robot2B/Gelios object approval specializes the common provider access-scope contract." },
|
{ "id": "gelios.access_scope.specializes_integration_scope", "from": ["gelios.access_scope"], "to": ["integration.access_scope"], "status": "product-required", "summary": "Gelios credential-visible capability scope specializes the common provider access-scope contract." },
|
||||||
{ "id": "gelios.collection_run.is_integration_collection_run", "from": ["gelios.collection_run"], "to": ["integration.collection_run"], "status": "product-required", "summary": "Gelios collection runs inherit the bounded collection and audit lifecycle of external integrations." },
|
{ "id": "gelios.collection_run.is_integration_collection_run", "from": ["gelios.collection_run"], "to": ["integration.collection_run"], "status": "product-required", "summary": "Gelios collection runs inherit the bounded collection and audit lifecycle of external integrations." },
|
||||||
{ "id": "gelios.raw_message.is_integration_raw_envelope", "from": ["gelios.raw_telemetry_message"], "to": ["integration.raw_envelope"], "status": "product-required", "summary": "A Gelios raw message is a restricted provider envelope, not a UI read model." },
|
{ "id": "gelios.raw_message.is_integration_raw_envelope", "from": ["gelios.raw_telemetry_message"], "to": ["integration.raw_envelope"], "status": "product-required", "summary": "A Gelios raw message is a restricted provider envelope, not a UI read model." },
|
||||||
{ "id": "gelios.unit.is_integration_canonical_subject", "from": ["gelios.unit"], "to": ["integration.canonical_subject"], "status": "product-required", "summary": "A Gelios unit is a stable canonical subject produced by the provider adapter." },
|
{ "id": "gelios.unit.is_integration_canonical_subject", "from": ["gelios.unit"], "to": ["integration.canonical_subject"], "status": "product-required", "summary": "A Gelios unit is a stable canonical subject produced by the provider adapter." },
|
||||||
{ "id": "gelios.command_template.is_integration_command_capability", "from": ["gelios.command_template"], "to": ["integration.command_capability"], "status": "product-required", "summary": "Gelios command templates are catalogued provider command capabilities only." },
|
{ "id": "gelios.command_template.is_integration_command_capability", "from": ["gelios.command_template"], "to": ["integration.command_capability"], "status": "product-required", "summary": "Gelios command templates are catalogued provider command capabilities only." },
|
||||||
{ "id": "gelios.command_dispatch.is_integration_command_intent", "from": ["gelios.command_dispatch"], "to": ["integration.command_intent"], "status": "future-concept", "summary": "Any future Gelios command dispatch must become an explicitly governed generic command intent." },
|
{ "id": "gelios.command_dispatch.is_integration_command_intent", "from": ["gelios.command_dispatch"], "to": ["integration.command_intent"], "status": "future-concept", "summary": "Any future Gelios command dispatch must become an explicitly governed generic command intent." },
|
||||||
{ "id": "gelios.command_audit.is_integration_command_audit", "from": ["gelios.command_audit"], "to": ["integration.command_audit"], "status": "future-concept", "summary": "Gelios command audit specializes the generic red-domain audit contract." },
|
{ "id": "gelios.command_audit.is_integration_command_audit", "from": ["gelios.command_audit"], "to": ["integration.command_audit"], "status": "future-concept", "summary": "Gelios command audit specializes the generic red-domain audit contract." },
|
||||||
{ "id": "gelios.integration.enforces_scope", "from": ["gelios.integration"], "to": ["gelios.access_scope"], "status": "product-required", "summary": "The Gateway applies an approved Robot2B scope before reading or publishing data." },
|
{ "id": "gelios.integration.enforces_scope", "from": ["gelios.integration"], "to": ["gelios.access_scope"], "status": "product-required", "summary": "NDC L2 resolves selected safe-read capabilities and current provider visibility from the bound credential on every collection run." },
|
||||||
{ "id": "gelios.access_scope.includes_unit", "from": ["gelios.access_scope"], "to": ["gelios.unit", "gelios.unit_group"], "status": "product-required", "summary": "An approved scope can include explicit units and approved group criteria, but group membership alone is not authoritative." },
|
{ "id": "gelios.access_scope.includes_unit", "from": ["gelios.access_scope"], "to": ["gelios.unit", "gelios.unit_group"], "status": "product-required", "summary": "Scope dynamically includes every unit and group record returned by the selected capability; it contains no static entity allowlist." },
|
||||||
{ "id": "gelios.unit.belongs_to_group", "from": ["gelios.unit"], "to": ["gelios.unit_group"], "status": "source-evidenced", "summary": "A provider unit can belong to one or more provider unit groups." },
|
{ "id": "gelios.unit.belongs_to_group", "from": ["gelios.unit"], "to": ["gelios.unit_group"], "status": "source-evidenced", "summary": "A provider unit can belong to one or more provider unit groups." },
|
||||||
{ "id": "gelios.unit.uses_tracker_device", "from": ["gelios.unit"], "to": ["gelios.tracker_device"], "status": "source-evidenced", "summary": "A tracked unit is associated with a hardware tracker device." },
|
{ "id": "gelios.unit.uses_tracker_device", "from": ["gelios.unit"], "to": ["gelios.tracker_device"], "status": "source-evidenced", "summary": "A tracked unit is associated with a hardware tracker device." },
|
||||||
{ "id": "gelios.unit.has_telemetry_snapshot", "from": ["gelios.unit"], "to": ["gelios.telemetry_snapshot"], "status": "product-required", "summary": "The Gateway maintains normalized current telemetry for an approved unit." },
|
{ "id": "gelios.unit.has_telemetry_snapshot", "from": ["gelios.unit"], "to": ["gelios.telemetry_snapshot"], "status": "product-required", "summary": "The versioned NDC L2 mapping produces normalized current telemetry for every valid returned unit." },
|
||||||
{ "id": "gelios.raw_message.normalizes_to_snapshot", "from": ["gelios.raw_telemetry_message"], "to": ["gelios.telemetry_snapshot"], "status": "product-required", "summary": "Restricted raw provider telemetry can be normalized into the stable snapshot contract." },
|
{ "id": "gelios.raw_message.normalizes_to_snapshot", "from": ["gelios.raw_telemetry_message"], "to": ["gelios.telemetry_snapshot"], "status": "product-required", "summary": "Restricted raw provider telemetry can be normalized into the stable snapshot contract." },
|
||||||
{ "id": "gelios.telemetry_snapshot.has_position_fix", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.position_fix"], "status": "product-required", "summary": "A current telemetry snapshot can carry one time-qualified position fix." },
|
{ "id": "gelios.telemetry_snapshot.has_position_fix", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.position_fix"], "status": "product-required", "summary": "A current telemetry snapshot can carry one time-qualified position fix." },
|
||||||
{ "id": "gelios.telemetry_snapshot.has_operational_status", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.operational_status"], "status": "product-required", "summary": "Gateway derives normalized operational state from telemetry and data-quality policy." },
|
{ "id": "gelios.telemetry_snapshot.has_operational_status", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.operational_status"], "status": "product-required", "summary": "The semantic mapping derives normalized operational state from telemetry and data-quality policy." },
|
||||||
{ "id": "gelios.telemetry_snapshot.has_parameter", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.telemetry_parameter"], "status": "source-evidenced", "summary": "A provider snapshot can contain dynamic telemetry parameters." },
|
{ "id": "gelios.telemetry_snapshot.has_parameter", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.telemetry_parameter"], "status": "source-evidenced", "summary": "A provider snapshot can contain dynamic telemetry parameters." },
|
||||||
{ "id": "gelios.unit.has_sensor", "from": ["gelios.unit"], "to": ["gelios.sensor_definition"], "status": "source-evidenced", "summary": "A unit exposes zero or more sensor definitions." },
|
{ "id": "gelios.unit.has_sensor", "from": ["gelios.unit"], "to": ["gelios.sensor_definition"], "status": "source-evidenced", "summary": "A unit exposes zero or more sensor definitions." },
|
||||||
{ "id": "gelios.sensor_definition.has_conversion", "from": ["gelios.sensor_definition"], "to": ["gelios.sensor_conversion"], "status": "source-evidenced", "summary": "A sensor can define conversion/calibration rows." },
|
{ "id": "gelios.sensor_definition.has_conversion", "from": ["gelios.sensor_definition"], "to": ["gelios.sensor_conversion"], "status": "source-evidenced", "summary": "A sensor can define conversion/calibration rows." },
|
||||||
|
|
@ -29,14 +29,14 @@
|
||||||
{ "id": "gelios.geozone.belongs_to_group", "from": ["gelios.geozone"], "to": ["gelios.geozone_group"], "status": "source-evidenced", "summary": "A provider geozone can belong to a provider geozone group." },
|
{ "id": "gelios.geozone.belongs_to_group", "from": ["gelios.geozone"], "to": ["gelios.geozone_group"], "status": "source-evidenced", "summary": "A provider geozone can belong to a provider geozone group." },
|
||||||
{ "id": "gelios.report_result.uses_template", "from": ["gelios.report_result"], "to": ["gelios.report_template"], "status": "source-evidenced", "summary": "A report result is produced from an approved report template and bounded request." },
|
{ "id": "gelios.report_result.uses_template", "from": ["gelios.report_result"], "to": ["gelios.report_template"], "status": "source-evidenced", "summary": "A report result is produced from an approved report template and bounded request." },
|
||||||
{ "id": "gelios.collection_run.checkpoints_with_cursor", "from": ["gelios.collection_run"], "to": ["gelios.ingestion_cursor"], "status": "product-required", "summary": "Collection runs advance resumable ingestion only through a durable cursor." },
|
{ "id": "gelios.collection_run.checkpoints_with_cursor", "from": ["gelios.collection_run"], "to": ["gelios.ingestion_cursor"], "status": "product-required", "summary": "Collection runs advance resumable ingestion only through a durable cursor." },
|
||||||
{ "id": "gelios.collection_run.collects_unit", "from": ["gelios.collection_run"], "to": ["gelios.unit"], "status": "product-required", "summary": "A read-only collection run records the approved unit scope it addressed." },
|
{ "id": "gelios.collection_run.collects_unit", "from": ["gelios.collection_run"], "to": ["gelios.unit"], "status": "product-required", "summary": "A safe-read collection run records every valid credential-visible unit returned by the selected capability." },
|
||||||
{ "id": "gelios.unit.has_command_template", "from": ["gelios.unit"], "to": ["gelios.command_template"], "status": "source-evidenced", "summary": "Command templates are catalogued per unit through read-only routes." },
|
{ "id": "gelios.unit.has_command_template", "from": ["gelios.unit"], "to": ["gelios.command_template"], "status": "source-evidenced", "summary": "Command templates are catalogued per unit through read-only routes." },
|
||||||
{ "id": "gelios.command_template.belongs_to_group", "from": ["gelios.command_template"], "to": ["gelios.command_group"], "status": "source-evidenced", "summary": "A command template can belong to a provider command group." },
|
{ "id": "gelios.command_template.belongs_to_group", "from": ["gelios.command_template"], "to": ["gelios.command_group"], "status": "source-evidenced", "summary": "A command template can belong to a provider command group." },
|
||||||
{ "id": "gelios.command_dispatch.uses_template", "from": ["gelios.command_dispatch"], "to": ["gelios.command_template"], "status": "product-required", "summary": "A dispatch may reference a reviewed command template." },
|
{ "id": "gelios.command_dispatch.uses_template", "from": ["gelios.command_dispatch"], "to": ["gelios.command_template"], "status": "future-concept", "summary": "A future dispatch may reference a reviewed command template." },
|
||||||
{ "id": "gelios.command_dispatch.targets_scope", "from": ["gelios.command_dispatch"], "to": ["gelios.unit", "gelios.unit_group", "gelios.access_scope"], "status": "product-required", "summary": "A dispatch target must resolve to an approved unit or explicitly approved group in scope." },
|
{ "id": "gelios.command_dispatch.targets_scope", "from": ["gelios.command_dispatch"], "to": ["gelios.unit", "gelios.unit_group", "gelios.access_scope"], "status": "future-concept", "summary": "A future dispatch target must resolve to an approved unit or explicitly approved group in scope." },
|
||||||
{ "id": "gelios.command_dispatch.has_delivery", "from": ["gelios.command_dispatch"], "to": ["gelios.command_delivery"], "status": "product-required", "summary": "A dispatch is associated with delivery/task state rather than assumed successful on request creation." },
|
{ "id": "gelios.command_dispatch.has_delivery", "from": ["gelios.command_dispatch"], "to": ["gelios.command_delivery"], "status": "future-concept", "summary": "A future dispatch is associated with delivery/task state rather than assumed successful on request creation." },
|
||||||
{ "id": "gelios.command_dispatch.has_audit", "from": ["gelios.command_dispatch"], "to": ["gelios.command_audit"], "status": "product-required", "summary": "Every dispatch requires immutable user-confirmation and outcome audit." },
|
{ "id": "gelios.command_dispatch.has_audit", "from": ["gelios.command_dispatch"], "to": ["gelios.command_audit"], "status": "future-concept", "summary": "Every future dispatch requires immutable user-confirmation and outcome audit." },
|
||||||
{ "id": "gelios.unit.is_map_moving_object", "from": ["gelios.unit"], "to": ["map.moving_object"], "status": "product-required", "summary": "An approved Gelios unit is rendered through the provider-neutral moving-object contract, never as a Cesium entity identity." },
|
{ "id": "gelios.unit.is_map_moving_object", "from": ["gelios.unit"], "to": ["map.moving_object"], "status": "product-required", "summary": "A valid credential-visible Gelios unit is exposed through the provider-neutral moving-object contract, never as a renderer entity identity." },
|
||||||
{ "id": "gelios.position_fix.positions_map_subject", "from": ["gelios.position_fix"], "to": ["map.moving_object"], "status": "product-required", "summary": "A normalized position fix updates the stable map subject bound to the Gelios unit." },
|
{ "id": "gelios.position_fix.positions_map_subject", "from": ["gelios.position_fix"], "to": ["map.moving_object"], "status": "product-required", "summary": "A normalized position fix updates the stable map subject bound to the Gelios unit." },
|
||||||
{ "id": "gelios.geozone.is_map_zone", "from": ["gelios.geozone"], "to": ["map.zone"], "status": "product-required", "summary": "An approved Gelios geozone is exposed to the map through the generic zone contract." },
|
{ "id": "gelios.geozone.is_map_zone", "from": ["gelios.geozone"], "to": ["map.zone"], "status": "product-required", "summary": "An approved Gelios geozone is exposed to the map through the generic zone contract." },
|
||||||
{ "id": "gelios.geopoint.is_map_place_target", "from": ["gelios.geopoint"], "to": ["map.place_target"], "status": "product-required", "summary": "An approved Gelios geopoint is exposed through a provider-neutral place target." }
|
{ "id": "gelios.geopoint.is_map_place_target", "from": ["gelios.geopoint"], "to": ["map.place_target"], "status": "product-required", "summary": "An approved Gelios geopoint is exposed through a provider-neutral place target." }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"aliases": [
|
"aliases": [
|
||||||
{"alias":"коннектор","canonicalId":"integration.connection","language":"ru","status":"product-required"},
|
{"alias":"коннектор","canonicalId":"integration.connection","language":"ru","status":"product-required"},
|
||||||
{"alias":"connector","canonicalId":"integration.connection","language":"en","status":"product-required"},
|
{"alias":"connector","canonicalId":"integration.connection","language":"en","status":"product-required"},
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"entities": [
|
"entities": [
|
||||||
{"id":"integration.provider","name":"External Provider","surface":"integration","status":["product-required"],"authority":"Platform External Provider Data Plane","summary":"A third-party product or API family integrated through an app-owned adapter, not a tenant or a renderer."},
|
{"id":"integration.provider","name":"External Provider","surface":"integration","status":["product-required"],"authority":"Versioned provider package + Ontology Core","summary":"A third-party product or API family integrated through NDC L2, not a tenant, runtime service or renderer."},
|
||||||
{"id":"integration.connection","name":"Provider Connection","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"A tenant-scoped configured connection to one provider account or endpoint; it contains policy and secret references, never secret values."},
|
{"id":"integration.connection","name":"Provider Connection","surface":"integration","status":["product-required"],"authority":"NDC L2 connection","summary":"A tenant-scoped instance of one versioned provider package for one provider account; it contains policy and opaque credential references, never secret values."},
|
||||||
{"id":"integration.credential_reference","name":"Provider Credential Reference","surface":"integration","status":["product-required"],"authority":"Provider Gateway / secret store","summary":"Opaque reference to a server-side secret or token lifecycle, not a token, login or password."},
|
{"id":"integration.credential_reference","name":"Provider Credential Reference","surface":"integration","status":["product-required"],"authority":"NDC L2 Credentials","summary":"Opaque reference to an NDC L2-managed provider credential lifecycle, not a credential value, login or password."},
|
||||||
{"id":"integration.access_scope","name":"Integration Access Scope","surface":"integration","status":["product-required"],"authority":"Client owner + Provider Gateway","summary":"Approved product boundary for source accounts, objects, fields and permitted operations; provider visibility alone is insufficient."},
|
{"id":"integration.access_scope","name":"Integration Access Scope","surface":"integration","status":["product-required"],"authority":"Provider credential visibility + NDC L2 connection profile","summary":"Selected safe-read capabilities and fields applied to every valid entity visible to the bound credential. Static entity allowlists and provider-group business filters are not collection scope."},
|
||||||
{"id":"integration.capability","name":"Provider Capability","surface":"integration","status":["product-required"],"authority":"Provider Adapter","summary":"Versioned provider operation or feature classified as read, metadata, write, destructive or unknown."},
|
{"id":"integration.capability","name":"Provider Capability","surface":"integration","status":["product-required"],"authority":"Versioned provider package","summary":"Versioned provider operation or feature classified as read, metadata, write, destructive or unknown."},
|
||||||
{"id":"integration.field_definition","name":"Provider Field Definition","surface":"integration","status":["product-required"],"authority":"Provider Adapter + Ontology Core","summary":"A documented source field, its semantics, sensitivity, units and normalisation mapping; it is not a live value."},
|
{"id":"integration.field_definition","name":"Provider Field Definition","surface":"integration","status":["product-required"],"authority":"Versioned provider package + Ontology Core","summary":"A documented source field, its semantics, sensitivity, units and normalisation mapping; it is not a live value."},
|
||||||
{"id":"integration.collection_profile","name":"Collection Profile","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Approved rate, paging, cursor, capability and field selection for a connection; prevents uncontrolled account-wide polling."},
|
{"id":"integration.collection_profile","name":"Collection Profile","surface":"integration","status":["product-required"],"authority":"NDC L2 connection profile","summary":"Rate, paging, cursor, safe capability, field policy and batching selection for one connection; every valid entity returned for the credential is processed."},
|
||||||
{"id":"integration.retention_policy","name":"Integration Retention Policy","surface":"integration","status":["product-required"],"authority":"Client owner + Provider Gateway","summary":"Lifecycle rule for current state, raw envelope, normalised history, aggregates and backups."},
|
{"id":"integration.retention_policy","name":"Integration Retention Policy","surface":"integration","status":["product-required"],"authority":"External Data Plane Data Product definition","summary":"Lifecycle rule for current state, normalised history, aggregates and any separately approved raw reference."},
|
||||||
{"id":"integration.collection_run","name":"Integration Collection Run","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Auditable bounded execution of a safe-read collection profile with cursor, response metrics and outcome."},
|
{"id":"integration.collection_run","name":"Integration Collection Run","surface":"integration","status":["product-required"],"authority":"NDC L2 workflow","summary":"Auditable bounded execution of a safe-read collection profile with credential-visible entity scope, cursor, response metrics and outcome."},
|
||||||
{"id":"integration.raw_envelope","name":"Raw Provider Envelope","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Restricted provenance-preserving representation of a safe-read provider response, stored in a bounded cold layer rather than exposed to UI."},
|
{"id":"integration.raw_envelope","name":"Raw Provider Envelope","surface":"integration","status":["product-required"],"authority":"Restricted raw-data policy","summary":"Optional provenance reference to a separately approved bounded raw layer; inline provider payload is not a Data Product publish field."},
|
||||||
{"id":"integration.canonical_subject","name":"Canonical Integrated Subject","surface":"integration","status":["product-required"],"authority":"Provider Domain Adapter","summary":"Stable domain subject normalised from a provider object and suitable for relations, analytics and interfaces."},
|
{"id":"integration.canonical_subject","name":"Canonical Integrated Subject","surface":"integration","status":["product-required"],"authority":"NDC L2 semantic mapping + Ontology Core","summary":"Stable domain subject normalised from a provider object and suitable for relations, analytics and interfaces."},
|
||||||
{"id":"integration.read_model","name":"Integration Read Model","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Scoped, versioned current, history or aggregate projection delivered to approved consumers without raw provider bypass."},
|
{"id":"integration.read_model","name":"Integration Read Model","surface":"integration","status":["product-required"],"authority":"External Data Plane","summary":"Scoped, versioned Data Product current/history projection delivered to approved consumers without provider transport or raw bypass."},
|
||||||
{"id":"integration.realtime_channel","name":"Integration Realtime Channel","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Internal bounded event or subscription contract for projection changes; its emission rate is independent from provider collection rate."},
|
{"id":"integration.realtime_channel","name":"Integration Realtime Channel","surface":"integration","status":["product-required"],"authority":"External Data Plane","summary":"Bounded snapshot-and-patch contract for Data Product changes; its delivery and history cadence are independent from provider collection rate."},
|
||||||
{"id":"integration.command_capability","name":"Provider Command Capability","surface":"integration","status":["product-required"],"authority":"Provider Adapter","summary":"Catalogued command metadata, parameters and risk classification. It is not permission or a send route."},
|
{"id":"integration.command_capability","name":"Provider Command Capability","surface":"integration","status":["product-required"],"authority":"Versioned provider package","summary":"Catalogued command metadata, parameters and risk classification. It is not permission or a send route."},
|
||||||
{"id":"integration.command_intent","name":"Provider Command Intent","surface":"integration","status":["future-concept"],"authority":"Future Provider Command Gateway","summary":"A future explicitly confirmed, scoped and idempotent request to execute a catalogued command; no read collector may create it."},
|
{"id":"integration.command_intent","name":"Provider Command Intent","surface":"integration","status":["future-concept"],"authority":"Future Provider Command Gateway","summary":"A future explicitly confirmed, scoped and idempotent request to execute a catalogued command; no read collector may create it."},
|
||||||
{"id":"integration.command_audit","name":"Provider Command Audit","surface":"integration","status":["future-concept"],"authority":"Future Provider Command Gateway","summary":"Immutable approval, execution and delivery audit for a red-domain command lifecycle."}
|
{"id":"integration.command_audit","name":"Provider Command Audit","surface":"integration","status":["future-concept"],"authority":"Future Provider Command Gateway","summary":"Immutable approval, execution and delivery audit for a red-domain command lifecycle."}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
"sourceRoots": ["platform/docs", "platform/packages", "platform/services"],
|
"sourceRoots": ["platform/docs", "platform/packages", "platform/services"],
|
||||||
"ledgers": [
|
"ledgers": [
|
||||||
{
|
{
|
||||||
"id": "external_provider_data_plane_adr",
|
"id": "l2_owned_external_connectors_adr",
|
||||||
"path": "../../docs/ADR_EXTERNAL_PROVIDER_DATA_PLANE.md",
|
"path": "../../docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md",
|
||||||
"entityIds": ["integration.provider", "integration.connection", "integration.access_scope", "integration.capability", "integration.collection_profile", "integration.read_model", "integration.command_capability"]
|
"entityIds": ["integration.provider", "integration.connection", "integration.access_scope", "integration.capability", "integration.collection_profile", "integration.read_model", "integration.command_capability"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"baselineDocs": ["../../docs/ADR_EXTERNAL_PROVIDER_DATA_PLANE.md", "../../packages/external-provider-contract/README.md"],
|
"baselineDocs": ["../../docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md", "../../packages/external-provider-contract/README.md"],
|
||||||
"restrictions": ["The package is a semantic contract. Provider secrets, runtime data and command transport remain outside Ontology Core."]
|
"restrictions": ["The package is a semantic contract. Provider secrets, runtime data and command transport remain outside Ontology Core."]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"rules": [
|
"rules": [
|
||||||
{"id":"guardrail.integration.ontology_is_not_data_plane","severity":"error","summary":"Ontology Core may describe providers and contracts but must not store secret values, live payloads, tenant telemetry, raw envelopes or provider execution state.","entityIds":["integration.provider","integration.connection","integration.credential_reference","integration.raw_envelope","integration.read_model"]},
|
{"id":"guardrail.integration.ontology_is_not_data_plane","severity":"error","summary":"Ontology Core may describe providers and contracts but must not store secret values, live payloads, tenant telemetry, raw envelopes or provider execution state.","entityIds":["integration.provider","integration.connection","integration.credential_reference","integration.raw_envelope","integration.read_model"]},
|
||||||
{"id":"guardrail.integration.scope_precedes_collection","severity":"error","summary":"A collection run must resolve tenant scope, field policy and bounded collection profile before it addresses a provider. Account visibility does not define product scope.","entityIds":["integration.connection","integration.access_scope","integration.collection_profile","integration.collection_run"]},
|
{"id":"guardrail.integration.scope_precedes_collection","severity":"error","summary":"A collection run must resolve tenant/connection, safe capabilities, field policy and bounded profile before calling a provider, then process every valid entity returned for the bound credential. Static entity allowlists and provider-group business filters are forbidden at collection boundary.","entityIds":["integration.connection","integration.access_scope","integration.collection_profile","integration.collection_run"]},
|
||||||
{"id":"guardrail.integration.read_model_not_raw_bypass","severity":"error","summary":"L2 workflows, interfaces and renderer adapters consume scoped read models or realtime channels, never provider credentials, raw envelopes or direct adapter database access.","entityIds":["integration.credential_reference","integration.raw_envelope","integration.read_model","integration.realtime_channel"]},
|
{"id":"guardrail.integration.read_model_not_raw_bypass","severity":"error","summary":"NDC L2 workflows, interfaces and renderer adapters consume scoped Data Products or realtime channels, never provider credentials, raw envelopes or direct adapter database access.","entityIds":["integration.credential_reference","integration.raw_envelope","integration.read_model","integration.realtime_channel"]},
|
||||||
{"id":"guardrail.integration.commands_are_separate_red_domain","severity":"error","summary":"Cataloguing a command does not activate it. Read collectors, L2 workflows, maps and AI Workspace may not create command intent or delivery. A later command gateway requires explicit human confirmation, scope, idempotency and audit.","entityIds":["integration.command_capability","integration.command_intent","integration.command_audit","integration.collection_run"]},
|
{"id":"guardrail.integration.commands_are_separate_red_domain","severity":"error","summary":"Cataloguing a command does not activate it. Read collectors, NDC L2 workflows, maps and AI Workspace may not create command intent or delivery. A later command boundary requires explicit human confirmation, scope, idempotency and audit.","entityIds":["integration.command_capability","integration.command_intent","integration.command_audit","integration.collection_run"]},
|
||||||
{"id":"guardrail.integration.full_catalog_not_full_rate","severity":"warning","summary":"Every provider field may be catalogued, but enabling continuous collection requires an explicit collection, retention and rate policy. Do not turn a capability catalog into unbounded account polling.","entityIds":["integration.capability","integration.field_definition","integration.collection_profile","integration.retention_policy"]}
|
{"id":"guardrail.integration.full_catalog_not_full_rate","severity":"warning","summary":"Every provider field may be catalogued, but enabling continuous collection requires an explicit collection, retention and rate policy. Do not turn a capability catalog into unbounded account polling.","entityIds":["integration.capability","integration.field_definition","integration.collection_profile","integration.retention_policy"]}
|
||||||
],
|
],
|
||||||
"blockedConflations": [
|
"blockedConflations": [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "integration",
|
"id": "integration",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"status": "product-required/source-evidenced",
|
"status": "product-required/source-evidenced",
|
||||||
"summary": "Provider-neutral external integration ontology: tenant-scoped connections, capabilities, collection, controlled storage projections and a separated red command domain."
|
"summary": "Provider-neutral external integration ontology: tenant-scoped connections, capabilities, collection, controlled storage projections and a separated red command domain."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"updatedAt": "2026-07-13",
|
"updatedAt": "2026-07-16",
|
||||||
"relations": [
|
"relations": [
|
||||||
{"id":"integration.provider.offers_connection","from":["integration.provider"],"to":["integration.connection"],"status":"product-required","summary":"A provider is connected through one or more tenant-scoped connection instances."},
|
{"id":"integration.provider.offers_connection","from":["integration.provider"],"to":["integration.connection"],"status":"product-required","summary":"A provider is connected through one or more tenant-scoped connection instances."},
|
||||||
{"id":"integration.connection.references_credential","from":["integration.connection"],"to":["integration.credential_reference"],"status":"product-required","summary":"A connection uses an opaque server-side credential reference rather than exposing a secret."},
|
{"id":"integration.connection.references_credential","from":["integration.connection"],"to":["integration.credential_reference"],"status":"product-required","summary":"A connection uses an opaque server-side credential reference rather than exposing a secret."},
|
||||||
{"id":"integration.connection.enforces_scope","from":["integration.connection"],"to":["integration.access_scope"],"status":"product-required","summary":"Every source request is bounded by an approved product scope."},
|
{"id":"integration.connection.enforces_scope","from":["integration.connection"],"to":["integration.access_scope"],"status":"product-required","summary":"Every source request is bounded by selected safe capabilities and field policy while entity membership is dynamically all valid objects visible to the bound credential."},
|
||||||
{"id":"integration.provider.declares_capability","from":["integration.provider"],"to":["integration.capability","integration.command_capability"],"status":"product-required","summary":"Adapter manifests catalogue source read and command capabilities independently from their activation."},
|
{"id":"integration.provider.declares_capability","from":["integration.provider"],"to":["integration.capability","integration.command_capability"],"status":"product-required","summary":"Adapter manifests catalogue source read and command capabilities independently from their activation."},
|
||||||
{"id":"integration.capability.exposes_field","from":["integration.capability"],"to":["integration.field_definition"],"status":"product-required","summary":"A safe-read capability documents the fields it may return before a collection profile enables them."},
|
{"id":"integration.capability.exposes_field","from":["integration.capability"],"to":["integration.field_definition"],"status":"product-required","summary":"A safe-read capability documents the fields it may return before a collection profile enables them."},
|
||||||
{"id":"integration.collection_profile.selects_capability","from":["integration.collection_profile"],"to":["integration.capability","integration.field_definition"],"status":"product-required","summary":"A profile selects bounded capabilities and fields rather than polling every API feature continuously."},
|
{"id":"integration.collection_profile.selects_capability","from":["integration.collection_profile"],"to":["integration.capability","integration.field_definition"],"status":"product-required","summary":"A profile selects bounded safe capabilities and fields rather than polling every API feature; it does not select business entity IDs."},
|
||||||
{"id":"integration.collection_profile.applies_retention","from":["integration.collection_profile"],"to":["integration.retention_policy"],"status":"product-required","summary":"Collection and storage lifecycle are explicit per connection profile."},
|
{"id":"integration.collection_profile.applies_retention","from":["integration.collection_profile"],"to":["integration.retention_policy"],"status":"product-required","summary":"Collection and storage lifecycle are explicit per connection profile."},
|
||||||
{"id":"integration.collection_run.uses_connection","from":["integration.collection_run"],"to":["integration.connection","integration.collection_profile"],"status":"product-required","summary":"A collection run records which connection and bounded policy it used."},
|
{"id":"integration.collection_run.uses_connection","from":["integration.collection_run"],"to":["integration.connection","integration.collection_profile"],"status":"product-required","summary":"A collection run records which connection and bounded policy it used."},
|
||||||
{"id":"integration.collection_run.records_raw_envelope","from":["integration.collection_run"],"to":["integration.raw_envelope"],"status":"product-required","summary":"Safe-read response provenance is retained only according to the active policy."},
|
{"id":"integration.collection_run.records_raw_envelope","from":["integration.collection_run"],"to":["integration.raw_envelope"],"status":"product-required","summary":"Safe-read response provenance is retained only according to the active policy."},
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,33 @@
|
||||||
{
|
{
|
||||||
"contractVersion": "0.1.0",
|
"schemaVersion": "nodedc.data-product.publish/v1",
|
||||||
"kind": "map.moving_object.current_position",
|
"batch": {
|
||||||
"source": "gelios",
|
"runId": "gelios-ontology-fixture-20260716",
|
||||||
"subjectId": "gelios.unit:example-unit-id",
|
"sequence": 0,
|
||||||
"observedAt": "2026-07-13T00:00:00.000Z",
|
"idempotencyKey": "gelios-ontology-fixture-20260716.batch-0"
|
||||||
"receivedAt": "2026-07-13T00:00:03.000Z",
|
|
||||||
"position": {
|
|
||||||
"latitude": 55.000001,
|
|
||||||
"longitude": 37.000001,
|
|
||||||
"heightMeters": 0,
|
|
||||||
"courseDegrees": 90,
|
|
||||||
"speedKph": 0,
|
|
||||||
"satellites": 0
|
|
||||||
},
|
},
|
||||||
"operationalStatus": "unknown",
|
"facts": [
|
||||||
"display": {
|
{
|
||||||
"label": "Robot2B unit",
|
"sourceId": "gelios-unit-1001",
|
||||||
"visibilityProfile": "fleet-default"
|
"semanticType": "map.moving_object",
|
||||||
|
"observedAt": "2026-07-16T12:00:00.000Z",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.6173, 55.7558]
|
||||||
},
|
},
|
||||||
"policy": {
|
"attributes": {
|
||||||
"scopeApproved": true,
|
"course_degrees": 90,
|
||||||
"fieldSet": "studio-current-position-v1"
|
"display_name": "Synthetic Gelios unit",
|
||||||
|
"elevation_meters": 0,
|
||||||
|
"hdop": 0.8,
|
||||||
|
"horizontal_accuracy_meters": 4.2,
|
||||||
|
"object_kind": "tracked_unit",
|
||||||
|
"operational_status": "active",
|
||||||
|
"position_source": "gelios",
|
||||||
|
"position_valid": true,
|
||||||
|
"quality_flags": [],
|
||||||
|
"satellite_count": 11,
|
||||||
|
"speed_kph": 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue