feat(data-plane): add provider contracts and ontology delivery
This commit is contained in:
@@ -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