371 lines
13 KiB
JavaScript
371 lines
13 KiB
JavaScript
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);
|
|
}
|