feat(connectors): version provider packages and ontology contracts
This commit is contained in:
@@ -15,6 +15,10 @@ import { geliosPositionsCurrentExample } from "../examples/gelios-positions-curr
|
||||
|
||||
assert.equal(validateProviderManifest(geliosPositionsCurrentExample.providerManifest).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(validateDataProduct(geliosPositionsCurrentExample.dataProduct).ok, true);
|
||||
assert.equal(validateFoundryBinding(geliosPositionsCurrentExample.foundryBinding).ok, true);
|
||||
@@ -108,6 +112,10 @@ assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { metadata: "ndc_edprb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE" } }],
|
||||
}).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({
|
||||
...neutralIntakeBatch,
|
||||
raw: { contentType: "application/json", hash: "prefix ndc_edpwb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE suffix", ref: "restricted://raw/fixture" },
|
||||
|
||||
@@ -34,6 +34,10 @@ assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
facts: [{ ...fact, attributes: { accessToken: "must-never-cross-the-boundary" } }],
|
||||
}).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({
|
||||
...publish,
|
||||
facts: [fact, { ...fact, observedAt: "2026-07-15T10:00:01.000Z" }],
|
||||
@@ -85,6 +89,10 @@ assert.equal(validateDataProductSnapshot({
|
||||
...snapshot,
|
||||
facts: [{ ...canonicalFact, attributes: { status: "ndc_edprb_forbidden-reader-token" } }],
|
||||
}).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 = {
|
||||
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"),
|
||||
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 = {
|
||||
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");
|
||||
Reference in New Issue
Block a user