feat(data-plane): add provider contracts and ontology delivery
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
EXTERNAL_PROVIDER_CONTRACT_VERSION,
|
||||
FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION,
|
||||
assertValid,
|
||||
validateCollectionProfile,
|
||||
validateConnectionProfile,
|
||||
validateDataProduct,
|
||||
validateFoundryBinding,
|
||||
validateFoundryBindingUpsert,
|
||||
validateIntakeBatch,
|
||||
validateProviderManifest,
|
||||
} from "../src/index.mjs";
|
||||
import { geliosPositionsCurrentExample } from "../examples/gelios-positions-current.v1.mjs";
|
||||
|
||||
assert.equal(validateProviderManifest(geliosPositionsCurrentExample.providerManifest).ok, true);
|
||||
assert.equal(validateConnectionProfile(geliosPositionsCurrentExample.connection).ok, true);
|
||||
assert.equal(validateCollectionProfile(geliosPositionsCurrentExample.collectionProfile).ok, true);
|
||||
assert.equal(validateDataProduct(geliosPositionsCurrentExample.dataProduct).ok, true);
|
||||
assert.equal(validateFoundryBinding(geliosPositionsCurrentExample.foundryBinding).ok, true);
|
||||
|
||||
const foundryBindingUpsert = {
|
||||
schemaVersion: FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION,
|
||||
applicationId: "11111111-1111-4111-8111-111111111111",
|
||||
pageId: "map",
|
||||
idempotencyKey: "foundry-binding-0123456789abcdef0123456789abcdef",
|
||||
binding: {
|
||||
id: "fleet-live-points",
|
||||
dataProductId: "fleet.positions.current.v1",
|
||||
slotId: "points",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fieldProjection: ["name", "speed", "course"],
|
||||
},
|
||||
};
|
||||
assert.equal(validateFoundryBindingUpsert(foundryBindingUpsert).ok, true);
|
||||
assert.equal(validateFoundryBindingUpsert({
|
||||
...foundryBindingUpsert,
|
||||
binding: { ...foundryBindingUpsert.binding, semanticTypes: ["map.moving_object", "map.moving_object"] },
|
||||
}).errors.includes("binding.semanticTypes_must_not_contain_duplicates"), true);
|
||||
assert.equal(validateFoundryBindingUpsert({
|
||||
...foundryBindingUpsert,
|
||||
binding: { ...foundryBindingUpsert.binding, accessToken: "forbidden" },
|
||||
}).errors.includes("binding.accessToken_not_allowed"), true);
|
||||
assert.equal(validateFoundryBindingUpsert({
|
||||
...foundryBindingUpsert,
|
||||
binding: { ...foundryBindingUpsert.binding, fieldProjection: ["ndc_edprb_forbidden-reader-token"] },
|
||||
}).errors.includes("binding_must_not_contain_secret_material"), true);
|
||||
|
||||
const attributesWithSerializedSize = (size) => ({
|
||||
blob: "x".repeat(size - Buffer.byteLength(JSON.stringify({ blob: "" }))),
|
||||
});
|
||||
|
||||
const neutralIntakeBatch = {
|
||||
schemaVersion: EXTERNAL_PROVIDER_CONTRACT_VERSION,
|
||||
source: {
|
||||
providerId: "example-provider",
|
||||
tenantId: "sample-tenant",
|
||||
connectionId: "sample-connection",
|
||||
},
|
||||
contract: {
|
||||
dataProductId: "fleet.positions.current.v1",
|
||||
ontologyRevision: "ontology.example-fleet.v1",
|
||||
version: "1.0.0",
|
||||
},
|
||||
batch: {
|
||||
runId: "run-20260714-001",
|
||||
sequence: 0,
|
||||
idempotencyKey: "run-20260714-001.batch-0",
|
||||
receivedAt: "2026-07-14T18:00:00.000Z",
|
||||
},
|
||||
raw: {
|
||||
contentType: "application/json",
|
||||
hash: "sha256:example",
|
||||
ref: "restricted://raw/run-20260714-001",
|
||||
retentionDays: 14,
|
||||
},
|
||||
facts: [{
|
||||
sourceId: "source-object-42",
|
||||
semanticType: "map.moving_object",
|
||||
observedAt: "2026-07-14T17:59:58.000Z",
|
||||
geometry: { type: "Point", coordinates: [37.6173, 55.7558] },
|
||||
attributes: { status: "active" },
|
||||
}],
|
||||
};
|
||||
|
||||
assert.equal(validateIntakeBatch(neutralIntakeBatch).ok, true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
raw: { contentType: "application/json", payload: { safe: "fixture" } },
|
||||
}).errors.includes("raw.inline_payload_not_supported"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
raw: { contentType: "application/json", payload: "unsafe-unstructured-inline-raw" },
|
||||
}).errors.includes("raw.inline_payload_not_supported"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
raw: { contentType: "application/json", payload: { providerAccessToken: "must-never-be-stored" } },
|
||||
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { authorization: "Bearer must-never-be-stored" } }],
|
||||
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { metadata: "ndc_edpwb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE" } }],
|
||||
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||
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,
|
||||
raw: { contentType: "application/json", hash: "prefix ndc_edpwb_abcdefghijklmnopqrstuvwxyz0123456789ABCDE suffix", ref: "restricted://raw/fixture" },
|
||||
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: { metadata: "Bearer opaque-secret-material" } }],
|
||||
}).errors.includes("intake_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
raw: { contentType: "application/json", hash: "sha256:fixture", ref: "restricted://raw?access_token=must-never-be-stored" },
|
||||
}).errors.includes("raw.ref_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], sourceId: "" }],
|
||||
}).errors.includes("facts[0].sourceId_invalid"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
batch: { ...neutralIntakeBatch.batch, sequence: 2_147_483_647 },
|
||||
}).ok, true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
batch: { ...neutralIntakeBatch.batch, sequence: 2_147_483_648 },
|
||||
}).errors.includes("batch.sequence_must_be_integer_0_to_2147483647"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: attributesWithSerializedSize(64 * 1024) }],
|
||||
}).ok, true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], attributes: attributesWithSerializedSize((64 * 1024) + 1) }],
|
||||
}).errors.includes("facts[0].attributes_size_exceeded"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], geometry: { type: "Point", coordinates: [180.0001, 55.7558] } }],
|
||||
}).errors.includes("facts[0].geometry.longitude_out_of_range"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
facts: [{ ...neutralIntakeBatch.facts[0], geometry: { type: "Point", coordinates: [37.6173, -90.0001] } }],
|
||||
}).errors.includes("facts[0].geometry.latitude_out_of_range"), true);
|
||||
assert.equal(validateIntakeBatch({
|
||||
...neutralIntakeBatch,
|
||||
debug: true,
|
||||
}).errors.includes("intakeBatch.debug_not_allowed"), true);
|
||||
|
||||
assert.equal(validateConnectionProfile({
|
||||
...geliosPositionsCurrentExample.connection,
|
||||
apiToken: "must-never-be-here",
|
||||
}).ok, false);
|
||||
assert.equal(validateConnectionProfile({
|
||||
...geliosPositionsCurrentExample.connection,
|
||||
credentialRef: {
|
||||
...geliosPositionsCurrentExample.connection.credentialRef,
|
||||
reference: "ndc_edpwb_not-allowed-even-in-a-non-secret-field",
|
||||
},
|
||||
}).errors.includes("profile_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateConnectionProfile({
|
||||
...geliosPositionsCurrentExample.connection,
|
||||
credentialRef: {
|
||||
...geliosPositionsCurrentExample.connection.credentialRef,
|
||||
reference: "opaque-reference?access_token=must-not-cross-the-boundary",
|
||||
},
|
||||
}).errors.includes("profile_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateConnectionProfile({
|
||||
...geliosPositionsCurrentExample.connection,
|
||||
credentialRef: { ...geliosPositionsCurrentExample.connection.credentialRef, namespace: "unexpected" },
|
||||
}).errors.includes("credentialRef.namespace_not_allowed"), true);
|
||||
assert.equal(validateConnectionProfile({
|
||||
...geliosPositionsCurrentExample.connection,
|
||||
scope: { ...geliosPositionsCurrentExample.connection.scope, providerSelector: "unexpected" },
|
||||
}).errors.includes("scope.providerSelector_not_allowed"), true);
|
||||
assert.equal(validateCollectionProfile({
|
||||
...geliosPositionsCurrentExample.collectionProfile,
|
||||
mode: "manual",
|
||||
}).errors.includes("manual_profile_must_not_define_intervalMs"), true);
|
||||
assert.equal(validateCollectionProfile({
|
||||
...geliosPositionsCurrentExample.collectionProfile,
|
||||
schedule: { intervalMs: 3000, jitterMs: 100 },
|
||||
}).errors.includes("schedule.jitterMs_not_allowed"), true);
|
||||
assert.equal(validateCollectionProfile({
|
||||
...geliosPositionsCurrentExample.collectionProfile,
|
||||
capabilityIds: ["ndc_edprb_forbidden-reader-token"],
|
||||
}).errors.includes("collectionProfile_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateFoundryBinding({
|
||||
...geliosPositionsCurrentExample.foundryBinding,
|
||||
providerId: "gelios",
|
||||
}).errors.includes("binding_must_reference_data_product_not_provider_transport"), true);
|
||||
assert.equal(validateFoundryBinding({
|
||||
...geliosPositionsCurrentExample.foundryBinding,
|
||||
applicationId: undefined,
|
||||
}).errors.includes("applicationId_invalid"), true);
|
||||
assert.equal(validateFoundryBinding({
|
||||
...geliosPositionsCurrentExample.foundryBinding,
|
||||
pageId: undefined,
|
||||
}).errors.includes("pageId_invalid"), true);
|
||||
assert.equal(validateFoundryBinding({
|
||||
...geliosPositionsCurrentExample.foundryBinding,
|
||||
templateId: undefined,
|
||||
}).ok, true);
|
||||
assert.equal(validateFoundryBinding({
|
||||
...geliosPositionsCurrentExample.foundryBinding,
|
||||
rendererOptions: {},
|
||||
}).errors.includes("foundryBinding.rendererOptions_not_allowed"), true);
|
||||
assert.equal(validateFoundryBinding({
|
||||
...geliosPositionsCurrentExample.foundryBinding,
|
||||
semanticType: "ndc_edprb_forbidden-reader-token",
|
||||
}).errors.includes("binding_must_not_contain_secret_material"), true);
|
||||
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
tenantId: "must-not-live-in-provider-manifest",
|
||||
}).errors.includes("manifest_must_not_contain_connection_runtime_state"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
apiToken: "must-never-be-here",
|
||||
}).errors.includes("manifest_must_not_contain_secret_material"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
endpoint: "https://must-live-in-l2-template.invalid",
|
||||
}).errors.includes("manifest_must_not_contain_provider_transport"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
capabilities: [{ id: "gelios.units.current.read", classification: "not-a-class" }],
|
||||
}).errors.includes("capabilities[0].classification_invalid"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
metadata: {},
|
||||
}).errors.includes("providerManifest.metadata_not_allowed"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
ontology: { ...geliosPositionsCurrentExample.providerManifest.ontology, transport: "unexpected" },
|
||||
}).errors.includes("ontology.transport_not_allowed"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
capabilities: [{ id: "gelios.units.current.read", classification: "read", endpoint: "/units" }],
|
||||
}).errors.includes("capabilities[0].endpoint_not_allowed"), true);
|
||||
assert.equal(validateProviderManifest({
|
||||
...geliosPositionsCurrentExample.providerManifest,
|
||||
dataProductIds: ["ndc_edpwb_forbidden-writer-token"],
|
||||
}).errors.includes("manifest_must_not_contain_secret_material"), true);
|
||||
|
||||
assert.equal(validateDataProduct({
|
||||
...geliosPositionsCurrentExample.dataProduct,
|
||||
delivery: { mode: "snapshot+patch", transport: "sse" },
|
||||
}).errors.includes("delivery.transport_not_allowed"), true);
|
||||
|
||||
assert.throws(() => assertValid(validateDataProduct, {
|
||||
schemaVersion: EXTERNAL_PROVIDER_CONTRACT_VERSION,
|
||||
id: "fleet.positions.current.v1",
|
||||
version: "1.0.0",
|
||||
delivery: { mode: "snapshot" },
|
||||
semanticTypes: [],
|
||||
fields: [],
|
||||
access: { audience: "public" },
|
||||
}));
|
||||
|
||||
console.log("external-provider-contract: ok");
|
||||
@@ -0,0 +1,111 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||
DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||
DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION,
|
||||
validateDataProductPatch,
|
||||
validateDataProductPublish,
|
||||
validateDataProductSnapshot,
|
||||
} from "../src/index.mjs";
|
||||
|
||||
const fact = {
|
||||
sourceId: "unit-42",
|
||||
semanticType: "map.moving_object",
|
||||
observedAt: "2026-07-15T10:00:00.000Z",
|
||||
attributes: { status: "online" },
|
||||
geometry: { type: "Point", coordinates: [37.6173, 55.7558] },
|
||||
};
|
||||
|
||||
const attributesWithSerializedSize = (size) => ({
|
||||
blob: "x".repeat(size - Buffer.byteLength(JSON.stringify({ blob: "" }))),
|
||||
});
|
||||
|
||||
const publish = {
|
||||
schemaVersion: DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
|
||||
batch: { runId: "execution-42", sequence: 0, idempotencyKey: "execution-42.node-01.chunk-0" },
|
||||
facts: [fact],
|
||||
};
|
||||
assert.equal(validateDataProductPublish(publish).ok, true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
source: { tenantId: "forged" },
|
||||
}).errors.includes("publish.source_not_allowed"), true);
|
||||
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, { ...fact, observedAt: "2026-07-15T10:00:01.000Z" }],
|
||||
}).errors.includes("facts_duplicate_entity_key"), true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
batch: { ...publish.batch, sequence: 2_147_483_647 },
|
||||
}).ok, true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
batch: { ...publish.batch, sequence: 2_147_483_648 },
|
||||
}).errors.includes("batch.sequence_must_be_integer_0_to_2147483647"), true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
facts: [{ ...fact, attributes: attributesWithSerializedSize(64 * 1024) }],
|
||||
}).ok, true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
facts: [{ ...fact, attributes: attributesWithSerializedSize((64 * 1024) + 1) }],
|
||||
}).errors.includes("facts[0].attributes_size_exceeded"), true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
facts: [{ ...fact, attributes: attributesWithSerializedSize((64 * 1024) + 1) }],
|
||||
}, { maxAttributesBytes: 1024 * 1024 }).errors.includes("facts[0].attributes_size_exceeded"), true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
facts: [{ ...fact, geometry: { type: "Point", coordinates: [-180.0001, 55.7558] } }],
|
||||
}).errors.includes("facts[0].geometry.longitude_out_of_range"), true);
|
||||
assert.equal(validateDataProductPublish({
|
||||
...publish,
|
||||
facts: [{ ...fact, geometry: { type: "Point", coordinates: [37.6173, 90.0001] } }],
|
||||
}).errors.includes("facts[0].geometry.latitude_out_of_range"), true);
|
||||
|
||||
const canonicalFact = { ...fact, receivedAt: "2026-07-15T10:00:01.000Z" };
|
||||
const snapshot = {
|
||||
schemaVersion: DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION,
|
||||
dataProduct: { id: "fleet.positions.current.v1", version: "1.0.0" },
|
||||
generatedAt: "2026-07-15T10:00:01.000Z",
|
||||
cursor: "12",
|
||||
facts: [canonicalFact],
|
||||
};
|
||||
assert.equal(validateDataProductSnapshot(snapshot).ok, true);
|
||||
assert.equal(validateDataProductSnapshot({ ...snapshot, transport: "sse" }).errors.includes("snapshot.transport_not_allowed"), true);
|
||||
assert.equal(validateDataProductSnapshot({
|
||||
...snapshot,
|
||||
facts: [{ ...canonicalFact, attributes: attributesWithSerializedSize((64 * 1024) + 1) }],
|
||||
}).errors.includes("facts[0].attributes_size_exceeded"), true);
|
||||
assert.equal(validateDataProductSnapshot({
|
||||
...snapshot,
|
||||
facts: [{ ...canonicalFact, attributes: { status: "ndc_edprb_forbidden-reader-token" } }],
|
||||
}).errors.includes("snapshot_must_not_contain_secret_material"), true);
|
||||
|
||||
const patch = {
|
||||
schemaVersion: DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||
dataProduct: { id: "fleet.positions.current.v1", version: "1.0.0" },
|
||||
cursor: "13",
|
||||
previousCursor: "12",
|
||||
emittedAt: "2026-07-15T10:00:02.000Z",
|
||||
operations: [{ op: "upsert", fact: canonicalFact }],
|
||||
};
|
||||
assert.equal(validateDataProductPatch(patch).ok, true);
|
||||
assert.equal(validateDataProductPatch({
|
||||
...patch,
|
||||
operations: [{ op: "delete", fact: canonicalFact }],
|
||||
}).errors.includes("operations[0].op_must_be_upsert"), true);
|
||||
assert.equal(validateDataProductPatch({
|
||||
...patch,
|
||||
operations: [{ op: "upsert", fact: { ...canonicalFact, attributes: attributesWithSerializedSize((64 * 1024) + 1) } }],
|
||||
}).errors.includes("operations[0].fact.attributes_size_exceeded"), true);
|
||||
assert.equal(validateDataProductPatch({
|
||||
...patch,
|
||||
operations: [{ op: "upsert", fact: { ...canonicalFact, attributes: { status: "ndc_edpwb_forbidden-writer-token" } } }],
|
||||
}).errors.includes("patch_must_not_contain_secret_material"), true);
|
||||
|
||||
console.log("data-product-contract: ok");
|
||||
@@ -0,0 +1,242 @@
|
||||
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");
|
||||
@@ -0,0 +1,370 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
ENGINE_PRIVATE_EXTENSION_APPLY_RECEIPT_SCHEMA_VERSION,
|
||||
ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION,
|
||||
ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES,
|
||||
ENGINE_PRIVATE_EXTENSION_INACTIVE_BASELINE,
|
||||
ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY,
|
||||
ENGINE_PRIVATE_EXTENSION_NODE_TYPES,
|
||||
ENGINE_PRIVATE_EXTENSION_OPERATION_SCHEMA_VERSION,
|
||||
ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME,
|
||||
ENGINE_PRIVATE_EXTENSION_PLAN_REQUEST_SCHEMA_VERSION,
|
||||
ENGINE_PRIVATE_EXTENSION_PLAN_SCHEMA_VERSION,
|
||||
ENGINE_PRIVATE_EXTENSION_READ_CAPABILITY,
|
||||
ENGINE_PRIVATE_EXTENSION_STATUS_SCHEMA_VERSION,
|
||||
authorizeEnginePrivateExtensionOperation,
|
||||
computeEnginePrivateExtensionPlanHash,
|
||||
validateEnginePrivateExtensionApplyReceipt,
|
||||
validateEnginePrivateExtensionApplyRequest,
|
||||
validateEnginePrivateExtensionOperation,
|
||||
validateEnginePrivateExtensionPlan,
|
||||
validateEnginePrivateExtensionPlanRequest,
|
||||
validateEnginePrivateExtensionStatus,
|
||||
} from "../src/index.mjs";
|
||||
|
||||
const digest = "03413c6f3c706a1f6a9597a1cf1730504e9719228d0c77fdfb93bbdcc57a4cf3";
|
||||
const release = Object.freeze({
|
||||
kind: "release",
|
||||
packageName: ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME,
|
||||
releaseId: "0.1.0-03413c6f3c706a1f",
|
||||
packageSha256: digest,
|
||||
});
|
||||
const inactive = Object.freeze({
|
||||
kind: "inactive-baseline",
|
||||
packageName: ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME,
|
||||
baselineId: ENGINE_PRIVATE_EXTENSION_INACTIVE_BASELINE,
|
||||
});
|
||||
const manage = [ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY];
|
||||
|
||||
const activateRequest = {
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_PLAN_REQUEST_SCHEMA_VERSION,
|
||||
requestId: "extension-request-20260715-001",
|
||||
idempotencyKey: "extension-request-20260715-001",
|
||||
action: "activate",
|
||||
requestedAt: "2026-07-15T18:00:00.000Z",
|
||||
requestExpiresAt: "2026-07-15T18:10:00.000Z",
|
||||
expectedCurrentGeneration: 0,
|
||||
target: release,
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
validateEnginePrivateExtensionPlanRequest(activateRequest, {
|
||||
now: "2026-07-15T18:00:01.000Z",
|
||||
grantedCapabilities: manage,
|
||||
}),
|
||||
{ ok: true, errors: [] },
|
||||
);
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionPlanRequest(activateRequest, {
|
||||
now: "2026-07-15T18:00:01.000Z",
|
||||
grantedCapabilities: ["engine.l2.deploy"],
|
||||
}).errors.includes("engine_private_extension_capability_required"),
|
||||
true,
|
||||
);
|
||||
assert.equal(authorizeEnginePrivateExtensionOperation("status", [ENGINE_PRIVATE_EXTENSION_READ_CAPABILITY]).ok, true);
|
||||
assert.equal(authorizeEnginePrivateExtensionOperation("apply", [ENGINE_PRIVATE_EXTENSION_READ_CAPABILITY]).ok, false);
|
||||
|
||||
const activatePlan = withPlanHash({
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_PLAN_SCHEMA_VERSION,
|
||||
planId: "extension-plan-20260715-001",
|
||||
planHash: hash("0"),
|
||||
requestId: activateRequest.requestId,
|
||||
idempotencyKey: activateRequest.idempotencyKey,
|
||||
action: "activate",
|
||||
createdAt: "2026-07-15T18:00:01.000Z",
|
||||
expiresAt: "2026-07-15T18:09:00.000Z",
|
||||
singleUse: true,
|
||||
requiredCapability: ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY,
|
||||
expectedCurrentGeneration: 0,
|
||||
nextGeneration: 1,
|
||||
currentState: inactive,
|
||||
targetState: release,
|
||||
recoveryState: inactive,
|
||||
actions: [
|
||||
"verify_staged_immutable_release",
|
||||
"prepare_sealed_package_tree",
|
||||
"verify_community_package_loader_policy",
|
||||
"quiesce_deploy_run_and_drain_queue",
|
||||
"record_recovery_state",
|
||||
"atomic_switch_current",
|
||||
"force_recreate_main_workers_webhooks_as_version_barrier",
|
||||
"verify_exact_runtime_acceptance",
|
||||
"commit_active_state",
|
||||
"resume_deploy_run",
|
||||
],
|
||||
transition: transition(),
|
||||
acceptance: acceptanceSpec(release),
|
||||
failurePolicy: failurePolicy(),
|
||||
});
|
||||
|
||||
assert.deepEqual(validateEnginePrivateExtensionPlan(activatePlan, { request: activateRequest }), { ok: true, errors: [] });
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionPlan({ ...activatePlan, nextGeneration: 2 }).errors.includes("nextGeneration_must_increment_current_generation"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionPlan({ ...activatePlan, transition: { ...transition(), runtimeAction: "restart" } })
|
||||
.errors.includes("transition_policy_mismatch"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionPlan({
|
||||
...activatePlan,
|
||||
transition: { ...transition(), loaderMode: "custom-extension", loaderPath: "N8N_CUSTOM_EXTENSIONS" },
|
||||
}).errors.includes("transition_policy_mismatch"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionPlan({
|
||||
...activatePlan,
|
||||
transition: { ...transition(), hotReload: true },
|
||||
}).errors.includes("transition_policy_mismatch"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionPlan({
|
||||
...activatePlan,
|
||||
transition: {
|
||||
...transition(),
|
||||
loaderEnvironment: { ...transition().loaderEnvironment, N8N_REINSTALL_MISSING_PACKAGES: "true" },
|
||||
},
|
||||
}).errors.includes("transition_policy_mismatch"),
|
||||
true,
|
||||
);
|
||||
|
||||
const applyRequest = {
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION,
|
||||
planId: activatePlan.planId,
|
||||
planHash: activatePlan.planHash,
|
||||
idempotencyKey: activatePlan.idempotencyKey,
|
||||
confirmedAt: "2026-07-15T18:00:02.000Z",
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionApplyRequest(applyRequest, {
|
||||
plan: activatePlan,
|
||||
now: "2026-07-15T18:00:02.000Z",
|
||||
grantedCapabilities: manage,
|
||||
}), { ok: true, errors: [] });
|
||||
assert.equal(validateEnginePrivateExtensionApplyRequest(applyRequest, {
|
||||
plan: activatePlan,
|
||||
now: "2026-07-15T18:10:00.000Z",
|
||||
grantedCapabilities: manage,
|
||||
}).errors.includes("plan_expired"), true);
|
||||
|
||||
const receipt = {
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_APPLY_RECEIPT_SCHEMA_VERSION,
|
||||
operationId: "extension-operation-20260715-001",
|
||||
planId: activatePlan.planId,
|
||||
planHash: activatePlan.planHash,
|
||||
action: "activate",
|
||||
acceptedAt: "2026-07-15T18:00:02.100Z",
|
||||
state: "queued",
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionApplyReceipt(receipt, { plan: activatePlan }), { ok: true, errors: [] });
|
||||
assert.equal(validateEnginePrivateExtensionApplyReceipt({ ...receipt, state: "active" })
|
||||
.errors.includes("apply_receipt_state_must_be_queued"), true);
|
||||
|
||||
const activeOperation = {
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_OPERATION_SCHEMA_VERSION,
|
||||
operationId: receipt.operationId,
|
||||
planId: activatePlan.planId,
|
||||
planHash: activatePlan.planHash,
|
||||
action: "activate",
|
||||
state: "active",
|
||||
outcome: "committed",
|
||||
phase: "complete",
|
||||
expectedCurrentGeneration: 0,
|
||||
nextGeneration: 1,
|
||||
targetState: release,
|
||||
recoveryState: inactive,
|
||||
effectiveState: release,
|
||||
runtime: runtime(1, 2, 2),
|
||||
acceptance: acceptanceReport(release, "accepted"),
|
||||
updatedAt: "2026-07-15T18:00:32.000Z",
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionOperation(activeOperation), { ok: true, errors: [] });
|
||||
const unexpectedSchema = structuredClone(activeOperation);
|
||||
unexpectedSchema.acceptance.nodeTypes.observed.push("n8n-nodes-ndc.unreviewedNode");
|
||||
assert.equal(validateEnginePrivateExtensionOperation(unexpectedSchema).errors
|
||||
.includes("acceptance.nodeTypes.observed_exact_set_required"), true);
|
||||
|
||||
const automaticRollback = {
|
||||
...activeOperation,
|
||||
state: "rolled-back",
|
||||
outcome: "automatically-rolled-back",
|
||||
phase: "complete",
|
||||
effectiveState: inactive,
|
||||
acceptance: acceptanceReport(inactive, "accepted"),
|
||||
errorCode: "runtime_acceptance_failed",
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionOperation(automaticRollback), { ok: true, errors: [] });
|
||||
assert.equal(
|
||||
validateEnginePrivateExtensionOperation({ ...automaticRollback, errorCode: undefined })
|
||||
.errors.includes("errorCode_invalid"),
|
||||
true,
|
||||
);
|
||||
|
||||
const activeStatus = {
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_STATUS_SCHEMA_VERSION,
|
||||
packageName: ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME,
|
||||
generation: 1,
|
||||
health: "ready",
|
||||
currentState: release,
|
||||
previousState: inactive,
|
||||
runtime: runtime(1, 2, 2),
|
||||
acceptance: acceptanceReport(release, "accepted"),
|
||||
updatedAt: "2026-07-15T18:00:33.000Z",
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionStatus(activeStatus), { ok: true, errors: [] });
|
||||
|
||||
const rollbackRequest = {
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_PLAN_REQUEST_SCHEMA_VERSION,
|
||||
requestId: "extension-rollback-request-20260715-001",
|
||||
idempotencyKey: "extension-rollback-request-20260715-001",
|
||||
action: "rollback",
|
||||
requestedAt: "2026-07-15T18:05:00.000Z",
|
||||
requestExpiresAt: "2026-07-15T18:15:00.000Z",
|
||||
expectedCurrentGeneration: 1,
|
||||
target: { kind: "previous-state", packageName: ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME },
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionPlanRequest(rollbackRequest, {
|
||||
now: "2026-07-15T18:05:01.000Z",
|
||||
grantedCapabilities: manage,
|
||||
}), { ok: true, errors: [] });
|
||||
|
||||
const rollbackPlan = withPlanHash({
|
||||
schemaVersion: ENGINE_PRIVATE_EXTENSION_PLAN_SCHEMA_VERSION,
|
||||
planId: "extension-rollback-plan-20260715-001",
|
||||
planHash: hash("0"),
|
||||
requestId: rollbackRequest.requestId,
|
||||
idempotencyKey: rollbackRequest.idempotencyKey,
|
||||
action: "rollback",
|
||||
createdAt: "2026-07-15T18:05:01.000Z",
|
||||
expiresAt: "2026-07-15T18:14:00.000Z",
|
||||
singleUse: true,
|
||||
requiredCapability: ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY,
|
||||
expectedCurrentGeneration: 1,
|
||||
nextGeneration: 2,
|
||||
currentState: release,
|
||||
targetState: inactive,
|
||||
recoveryState: release,
|
||||
actions: [
|
||||
"verify_previous_activation_state",
|
||||
"verify_community_package_loader_policy",
|
||||
"quiesce_deploy_run_and_drain_queue",
|
||||
"record_recovery_state",
|
||||
"atomic_switch_current_to_previous",
|
||||
"force_recreate_main_workers_webhooks_as_version_barrier",
|
||||
"verify_exact_runtime_acceptance",
|
||||
"commit_rolled_back_state",
|
||||
"resume_deploy_run",
|
||||
],
|
||||
transition: transition(),
|
||||
acceptance: acceptanceSpec(inactive),
|
||||
failurePolicy: failurePolicy(),
|
||||
});
|
||||
assert.deepEqual(validateEnginePrivateExtensionPlan(rollbackPlan, { request: rollbackRequest }), { ok: true, errors: [] });
|
||||
|
||||
const explicitRollbackOperation = {
|
||||
...activeOperation,
|
||||
operationId: "extension-operation-rollback-20260715-001",
|
||||
planId: rollbackPlan.planId,
|
||||
planHash: rollbackPlan.planHash,
|
||||
action: "rollback",
|
||||
state: "rolled-back",
|
||||
outcome: "explicitly-rolled-back",
|
||||
expectedCurrentGeneration: 1,
|
||||
nextGeneration: 2,
|
||||
targetState: inactive,
|
||||
recoveryState: release,
|
||||
effectiveState: inactive,
|
||||
runtime: runtime(2, 2, 2),
|
||||
acceptance: acceptanceReport(inactive, "accepted"),
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionOperation(explicitRollbackOperation), { ok: true, errors: [] });
|
||||
|
||||
const quarantinedStatus = {
|
||||
...activeStatus,
|
||||
health: "quarantined",
|
||||
activeOperationId: "extension-operation-failed-20260715-001",
|
||||
acceptance: acceptanceReport(release, "rejected"),
|
||||
errorCode: "automatic_rollback_failed",
|
||||
};
|
||||
assert.deepEqual(validateEnginePrivateExtensionStatus(quarantinedStatus), { ok: true, errors: [] });
|
||||
|
||||
const pathInjection = structuredClone(activateRequest);
|
||||
pathInjection.target.releaseId = "../../runtime";
|
||||
assert.equal(validateEnginePrivateExtensionPlanRequest(pathInjection, {
|
||||
now: "2026-07-15T18:00:01.000Z",
|
||||
grantedCapabilities: manage,
|
||||
}).errors.includes("target.releaseId_invalid"), true);
|
||||
|
||||
const commandInjection = { ...activateRequest, command: "docker exec runtime npm install" };
|
||||
assert.equal(validateEnginePrivateExtensionPlanRequest(commandInjection, {
|
||||
now: "2026-07-15T18:00:01.000Z",
|
||||
grantedCapabilities: manage,
|
||||
}).errors.includes("enginePrivateExtensionPlanRequest.command_not_allowed"), true);
|
||||
|
||||
console.log("external-provider Engine private-extension contract: ok");
|
||||
|
||||
function transition() {
|
||||
return {
|
||||
mountMode: "read-only",
|
||||
loaderMode: "community-package",
|
||||
loaderPath: "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc",
|
||||
loaderEnvironment: {
|
||||
N8N_COMMUNITY_PACKAGES_ENABLED: "true",
|
||||
N8N_COMMUNITY_PACKAGES_PREVENT_LOADING: "false",
|
||||
N8N_REINSTALL_MISSING_PACKAGES: "false",
|
||||
},
|
||||
quiesceMode: "block-deploy-run-and-drain-queue",
|
||||
stateSwitch: "atomic-current-previous",
|
||||
runtimeAction: "force-recreate",
|
||||
scope: "main-workers-webhooks",
|
||||
requireUniformGeneration: true,
|
||||
hotReload: false,
|
||||
preserveCredentials: true,
|
||||
};
|
||||
}
|
||||
|
||||
function failurePolicy() {
|
||||
return {
|
||||
mode: "automatic-rollback",
|
||||
rollbackFailureOutcome: "quarantined",
|
||||
preserveImmutableRelease: true,
|
||||
preserveCredentials: true,
|
||||
};
|
||||
}
|
||||
|
||||
function acceptanceSpec(state) {
|
||||
return {
|
||||
mode: "exact",
|
||||
nodeTypes: state.kind === "release" ? [...ENGINE_PRIVATE_EXTENSION_NODE_TYPES] : [],
|
||||
credentialSchemas: state.kind === "release" ? [...ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES] : [],
|
||||
requireUniformGeneration: true,
|
||||
};
|
||||
}
|
||||
|
||||
function acceptanceReport(state, status) {
|
||||
const nodes = state.kind === "release" ? [...ENGINE_PRIVATE_EXTENSION_NODE_TYPES] : [];
|
||||
const credentials = state.kind === "release" ? [...ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES] : [];
|
||||
return {
|
||||
state: status,
|
||||
nodeTypes: { expected: nodes, observed: status === "pending" ? [] : nodes },
|
||||
credentialSchemas: { expected: credentials, observed: status === "pending" ? [] : credentials },
|
||||
uniformGeneration: status === "accepted",
|
||||
};
|
||||
}
|
||||
|
||||
function runtime(generation, expectedInstances, readyInstances) {
|
||||
return { mode: "force-recreate", generation, expectedInstances, readyInstances };
|
||||
}
|
||||
|
||||
function withPlanHash(plan) {
|
||||
plan.planHash = computeEnginePrivateExtensionPlanHash(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
function hash(character) {
|
||||
return "sha256:" + character.repeat(64);
|
||||
}
|
||||
Reference in New Issue
Block a user