feat: establish standalone Device Core repository
This commit is contained in:
@@ -0,0 +1,589 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
} from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { createControlCoreApp } from "../src/app.mjs";
|
||||
|
||||
const gatewayToken = "test-only-gateway-token-with-32-bytes";
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
const managementToken = "test-only-management-token-with-32-bytes";
|
||||
const fakeImei = "000000000000001";
|
||||
|
||||
test("health reports database readiness and disabled command transport", async () => {
|
||||
const runtime = await startTestServer({
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`${runtime.baseUrl}/healthz`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), {
|
||||
ok: true,
|
||||
service: "nodedc-device-control-core",
|
||||
database: "ready",
|
||||
discoveryIngest: "disabled",
|
||||
managementApi: "disabled",
|
||||
edgeChannels: {
|
||||
enabled: false,
|
||||
configured: 0,
|
||||
accepted: 0,
|
||||
degraded: 0,
|
||||
},
|
||||
commandTransport: "disabled",
|
||||
});
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API is closed by default", async () => {
|
||||
const runtime = await startTestServer({
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal((await response.json()).error, "device_management_api_disabled");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API cannot start without its repository boundary and strong token", () => {
|
||||
assert.throws(
|
||||
() => createControlCoreApp({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: { health: async () => "ready" },
|
||||
}),
|
||||
/device_management_repository_required/,
|
||||
);
|
||||
assert.throws(
|
||||
() => createControlCoreApp({
|
||||
managementApiEnabled: true,
|
||||
managementToken: "short",
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({}),
|
||||
},
|
||||
}),
|
||||
/device_management_token_invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() => createControlCoreApp({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({}),
|
||||
},
|
||||
}),
|
||||
/device_identifier_pepper_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("management API requires service auth and an idempotency key", async () => {
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => {
|
||||
throw new Error("must_not_execute");
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const unauthorized = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
},
|
||||
);
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const missingIdempotency = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders({ includeIdempotency: false }),
|
||||
body: JSON.stringify(ownerScopeCommand()),
|
||||
},
|
||||
);
|
||||
assert.equal(missingIdempotency.status, 400);
|
||||
assert.equal(
|
||||
(await missingIdempotency.json()).error,
|
||||
"device_management_header_required",
|
||||
);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API forwards only normalized actor and command data", async () => {
|
||||
let executed;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async (value) => {
|
||||
executed = value;
|
||||
return {
|
||||
replayed: false,
|
||||
result: {
|
||||
created: true,
|
||||
ownerScope: {
|
||||
ownerScopeRef: "owner-scope:11111111-1111-4111-8111-111111111111",
|
||||
scopeKind: "company",
|
||||
ownerRef: "client:example",
|
||||
displayName: "Example Company",
|
||||
lifecycleState: "active",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify(ownerScopeCommand()),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("idempotency-key"), "phase2-test-0001");
|
||||
assert.equal(response.headers.get("idempotency-replayed"), "false");
|
||||
assert.match(executed.requestDigest, /^sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(executed.commandKind, "owner_scope.ensure");
|
||||
assert.deepEqual(executed.command, ownerScopeCommand());
|
||||
assert.deepEqual(executed.actor, {
|
||||
userRef: "user:engineer",
|
||||
hubRole: "admin",
|
||||
groupRefs: ["group:engineers", "group:operators"],
|
||||
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
|
||||
});
|
||||
assert.equal((await response.json()).result.created, true);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API rejects unexpected fields before repository execution", async () => {
|
||||
let executions = 0;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => {
|
||||
executions += 1;
|
||||
return { replayed: false, result: {} };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify({
|
||||
...ownerScopeCommand(),
|
||||
credential: "must-never-cross-boundary",
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(
|
||||
(await response.json()).error,
|
||||
/device_management_command_field_unexpected:credential/,
|
||||
);
|
||||
assert.equal(executions, 0);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API exposes a repository idempotency conflict without retrying", async () => {
|
||||
let executions = 0;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => {
|
||||
executions += 1;
|
||||
const error = new Error("device_idempotency_key_conflict");
|
||||
error.statusCode = 409;
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify(ownerScopeCommand()),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 409);
|
||||
assert.equal((await response.json()).error, "device_idempotency_key_conflict");
|
||||
assert.equal(executions, 1);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("typed service ping accepts a transient access code and never echoes it", async () => {
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
||||
let planned;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
||||
},
|
||||
typedCommandRuntime: {
|
||||
status: () => ({ commandTransport: "typed-service-ping-v1" }),
|
||||
planServicePing: async (value) => {
|
||||
planned = value;
|
||||
return {
|
||||
replayed: false,
|
||||
command: {
|
||||
commandRef: "command:33333333-3333-4333-8333-333333333333",
|
||||
deviceRef,
|
||||
commandType: "service.ping",
|
||||
lifecycleState: "queued",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/commands:service-ping`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify({
|
||||
projectRef,
|
||||
deviceRef,
|
||||
accessCode: "654321",
|
||||
expiresInSeconds: 300,
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.result.lifecycleState, "queued");
|
||||
assert.equal(JSON.stringify(body).includes("654321"), false);
|
||||
assert.equal(planned.input.accessCode, "654321");
|
||||
assert.equal(planned.idempotencyKey, "phase2-test-0001");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("project query is service-authenticated and forwards only the trusted actor", async () => {
|
||||
let queriedActor;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
||||
listAccessibleProjects: async (actor) => {
|
||||
queriedActor = actor;
|
||||
return [{ projectRef: "project:11111111-1111-4111-8111-111111111111" }];
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const unauthorized = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects`,
|
||||
);
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects`,
|
||||
{ headers: managementHeaders() },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((await response.json()).projects.length, 1);
|
||||
assert.deepEqual(queriedActor, {
|
||||
userRef: "user:engineer",
|
||||
hubRole: "admin",
|
||||
groupRefs: ["group:engineers", "group:operators"],
|
||||
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
|
||||
});
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("project workspace query accepts only a canonical project path", async () => {
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
let queried;
|
||||
const runtime = await startTestServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
||||
getProjectWorkspace: async (actor, id) => {
|
||||
queried = { actor, id };
|
||||
return { project: { projectRef: `project:${id}` }, devices: [] };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects/${projectId}/workspace`,
|
||||
{ headers: managementHeaders() },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((await response.json()).workspace.devices.length, 0);
|
||||
assert.equal(queried.id, projectId);
|
||||
|
||||
const invalid = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects/not-a-project/workspace`,
|
||||
{ headers: managementHeaders() },
|
||||
);
|
||||
assert.equal(invalid.status, 404);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("discovery ingest is closed by default", async () => {
|
||||
const runtime = await startTestServer({
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(
|
||||
(await response.json()).error,
|
||||
"device_discovery_ingest_disabled",
|
||||
);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("authenticated ingest stores only digest and returns a masked view", async () => {
|
||||
let stored;
|
||||
const runtime = await startTestServer({
|
||||
discoveryIngestEnabled: true,
|
||||
gatewayToken,
|
||||
identifierPepper,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
upsertQuarantineDiscovery: async (value) => {
|
||||
stored = value;
|
||||
return {
|
||||
created: true,
|
||||
value: {
|
||||
...value.safeView,
|
||||
discoveryRef: "discovery:test-001",
|
||||
},
|
||||
};
|
||||
},
|
||||
acceptAdapterMessage: async () => {
|
||||
throw new Error("must_not_accept_message");
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const unauthorized = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(fakeSignal()),
|
||||
},
|
||||
);
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${gatewayToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(fakeSignal()),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 201);
|
||||
const body = await response.json();
|
||||
const serialized = JSON.stringify(body);
|
||||
assert.equal(serialized.includes(fakeImei), false);
|
||||
assert.equal(body.discovery.identifier.masked, "***********0001");
|
||||
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(stored.sessionRef, "session:test-001");
|
||||
assert.equal(stored.routeRef, null);
|
||||
assert.equal(JSON.stringify(stored).includes(fakeImei), false);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("gateway message endpoint returns acceptance only after repository commit", async () => {
|
||||
let stored;
|
||||
const runtime = await startTestServer({
|
||||
discoveryIngestEnabled: true,
|
||||
gatewayToken,
|
||||
identifierPepper,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
upsertQuarantineDiscovery: async () => {
|
||||
throw new Error("must_not_observe_discovery");
|
||||
},
|
||||
acceptAdapterMessage: async (value) => {
|
||||
stored = value;
|
||||
return {
|
||||
acceptance: {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test-001",
|
||||
idempotencyKey: value.safeView.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/gateway/messages:accept`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${gatewayToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(fakeAdapterMessage()),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 201);
|
||||
const body = await response.json();
|
||||
assert.equal(body.acceptance.status, "accepted");
|
||||
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.match(stored.requestDigest, /^sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(stored.safeView.identifier.masked, "***********0001");
|
||||
assert.equal(JSON.stringify(stored).includes(fakeImei), false);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
function fakeSignal() {
|
||||
return {
|
||||
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
sessionRef: "session:test-001",
|
||||
modelProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
observedAt: "2026-07-25T00:00:00.000Z",
|
||||
identifier: { kind: "imei", value: fakeImei },
|
||||
evidence: {
|
||||
transport: "tcp",
|
||||
bytesObserved: 128,
|
||||
framingStatus: "verified",
|
||||
specificationRef: "arusnavi.internal.framing.test-v1",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fakeAdapterMessage() {
|
||||
return {
|
||||
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
edgeRef: "edge:test-001",
|
||||
adapterRef: "arusnavi-b2",
|
||||
protocolProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
sessionRef: "session:test-001",
|
||||
messageRef: "package:1:test",
|
||||
messageType: "telemetry.package",
|
||||
sequence: 1,
|
||||
observedAt: "2026-08-11T12:00:00.000Z",
|
||||
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
||||
identifier: { kind: "imei", value: fakeImei },
|
||||
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
|
||||
payload: {
|
||||
packageNumber: 1,
|
||||
packetCount: 1,
|
||||
packageDigest: `sha256:${"b".repeat(64)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function managementHeaders({ includeIdempotency = true } = {}) {
|
||||
return {
|
||||
Authorization: `Bearer ${managementToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(includeIdempotency ? { "Idempotency-Key": "phase2-test-0001" } : {}),
|
||||
"X-NODEDC-User-Ref": "user:engineer",
|
||||
"X-NODEDC-Hub-Role": "admin",
|
||||
"X-NODEDC-Group-Refs": "group:operators,group:engineers",
|
||||
"X-NODEDC-Owner-Scopes": "company=client:example",
|
||||
};
|
||||
}
|
||||
|
||||
function ownerScopeCommand() {
|
||||
return {
|
||||
scopeKind: "company",
|
||||
ownerRef: "client:example",
|
||||
displayName: "Example Company",
|
||||
};
|
||||
}
|
||||
|
||||
async function startTestServer(options) {
|
||||
const server = createControlCoreApp({ identifierPepper, ...options });
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_CONTROL_RESOURCE_COMMAND_KINDS,
|
||||
normalizeControlResourceManagementCommand,
|
||||
} from "../src/control-resource-management.mjs";
|
||||
import {
|
||||
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
|
||||
normalizeDeviceManagementCommand,
|
||||
} from "../src/management-command.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
||||
const collectionRef = "collection:33333333-3333-4333-8333-333333333333";
|
||||
const bindingRef = "binding:44444444-4444-4444-8444-444444444444";
|
||||
const revisionRef =
|
||||
"configuration-revision:55555555-5555-4555-8555-555555555555";
|
||||
|
||||
test("control resource commands join the strict idempotent surface", () => {
|
||||
for (const kind of DEVICE_CONTROL_RESOURCE_COMMAND_KINDS) {
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
|
||||
}
|
||||
assert.equal(
|
||||
normalizeDeviceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
bindingInput(),
|
||||
).projectId,
|
||||
projectRef.slice("project:".length),
|
||||
);
|
||||
});
|
||||
|
||||
test("binding input is source-scoped and cannot claim external approval", () => {
|
||||
const command = normalizeControlResourceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
bindingInput(),
|
||||
);
|
||||
|
||||
assert.deepEqual(command.source, {
|
||||
kind: "collection",
|
||||
id: collectionRef.slice("collection:".length),
|
||||
});
|
||||
assert.deepEqual(command.capabilities, ["inspect", "observe"]);
|
||||
assert.equal("lifecycleState" in command, false);
|
||||
assert.equal("externalApprovalRef" in command, false);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
{ ...bindingInput(), externalApprovalRef: "approval:forged" },
|
||||
),
|
||||
/device_management_command_field_unexpected:externalApprovalRef/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_binding.ensure",
|
||||
{ ...bindingInput(), targetRef: "ndc-credref:must-not-be-a-target" },
|
||||
),
|
||||
/device_binding_target_ref_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("binding revoke uses only project, binding and bounded reason refs", () => {
|
||||
const command = normalizeControlResourceManagementCommand(
|
||||
"device_binding.revoke",
|
||||
{
|
||||
projectRef,
|
||||
bindingRef,
|
||||
resolutionCode: "operator.unbound",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.bindingId, bindingRef.slice("binding:".length));
|
||||
assert.equal(command.resolutionCode, "operator.unbound");
|
||||
});
|
||||
|
||||
test("configuration is canonical, bounded and secret-free before hashing", () => {
|
||||
const first = normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: {
|
||||
reporting_interval_seconds: 15,
|
||||
motion: { enabled: true, threshold: 3.5 },
|
||||
channels: ["gps", "voltage"],
|
||||
},
|
||||
changeSummary: "Pilot reporting profile",
|
||||
},
|
||||
);
|
||||
const reordered = normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: {
|
||||
channels: ["gps", "voltage"],
|
||||
motion: { threshold: 3.5, enabled: true },
|
||||
reporting_interval_seconds: 15,
|
||||
},
|
||||
changeSummary: "Pilot reporting profile",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(first.configurationDigest, reordered.configurationDigest);
|
||||
assert.equal(Object.isFrozen(first.configuration.motion), true);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: { api_token: "forbidden" },
|
||||
},
|
||||
),
|
||||
/forbidden_device_field/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeControlResourceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configuration: { tracker_imei: "000000000000001" },
|
||||
},
|
||||
),
|
||||
/safe_projection_contains_unmasked_imei/,
|
||||
);
|
||||
});
|
||||
|
||||
test("desired configuration binds one exact immutable revision", () => {
|
||||
const command = normalizeControlResourceManagementCommand(
|
||||
"device_configuration_desired.set",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
configurationRevisionRef: revisionRef,
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.deviceId, deviceRef.slice("device:".length));
|
||||
assert.equal(
|
||||
command.configurationRevisionId,
|
||||
revisionRef.slice("configuration-revision:".length),
|
||||
);
|
||||
assert.equal("applied" in command, false);
|
||||
});
|
||||
|
||||
function bindingInput() {
|
||||
return {
|
||||
projectRef,
|
||||
bindingKey: "robot2b-map",
|
||||
displayName: "Robot2B map binding",
|
||||
source: { kind: "collection", ref: collectionRef },
|
||||
targetKind: "foundry.application",
|
||||
targetRef: "foundry-application:robot2b-test",
|
||||
capabilities: ["observe", "inspect"],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const schemaUrl = new URL(
|
||||
"../migrations/010_device_control_resources.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const commandsUrl = new URL(
|
||||
"../migrations/011_device_control_resource_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
const appUrl = new URL("../src/app.mjs", import.meta.url);
|
||||
|
||||
test("control resource schema separates bindings, revisions and current state", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_resource_bindings/);
|
||||
assert.match(sql, /pending_external_approval/);
|
||||
assert.match(sql, /external_approval_digest/);
|
||||
assert.match(sql, /device_binding_source_scope_mismatch/);
|
||||
assert.match(sql, /create table if not exists device_configuration_revisions/);
|
||||
assert.match(sql, /create table if not exists device_configuration_state/);
|
||||
assert.match(sql, /device_configuration_revision_scope_mismatch/);
|
||||
assert.match(sql, /unique \(device_id, revision_number\)/);
|
||||
});
|
||||
|
||||
test("command ledger has honest ordered states without a transport API", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
const app = await readFile(appUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_commands/);
|
||||
assert.match(sql, /create table if not exists device_command_events/);
|
||||
for (const state of [
|
||||
"draft",
|
||||
"planned",
|
||||
"awaiting_confirmation",
|
||||
"queued",
|
||||
"dispatched",
|
||||
"acknowledged",
|
||||
"verified",
|
||||
"failed",
|
||||
"expired",
|
||||
"unknown",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`'${state}'`));
|
||||
}
|
||||
assert.match(sql, /device_command_initial_event_invalid/);
|
||||
assert.match(sql, /device_command_event_sequence_invalid/);
|
||||
assert.match(sql, /device_command_event_transition_invalid/);
|
||||
assert.match(sql, /device_command_event_projection_mismatch/);
|
||||
assert.match(sql, /device_command_events_current_projection_guard/);
|
||||
assert.match(sql, /device_command_current_projection_mismatch/);
|
||||
assert.doesNotMatch(app, /device-commands:(?:plan|confirm|dispatch)/);
|
||||
});
|
||||
|
||||
test("configuration, command history and audit are append-only", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
for (const table of [
|
||||
"device_configuration_revisions",
|
||||
"device_command_events",
|
||||
"device_audit_events",
|
||||
]) {
|
||||
assert.match(
|
||||
sql,
|
||||
new RegExp(`${table}_immutable_guard[\\s\\S]*before update or delete or truncate`),
|
||||
);
|
||||
}
|
||||
assert.match(sql, /device_immutable_record_mutation_forbidden/);
|
||||
assert.match(sql, /device_transfer_active_resource_binding/);
|
||||
assert.match(sql, /device_transfer_applied_configuration/);
|
||||
assert.match(sql, /device_transfer_nonterminal_command/);
|
||||
});
|
||||
|
||||
test("control resource schema contains no seeded device or raw secret material", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|gelios|\bb2\b|imei/i);
|
||||
assert.doesNotMatch(sql, /password\s+text|token\s+text|secret\s+text|raw_command|raw_packet/i);
|
||||
});
|
||||
|
||||
test("commands extend receipts only after their schema", async () => {
|
||||
const commands = await readFile(commandsUrl, "utf8");
|
||||
const repository = await readFile(repositoryUrl, "utf8");
|
||||
|
||||
for (const kind of [
|
||||
"device_binding.ensure",
|
||||
"device_binding.revoke",
|
||||
"device_configuration_revision.create",
|
||||
"device_configuration_desired.set",
|
||||
]) {
|
||||
assert.match(commands, new RegExp(`'${kind.replace(".", "\\.")}'`));
|
||||
}
|
||||
const schemaIndex = repository.indexOf("010_device_control_resources.sql");
|
||||
const commandsIndex = repository.indexOf(
|
||||
"011_device_control_resource_commands.sql",
|
||||
);
|
||||
assert.notEqual(schemaIndex, -1);
|
||||
assert.notEqual(commandsIndex, -1);
|
||||
assert.ok(schemaIndex < commandsIndex);
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
|
||||
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const ownerId = "22222222-2222-4222-8222-222222222222";
|
||||
const deviceId = "33333333-3333-4333-8333-333333333333";
|
||||
const collectionId = "44444444-4444-4444-8444-444444444444";
|
||||
const bindingId = "55555555-5555-4555-8555-555555555555";
|
||||
const revisionId = "66666666-6666-4666-8666-666666666666";
|
||||
|
||||
test("creates only a pending collection binding owned by the source project", async () => {
|
||||
const actor = managementActor();
|
||||
const command = normalizeDeviceManagementCommand("device_binding.ensure", {
|
||||
projectRef: `project:${projectId}`,
|
||||
bindingKey: "robot2b-map",
|
||||
displayName: "Robot2B map binding",
|
||||
source: { kind: "collection", ref: `collection:${collectionId}` },
|
||||
targetKind: "foundry.application",
|
||||
targetRef: "foundry-application:robot2b-test",
|
||||
capabilities: ["observe", "inspect"],
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-binding"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_collections", {
|
||||
rows: [{ id: collectionId, project_id: projectId, lifecycle_state: "active" }],
|
||||
}),
|
||||
step("insert into device_resource_bindings", {
|
||||
rows: [bindingRow({ source_kind: "collection", device_id: null })],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_binding.ensure",
|
||||
command,
|
||||
digestCharacter: "a",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.binding.lifecycleState, "pending_external_approval");
|
||||
assert.equal(result.result.binding.source.ref, `collection:${collectionId}`);
|
||||
assert.equal("externalApprovalRef" in result.result.binding, false);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("creates an immutable configuration revision from the active profile schema", async () => {
|
||||
const actor = managementActor();
|
||||
const command = createConfigurationCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-configuration-revision"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("from device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: "vendor.model.protocol.v1",
|
||||
schema_artifact_ref: "schema:vendor.model.protocol.v1",
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("from device_configuration_revisions", {
|
||||
rows: [{ next_revision: "1" }],
|
||||
}),
|
||||
step("insert into device_configuration_revisions", {
|
||||
rows: [configurationRevisionRow({
|
||||
configuration_digest: command.configurationDigest,
|
||||
configuration: command.configuration,
|
||||
})],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_configuration_revision.create",
|
||||
command,
|
||||
digestCharacter: "b",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.configurationRevision.revisionNumber, 1);
|
||||
assert.deepEqual(result.result.configurationRevision.configuration, {
|
||||
reporting_interval_seconds: 15,
|
||||
});
|
||||
assert.equal(
|
||||
result.result.configurationRevision.schemaArtifactRef,
|
||||
"schema:vendor.model.protocol.v1",
|
||||
);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("sets desired configuration without claiming runtime apply", async () => {
|
||||
const actor = managementActor();
|
||||
const command = normalizeDeviceManagementCommand(
|
||||
"device_configuration_desired.set",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
configurationRevisionRef: `configuration-revision:${revisionId}`,
|
||||
},
|
||||
);
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-configuration-desired"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("from device_configuration_revisions", {
|
||||
rows: [configurationRevisionRow()],
|
||||
}),
|
||||
step("from device_configuration_state", { rows: [] }),
|
||||
step("insert into device_configuration_state", {
|
||||
rows: [{
|
||||
device_id: deviceId,
|
||||
project_id: projectId,
|
||||
desired_revision_id: revisionId,
|
||||
applied_revision_id: null,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_configuration_desired.set",
|
||||
command,
|
||||
digestCharacter: "c",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.changed, true);
|
||||
assert.equal(
|
||||
result.result.configurationState.desiredConfigurationRevisionRef,
|
||||
`configuration-revision:${revisionId}`,
|
||||
);
|
||||
assert.equal(
|
||||
result.result.configurationState.appliedConfigurationRevisionRef,
|
||||
null,
|
||||
);
|
||||
assert.equal("applied" in result.result, false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
function createConfigurationCommand() {
|
||||
return normalizeDeviceManagementCommand(
|
||||
"device_configuration_revision.create",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
configuration: { reporting_interval_seconds: 15 },
|
||||
changeSummary: "Pilot reporting profile",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function managementActor() {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:device-engineer",
|
||||
hubRole: "member",
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
});
|
||||
}
|
||||
|
||||
function projectStep() {
|
||||
return step("from device_projects p", {
|
||||
rows: [{
|
||||
id: projectId,
|
||||
owner_scope_id: ownerId,
|
||||
lifecycle_state: "active",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example-company",
|
||||
owner_display_name: "Example Company",
|
||||
owner_lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function grantsStep(actor) {
|
||||
return step("from device_project_grants", {
|
||||
rows: [{
|
||||
id: "77777777-7777-4777-8777-777777777777",
|
||||
principal_kind: "user",
|
||||
principal_ref: actor.userRef,
|
||||
project_role: "engineer",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function deviceRow() {
|
||||
return {
|
||||
id: deviceId,
|
||||
contour_id: null,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
lifecycle_state: "claimed",
|
||||
};
|
||||
}
|
||||
|
||||
function bindingRow(overrides = {}) {
|
||||
return {
|
||||
id: bindingId,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
binding_key: "robot2b-map",
|
||||
display_name: "Robot2B map binding",
|
||||
source_kind: "device",
|
||||
device_id: deviceId,
|
||||
collection_id: collectionId,
|
||||
target_kind: "foundry.application",
|
||||
target_ref: "foundry-application:robot2b-test",
|
||||
capabilities: ["inspect", "observe"],
|
||||
lifecycle_state: "pending_external_approval",
|
||||
source_approved_at: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function configurationRevisionRow(overrides = {}) {
|
||||
return {
|
||||
id: revisionId,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
device_id: deviceId,
|
||||
revision_number: "1",
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
schema_artifact_ref: "schema:vendor.model.protocol.v1",
|
||||
configuration_digest: `sha256:${"d".repeat(64)}`,
|
||||
configuration: { reporting_interval_seconds: 15 },
|
||||
change_summary: "Pilot reporting profile",
|
||||
created_at: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function receiptStep(id) {
|
||||
return step("insert into device_management_command_receipts", {
|
||||
rows: [{ id }],
|
||||
});
|
||||
}
|
||||
|
||||
function commandInput({ actor, commandKind, command, digestCharacter }) {
|
||||
return {
|
||||
idempotencyKey: `phase25-${commandKind.replaceAll(".", "-")}-0001`,
|
||||
commandKind,
|
||||
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
|
||||
actor,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
function repositoryWithClient(client) {
|
||||
return new PostgresDeviceRepository({
|
||||
pool: {
|
||||
query: async () => ({ rows: [] }),
|
||||
connect: async () => client,
|
||||
end: async () => undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { resolveDeviceDatabaseUrl } from "../src/database-config.mjs";
|
||||
|
||||
test("builds the database URL from a file-backed password", async () => {
|
||||
const password = "test-only-database-password-with-32-bytes";
|
||||
const url = await resolveDeviceDatabaseUrl(
|
||||
{
|
||||
DEVICE_DATABASE_HOST: "device-postgres",
|
||||
DEVICE_DATABASE_PORT: "5432",
|
||||
DEVICE_DATABASE_NAME: "device_plane",
|
||||
DEVICE_DATABASE_USER: "device_plane",
|
||||
DEVICE_DATABASE_PASSWORD_FILE: "/run/test/postgres-password",
|
||||
},
|
||||
async (path, encoding) => {
|
||||
assert.equal(path, "/run/test/postgres-password");
|
||||
assert.equal(encoding, "utf8");
|
||||
return `${password}\n`;
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
url,
|
||||
`postgresql://device_plane:${encodeURIComponent(password)}@device-postgres:5432/device_plane?sslmode=disable`,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects a short file-backed database password", async () => {
|
||||
await assert.rejects(
|
||||
resolveDeviceDatabaseUrl(
|
||||
{
|
||||
DEVICE_DATABASE_HOST: "device-postgres",
|
||||
DEVICE_DATABASE_NAME: "device_plane",
|
||||
DEVICE_DATABASE_USER: "device_plane",
|
||||
DEVICE_DATABASE_PASSWORD_FILE: "/run/test/postgres-password",
|
||||
},
|
||||
async () => "too-short",
|
||||
),
|
||||
/device_database_password_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps an explicit database URL as a compatibility-only boundary", async () => {
|
||||
const explicit = "postgresql://local:test@127.0.0.1:5432/device_plane";
|
||||
assert.equal(
|
||||
await resolveDeviceDatabaseUrl({ DEVICE_DATABASE_URL: explicit }),
|
||||
explicit,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/013_device_edge_channels.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
test("Edge channel migration extends the canonical Edge without storing keys", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /alter table device_edges/);
|
||||
assert.match(sql, /channel_endpoint text/);
|
||||
assert.match(sql, /channel_generation_ref text/);
|
||||
assert.match(sql, /channel_trust_bundle_ref text/);
|
||||
assert.match(sql, /channel_certificate_identities jsonb/);
|
||||
assert.match(sql, /channel_lifecycle_state in \('disabled', 'active', 'revoked'\)/);
|
||||
assert.match(sql, /where channel_lifecycle_state = 'active'/);
|
||||
assert.doesNotMatch(sql, /private[_ ]?key/i);
|
||||
assert.doesNotMatch(sql, /password/i);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/015_device_integration_identity.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("integration identity is stored separately from display name and restricted identifiers", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
assert.match(sql, /add column if not exists integration_device_id text/i);
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
|
||||
});
|
||||
|
||||
test("integration identity migration follows registry profile commands", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
assert.ok(
|
||||
source.indexOf("014_device_registry_profile_commands.sql")
|
||||
< source.indexOf("015_device_integration_identity.sql"),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/007_device_lifecycle_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("lifecycle command migration extends the durable receipt allowlist", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
for (const kind of [
|
||||
"device.claim",
|
||||
"device.transfer",
|
||||
"discovery.reject",
|
||||
"discovery.expire",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`'${kind.replace(".", "\\.")}'`));
|
||||
}
|
||||
});
|
||||
|
||||
test("lifecycle command migration contains no runtime entity or secret", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
|
||||
assert.doesNotMatch(sql, /password|secret|credential|private_key/i);
|
||||
});
|
||||
|
||||
test("repository applies lifecycle commands after ownership schema", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const lifecycleIndex = source.indexOf("006_device_lifecycle_ownership.sql");
|
||||
const commandsIndex = source.indexOf("007_device_lifecycle_commands.sql");
|
||||
|
||||
assert.notEqual(lifecycleIndex, -1);
|
||||
assert.notEqual(commandsIndex, -1);
|
||||
assert.ok(lifecycleIndex < commandsIndex);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/006_device_lifecycle_ownership.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("lifecycle migration separates direct ownership from legacy contours", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /alter column contour_id drop not null/);
|
||||
assert.match(sql, /add column if not exists owner_scope_id uuid/);
|
||||
assert.match(sql, /device_instances_project_owner_fk/);
|
||||
assert.match(sql, /device_instances_ownership_mode_check/);
|
||||
assert.match(sql, /references device_projects\(id, owner_scope_id\)/);
|
||||
assert.match(sql, /\) not valid;/);
|
||||
});
|
||||
|
||||
test("route-bound discovery and enrollment evidence are DB constrained", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /device_discoveries_route_context_fk/);
|
||||
assert.match(sql, /device_discoveries_enrollment_context_fk/);
|
||||
assert.match(sql, /device_enrollment_observed_discovery_fk/);
|
||||
assert.match(sql, /device_enrollment_intents_active_identity_idx/);
|
||||
assert.match(sql, /where lifecycle_state in \('pending', 'observed', 'claimed'\)/);
|
||||
});
|
||||
|
||||
test("ownership history supports transfer without rewriting session provenance", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_ownership_transitions/);
|
||||
assert.match(sql, /transition_kind in \('claim', 'transfer'\)/);
|
||||
assert.match(sql, /device_ownership_single_claim_idx/);
|
||||
assert.match(sql, /device_assert_session_current_project/);
|
||||
assert.match(sql, /device_assert_enrollment_current_project/);
|
||||
assert.match(sql, /drop constraint if exists device_sessions_device_id_project_id_fkey/);
|
||||
});
|
||||
|
||||
test("lifecycle migration contains no tenant, device, route or credential seed", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
|
||||
assert.doesNotMatch(sql, /155\.212\.|device\.nodedc\.ru|synology/i);
|
||||
assert.doesNotMatch(sql, /password|secret|private_key|credential_ref/i);
|
||||
});
|
||||
|
||||
test("repository applies lifecycle migration after registry commands", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const commandsIndex = source.indexOf("005_device_registry_commands.sql");
|
||||
const lifecycleIndex = source.indexOf("006_device_lifecycle_ownership.sql");
|
||||
|
||||
assert.notEqual(commandsIndex, -1);
|
||||
assert.notEqual(lifecycleIndex, -1);
|
||||
assert.ok(commandsIndex < lifecycleIndex);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/005_device_registry_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("registry command migration extends the durable receipt allowlist", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
for (const kind of [
|
||||
"adapter_package.ensure",
|
||||
"adapter_version.register",
|
||||
"model_profile.register",
|
||||
"edge.ensure",
|
||||
"route.ensure",
|
||||
"enrollment_intent.ensure",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`'${kind.replace(".", "\\.")}'`));
|
||||
}
|
||||
assert.doesNotMatch(sql, /session\.(ensure|create|upsert)/);
|
||||
});
|
||||
|
||||
test("registry command migration contains no environment or device data", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
|
||||
assert.doesNotMatch(sql, /password|secret|credential|private_key/i);
|
||||
});
|
||||
|
||||
test("repository applies registry commands after registry schema", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const schemaIndex = source.indexOf("004_device_registry_foundation.sql");
|
||||
const commandsIndex = source.indexOf("005_device_registry_commands.sql");
|
||||
|
||||
assert.notEqual(schemaIndex, -1);
|
||||
assert.notEqual(commandsIndex, -1);
|
||||
assert.ok(schemaIndex < commandsIndex);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/004_device_registry_foundation.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("registry migration defines generic catalog, edge and runtime boundaries", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
for (const table of [
|
||||
"device_adapter_packages",
|
||||
"device_adapter_versions",
|
||||
"device_edges",
|
||||
"device_routes",
|
||||
"device_sessions",
|
||||
"device_enrollment_intents",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`create table if not exists ${table}`));
|
||||
}
|
||||
assert.match(sql, /add column if not exists adapter_version_id uuid/);
|
||||
assert.match(sql, /content_digest ~ '\^sha256:\[a-f0-9\]\{64\}\$'/);
|
||||
assert.match(sql, /expected_identifier_digest ~ '\^hmac-sha256:\[a-f0-9\]\{64\}\$'/);
|
||||
});
|
||||
|
||||
test("registry migration enforces project, route, edge and device isolation", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(
|
||||
sql,
|
||||
/foreign key \(route_id, edge_id, project_id\)\s+references device_routes\(id, edge_id, project_id\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/foreign key \(route_id, project_id, model_profile_ref\)\s+references device_routes\(id, project_id, model_profile_ref\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/foreign key \(device_id, project_id\)\s+references device_instances\(id, project_id\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/foreign key \(claimed_device_id, project_id\)\s+references device_instances\(id, project_id\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("registry migration stores no device, tenant, network or credential seed", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
|
||||
assert.doesNotMatch(sql, /155\.212\.|device\.nodedc\.ru|synology/i);
|
||||
assert.doesNotMatch(sql, /password|secret|private_key|credential_ref/i);
|
||||
});
|
||||
|
||||
test("repository applies registry migration after management receipts", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const managementIndex = source.indexOf("003_device_management_commands.sql");
|
||||
const registryIndex = source.indexOf("004_device_registry_foundation.sql");
|
||||
|
||||
assert.notEqual(managementIndex, -1);
|
||||
assert.notEqual(registryIndex, -1);
|
||||
assert.ok(managementIndex < registryIndex);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/014_device_registry_profile_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("registry profile command migration enables the bounded device update", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /'device\.update'/);
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
|
||||
assert.doesNotMatch(sql, /password|secret|private_key|private-key|token\s*=/i);
|
||||
});
|
||||
|
||||
test("repository applies the profile command after edge channel schema", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const edgeChannelIndex = source.indexOf("013_device_edge_channels.sql");
|
||||
const profileCommandIndex = source.indexOf("014_device_registry_profile_commands.sql");
|
||||
|
||||
assert.notEqual(edgeChannelIndex, -1);
|
||||
assert.notEqual(profileCommandIndex, -1);
|
||||
assert.ok(edgeChannelIndex < profileCommandIndex);
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { observeQuarantineDiscovery } from "../src/discovery-repository.mjs";
|
||||
|
||||
const observedAt = "2026-08-10T00:00:00.000Z";
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const routeId = "22222222-2222-4222-8222-222222222222";
|
||||
const enrollmentId = "33333333-3333-4333-8333-333333333333";
|
||||
const discoveryId = "44444444-4444-4444-8444-444444444444";
|
||||
const identifierDigest = `hmac-sha256:${"a".repeat(64)}`;
|
||||
|
||||
test("observation expires stale intents before matching an identity", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/discovery-repository.mjs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(source, /expires_at <= \$6/);
|
||||
assert.match(source, /expires_at is null or expires_at > \$6/);
|
||||
assert.match(source, /resolution_code = 'deadline_elapsed'/);
|
||||
});
|
||||
|
||||
test("legacy discovery remains unbound quarantine without a route reference", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_discoveries", {
|
||||
rows: [discoveryRow({ project_id: null, route_id: null })],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
|
||||
const result = await observeQuarantineDiscovery({
|
||||
pool: poolWithClient(client),
|
||||
identifierDigest,
|
||||
safeView: safeView(),
|
||||
sessionRef: "session:legacy-test",
|
||||
});
|
||||
|
||||
assert.equal(result.created, true);
|
||||
assert.equal("routeRef" in result.value, false);
|
||||
assert.equal("enrollmentIntentRef" in result.value, false);
|
||||
assertSafeProjection(result.value);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("route-bound discovery atomically observes only its matching enrollment", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("from device_routes", {
|
||||
rows: [{
|
||||
id: routeId,
|
||||
project_id: projectId,
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
protocol: "GENERIC_TCP",
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("update device_enrollment_intents"),
|
||||
step("from device_enrollment_intents", {
|
||||
rows: [{
|
||||
id: enrollmentId,
|
||||
project_id: projectId,
|
||||
route_id: routeId,
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
lifecycle_state: "pending",
|
||||
}],
|
||||
}),
|
||||
step("insert into device_discoveries", {
|
||||
rows: [discoveryRow({
|
||||
project_id: projectId,
|
||||
route_id: routeId,
|
||||
enrollment_intent_id: enrollmentId,
|
||||
})],
|
||||
}),
|
||||
step("update device_enrollment_intents", {
|
||||
rows: [{ id: enrollmentId }],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
|
||||
const result = await observeQuarantineDiscovery({
|
||||
pool: poolWithClient(client),
|
||||
identifierDigest,
|
||||
safeView: safeView(`route:${routeId}`),
|
||||
sessionRef: "session:route-test",
|
||||
routeRef: `route:${routeId}`,
|
||||
});
|
||||
|
||||
assert.equal(result.value.routeRef, `route:${routeId}`);
|
||||
assert.equal(
|
||||
result.value.enrollmentIntentRef,
|
||||
`enrollment-intent:${enrollmentId}`,
|
||||
);
|
||||
assertSafeProjection(result.value);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("inactive or mismatched routes fail before a discovery is stored", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("from device_routes", {
|
||||
rows: [{
|
||||
id: routeId,
|
||||
project_id: projectId,
|
||||
model_profile_ref: "other.profile.v1",
|
||||
protocol: "OTHER_TCP",
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
|
||||
await assert.rejects(
|
||||
observeQuarantineDiscovery({
|
||||
pool: poolWithClient(client),
|
||||
identifierDigest,
|
||||
safeView: safeView(`route:${routeId}`),
|
||||
sessionRef: "session:mismatch-test",
|
||||
routeRef: `route:${routeId}`,
|
||||
}),
|
||||
/device_discovery_route_profile_mismatch/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function safeView(routeRef = null) {
|
||||
return {
|
||||
schemaVersion: "nodedc.device.discovery-view.v1",
|
||||
...(routeRef ? { routeRef } : {}),
|
||||
modelProfileRef: "vendor.model.protocol.v1",
|
||||
protocol: "GENERIC_TCP",
|
||||
observedAt,
|
||||
lifecycleState: "quarantine",
|
||||
identifier: { kind: "serial", masked: "********0001" },
|
||||
evidence: {
|
||||
transport: "tcp",
|
||||
bytesObserved: 32,
|
||||
framingStatus: "verified",
|
||||
specificationRef: "vendor.protocol.v1",
|
||||
},
|
||||
commandTransport: "disabled",
|
||||
};
|
||||
}
|
||||
|
||||
function discoveryRow(overrides = {}) {
|
||||
return {
|
||||
id: discoveryId,
|
||||
lifecycle_state: "quarantine",
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
protocol: "GENERIC_TCP",
|
||||
identifier_kind: "serial",
|
||||
identifier_masked: "********0001",
|
||||
first_observed_at: new Date(observedAt),
|
||||
last_observed_at: new Date(observedAt),
|
||||
evidence: safeView().evidence,
|
||||
project_id: null,
|
||||
route_id: null,
|
||||
enrollment_intent_id: null,
|
||||
created: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function poolWithClient(client) {
|
||||
return { connect: async () => client };
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
createDeviceEdgeChannelSupervisor,
|
||||
} from "../src/edge-channel-supervisor.mjs";
|
||||
|
||||
test("supervisor reconciles one in-process client per active Edge", async () => {
|
||||
let registrations = [registration("channel-generation:1")];
|
||||
const clients = [];
|
||||
const ingestCalls = [];
|
||||
const supervisor = createDeviceEdgeChannelSupervisor({
|
||||
repository: {
|
||||
listActiveEdgeChannelRegistrations: async () => registrations,
|
||||
},
|
||||
gatewayIngest: {
|
||||
observeDiscovery: async (...args) => ingestCalls.push(["discovery", ...args]),
|
||||
acceptMessage: async (...args) => ingestCalls.push(["message", ...args]),
|
||||
},
|
||||
coreIdentity: {
|
||||
identityRef: "workload:device-control-core",
|
||||
key: "test-key",
|
||||
cert: "test-cert",
|
||||
},
|
||||
readPeerTrust: async () => "test-edge-certificate",
|
||||
clientFactory: (options) => {
|
||||
const state = { started: 0, stopped: 0, options };
|
||||
clients.push(state);
|
||||
return {
|
||||
async start() { state.started += 1; },
|
||||
async stop() { state.stopped += 1; },
|
||||
status: () => ({ channel: "accepted", lastErrorCode: null }),
|
||||
};
|
||||
},
|
||||
reconcileIntervalMs: 300_000,
|
||||
});
|
||||
|
||||
await supervisor.start();
|
||||
assert.equal(clients.length, 1);
|
||||
assert.equal(supervisor.status().accepted, 1);
|
||||
assert.equal(clients[0].options.registration.channelGeneration, "channel-generation:1");
|
||||
await clients[0].options.observeDiscovery({ signal: true });
|
||||
await clients[0].options.acceptMessage({ message: true });
|
||||
assert.deepEqual(ingestCalls, [
|
||||
[
|
||||
"discovery",
|
||||
{ signal: true },
|
||||
{ authenticatedEdgeRef: "edge:pilot" },
|
||||
],
|
||||
[
|
||||
"message",
|
||||
{ message: true },
|
||||
{ authenticatedEdgeRef: "edge:pilot" },
|
||||
],
|
||||
]);
|
||||
|
||||
await supervisor.reconcile();
|
||||
assert.equal(clients.length, 1);
|
||||
|
||||
registrations = [registration("channel-generation:2")];
|
||||
await supervisor.reconcile();
|
||||
assert.equal(clients.length, 2);
|
||||
assert.equal(clients[0].stopped, 1);
|
||||
assert.equal(clients[1].started, 1);
|
||||
|
||||
registrations = [];
|
||||
await supervisor.reconcile();
|
||||
assert.equal(clients[1].stopped, 1);
|
||||
assert.equal(supervisor.status().configured, 0);
|
||||
assert.equal(JSON.stringify(supervisor.status()).includes("155.212"), false);
|
||||
await supervisor.stop();
|
||||
});
|
||||
|
||||
test("supervisor keeps a failed trust enrollment isolated from other Edges", async () => {
|
||||
const supervisor = createDeviceEdgeChannelSupervisor({
|
||||
repository: {
|
||||
listActiveEdgeChannelRegistrations: async () => [
|
||||
registration("channel-generation:1", "edge:good"),
|
||||
registration("channel-generation:1", "edge:bad"),
|
||||
],
|
||||
},
|
||||
gatewayIngest: {
|
||||
observeDiscovery: async () => ({}),
|
||||
acceptMessage: async () => ({}),
|
||||
},
|
||||
coreIdentity: {
|
||||
identityRef: "workload:device-control-core",
|
||||
key: "test-key",
|
||||
cert: "test-cert",
|
||||
},
|
||||
readPeerTrust: async ({ registration: value }) => {
|
||||
if (value.edgeRegistrationId === "edge:bad") {
|
||||
throw new Error("device_edge_channel_trust_bundle_identity_mismatch");
|
||||
}
|
||||
return "test-edge-certificate";
|
||||
},
|
||||
clientFactory: () => ({
|
||||
start: async () => undefined,
|
||||
stop: async () => undefined,
|
||||
status: () => ({ channel: "accepted", lastErrorCode: null }),
|
||||
}),
|
||||
reconcileIntervalMs: 300_000,
|
||||
});
|
||||
|
||||
await supervisor.start();
|
||||
const status = supervisor.status();
|
||||
assert.equal(status.configured, 2);
|
||||
assert.equal(status.accepted, 1);
|
||||
assert.equal(status.degraded, 1);
|
||||
assert.equal(status.commandTransport, "disabled");
|
||||
assert.equal(status.edges.find((item) => item.edgeRegistrationId === "edge:bad")
|
||||
.lastErrorCode, "device_edge_channel_trust_bundle_identity_mismatch");
|
||||
await supervisor.stop();
|
||||
});
|
||||
|
||||
function registration(channelGeneration, edgeRegistrationId = "edge:pilot") {
|
||||
return {
|
||||
edgeRegistrationId,
|
||||
endpoint: "https://155.212.211.15/",
|
||||
servername: "155.212.211.15",
|
||||
channelGeneration,
|
||||
trustBundleRef: "edge-trust:moscow-edge",
|
||||
certificateIdentities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
lifecycleState: "active",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
} from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { createDeviceGatewayIngest } from "../src/gateway-ingest.mjs";
|
||||
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
const rawImei = "000000000000001";
|
||||
|
||||
test("shared gateway ingest masks identifiers for HTTP and Edge callers", async () => {
|
||||
const stored = [];
|
||||
const ingest = createDeviceGatewayIngest({
|
||||
identifierPepper,
|
||||
repository: {
|
||||
async upsertQuarantineDiscovery(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
created: true,
|
||||
value: { ...value.safeView, discoveryRef: "discovery:test" },
|
||||
};
|
||||
},
|
||||
async acceptAdapterMessage(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
acceptance: {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test",
|
||||
idempotencyKey: value.safeView.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const discovery = await ingest.observeDiscovery(discoverySignal());
|
||||
const acceptance = await ingest.acceptMessage(adapterMessage());
|
||||
|
||||
assert.equal(discovery.value.identifier.masked, "***********0001");
|
||||
assert.equal(acceptance.value.status, "accepted");
|
||||
assert.equal(
|
||||
acceptance.claimedDeviceRef,
|
||||
"device:11111111-1111-4111-8111-111111111111",
|
||||
);
|
||||
assert.match(stored[0].identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.match(stored[1].requestDigest, /^sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(JSON.stringify(stored).includes(rawImei), false);
|
||||
});
|
||||
|
||||
test("authenticated Edge identity resolves the allowlisted project route", async () => {
|
||||
const stored = [];
|
||||
const resolutions = [];
|
||||
const authenticatedEdgeRef = "edge:11111111-1111-4111-8111-111111111111";
|
||||
const routeRef = "route:22222222-2222-4222-8222-222222222222";
|
||||
const ingest = createDeviceGatewayIngest({
|
||||
identifierPepper,
|
||||
repository: {
|
||||
async resolveInboundRoute(value) {
|
||||
resolutions.push(value);
|
||||
return routeRef;
|
||||
},
|
||||
async upsertQuarantineDiscovery(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
created: false,
|
||||
value: { ...value.safeView, discoveryRef: "discovery:test" },
|
||||
};
|
||||
},
|
||||
async acceptAdapterMessage(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
acceptance: {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test",
|
||||
idempotencyKey: value.safeView.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
claimedDeviceRef: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await ingest.observeDiscovery(discoverySignal(), { authenticatedEdgeRef });
|
||||
await ingest.acceptMessage(adapterMessage(), { authenticatedEdgeRef });
|
||||
|
||||
assert.equal(resolutions.length, 2);
|
||||
assert.equal(resolutions[0].edgeRef, authenticatedEdgeRef);
|
||||
assert.match(resolutions[0].identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(stored[0].safeView.routeRef, routeRef);
|
||||
assert.equal(stored[1].safeView.routeRef, routeRef);
|
||||
assert.equal(stored[1].safeView.edgeRef, authenticatedEdgeRef);
|
||||
assert.notEqual(stored[1].safeView.edgeRef, adapterMessage().edgeRef);
|
||||
assert.equal(JSON.stringify(resolutions).includes(rawImei), false);
|
||||
});
|
||||
|
||||
function discoverySignal() {
|
||||
return {
|
||||
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
sessionRef: "session:test",
|
||||
modelProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
observedAt: "2026-08-11T12:00:00.000Z",
|
||||
identifier: { kind: "imei", value: rawImei },
|
||||
evidence: {
|
||||
transport: "tcp",
|
||||
bytesObserved: 16,
|
||||
framingStatus: "verified",
|
||||
specificationRef: "arusnavi.internal.framing.test-v1",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function adapterMessage() {
|
||||
return {
|
||||
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
edgeRef: "edge:test",
|
||||
adapterRef: "arusnavi-b2",
|
||||
protocolProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
sessionRef: "session:test",
|
||||
messageRef: "package:1:test",
|
||||
messageType: "telemetry.package",
|
||||
sequence: 1,
|
||||
observedAt: "2026-08-11T12:00:00.000Z",
|
||||
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
||||
identifier: { kind: "imei", value: rawImei },
|
||||
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
|
||||
payload: {
|
||||
packageNumber: 1,
|
||||
packetCount: 1,
|
||||
packageDigest: `sha256:${"b".repeat(64)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/012_device_gateway_message_receipts.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("gateway receipts persist only typed bounded Core acceptance evidence", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_gateway_message_receipts/);
|
||||
assert.match(sql, /unique \(idempotency_key\)/);
|
||||
assert.match(sql, /unique \(edge_ref, session_ref, message_ref\)/);
|
||||
assert.match(sql, /identifier_digest text not null/);
|
||||
assert.match(sql, /identifier_masked text not null/);
|
||||
assert.match(sql, /payload_schema_ref text not null/);
|
||||
assert.match(sql, /payload jsonb not null/);
|
||||
assert.match(sql, /device_gateway_message_receipts_immutable_guard/);
|
||||
assert.match(sql, /execute function device_reject_immutable_mutation\(\)/);
|
||||
assert.doesNotMatch(sql, /execute function reject_device_immutable_record_mutation\(\)/);
|
||||
assert.doesNotMatch(sql, /raw_packet|raw_identifier|password|token|secret/i);
|
||||
assert.doesNotMatch(sql, /insert\s+into|arusnavi|gelios|\bb2\b|imei/i);
|
||||
});
|
||||
|
||||
test("gateway receipt migration follows the generic control resource schema", async () => {
|
||||
const repository = await readFile(repositoryUrl, "utf8");
|
||||
const controlResourceIndex = repository.indexOf(
|
||||
"011_device_control_resource_commands.sql",
|
||||
);
|
||||
const gatewayReceiptIndex = repository.indexOf(
|
||||
"012_device_gateway_message_receipts.sql",
|
||||
);
|
||||
|
||||
assert.notEqual(controlResourceIndex, -1);
|
||||
assert.notEqual(gatewayReceiptIndex, -1);
|
||||
assert.ok(controlResourceIndex < gatewayReceiptIndex);
|
||||
assert.doesNotMatch(repository, /arusnavi-b2-adapter/);
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { acceptGatewayMessage } from "../src/gateway-message-repository.mjs";
|
||||
|
||||
const acceptedAt = new Date("2026-08-11T12:00:00.000Z");
|
||||
const idempotencyKey = `sha256:${"a".repeat(64)}`;
|
||||
const requestDigest = `sha256:${"b".repeat(64)}`;
|
||||
|
||||
test("commits a gateway receipt before returning Core acceptance", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_gateway_message_receipts", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
idempotency_key: idempotencyKey,
|
||||
accepted_at: acceptedAt,
|
||||
}],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
|
||||
const result = await acceptGatewayMessage(messageInput(client));
|
||||
|
||||
assert.equal(result.acceptance.status, "accepted");
|
||||
assert.equal(result.acceptance.replayed, false);
|
||||
assert.equal(result.acceptance.idempotencyKey, idempotencyKey);
|
||||
assert.equal(result.acceptance.acceptedAt, acceptedAt.toISOString());
|
||||
assert.equal(result.claimedDeviceRef, null);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("replays one durable receipt for the same normalized request", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_gateway_message_receipts", { rows: [] }),
|
||||
step("from device_gateway_message_receipts", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
idempotency_key: idempotencyKey,
|
||||
request_digest: requestDigest,
|
||||
accepted_at: acceptedAt,
|
||||
}],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
|
||||
const result = await acceptGatewayMessage(messageInput(client));
|
||||
|
||||
assert.equal(result.acceptance.status, "accepted");
|
||||
assert.equal(result.acceptance.replayed, true);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("rejects idempotency reuse with different content", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_gateway_message_receipts", { rows: [] }),
|
||||
step("from device_gateway_message_receipts", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
idempotency_key: idempotencyKey,
|
||||
request_digest: `sha256:${"c".repeat(64)}`,
|
||||
accepted_at: acceptedAt,
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
|
||||
await assert.rejects(
|
||||
acceptGatewayMessage(messageInput(client)),
|
||||
/device_gateway_idempotency_conflict/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("fails closed when a route does not match its Edge contract", async () => {
|
||||
const routeId = "22222222-2222-4222-8222-222222222222";
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("from device_routes r", {
|
||||
rows: [{
|
||||
id: routeId,
|
||||
project_id: "33333333-3333-4333-8333-333333333333",
|
||||
edge_id: "44444444-4444-4444-8444-444444444444",
|
||||
model_profile_ref: "generic.model.protocol.v1",
|
||||
protocol: "GENERIC_TCP",
|
||||
lifecycle_state: "active",
|
||||
edge_lifecycle_state: "active",
|
||||
profile_lifecycle_state: "active",
|
||||
adapter_ref: "generic-adapter",
|
||||
adapter_lifecycle_state: "active",
|
||||
adapter_version_lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
const input = messageInput(client);
|
||||
input.safeView.routeRef = `route:${routeId}`;
|
||||
input.safeView.edgeRef = "edge:55555555-5555-4555-8555-555555555555";
|
||||
|
||||
await assert.rejects(
|
||||
acceptGatewayMessage(input),
|
||||
/device_gateway_route_contract_mismatch/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function messageInput(client) {
|
||||
return {
|
||||
pool: {
|
||||
connect: async () => client,
|
||||
},
|
||||
identifierDigest: `hmac-sha256:${"d".repeat(64)}`,
|
||||
requestDigest,
|
||||
safeView: {
|
||||
edgeRef: "edge:test-001",
|
||||
adapterRef: "generic-adapter",
|
||||
protocolProfileRef: "generic.model.protocol.v1",
|
||||
protocol: "GENERIC_TCP",
|
||||
sessionRef: "session:test-001",
|
||||
messageRef: "message:test-001",
|
||||
messageType: "telemetry.sample",
|
||||
sequence: 1,
|
||||
idempotencyKey,
|
||||
identifier: {
|
||||
kind: "serial",
|
||||
masked: "********0001",
|
||||
},
|
||||
payloadSchemaRef: "generic.telemetry.v1",
|
||||
payload: { value: 1 },
|
||||
observedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
resolveInboundRoute,
|
||||
} from "../src/inbound-route-repository.mjs";
|
||||
|
||||
const edgeRef = "edge:11111111-1111-4111-8111-111111111111";
|
||||
const routeId = "22222222-2222-4222-8222-222222222222";
|
||||
const identifierDigest = `hmac-sha256:${"a".repeat(64)}`;
|
||||
|
||||
test("inbound route resolution is scoped by authenticated Edge and enrollment", async () => {
|
||||
const queries = [];
|
||||
const client = {
|
||||
async query(sql, values) {
|
||||
queries.push({ sql, values });
|
||||
return { rows: [{ id: routeId }] };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await resolveInboundRoute(client, input());
|
||||
|
||||
assert.equal(result, `route:${routeId}`);
|
||||
assert.equal(queries.length, 1);
|
||||
assert.match(queries[0].sql, /device_enrollment_intents/);
|
||||
assert.match(queries[0].sql, /r\.edge_id = \$1/);
|
||||
assert.match(queries[0].sql, /ei\.expected_identifier_digest = \$5/);
|
||||
assert.deepEqual(queries[0].values, [
|
||||
edgeRef.slice("edge:".length),
|
||||
"arusnavi.b2.internal.v1",
|
||||
"INTERNAL",
|
||||
"imei",
|
||||
identifierDigest,
|
||||
"2026-08-13T09:00:00.000Z",
|
||||
]);
|
||||
});
|
||||
|
||||
test("inbound route resolution leaves unknown identifiers quarantined", async () => {
|
||||
const result = await resolveInboundRoute(
|
||||
{ query: async () => ({ rows: [] }) },
|
||||
input(),
|
||||
);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("inbound route resolution fails closed on ambiguous ownership", async () => {
|
||||
await assert.rejects(
|
||||
() => resolveInboundRoute(
|
||||
{ query: async () => ({ rows: [{ id: routeId }, { id: routeId }] }) },
|
||||
input(),
|
||||
),
|
||||
(error) => {
|
||||
assert.equal(error.message, "device_inbound_route_ambiguous");
|
||||
assert.equal(error.statusCode, 409);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function input() {
|
||||
return {
|
||||
edgeRef,
|
||||
modelProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
identifierKind: "imei",
|
||||
identifierDigest,
|
||||
observedAt: "2026-08-13T09:00:00.000Z",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createControlCoreApp } from "../src/app.mjs";
|
||||
|
||||
const managementToken = "test-only-management-token-with-32-bytes";
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
|
||||
test("management API forwards a normalized generic Edge registration", async () => {
|
||||
let executed;
|
||||
const runtime = await startServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async (input) => {
|
||||
executed = input;
|
||||
return { replayed: false, result: { created: true } };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/edges:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify({
|
||||
edgeKey: "generic-edge",
|
||||
displayName: "Generic Edge",
|
||||
deploymentRef: "deployment:device-edge/pilot",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(executed.commandKind, "edge.ensure");
|
||||
assert.deepEqual(executed.command, {
|
||||
edgeKey: "generic-edge",
|
||||
displayName: "Generic Edge",
|
||||
deploymentRef: "deployment:device-edge/pilot",
|
||||
lifecycleState: "provisioning",
|
||||
});
|
||||
assert.equal(executed.actor.hubRole, "owner");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API exposes no user-owned session mutation", async () => {
|
||||
let executions = 0;
|
||||
const runtime = await startServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => {
|
||||
executions += 1;
|
||||
return { replayed: false, result: {} };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/sessions:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: "{}",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(executions, 0);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API forwards claim as evidence references without identity input", async () => {
|
||||
let executed;
|
||||
const runtime = await startServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async (input) => {
|
||||
executed = input;
|
||||
return { replayed: false, result: { created: true } };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/devices:claim`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify({
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
enrollmentIntentRef:
|
||||
"enrollment-intent:22222222-2222-4222-8222-222222222222",
|
||||
discoveryRef: "discovery:33333333-3333-4333-8333-333333333333",
|
||||
deviceKey: "pilot-device",
|
||||
displayName: "Pilot device",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(executed.commandKind, "device.claim");
|
||||
assert.equal(executed.command.deviceKey, "pilot-device");
|
||||
assert.equal("identifier" in executed.command, false);
|
||||
assert.equal("credentialRef" in executed.command, false);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API derives enrollment identity inside Core and never forwards raw IMEI", async () => {
|
||||
let executed;
|
||||
const runtime = await startServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async (input) => {
|
||||
executed = input;
|
||||
return {
|
||||
replayed: false,
|
||||
result: {
|
||||
enrollmentIntent: {
|
||||
identifier: {
|
||||
kind: input.command.identifierKind,
|
||||
masked: input.command.identifierMasked,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const rawImei = "123456789012345";
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/enrollment-intents:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...managementHeaders(),
|
||||
"Idempotency-Key": "phase25-enrollment-0001",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
enrollmentKey: "pilot-device",
|
||||
routeRef: "route:22222222-2222-4222-8222-222222222222",
|
||||
modelProfileRef: "arusnavi.b2.v1",
|
||||
displayName: "Pilot device",
|
||||
identifier: { kind: "imei", value: rawImei },
|
||||
expiresAt: null,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const body = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(executed.command.identifierKind, "imei");
|
||||
assert.equal(executed.command.identifierMasked, "***********2345");
|
||||
assert.match(executed.command.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(JSON.stringify(executed).includes(rawImei), false);
|
||||
assert.equal(JSON.stringify(body).includes(rawImei), false);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API rejects client-supplied enrollment digests", async () => {
|
||||
let executions = 0;
|
||||
const runtime = await startServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => {
|
||||
executions += 1;
|
||||
return { replayed: false, result: {} };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/enrollment-intents:ensure`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...managementHeaders(),
|
||||
"Idempotency-Key": "phase25-enrollment-reject-0001",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
enrollmentKey: "pilot-device",
|
||||
routeRef: "route:22222222-2222-4222-8222-222222222222",
|
||||
modelProfileRef: "arusnavi.b2.v1",
|
||||
displayName: "Pilot device",
|
||||
identifier: { kind: "imei", value: "123456789012345" },
|
||||
identifierDigest: `hmac-sha256:${"a".repeat(64)}`,
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(
|
||||
(await response.json()).error,
|
||||
"device_enrollment_input_field_unexpected",
|
||||
);
|
||||
assert.equal(executions, 0);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("management API accepts only a canonical credential reference", async () => {
|
||||
let executed;
|
||||
const runtime = await startServer({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async (input) => {
|
||||
executed = input;
|
||||
return {
|
||||
replayed: false,
|
||||
result: {
|
||||
credentialBinding: {
|
||||
credentialBindingRef:
|
||||
"credential-binding:44444444-4444-4444-8444-444444444444",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/device-credential-bindings:upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: managementHeaders(),
|
||||
body: JSON.stringify({
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
deviceRef: "device:22222222-2222-4222-8222-222222222222",
|
||||
purpose: "tracker.command",
|
||||
credentialRef: {
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(
|
||||
executed.commandKind,
|
||||
"device_credential_binding.upsert",
|
||||
);
|
||||
assert.deepEqual(executed.command.credentialRef, {
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
});
|
||||
|
||||
const rejected = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/management/device-credential-bindings:upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...managementHeaders(),
|
||||
"Idempotency-Key": "phase24-credential-invalid-0001",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
deviceRef: "device:22222222-2222-4222-8222-222222222222",
|
||||
purpose: "tracker.command",
|
||||
credentialRef: {
|
||||
owner: "device_core",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(rejected.status, 400);
|
||||
assert.equal(
|
||||
(await rejected.json()).error,
|
||||
"ndc_credential_reference_owner_invalid",
|
||||
);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
async function startServer(options) {
|
||||
const server = createControlCoreApp({ identifierPepper, ...options });
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function managementHeaders() {
|
||||
return {
|
||||
Authorization: `Bearer ${managementToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": "phase23-edge-0001",
|
||||
"X-NODEDC-User-Ref": "user:platform-owner",
|
||||
"X-NODEDC-Hub-Role": "owner",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const repositorySource = new URL(
|
||||
"../src/infrastructure-repository.mjs",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
test("catalog, Edge and route upserts enforce irreversible lifecycle transitions", async () => {
|
||||
const source = await readFile(repositorySource, "utf8");
|
||||
|
||||
assert.match(source, /device_adapter_versions\.lifecycle_state = 'draft'[\s\S]*excluded\.lifecycle_state in \('active', 'retired'\)/);
|
||||
assert.match(source, /device_model_profiles\.lifecycle_state = 'active'[\s\S]*excluded\.lifecycle_state = 'retired'/);
|
||||
assert.match(source, /device_edges\.lifecycle_state = 'suspended'[\s\S]*excluded\.lifecycle_state in \('active', 'retired'\)/);
|
||||
assert.match(source, /device_routes\.lifecycle_state = 'draft'[\s\S]*excluded\.lifecycle_state in \('active', 'retired'\)/);
|
||||
assert.doesNotMatch(source, /device_(?:adapter_versions|model_profiles|edges|routes)\.lifecycle_state = 'retired'[\s\S]{0,160}excluded\.lifecycle_state = 'active'/);
|
||||
});
|
||||
|
||||
test("legacy model adoption is a one-way exact-identity registry transition", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/infrastructure-repository.mjs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(source, /profile ->> 'schemaVersion' = excluded\.schema_version/);
|
||||
assert.match(source, /profile ->> 'profileRef' = excluded\.profile_ref/);
|
||||
assert.match(source, /profile ->> 'vendor' = excluded\.vendor/);
|
||||
assert.match(source, /profile ->> 'model' = excluded\.model/);
|
||||
assert.match(source, /profile ->> 'deviceType' = excluded\.device_type/);
|
||||
assert.match(source, /profile ->> 'protocol' = excluded\.protocol/);
|
||||
assert.match(source, /adapter_version_id is null[\s\S]*schema_artifact_ref is null[\s\S]*profile_digest is null[\s\S]*cardinality\(device_model_profiles\.capabilities\) = 0[\s\S]*lifecycle_state = 'active'[\s\S]*excluded\.lifecycle_state = 'draft'/);
|
||||
});
|
||||
|
||||
test("adopted rich profile lifecycle changes preserve exact registry identity", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/infrastructure-repository.mjs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(source, /profile = excluded\.profile[\s\S]*or \([\s\S]*profile ->> 'schemaVersion' = excluded\.schema_version[\s\S]*profile ->> 'profileRef' = excluded\.profile_ref[\s\S]*profile ->> 'vendor' = excluded\.vendor[\s\S]*profile ->> 'model' = excluded\.model[\s\S]*profile ->> 'deviceType' = excluded\.device_type[\s\S]*profile ->> 'protocol' = excluded\.protocol/);
|
||||
assert.match(source, /adapter_version_id = excluded\.adapter_version_id[\s\S]*schema_artifact_ref = excluded\.schema_artifact_ref[\s\S]*profile_digest = excluded\.profile_digest[\s\S]*capabilities = excluded\.capabilities/);
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
assertPlatformCatalogAuthority,
|
||||
DEVICE_INFRASTRUCTURE_COMMAND_KINDS,
|
||||
normalizeInfrastructureManagementCommand,
|
||||
} from "../src/infrastructure-management.mjs";
|
||||
import {
|
||||
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
|
||||
normalizeDeviceManagementCommand,
|
||||
} from "../src/management-command.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const packageRef = "adapter-package:22222222-2222-4222-8222-222222222222";
|
||||
const versionRef = "adapter-version:33333333-3333-4333-8333-333333333333";
|
||||
const edgeRef = "edge:44444444-4444-4444-8444-444444444444";
|
||||
const routeRef = "route:55555555-5555-4555-8555-555555555555";
|
||||
const digest = `sha256:${"a".repeat(64)}`;
|
||||
const identifierDigest = `hmac-sha256:${"b".repeat(64)}`;
|
||||
|
||||
test("aggregates project and infrastructure commands without a session mutation", () => {
|
||||
for (const kind of DEVICE_INFRASTRUCTURE_COMMAND_KINDS) {
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
|
||||
}
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes("project.ensure"), true);
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes("session.ensure"), false);
|
||||
assert.throws(
|
||||
() => normalizeDeviceManagementCommand("session.ensure", {}),
|
||||
/device_management_command_kind_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes immutable adapter version metadata and sorted capabilities", () => {
|
||||
const command = normalizeInfrastructureManagementCommand(
|
||||
"adapter_version.register",
|
||||
{
|
||||
adapterPackageRef: packageRef,
|
||||
version: "1.2.3",
|
||||
runtimePackageRef: "artifact:device-adapters/generic-1.2.3",
|
||||
contentDigest: digest,
|
||||
contractVersion: "nodedc.device-adapter.v1",
|
||||
capabilities: ["telemetry.observe", "command.typed", "telemetry.observe"],
|
||||
lifecycleState: "active",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.adapterPackageId, packageRef.slice("adapter-package:".length));
|
||||
assert.deepEqual(command.capabilities, ["command.typed", "telemetry.observe"]);
|
||||
assert.equal(command.contentDigest, digest);
|
||||
});
|
||||
|
||||
test("normalizes a generic model profile as artifact metadata, not executable payload", () => {
|
||||
const command = normalizeInfrastructureManagementCommand(
|
||||
"model_profile.register",
|
||||
{
|
||||
adapterVersionRef: versionRef,
|
||||
profileRef: "vendor.model.protocol.v1",
|
||||
schemaVersion: "nodedc.device-model-profile.v1",
|
||||
vendor: "Example Vendor",
|
||||
model: "Model One",
|
||||
deviceType: "tracker",
|
||||
protocol: "GENERIC_TCP",
|
||||
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
|
||||
profileDigest: digest,
|
||||
capabilities: ["telemetry.observe"],
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.adapterVersionId, versionRef.slice("adapter-version:".length));
|
||||
assert.equal(command.protocol, "GENERIC_TCP");
|
||||
assert.equal(command.lifecycleState, "draft");
|
||||
assert.equal("profile" in command, false);
|
||||
assert.equal("source" in command, false);
|
||||
});
|
||||
|
||||
test("route and enrollment commands resolve only scoped references", () => {
|
||||
const route = normalizeInfrastructureManagementCommand("route.ensure", {
|
||||
projectRef,
|
||||
routeKey: "primary-ingress",
|
||||
displayName: "Primary ingress",
|
||||
edgeRef,
|
||||
modelProfileRef: "vendor.model.protocol.v1",
|
||||
listenerRef: "listener:generic-tcp-primary",
|
||||
protocol: "GENERIC_TCP",
|
||||
direction: "bidirectional",
|
||||
});
|
||||
const enrollment = normalizeInfrastructureManagementCommand(
|
||||
"enrollment_intent.ensure",
|
||||
{
|
||||
projectRef,
|
||||
enrollmentKey: "pilot-device",
|
||||
routeRef,
|
||||
modelProfileRef: "vendor.model.protocol.v1",
|
||||
displayName: "Pilot device",
|
||||
identifierKind: "serial",
|
||||
identifierDigest,
|
||||
identifierMasked: "********0001",
|
||||
expiresAt: "2026-09-01T00:00:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(route.projectId, projectRef.slice("project:".length));
|
||||
assert.equal(route.edgeId, edgeRef.slice("edge:".length));
|
||||
assert.equal(enrollment.routeId, routeRef.slice("route:".length));
|
||||
assert.equal(enrollment.identifierDigest, identifierDigest);
|
||||
});
|
||||
|
||||
test("enrollment contract rejects raw identifiers and credential-shaped fields", () => {
|
||||
const base = {
|
||||
projectRef,
|
||||
enrollmentKey: "pilot-device",
|
||||
routeRef,
|
||||
modelProfileRef: "vendor.model.protocol.v1",
|
||||
displayName: "Pilot device",
|
||||
identifierKind: "imei",
|
||||
identifierDigest,
|
||||
identifierMasked: "***********0001",
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand(
|
||||
"enrollment_intent.ensure",
|
||||
{ ...base, identifierMasked: "000000000000001" },
|
||||
),
|
||||
/safe_projection_contains_unmasked_imei/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand(
|
||||
"enrollment_intent.ensure",
|
||||
{ ...base, credential: "forbidden" },
|
||||
),
|
||||
/device_management_command_field_unexpected:credential/,
|
||||
);
|
||||
});
|
||||
|
||||
test("shared catalog and Edge authority requires the Hub owner ceiling", () => {
|
||||
assert.doesNotThrow(() => assertPlatformCatalogAuthority(actor("owner")));
|
||||
assert.throws(
|
||||
() => assertPlatformCatalogAuthority(actor("admin")),
|
||||
/device_platform_catalog_access_denied/,
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes only a pinned Core-initiated public Edge channel", () => {
|
||||
const command = normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "moscow-edge",
|
||||
displayName: "Moscow Edge",
|
||||
deploymentRef: "deployment:device-edge/moscow-1",
|
||||
lifecycleState: "active",
|
||||
channel: {
|
||||
endpoint: "https://155.212.211.15/",
|
||||
servername: "155.212.211.15",
|
||||
generationRef: "channel-generation:1",
|
||||
trustBundleRef: "edge-trust:moscow-edge",
|
||||
certificateIdentities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
lifecycleState: "active",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(command.channel.endpoint, "https://155.212.211.15/");
|
||||
assert.equal(command.channel.lifecycleState, "active");
|
||||
assert.equal(command.channel.certificateIdentities.length, 1);
|
||||
for (const endpoint of [
|
||||
"https://127.0.0.1/",
|
||||
"https://192.168.1.1/",
|
||||
"https://155.212.211.15:8443/",
|
||||
"https://155.212.211.15:9921/",
|
||||
"http://155.212.211.15/",
|
||||
]) {
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "bad-edge",
|
||||
displayName: "Bad Edge",
|
||||
channel: { ...command.channel, endpoint },
|
||||
}),
|
||||
/device_edge_channel_endpoint_invalid/,
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "bad-edge",
|
||||
displayName: "Bad Edge",
|
||||
channel: { ...command.channel, servername: "example.invalid" },
|
||||
}),
|
||||
/device_edge_channel_servername_mismatch/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "bad-edge",
|
||||
displayName: "Bad Edge",
|
||||
channel: { lifecycleState: "disabled", endpoint: command.channel.endpoint },
|
||||
}),
|
||||
/device_edge_channel_disabled_configuration_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
function actor(hubRole) {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:platform-admin",
|
||||
hubRole,
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
|
||||
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const edgeId = "22222222-2222-4222-8222-222222222222";
|
||||
const routeId = "33333333-3333-4333-8333-333333333333";
|
||||
const adapterPackageId = "44444444-4444-4444-8444-444444444444";
|
||||
const adapterVersionId = "55555555-5555-4555-8555-555555555555";
|
||||
|
||||
test("commits an owner-authorized generic Edge registration", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = normalizeDeviceManagementCommand("edge.ensure", {
|
||||
edgeKey: "generic-edge",
|
||||
displayName: "Generic Edge",
|
||||
deploymentRef: "deployment:device-edge/pilot",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-edge" }],
|
||||
}),
|
||||
step("insert into device_edges", {
|
||||
rows: [{
|
||||
id: edgeId,
|
||||
edge_key: command.edgeKey,
|
||||
display_name: command.displayName,
|
||||
deployment_ref: command.deploymentRef,
|
||||
lifecycle_state: command.lifecycleState,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: true,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "edge.ensure",
|
||||
command,
|
||||
digestCharacter: "a",
|
||||
}));
|
||||
|
||||
assert.equal(result.replayed, false);
|
||||
assert.equal(result.result.edge.edgeRef, `edge:${edgeId}`);
|
||||
assert.equal(result.result.edge.lifecycleState, "provisioning");
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("adopts a legacy metadata-only model profile into the versioned registry", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = normalizeDeviceManagementCommand("model_profile.register", {
|
||||
adapterVersionRef: `adapter-version:${adapterVersionId}`,
|
||||
profileRef: "vendor.model.protocol.v1",
|
||||
schemaVersion: "nodedc.device-model-profile.v1",
|
||||
vendor: "Example Vendor",
|
||||
model: "Model One",
|
||||
deviceType: "tracker",
|
||||
protocol: "GENERIC_TCP",
|
||||
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
|
||||
profileDigest: `sha256:${"e".repeat(64)}`,
|
||||
capabilities: ["telemetry.observe"],
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-profile" }],
|
||||
}),
|
||||
step("from device_adapter_versions av", {
|
||||
rows: [{
|
||||
id: adapterVersionId,
|
||||
adapter_package_id: adapterPackageId,
|
||||
version: "1.0.0",
|
||||
runtime_package_ref: "artifact:device-adapters/vendor-model-1.0.0",
|
||||
content_digest: `sha256:${"f".repeat(64)}`,
|
||||
contract_version: "nodedc.device-adapter.v1",
|
||||
capabilities: ["telemetry.observe"],
|
||||
lifecycle_state: "active",
|
||||
package_lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}],
|
||||
}),
|
||||
step("from device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: command.profileRef,
|
||||
adapter_version_id: null,
|
||||
schema_artifact_ref: null,
|
||||
profile_digest: null,
|
||||
capabilities: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("insert into device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: command.profileRef,
|
||||
schema_version: command.schemaVersion,
|
||||
vendor: command.vendor,
|
||||
model: command.model,
|
||||
device_type: command.deviceType,
|
||||
protocol: command.protocol,
|
||||
adapter_version_id: adapterVersionId,
|
||||
schema_artifact_ref: command.schemaArtifactRef,
|
||||
profile_digest: command.profileDigest,
|
||||
capabilities: command.capabilities,
|
||||
lifecycle_state: "draft",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: false,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "model_profile.register",
|
||||
command,
|
||||
digestCharacter: "e",
|
||||
}));
|
||||
|
||||
assert.equal(result.replayed, false);
|
||||
assert.equal(result.result.created, false);
|
||||
assert.equal(
|
||||
result.result.modelProfile.adapterVersionRef,
|
||||
`adapter-version:${adapterVersionId}`,
|
||||
);
|
||||
assert.equal(result.result.modelProfile.lifecycleState, "draft");
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("keeps non-legacy model profile identity conflicts fail-closed", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = normalizeDeviceManagementCommand("model_profile.register", {
|
||||
adapterVersionRef: `adapter-version:${adapterVersionId}`,
|
||||
profileRef: "vendor.model.protocol.v1",
|
||||
schemaVersion: "nodedc.device-model-profile.v1",
|
||||
vendor: "Example Vendor",
|
||||
model: "Model One",
|
||||
deviceType: "tracker",
|
||||
protocol: "GENERIC_TCP",
|
||||
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
|
||||
profileDigest: `sha256:${"e".repeat(64)}`,
|
||||
capabilities: ["telemetry.observe"],
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-profile-conflict" }],
|
||||
}),
|
||||
step("from device_adapter_versions av", {
|
||||
rows: [{
|
||||
id: adapterVersionId,
|
||||
adapter_package_id: adapterPackageId,
|
||||
lifecycle_state: "active",
|
||||
package_lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("from device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: command.profileRef,
|
||||
adapter_version_id: null,
|
||||
schema_artifact_ref: "artifact:legacy-but-partial",
|
||||
profile_digest: null,
|
||||
capabilities: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("insert into device_model_profiles"),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "model_profile.register",
|
||||
command,
|
||||
digestCharacter: "f",
|
||||
})),
|
||||
/device_model_profile_identity_conflict/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("activates an adopted rich legacy profile without replacing its immutable JSON", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = normalizeDeviceManagementCommand("model_profile.register", {
|
||||
adapterVersionRef: `adapter-version:${adapterVersionId}`,
|
||||
profileRef: "vendor.model.protocol.v1",
|
||||
schemaVersion: "nodedc.device-model-profile.v1",
|
||||
vendor: "Example Vendor",
|
||||
model: "Model One",
|
||||
deviceType: "tracker",
|
||||
protocol: "GENERIC_TCP",
|
||||
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
|
||||
profileDigest: `sha256:${"e".repeat(64)}`,
|
||||
capabilities: ["telemetry.observe"],
|
||||
lifecycleState: "active",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-profile-activation" }],
|
||||
}),
|
||||
step("from device_adapter_versions av", {
|
||||
rows: [{
|
||||
id: adapterVersionId,
|
||||
adapter_package_id: adapterPackageId,
|
||||
version: "1.0.0",
|
||||
runtime_package_ref: "artifact:device-adapters/vendor-model-1.0.0",
|
||||
content_digest: `sha256:${"f".repeat(64)}`,
|
||||
contract_version: "nodedc.device-adapter.v1",
|
||||
capabilities: ["telemetry.observe"],
|
||||
lifecycle_state: "active",
|
||||
package_lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}],
|
||||
}),
|
||||
step("from device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: command.profileRef,
|
||||
adapter_version_id: adapterVersionId,
|
||||
schema_artifact_ref: command.schemaArtifactRef,
|
||||
profile_digest: command.profileDigest,
|
||||
capabilities: command.capabilities,
|
||||
lifecycle_state: "draft",
|
||||
}],
|
||||
}),
|
||||
step("insert into device_model_profiles", {
|
||||
rows: [{
|
||||
profile_ref: command.profileRef,
|
||||
schema_version: command.schemaVersion,
|
||||
vendor: command.vendor,
|
||||
model: command.model,
|
||||
device_type: command.deviceType,
|
||||
protocol: command.protocol,
|
||||
adapter_version_id: adapterVersionId,
|
||||
schema_artifact_ref: command.schemaArtifactRef,
|
||||
profile_digest: command.profileDigest,
|
||||
capabilities: command.capabilities,
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: false,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "model_profile.register",
|
||||
command,
|
||||
digestCharacter: "d",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.created, false);
|
||||
assert.equal(result.result.modelProfile.lifecycleState, "active");
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("denies project route mutation without an explicit project grant", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = normalizeDeviceManagementCommand("route.ensure", {
|
||||
projectRef: `project:${projectId}`,
|
||||
routeKey: "generic-ingress",
|
||||
displayName: "Generic ingress",
|
||||
edgeRef: `edge:${edgeId}`,
|
||||
modelProfileRef: "vendor.model.protocol.v1",
|
||||
listenerRef: "listener:generic-tcp-primary",
|
||||
protocol: "GENERIC_TCP",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-route" }],
|
||||
}),
|
||||
step("from device_projects p", {
|
||||
rows: [{
|
||||
id: projectId,
|
||||
lifecycle_state: "active",
|
||||
owner_lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("from device_project_grants", { rows: [] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "route.ensure",
|
||||
command,
|
||||
digestCharacter: "b",
|
||||
})),
|
||||
/device_project_capability_denied/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("stores enrollment digest but returns and audits only its masked projection", async () => {
|
||||
const actor = managementActor("member");
|
||||
const command = normalizeDeviceManagementCommand(
|
||||
"enrollment_intent.ensure",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
enrollmentKey: "pilot-device",
|
||||
routeRef: `route:${routeId}`,
|
||||
modelProfileRef: "vendor.model.protocol.v1",
|
||||
displayName: "Pilot device",
|
||||
identifierKind: "serial",
|
||||
identifierDigest: `hmac-sha256:${"c".repeat(64)}`,
|
||||
identifierMasked: "********0001",
|
||||
expiresAt: "2026-09-01T00:00:00.000Z",
|
||||
},
|
||||
);
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-enrollment" }],
|
||||
}),
|
||||
step("from device_projects p", {
|
||||
rows: [{
|
||||
id: projectId,
|
||||
lifecycle_state: "active",
|
||||
owner_lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("from device_project_grants", {
|
||||
rows: [{
|
||||
id: "44444444-4444-4444-8444-444444444444",
|
||||
principal_kind: "user",
|
||||
principal_ref: actor.userRef,
|
||||
project_role: "engineer",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("from device_routes", {
|
||||
rows: [{
|
||||
id: routeId,
|
||||
project_id: projectId,
|
||||
route_key: "generic-ingress",
|
||||
display_name: "Generic ingress",
|
||||
edge_id: edgeId,
|
||||
model_profile_ref: command.modelProfileRef,
|
||||
listener_ref: "listener:generic-tcp-primary",
|
||||
protocol: "GENERIC_TCP",
|
||||
direction: "telemetry",
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_enrollment_intents", {
|
||||
rows: [{
|
||||
id: "55555555-5555-4555-8555-555555555555",
|
||||
project_id: projectId,
|
||||
enrollment_key: command.enrollmentKey,
|
||||
route_id: routeId,
|
||||
model_profile_ref: command.modelProfileRef,
|
||||
display_name: command.displayName,
|
||||
expected_identifier_kind: command.identifierKind,
|
||||
expected_identifier_masked: command.identifierMasked,
|
||||
lifecycle_state: "pending",
|
||||
expires_at: new Date(command.expiresAt),
|
||||
claimed_device_id: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: true,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "enrollment_intent.ensure",
|
||||
command,
|
||||
digestCharacter: "d",
|
||||
}));
|
||||
|
||||
assertSafeProjection(result.result.enrollmentIntent);
|
||||
assert.equal(
|
||||
result.result.enrollmentIntent.identifier.masked,
|
||||
command.identifierMasked,
|
||||
);
|
||||
assert.equal(JSON.stringify(result.result).includes(command.identifierDigest), false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("lists only bounded active Edge channel registrations without secrets", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin transaction read only"),
|
||||
step("from device_edges", {
|
||||
rows: [{
|
||||
id: edgeId,
|
||||
channel_endpoint: "https://155.212.211.15/",
|
||||
channel_servername: "155.212.211.15",
|
||||
channel_generation_ref: "channel-generation:1",
|
||||
channel_trust_bundle_ref: "edge-trust:moscow-edge",
|
||||
channel_certificate_identities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
channel_lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const registrations = await repository.listActiveEdgeChannelRegistrations(8);
|
||||
|
||||
assert.deepEqual(registrations[0], {
|
||||
edgeRegistrationId: `edge:${edgeId}`,
|
||||
endpoint: "https://155.212.211.15/",
|
||||
servername: "155.212.211.15",
|
||||
channelGeneration: "channel-generation:1",
|
||||
trustBundleRef: "edge-trust:moscow-edge",
|
||||
certificateIdentities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
lifecycleState: "active",
|
||||
});
|
||||
assert.equal(JSON.stringify(registrations).includes("private"), false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function managementActor(hubRole) {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:platform-owner",
|
||||
hubRole,
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
});
|
||||
}
|
||||
|
||||
function commandInput({ actor, commandKind, command, digestCharacter }) {
|
||||
return {
|
||||
idempotencyKey: `phase23-${commandKind.replaceAll(".", "-")}-0001`,
|
||||
commandKind,
|
||||
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
|
||||
actor,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
function repositoryWithClient(client) {
|
||||
return new PostgresDeviceRepository({
|
||||
pool: {
|
||||
query: async () => ({ rows: [] }),
|
||||
connect: async () => client,
|
||||
end: async () => undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_LIFECYCLE_COMMAND_KINDS,
|
||||
normalizeLifecycleManagementCommand,
|
||||
} from "../src/lifecycle-management.mjs";
|
||||
import {
|
||||
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
|
||||
normalizeDeviceManagementCommand,
|
||||
} from "../src/management-command.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const targetProjectRef = "project:22222222-2222-4222-8222-222222222222";
|
||||
const discoveryRef = "discovery:33333333-3333-4333-8333-333333333333";
|
||||
const enrollmentIntentRef =
|
||||
"enrollment-intent:44444444-4444-4444-8444-444444444444";
|
||||
const deviceRef = "device:55555555-5555-4555-8555-555555555555";
|
||||
|
||||
test("lifecycle commands join the same strict management command surface", () => {
|
||||
for (const kind of DEVICE_LIFECYCLE_COMMAND_KINDS) {
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
|
||||
}
|
||||
assert.equal(
|
||||
normalizeDeviceManagementCommand("device.claim", claimInput()).projectId,
|
||||
projectRef.slice("project:".length),
|
||||
);
|
||||
});
|
||||
|
||||
test("claim accepts only opaque evidence references and presentation fields", () => {
|
||||
const command = normalizeLifecycleManagementCommand(
|
||||
"device.claim",
|
||||
claimInput(),
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
command.enrollmentIntentId,
|
||||
enrollmentIntentRef.slice("enrollment-intent:".length),
|
||||
);
|
||||
assert.equal(command.discoveryId, discoveryRef.slice("discovery:".length));
|
||||
assert.equal(command.deviceKey, "pilot-device");
|
||||
assert.equal("identifier" in command, false);
|
||||
assert.equal("credentialRef" in command, false);
|
||||
});
|
||||
|
||||
test("claim rejects raw identity and credential-shaped input", () => {
|
||||
assert.throws(
|
||||
() => normalizeLifecycleManagementCommand("device.claim", {
|
||||
...claimInput(),
|
||||
identifier: "000000000000001",
|
||||
}),
|
||||
/device_management_command_field_unexpected:identifier/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeLifecycleManagementCommand("device.claim", {
|
||||
...claimInput(),
|
||||
credentialRef: "secret:test",
|
||||
}),
|
||||
/device_management_command_field_unexpected:credentialRef/,
|
||||
);
|
||||
});
|
||||
|
||||
test("device update keeps display name and integration identity as separate presentation fields", () => {
|
||||
const command = normalizeLifecycleManagementCommand("device.update", {
|
||||
projectRef,
|
||||
deviceRef,
|
||||
displayName: " Trike 8028 ",
|
||||
integrationDeviceId: " 8028 ",
|
||||
});
|
||||
|
||||
assert.equal(command.projectId, projectRef.slice("project:".length));
|
||||
assert.equal(command.deviceId, deviceRef.slice("device:".length));
|
||||
assert.equal(command.displayName, "Trike 8028");
|
||||
assert.equal(command.integrationDeviceId, "8028");
|
||||
assert.throws(
|
||||
() => normalizeLifecycleManagementCommand("device.update", {
|
||||
projectRef,
|
||||
deviceRef,
|
||||
displayName: "Trike 8028",
|
||||
integrationDeviceId: "8028",
|
||||
identifier: "000000000000001",
|
||||
}),
|
||||
/device_management_command_field_unexpected:identifier/,
|
||||
);
|
||||
});
|
||||
|
||||
test("legacy device update does not implicitly clear the integration identity", () => {
|
||||
const command = normalizeLifecycleManagementCommand("device.update", {
|
||||
projectRef,
|
||||
deviceRef,
|
||||
displayName: "Trike 8028",
|
||||
});
|
||||
assert.equal(command.integrationDeviceId, undefined);
|
||||
});
|
||||
|
||||
test("transfer binds both project boundaries and rejects a no-op", () => {
|
||||
const command = normalizeLifecycleManagementCommand("device.transfer", {
|
||||
deviceRef,
|
||||
sourceProjectRef: projectRef,
|
||||
targetProjectRef,
|
||||
targetDeviceKey: "transferred-device",
|
||||
});
|
||||
|
||||
assert.equal(command.deviceId, deviceRef.slice("device:".length));
|
||||
assert.notEqual(command.sourceProjectId, command.targetProjectId);
|
||||
assert.throws(
|
||||
() => normalizeLifecycleManagementCommand("device.transfer", {
|
||||
deviceRef,
|
||||
sourceProjectRef: projectRef,
|
||||
targetProjectRef: projectRef,
|
||||
targetDeviceKey: "same-project",
|
||||
}),
|
||||
/device_transfer_target_same_as_source/,
|
||||
);
|
||||
});
|
||||
|
||||
test("reject and expire require bounded machine-readable resolution codes", () => {
|
||||
for (const kind of ["discovery.reject", "discovery.expire"]) {
|
||||
const command = normalizeLifecycleManagementCommand(kind, {
|
||||
projectRef,
|
||||
discoveryRef,
|
||||
resolutionCode: "operator.identity_mismatch",
|
||||
});
|
||||
assert.equal(command.resolutionCode, "operator.identity_mismatch");
|
||||
}
|
||||
assert.throws(
|
||||
() => normalizeLifecycleManagementCommand("discovery.reject", {
|
||||
projectRef,
|
||||
discoveryRef,
|
||||
resolutionCode: "free form reason is forbidden",
|
||||
}),
|
||||
/device_discovery_resolution_code_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
function claimInput() {
|
||||
return {
|
||||
projectRef,
|
||||
enrollmentIntentRef,
|
||||
discoveryRef,
|
||||
deviceKey: "pilot-device",
|
||||
displayName: "Pilot device",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
|
||||
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const sourceProjectId = "11111111-1111-4111-8111-111111111111";
|
||||
const targetProjectId = "22222222-2222-4222-8222-222222222222";
|
||||
const sourceOwnerId = "33333333-3333-4333-8333-333333333333";
|
||||
const targetOwnerId = "44444444-4444-4444-8444-444444444444";
|
||||
const enrollmentId = "55555555-5555-4555-8555-555555555555";
|
||||
const discoveryId = "66666666-6666-4666-8666-666666666666";
|
||||
const deviceId = "77777777-7777-4777-8777-777777777777";
|
||||
const routeId = "88888888-8888-4888-8888-888888888888";
|
||||
const identifierDigest = `hmac-sha256:${"a".repeat(64)}`;
|
||||
|
||||
test("claims only matching observed enrollment evidence into direct ownership", async () => {
|
||||
const actor = managementActor("member");
|
||||
const command = normalizeDeviceManagementCommand("device.claim", {
|
||||
projectRef: `project:${sourceProjectId}`,
|
||||
enrollmentIntentRef: `enrollment-intent:${enrollmentId}`,
|
||||
discoveryRef: `discovery:${discoveryId}`,
|
||||
deviceKey: "pilot-device",
|
||||
displayName: "Pilot device",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-claim"),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "engineer"),
|
||||
step("from device_enrollment_intents", {
|
||||
rows: [enrollmentRow()],
|
||||
}),
|
||||
step("from device_discoveries", {
|
||||
rows: [discoveryRow()],
|
||||
}),
|
||||
step("insert into device_instances", {
|
||||
rows: [deviceRow({
|
||||
owner_scope_id: sourceOwnerId,
|
||||
project_id: sourceProjectId,
|
||||
device_key: command.deviceKey,
|
||||
display_name: command.displayName,
|
||||
})],
|
||||
}),
|
||||
step("insert into device_restricted_identifiers"),
|
||||
step("update device_discoveries", { rows: [{ id: discoveryId }] }),
|
||||
step("update device_enrollment_intents", { rows: [{ id: enrollmentId }] }),
|
||||
step("insert into device_ownership_transitions"),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.claim",
|
||||
command,
|
||||
digestCharacter: "b",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.device.projectRef, `project:${sourceProjectId}`);
|
||||
assert.equal(result.result.device.identifier.masked, "********0001");
|
||||
assert.equal(JSON.stringify(result.result).includes(identifierDigest), false);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("project owner can update device name and integration identity independently", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = normalizeDeviceManagementCommand("device.update", {
|
||||
projectRef: `project:${sourceProjectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
displayName: "Trike 8028",
|
||||
integrationDeviceId: "8028",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-device-update"),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("update device_instances", {
|
||||
rows: [deviceRow({
|
||||
display_name: command.displayName,
|
||||
integration_device_id: command.integrationDeviceId,
|
||||
})],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.update",
|
||||
command,
|
||||
digestCharacter: "4",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.updated, true);
|
||||
assert.equal(result.result.device.displayName, "Trike 8028");
|
||||
assert.equal(result.result.device.integrationDeviceId, "8028");
|
||||
assert.equal(result.result.device.projectRef, `project:${sourceProjectId}`);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("transfer requires explicit authority in the target project", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-denied"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
step("from device_project_grants", { rows: [] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "c",
|
||||
})),
|
||||
/device_project_capability_denied/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("authorized transfer preserves history and detaches source collections", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_configuration_state", { rows: [] }),
|
||||
step("from device_commands", { rows: [{ active: false }] }),
|
||||
step("delete from device_configuration_state", { rows: [], rowCount: 0 }),
|
||||
step("delete from device_collection_members", { rows: [], rowCount: 2 }),
|
||||
step("update device_instances", {
|
||||
rows: [deviceRow({
|
||||
owner_scope_id: targetOwnerId,
|
||||
project_id: targetProjectId,
|
||||
device_key: command.targetDeviceKey,
|
||||
})],
|
||||
}),
|
||||
step("update device_restricted_identifiers", { rows: [], rowCount: 1 }),
|
||||
step("insert into device_ownership_transitions"),
|
||||
step("insert into device_audit_events"),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "d",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.transferred, true);
|
||||
assert.equal(result.result.device.projectRef, `project:${targetProjectId}`);
|
||||
assert.equal(result.result.detachedCollectionCount, 2);
|
||||
assert.equal(result.result.transferredIdentifierCount, 1);
|
||||
assert.equal(result.result.clearedDesiredConfiguration, false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("transfer fails closed while a credential binding is active", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-credential-bound"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: true }] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "f",
|
||||
})),
|
||||
/device_transfer_active_credential_binding/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("transfer fails closed while a resource binding is pending approval", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-resource-bound"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: true }] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "1",
|
||||
})),
|
||||
/device_transfer_active_resource_binding/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("transfer fails closed with applied configuration", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-applied-config"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_configuration_state", {
|
||||
rows: [{
|
||||
desired_revision_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
applied_revision_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "2",
|
||||
})),
|
||||
/device_transfer_applied_configuration/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("transfer fails closed with a nonterminal command", async () => {
|
||||
const actor = managementActor("owner");
|
||||
const command = transferCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-transfer-command-active"),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
projectStep(targetProjectId, targetOwnerId),
|
||||
grantsStep(actor, "owner"),
|
||||
step("from device_sessions", { rows: [{ active: false }] }),
|
||||
step("from device_credential_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_resource_bindings", { rows: [{ active: false }] }),
|
||||
step("from device_configuration_state", { rows: [] }),
|
||||
step("from device_commands", { rows: [{ active: true }] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device.transfer",
|
||||
command,
|
||||
digestCharacter: "3",
|
||||
})),
|
||||
/device_transfer_nonterminal_command/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
});
|
||||
|
||||
test("reject resolves both quarantine and enrollment without exposing a digest", async () => {
|
||||
const actor = managementActor("member");
|
||||
const command = normalizeDeviceManagementCommand("discovery.reject", {
|
||||
projectRef: `project:${sourceProjectId}`,
|
||||
discoveryRef: `discovery:${discoveryId}`,
|
||||
resolutionCode: "operator.identity_mismatch",
|
||||
});
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-reject"),
|
||||
projectStep(sourceProjectId, sourceOwnerId),
|
||||
grantsStep(actor, "engineer"),
|
||||
step("from device_discoveries", { rows: [discoveryRow()] }),
|
||||
step("from device_enrollment_intents", { rows: [enrollmentRow()] }),
|
||||
step("update device_discoveries"),
|
||||
step("update device_enrollment_intents"),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "discovery.reject",
|
||||
command,
|
||||
digestCharacter: "e",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.discovery.lifecycleState, "rejected");
|
||||
assert.equal(JSON.stringify(result.result).includes(identifierDigest), false);
|
||||
assertSafeProjection(result.result);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function transferCommand() {
|
||||
return normalizeDeviceManagementCommand("device.transfer", {
|
||||
deviceRef: `device:${deviceId}`,
|
||||
sourceProjectRef: `project:${sourceProjectId}`,
|
||||
targetProjectRef: `project:${targetProjectId}`,
|
||||
targetDeviceKey: "transferred-device",
|
||||
});
|
||||
}
|
||||
|
||||
function enrollmentRow() {
|
||||
return {
|
||||
id: enrollmentId,
|
||||
project_id: sourceProjectId,
|
||||
route_id: routeId,
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
expected_identifier_kind: "serial",
|
||||
expected_identifier_digest: identifierDigest,
|
||||
expected_identifier_masked: "********0001",
|
||||
lifecycle_state: "observed",
|
||||
observed_discovery_id: discoveryId,
|
||||
claimed_device_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
function discoveryRow() {
|
||||
return {
|
||||
id: discoveryId,
|
||||
project_id: sourceProjectId,
|
||||
route_id: routeId,
|
||||
enrollment_intent_id: enrollmentId,
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
protocol: "GENERIC_TCP",
|
||||
identifier_kind: "serial",
|
||||
identifier_digest: identifierDigest,
|
||||
identifier_masked: "********0001",
|
||||
lifecycle_state: "quarantine",
|
||||
claimed_device_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
function deviceRow(overrides = {}) {
|
||||
return {
|
||||
id: deviceId,
|
||||
contour_id: null,
|
||||
owner_scope_id: sourceOwnerId,
|
||||
project_id: sourceProjectId,
|
||||
device_key: "pilot-device",
|
||||
model_profile_ref: "vendor.model.protocol.v1",
|
||||
display_name: "Pilot device",
|
||||
integration_device_id: null,
|
||||
identifier_kind: "serial",
|
||||
identifier_masked: "********0001",
|
||||
lifecycle_state: "claimed",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function managementActor(hubRole) {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:lifecycle-operator",
|
||||
hubRole,
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
});
|
||||
}
|
||||
|
||||
function projectStep(projectId, ownerScopeId) {
|
||||
return step("from device_projects p", {
|
||||
rows: [{
|
||||
id: projectId,
|
||||
owner_scope_id: ownerScopeId,
|
||||
lifecycle_state: "active",
|
||||
scope_kind: "company",
|
||||
owner_ref: `client:${ownerScopeId}`,
|
||||
owner_display_name: "Example Company",
|
||||
owner_lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function grantsStep(actor, projectRole) {
|
||||
return step("from device_project_grants", {
|
||||
rows: [{
|
||||
id: "99999999-9999-4999-8999-999999999999",
|
||||
principal_kind: "user",
|
||||
principal_ref: actor.userRef,
|
||||
project_role: projectRole,
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function receiptStep(id) {
|
||||
return step("insert into device_management_command_receipts", {
|
||||
rows: [{ id }],
|
||||
});
|
||||
}
|
||||
|
||||
function commandInput({ actor, commandKind, command, digestCharacter }) {
|
||||
return {
|
||||
idempotencyKey: `phase24-${commandKind.replaceAll(".", "-")}-0001`,
|
||||
commandKind,
|
||||
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
|
||||
actor,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
function repositoryWithClient(client) {
|
||||
return new PostgresDeviceRepository({
|
||||
pool: {
|
||||
query: async () => ({ rows: [] }),
|
||||
connect: async () => client,
|
||||
end: async () => undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/003_device_management_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("management migration pins idempotency, audit and owner invariants", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /device_project_grants_owner_user_only/);
|
||||
assert.match(sql, /project_role <> 'owner' or principal_kind = 'user'/);
|
||||
assert.match(sql, /add column if not exists project_id uuid references device_projects\(id\)/);
|
||||
assert.match(sql, /create table if not exists device_management_command_receipts/);
|
||||
assert.match(sql, /unique \(actor_ref, command_kind, idempotency_key\)/);
|
||||
assert.match(sql, /request_digest ~ '\^sha256:\[a-f0-9\]\{64\}\$'/);
|
||||
assert.match(sql, /lifecycle_state in \('pending', 'completed'\)/);
|
||||
});
|
||||
|
||||
test("management migration stores no tenant, device or credential seed", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|b2|imei/i);
|
||||
assert.doesNotMatch(sql, /password|secret|credential_ref/i);
|
||||
});
|
||||
|
||||
test("repository applies management migration after project access", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const projectAccessIndex = source.indexOf("002_device_project_access.sql");
|
||||
const managementIndex = source.indexOf("003_device_management_commands.sql");
|
||||
|
||||
assert.notEqual(projectAccessIndex, -1);
|
||||
assert.notEqual(managementIndex, -1);
|
||||
assert.ok(projectAccessIndex < managementIndex);
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
|
||||
import {
|
||||
normalizeManagementActor,
|
||||
normalizeManagementCommand,
|
||||
} from "../src/project-management.mjs";
|
||||
|
||||
const actor = normalizeManagementActor({
|
||||
userRef: "user:engineer",
|
||||
hubRole: "admin",
|
||||
groupRefs: [],
|
||||
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
|
||||
});
|
||||
const command = normalizeManagementCommand("owner_scope.ensure", {
|
||||
scopeKind: "company",
|
||||
ownerRef: "client:example",
|
||||
displayName: "Example Company",
|
||||
});
|
||||
const baseInput = {
|
||||
idempotencyKey: "phase2-repository-0001",
|
||||
commandKind: "owner_scope.ensure",
|
||||
requestDigest: `sha256:${"a".repeat(64)}`,
|
||||
actor,
|
||||
command,
|
||||
};
|
||||
|
||||
test("replays a completed command without executing the domain mutation", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", { rows: [] }),
|
||||
step("from device_management_command_receipts", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
request_digest: baseInput.requestDigest,
|
||||
lifecycle_state: "completed",
|
||||
response_body: { created: true, ownerScope: { ownerRef: "client:example" } },
|
||||
}],
|
||||
}),
|
||||
step("from device_owner_scopes", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example",
|
||||
display_name: "Example Company",
|
||||
lifecycle_state: "active",
|
||||
created_at: new Date("2026-08-10T00:00:00.000Z"),
|
||||
updated_at: new Date("2026-08-10T00:00:00.000Z"),
|
||||
}],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(baseInput);
|
||||
|
||||
assert.equal(result.replayed, true);
|
||||
assert.equal(result.result.ownerScope.ownerRef, "client:example");
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("rejects idempotency-key reuse with a different normalized request", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", { rows: [] }),
|
||||
step("from device_management_command_receipts", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
request_digest: `sha256:${"b".repeat(64)}`,
|
||||
lifecycle_state: "completed",
|
||||
response_body: { created: true },
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(baseInput),
|
||||
/device_idempotency_key_conflict/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("does not replay a completed project command after project access is revoked", async () => {
|
||||
const collectionCommand = normalizeManagementCommand("collection.ensure", {
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
collectionKey: "field-devices",
|
||||
name: "Field Devices",
|
||||
});
|
||||
const input = {
|
||||
idempotencyKey: "phase2-repository-collection-0001",
|
||||
commandKind: "collection.ensure",
|
||||
requestDigest: `sha256:${"c".repeat(64)}`,
|
||||
actor,
|
||||
command: collectionCommand,
|
||||
};
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", { rows: [] }),
|
||||
step("from device_management_command_receipts", {
|
||||
rows: [{
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
request_digest: input.requestDigest,
|
||||
lifecycle_state: "completed",
|
||||
response_body: { created: true },
|
||||
}],
|
||||
}),
|
||||
step("from device_projects p", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
owner_scope_id: "33333333-3333-4333-8333-333333333333",
|
||||
project_key: "field-devices",
|
||||
name: "Field Devices",
|
||||
description: null,
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example",
|
||||
owner_display_name: "Example Company",
|
||||
owner_lifecycle_state: "active",
|
||||
owner_created_at: now,
|
||||
owner_updated_at: now,
|
||||
}],
|
||||
}),
|
||||
step("from device_project_grants", { rows: [] }),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(input),
|
||||
/device_project_capability_denied/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("refuses to revoke the last active project owner", async () => {
|
||||
const ownerActor = normalizeManagementActor({
|
||||
...actor,
|
||||
hubRole: "owner",
|
||||
});
|
||||
const revokeOwner = normalizeManagementCommand("project_grant.upsert", {
|
||||
projectRef: "project:11111111-1111-4111-8111-111111111111",
|
||||
principalKind: "user",
|
||||
principalRef: ownerActor.userRef,
|
||||
projectRole: "owner",
|
||||
lifecycleState: "revoked",
|
||||
});
|
||||
const input = {
|
||||
idempotencyKey: "phase2-repository-owner-0001",
|
||||
commandKind: "project_grant.upsert",
|
||||
requestDigest: `sha256:${"d".repeat(64)}`,
|
||||
actor: ownerActor,
|
||||
command: revokeOwner,
|
||||
};
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-created" }],
|
||||
}),
|
||||
step("from device_projects p", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
owner_scope_id: "33333333-3333-4333-8333-333333333333",
|
||||
project_key: "field-devices",
|
||||
name: "Field Devices",
|
||||
description: null,
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example",
|
||||
owner_display_name: "Example Company",
|
||||
owner_lifecycle_state: "active",
|
||||
owner_created_at: now,
|
||||
owner_updated_at: now,
|
||||
}],
|
||||
}),
|
||||
step("from device_project_grants", {
|
||||
rows: [{
|
||||
id: "44444444-4444-4444-8444-444444444444",
|
||||
principal_kind: "user",
|
||||
principal_ref: ownerActor.userRef,
|
||||
project_role: "owner",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}],
|
||||
}),
|
||||
step("rollback"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
await assert.rejects(
|
||||
repository.executeManagementCommand(input),
|
||||
/device_project_last_owner_required/,
|
||||
);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("commits an authorized generic owner-scope command and durable receipt", async () => {
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
step("insert into device_management_command_receipts", {
|
||||
rows: [{ id: "receipt-created" }],
|
||||
}),
|
||||
step("insert into device_owner_scopes", {
|
||||
rows: [{
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example",
|
||||
display_name: "Example Company",
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created: true,
|
||||
}],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(baseInput);
|
||||
|
||||
assert.equal(result.replayed, false);
|
||||
assert.equal(result.result.created, true);
|
||||
assert.equal(result.result.ownerScope.ownerRef, "client:example");
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function repositoryWithClient(client) {
|
||||
return new PostgresDeviceRepository({
|
||||
pool: {
|
||||
query: async () => ({ rows: [] }),
|
||||
connect: async () => client,
|
||||
end: async () => undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
released: false,
|
||||
async query(sql) {
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const appUrl = new URL("../src/app.mjs", import.meta.url);
|
||||
const serverUrl = new URL("../src/server.mjs", import.meta.url);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
const managerComposeUrl = new URL("../../../docker-compose.device-manager.yml", import.meta.url);
|
||||
|
||||
test("management surface is internal, POST-only and disabled by default", async () => {
|
||||
const source = await readFile(appUrl, "utf8");
|
||||
|
||||
assert.match(source, /managementApiEnabled = false/);
|
||||
assert.match(source, /\/internal\/v1\/management\/owner-scopes:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/projects:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/collections:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/project-grants:upsert/);
|
||||
assert.match(source, /\/internal\/v1\/management\/device-bindings:ensure/);
|
||||
assert.match(source, /\/internal\/v1\/management\/device-configuration-revisions:create/);
|
||||
assert.match(source, /request\.method === "POST" && managementCommandKind/);
|
||||
assert.doesNotMatch(source, /\/api\/public\/.*management/);
|
||||
assert.doesNotMatch(source, /device-commands:(?:plan|confirm|dispatch)/);
|
||||
});
|
||||
|
||||
test("management API is enabled only through a runner-owned file token", async () => {
|
||||
const server = await readFile(serverUrl, "utf8");
|
||||
const compose = await readFile(managerComposeUrl, "utf8");
|
||||
|
||||
assert.match(server, /DEVICE_MANAGEMENT_API_ENABLED/);
|
||||
assert.match(server, /DEVICE_MANAGEMENT_CORE_TOKEN_FILE/);
|
||||
assert.match(compose, /DEVICE_MANAGEMENT_API_ENABLED: "true"/);
|
||||
assert.match(
|
||||
compose,
|
||||
/DEVICE_MANAGEMENT_CORE_TOKEN_FILE: \/run\/nodedc-secrets\/management-core-token/,
|
||||
);
|
||||
assert.match(
|
||||
compose,
|
||||
/source: \/volume1\/docker\/nodedc-device-plane\/secrets\/management-core-token/,
|
||||
);
|
||||
assert.doesNotMatch(compose, /DEVICE_MANAGEMENT_CORE_TOKEN:\s/);
|
||||
});
|
||||
|
||||
test("repository pins idempotency, audit and last-owner checks inside one transaction", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
|
||||
assert.match(source, /await client\.query\("begin"\)/);
|
||||
assert.match(source, /await client\.query\("commit"\)/);
|
||||
assert.match(source, /await client\.query\("rollback"\)/);
|
||||
assert.match(source, /device_idempotency_key_conflict/);
|
||||
assert.match(source, /device_project_last_owner_required/);
|
||||
assert.match(source, /for update of p/);
|
||||
assert.match(source, /authorizeManagementReplay/);
|
||||
assert.match(source, /insert into device_audit_events/);
|
||||
assert.match(source, /update device_management_command_receipts/);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/001_device_plane_foundation.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
test("foundation migration keeps restricted identifiers hashed and DB private", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
assert.match(sql, /identifier_digest text not null/);
|
||||
assert.match(sql, /identifier_masked text not null/);
|
||||
assert.doesNotMatch(sql, /imei\s+text/i);
|
||||
assert.doesNotMatch(sql, /password\s+text/i);
|
||||
assert.doesNotMatch(sql, /raw_packet/i);
|
||||
});
|
||||
|
||||
test("foundation migration has quarantine, contour, binding and audit tables", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
for (const table of [
|
||||
"device_model_profiles",
|
||||
"device_contours",
|
||||
"device_discoveries",
|
||||
"device_instances",
|
||||
"device_bindings",
|
||||
"device_audit_events",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`create table if not exists ${table}`));
|
||||
}
|
||||
assert.match(sql, /default 'quarantine'/);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/002_device_project_access.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("project access migration defines owner, project, collection and grant boundaries", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
for (const table of [
|
||||
"device_owner_scopes",
|
||||
"device_projects",
|
||||
"device_collections",
|
||||
"device_collection_members",
|
||||
"device_project_grants",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`create table if not exists ${table}`));
|
||||
}
|
||||
|
||||
assert.match(sql, /scope_kind in \('company', 'personal'\)/);
|
||||
assert.match(sql, /principal_kind in \('user', 'group'\)/);
|
||||
assert.match(
|
||||
sql,
|
||||
/project_role in \('viewer', 'operator', 'engineer', 'admin', 'owner'\)/,
|
||||
);
|
||||
assert.match(sql, /unique \(scope_kind, owner_ref\)/);
|
||||
assert.match(sql, /unique \(owner_scope_id, project_key\)/);
|
||||
assert.match(sql, /unique \(project_id, principal_kind, principal_ref\)/);
|
||||
assert.match(sql, /not \(capability_allow && capability_deny\)/);
|
||||
assert.match(
|
||||
sql,
|
||||
/foreign key \(collection_id, project_id\)\s+references device_collections\(id, project_id\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/foreign key \(device_id, project_id\)\s+references device_instances\(id, project_id\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("project access migration contains no tenant, device or credential seed", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
assert.doesNotMatch(sql, /dcctouch|arusnavi|b2|imei/i);
|
||||
assert.doesNotMatch(sql, /password|secret|token|credential_ref/i);
|
||||
});
|
||||
|
||||
test("repository applies project access migration after the foundation", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
const foundationIndex = source.indexOf("001_device_plane_foundation.sql");
|
||||
const projectAccessIndex = source.indexOf("002_device_project_access.sql");
|
||||
|
||||
assert.notEqual(foundationIndex, -1);
|
||||
assert.notEqual(projectAccessIndex, -1);
|
||||
assert.ok(foundationIndex < projectAccessIndex);
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
assertActorCanManageOwnerScope,
|
||||
assertGrantMutationAllowed,
|
||||
assertProjectCapability,
|
||||
normalizeManagementActor,
|
||||
normalizeManagementCommand,
|
||||
resolveProjectAccess,
|
||||
} from "../src/project-management.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
|
||||
test("normalizes strict generic management commands without seeded entities", () => {
|
||||
const project = normalizeManagementCommand("project.ensure", {
|
||||
scopeKind: "company",
|
||||
ownerRef: "client:example",
|
||||
projectKey: "field-devices",
|
||||
name: "Field Devices",
|
||||
description: "Generic project",
|
||||
});
|
||||
assert.deepEqual(project, {
|
||||
scopeKind: "company",
|
||||
ownerRef: "client:example",
|
||||
projectKey: "field-devices",
|
||||
name: "Field Devices",
|
||||
description: "Generic project",
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => normalizeManagementCommand("project.ensure", {
|
||||
scopeKind: "company",
|
||||
ownerRef: "client:example",
|
||||
projectKey: "field-devices",
|
||||
name: "Field Devices",
|
||||
rawPayload: "forbidden",
|
||||
}),
|
||||
/device_management_command_field_unexpected:rawPayload/,
|
||||
);
|
||||
});
|
||||
|
||||
test("company scope requires an asserted scope and Hub admin ceiling", () => {
|
||||
const scope = { scopeKind: "company", ownerRef: "client:example" };
|
||||
assert.doesNotThrow(() => assertActorCanManageOwnerScope(actor({
|
||||
hubRole: "admin",
|
||||
ownerScopes: [scope],
|
||||
}), scope));
|
||||
assert.throws(
|
||||
() => assertActorCanManageOwnerScope(actor({
|
||||
hubRole: "viewer",
|
||||
ownerScopes: [scope],
|
||||
}), scope),
|
||||
/device_owner_scope_access_denied/,
|
||||
);
|
||||
assert.throws(
|
||||
() => assertActorCanManageOwnerScope(actor({ hubRole: "owner" }), scope),
|
||||
/device_owner_scope_access_denied/,
|
||||
);
|
||||
});
|
||||
|
||||
test("personal scope is isolated to the matching Hub user", () => {
|
||||
const scope = { scopeKind: "personal", ownerRef: "user:engineer" };
|
||||
assert.doesNotThrow(() => assertActorCanManageOwnerScope(
|
||||
actor({ hubRole: "owner" }),
|
||||
scope,
|
||||
));
|
||||
assert.throws(
|
||||
() => assertActorCanManageOwnerScope(
|
||||
actor({ hubRole: "member" }),
|
||||
scope,
|
||||
),
|
||||
/device_owner_scope_access_denied/,
|
||||
);
|
||||
assert.throws(
|
||||
() => assertActorCanManageOwnerScope(
|
||||
actor({ userRef: "user:other", hubRole: "owner" }),
|
||||
scope,
|
||||
),
|
||||
/device_owner_scope_access_denied/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Hub owner has no project access without an explicit project grant", () => {
|
||||
const access = resolveProjectAccess({
|
||||
actor: actor({ hubRole: "owner" }),
|
||||
grants: [],
|
||||
});
|
||||
assert.equal(access.allowed, false);
|
||||
assert.deepEqual(access.capabilities, []);
|
||||
});
|
||||
|
||||
test("a direct user grant overrides broader group grants", () => {
|
||||
const access = resolveProjectAccess({
|
||||
actor: actor({ hubRole: "owner", groupRefs: ["group:admins"] }),
|
||||
grants: [
|
||||
grant({
|
||||
grantRef: "grant:group-admin",
|
||||
principalKind: "group",
|
||||
principalRef: "group:admins",
|
||||
projectRole: "admin",
|
||||
}),
|
||||
grant({
|
||||
grantRef: "grant:direct-viewer",
|
||||
principalKind: "user",
|
||||
principalRef: "user:engineer",
|
||||
projectRole: "viewer",
|
||||
}),
|
||||
],
|
||||
});
|
||||
assert.equal(access.projectRole, "viewer");
|
||||
assert.equal(access.capabilities.includes("access.manage"), false);
|
||||
});
|
||||
|
||||
test("matching group grants combine bounded operator and engineer capabilities", () => {
|
||||
const access = resolveProjectAccess({
|
||||
actor: actor({
|
||||
hubRole: "member",
|
||||
groupRefs: ["group:operators", "group:engineers"],
|
||||
}),
|
||||
grants: [
|
||||
grant({
|
||||
grantRef: "grant:operator",
|
||||
principalKind: "group",
|
||||
principalRef: "group:operators",
|
||||
projectRole: "operator",
|
||||
}),
|
||||
grant({
|
||||
grantRef: "grant:engineer",
|
||||
principalKind: "group",
|
||||
principalRef: "group:engineers",
|
||||
projectRole: "engineer",
|
||||
}),
|
||||
],
|
||||
});
|
||||
assert.equal(access.capabilities.includes("device.enroll"), true);
|
||||
assert.equal(access.capabilities.includes("command.dispatch"), true);
|
||||
assert.equal(access.capabilities.includes("access.manage"), false);
|
||||
});
|
||||
|
||||
test("configuration mutation belongs to engineer and admin, not operator", () => {
|
||||
const engineer = resolveProjectAccess({
|
||||
actor: actor({ groupRefs: ["group:engineers"] }),
|
||||
grants: [grant({
|
||||
principalKind: "group",
|
||||
principalRef: "group:engineers",
|
||||
projectRole: "engineer",
|
||||
})],
|
||||
});
|
||||
const operator = resolveProjectAccess({
|
||||
actor: actor({ groupRefs: ["group:operators"] }),
|
||||
grants: [grant({
|
||||
principalKind: "group",
|
||||
principalRef: "group:operators",
|
||||
projectRole: "operator",
|
||||
})],
|
||||
});
|
||||
|
||||
assert.equal(engineer.capabilities.includes("configuration.manage"), true);
|
||||
assert.equal(operator.capabilities.includes("configuration.manage"), false);
|
||||
});
|
||||
|
||||
test("Hub ceiling and explicit deny prevent privilege escalation", () => {
|
||||
const ownerGrant = grant({
|
||||
grantRef: "grant:owner",
|
||||
principalKind: "user",
|
||||
principalRef: "user:engineer",
|
||||
projectRole: "owner",
|
||||
capabilityDeny: ["credential.manage"],
|
||||
});
|
||||
const hubAdmin = resolveProjectAccess({
|
||||
actor: actor({ hubRole: "admin" }),
|
||||
grants: [ownerGrant],
|
||||
});
|
||||
const hubOwner = resolveProjectAccess({
|
||||
actor: actor({ hubRole: "owner" }),
|
||||
grants: [ownerGrant],
|
||||
});
|
||||
|
||||
assert.equal(hubAdmin.capabilities.includes("device.transfer"), false);
|
||||
assert.equal(hubOwner.capabilities.includes("device.transfer"), true);
|
||||
assert.equal(hubOwner.capabilities.includes("credential.manage"), false);
|
||||
});
|
||||
|
||||
test("owner grant mutations require both Hub and project ownership authority", () => {
|
||||
const grants = [grant({
|
||||
grantRef: "grant:owner",
|
||||
principalKind: "user",
|
||||
principalRef: "user:engineer",
|
||||
projectRole: "owner",
|
||||
})];
|
||||
const ownerCommand = normalizeManagementCommand("project_grant.upsert", {
|
||||
projectRef,
|
||||
principalKind: "user",
|
||||
principalRef: "user:second-owner",
|
||||
projectRole: "owner",
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertGrantMutationAllowed(
|
||||
actor({ hubRole: "admin" }),
|
||||
grants,
|
||||
ownerCommand,
|
||||
),
|
||||
/device_project_owner_transfer_denied/,
|
||||
);
|
||||
assert.doesNotThrow(() => assertGrantMutationAllowed(
|
||||
actor({ hubRole: "owner" }),
|
||||
grants,
|
||||
ownerCommand,
|
||||
));
|
||||
});
|
||||
|
||||
test("grant normalization rejects group owners and capability overlap", () => {
|
||||
assert.throws(
|
||||
() => normalizeManagementCommand("project_grant.upsert", {
|
||||
projectRef,
|
||||
principalKind: "group",
|
||||
principalRef: "group:owners",
|
||||
projectRole: "owner",
|
||||
}),
|
||||
/device_project_owner_must_be_user/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeManagementCommand("project_grant.upsert", {
|
||||
projectRef,
|
||||
principalKind: "user",
|
||||
principalRef: "user:operator",
|
||||
projectRole: "operator",
|
||||
capabilityAllow: ["command.dispatch"],
|
||||
capabilityDeny: ["command.dispatch"],
|
||||
}),
|
||||
/device_project_capability_overlap/,
|
||||
);
|
||||
});
|
||||
|
||||
test("capability checks fail closed for inactive or unrelated grants", () => {
|
||||
assert.throws(
|
||||
() => assertProjectCapability(
|
||||
actor({ hubRole: "owner" }),
|
||||
[grant({ lifecycleState: "revoked" })],
|
||||
"project.read",
|
||||
),
|
||||
/device_project_capability_denied/,
|
||||
);
|
||||
});
|
||||
|
||||
test("denying project.read collapses every derived capability", () => {
|
||||
const access = resolveProjectAccess({
|
||||
actor: actor({ hubRole: "owner" }),
|
||||
grants: [grant({
|
||||
projectRole: "owner",
|
||||
capabilityDeny: ["project.read"],
|
||||
})],
|
||||
});
|
||||
|
||||
assert.equal(access.allowed, false);
|
||||
assert.deepEqual(access.capabilities, []);
|
||||
assert.throws(
|
||||
() => assertProjectCapability(
|
||||
actor({ hubRole: "owner" }),
|
||||
[grant({
|
||||
projectRole: "owner",
|
||||
capabilityDeny: ["project.read"],
|
||||
})],
|
||||
"access.manage",
|
||||
),
|
||||
/device_project_capability_denied/,
|
||||
);
|
||||
});
|
||||
|
||||
function actor(overrides = {}) {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:engineer",
|
||||
hubRole: "member",
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function grant(overrides = {}) {
|
||||
return {
|
||||
grantRef: "grant:default",
|
||||
principalKind: "user",
|
||||
principalRef: "user:engineer",
|
||||
projectRole: "viewer",
|
||||
capabilityAllow: [],
|
||||
capabilityDeny: [],
|
||||
lifecycleState: "active",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getDeviceProjectWorkspace,
|
||||
listAccessibleDeviceProjects,
|
||||
} from "../src/project-query-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const actor = normalizeManagementActor({
|
||||
userRef: "user:device-admin",
|
||||
hubRole: "admin",
|
||||
groupRefs: ["group:engineers"],
|
||||
ownerScopes: [],
|
||||
});
|
||||
const timestamp = "2026-08-10T00:00:00.000Z";
|
||||
|
||||
test("project list applies direct-grant precedence and returns bounded summaries", async () => {
|
||||
const client = {
|
||||
async query(sql) {
|
||||
assert.match(sql, /from device_projects p/);
|
||||
return {
|
||||
rows: [
|
||||
projectGrantRow({
|
||||
grant_id: "22222222-2222-4222-8222-222222222222",
|
||||
principal_kind: "group",
|
||||
principal_ref: "group:engineers",
|
||||
project_role: "engineer",
|
||||
}),
|
||||
projectGrantRow({
|
||||
grant_id: "33333333-3333-4333-8333-333333333333",
|
||||
principal_kind: "user",
|
||||
principal_ref: "user:device-admin",
|
||||
project_role: "viewer",
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const projects = await listAccessibleDeviceProjects(client, actor);
|
||||
assert.equal(projects.length, 1);
|
||||
assert.equal(projects[0].access.projectRole, "viewer");
|
||||
assert.equal(projects[0].access.capabilities.includes("collection.manage"), false);
|
||||
assert.deepEqual(projects[0].counts, {
|
||||
devices: 3,
|
||||
collections: 2,
|
||||
discoveries: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("project workspace returns only masked identity projections", async () => {
|
||||
const client = workspaceClient();
|
||||
const workspace = await getDeviceProjectWorkspace(client, actor, projectId);
|
||||
|
||||
assert.equal(workspace.project.projectRef, `project:${projectId}`);
|
||||
assert.equal(workspace.devices[0].identifier.masked, "***********0001");
|
||||
assert.equal(workspace.discoveries[0].identifier.masked, "***********0001");
|
||||
assert.equal(workspace.enrollments[0].expectedIdentifier.masked, "***********0001");
|
||||
assert.equal(
|
||||
workspace.enrollments[0].enrollmentIntentRef,
|
||||
"enrollment-intent:77777777-7777-4777-8777-777777777777",
|
||||
);
|
||||
assert.equal(workspace.adapterPackages[0].packageKey, "generic-tracker");
|
||||
assert.equal(workspace.modelProfiles[0].modelProfileRef, "vendor.model.v1");
|
||||
assert.equal(workspace.routes[0].activeSessionCount, 1);
|
||||
assert.equal(workspace.sessions[0].frameCount, 12);
|
||||
assert.equal(workspace.bindings[0].lifecycleState, "pending_external_approval");
|
||||
assert.equal(workspace.configurationRevisions[0].revisionNumber, 1);
|
||||
assert.equal(workspace.commands[0].lifecycleState, "acknowledged");
|
||||
assert.equal(workspace.auditEvents[0].eventType, "device.observed");
|
||||
assert.equal(workspace.grants[0].principalRef, "user:device-admin");
|
||||
assert.equal(workspace.policies.commandTransport, "disabled");
|
||||
const serialized = JSON.stringify(workspace);
|
||||
assert.equal(serialized.includes("hmac-sha256"), false);
|
||||
assert.equal(serialized.includes("ndc-credref"), false);
|
||||
assert.equal(serialized.includes("transport-message-secret"), false);
|
||||
assert.equal(serialized.includes("external-approval-proof"), false);
|
||||
assert.equal(serialized.includes("raw-audit-payload"), false);
|
||||
});
|
||||
|
||||
test("project workspace authorization remains compatible with read-only transactions", async () => {
|
||||
const queries = [];
|
||||
const client = workspaceClient({ queries });
|
||||
|
||||
await getDeviceProjectWorkspace(client, actor, projectId);
|
||||
|
||||
assert.ok(queries.length > 0);
|
||||
assert.equal(
|
||||
queries.some((sql) => /\bfor\s+(?:no\s+key\s+)?(?:update|share)\b/i.test(sql)),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("project read source never selects identifier digests or credential refs", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/project-query-repository.mjs", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/\b(?:identifier_digest|expected_identifier_digest|credential_ref|parameters_digest|parameters_projection|transport_message_ref|external_approval_ref|external_approval_digest)\b/,
|
||||
);
|
||||
assert.doesNotMatch(source, /\b(?:dae\.payload|dcr\.configuration)\b/);
|
||||
});
|
||||
|
||||
function workspaceClient({ queries = [] } = {}) {
|
||||
let grantReads = 0;
|
||||
return {
|
||||
async query(sql) {
|
||||
queries.push(sql);
|
||||
if (/from device_projects p/.test(sql)) {
|
||||
return { rows: [projectGrantRow()] };
|
||||
}
|
||||
if (/from device_project_grants/.test(sql)) {
|
||||
grantReads += 1;
|
||||
return { rows: [storedGrantRow()] };
|
||||
}
|
||||
if (/from device_adapter_packages ap/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "99999999-9999-4999-8999-999999999999",
|
||||
package_key: "generic-tracker",
|
||||
display_name: "Generic tracker",
|
||||
publisher_ref: "publisher:nodedc",
|
||||
lifecycle_state: "active",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_adapter_versions av/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
adapter_package_id: "99999999-9999-4999-8999-999999999999",
|
||||
version: "1.0.0",
|
||||
runtime_package_ref: "artifact:generic-tracker:1.0.0",
|
||||
content_digest: `sha256:${"a".repeat(64)}`,
|
||||
contract_version: "device-adapter.v1",
|
||||
capabilities: ["telemetry"],
|
||||
lifecycle_state: "active",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_model_profiles dmp/.test(sql)) {
|
||||
return { rows: [{
|
||||
profile_ref: "vendor.model.v1",
|
||||
adapter_version_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
schema_version: "1.0.0",
|
||||
vendor: "Vendor",
|
||||
model: "Model",
|
||||
device_type: "tracker",
|
||||
protocol: "INTERNAL",
|
||||
schema_artifact_ref: "schema:vendor.model.v1",
|
||||
profile_digest: `sha256:${"b".repeat(64)}`,
|
||||
capabilities: ["telemetry"],
|
||||
lifecycle_state: "active",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_edges de/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||||
edge_key: "edge-one",
|
||||
display_name: "Edge one",
|
||||
deployment_ref: "deployment:edge-one",
|
||||
lifecycle_state: "active",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_routes dr/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
||||
route_key: "route-one",
|
||||
display_name: "Route one",
|
||||
edge_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||||
edge_name: "Edge one",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
profile_vendor: "Vendor",
|
||||
profile_model: "Model",
|
||||
listener_ref: "listener:generic",
|
||||
protocol: "INTERNAL",
|
||||
direction: "bidirectional",
|
||||
lifecycle_state: "active",
|
||||
session_count: "1",
|
||||
active_session_count: "1",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/select ds\.id, ds\.route_id, dr\.display_name/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
||||
route_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
||||
route_name: "Route one",
|
||||
device_id: "44444444-4444-4444-8444-444444444444",
|
||||
device_name: "Pilot device",
|
||||
protocol: "INTERNAL",
|
||||
lifecycle_state: "online",
|
||||
connected_at: timestamp,
|
||||
last_seen_at: timestamp,
|
||||
disconnected_at: null,
|
||||
close_reason_code: null,
|
||||
frame_count: "12",
|
||||
byte_count: "1024",
|
||||
}] };
|
||||
}
|
||||
if (/from device_resource_bindings drb/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
||||
binding_key: "foundry-map",
|
||||
display_name: "Foundry map",
|
||||
source_kind: "collection",
|
||||
device_id: null,
|
||||
collection_id: "55555555-5555-4555-8555-555555555555",
|
||||
source_name: "Pilot fleet",
|
||||
target_kind: "foundry.application",
|
||||
target_ref: "application:pilot-map",
|
||||
capabilities: ["observe"],
|
||||
lifecycle_state: "pending_external_approval",
|
||||
source_approved_at: timestamp,
|
||||
external_approval_ref: "external-approval-proof",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_configuration_revisions dcr/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "ffffffff-ffff-4fff-8fff-ffffffffffff",
|
||||
device_id: "44444444-4444-4444-8444-444444444444",
|
||||
device_name: "Pilot device",
|
||||
revision_number: "1",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
schema_artifact_ref: "schema:vendor.model.v1",
|
||||
configuration_digest: `sha256:${"c".repeat(64)}`,
|
||||
configuration: { raw: "must-not-leak" },
|
||||
change_summary: "Pilot configuration",
|
||||
created_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_configuration_state dcs/.test(sql)) {
|
||||
return { rows: [{
|
||||
device_id: "44444444-4444-4444-8444-444444444444",
|
||||
device_name: "Pilot device",
|
||||
desired_revision_id: "ffffffff-ffff-4fff-8fff-ffffffffffff",
|
||||
applied_revision_id: null,
|
||||
applied_at: null,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_commands dc/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "12121212-1212-4121-8121-121212121212",
|
||||
device_id: "44444444-4444-4444-8444-444444444444",
|
||||
device_name: "Pilot device",
|
||||
command_key: "safe-ping",
|
||||
command_catalog_ref: "catalog:safe-ping:v1",
|
||||
command_type: "device.ping",
|
||||
risk_class: "low",
|
||||
lifecycle_state: "acknowledged",
|
||||
planned_at: timestamp,
|
||||
expires_at: "2026-08-11T00:00:00.000Z",
|
||||
confirmed_at: timestamp,
|
||||
dispatched_at: timestamp,
|
||||
acknowledged_at: timestamp,
|
||||
terminal_at: null,
|
||||
terminal_reason_code: null,
|
||||
transport_message_ref: "transport-message-secret",
|
||||
parameters_projection: { raw: "must-not-leak" },
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_audit_events dae/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "13131313-1313-4131-8131-131313131313",
|
||||
event_type: "device.observed",
|
||||
actor_ref: "user:device-admin",
|
||||
device_id: "44444444-4444-4444-8444-444444444444",
|
||||
discovery_id: "66666666-6666-4666-8666-666666666666",
|
||||
occurred_at: timestamp,
|
||||
payload: { raw: "raw-audit-payload" },
|
||||
}] };
|
||||
}
|
||||
if (/from device_instances di\n left join lateral/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "44444444-4444-4444-8444-444444444444",
|
||||
device_key: "pilot-device",
|
||||
display_name: "Pilot device",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
lifecycle_state: "online",
|
||||
identifier_kind: "imei",
|
||||
identifier_masked: "***********0001",
|
||||
session_state: "online",
|
||||
last_seen_at: timestamp,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_collections dc/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "55555555-5555-4555-8555-555555555555",
|
||||
collection_key: "pilot-fleet",
|
||||
name: "Pilot fleet",
|
||||
description: null,
|
||||
lifecycle_state: "active",
|
||||
member_count: "1",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
if (/from device_discoveries dd/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "66666666-6666-4666-8666-666666666666",
|
||||
identifier_kind: "imei",
|
||||
identifier_masked: "***********0001",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
protocol: "INTERNAL",
|
||||
lifecycle_state: "quarantine",
|
||||
first_observed_at: timestamp,
|
||||
last_observed_at: timestamp,
|
||||
enrollment_intent_id: "77777777-7777-4777-8777-777777777777",
|
||||
claimed_device_id: null,
|
||||
}] };
|
||||
}
|
||||
if (/from device_enrollment_intents dei/.test(sql)) {
|
||||
return { rows: [{
|
||||
id: "77777777-7777-4777-8777-777777777777",
|
||||
enrollment_key: "pilot-enrollment",
|
||||
display_name: "Pilot device",
|
||||
model_profile_ref: "vendor.model.v1",
|
||||
expected_identifier_kind: "imei",
|
||||
expected_identifier_masked: "***********0001",
|
||||
lifecycle_state: "observed",
|
||||
observed_discovery_id: "66666666-6666-4666-8666-666666666666",
|
||||
claimed_device_id: null,
|
||||
expires_at: null,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
}] };
|
||||
}
|
||||
throw new Error(`unexpected_query:${sql}`);
|
||||
},
|
||||
get grantReads() {
|
||||
return grantReads;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function projectGrantRow(overrides = {}) {
|
||||
return {
|
||||
id: projectId,
|
||||
project_key: "pilot-project",
|
||||
name: "Pilot project",
|
||||
description: null,
|
||||
lifecycle_state: "active",
|
||||
owner_scope_id: "88888888-8888-4888-8888-888888888888",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:dctouch",
|
||||
owner_display_name: "DCTOUCH",
|
||||
owner_lifecycle_state: "active",
|
||||
grant_id: "33333333-3333-4333-8333-333333333333",
|
||||
principal_kind: "user",
|
||||
principal_ref: "user:device-admin",
|
||||
project_role: "viewer",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
grant_lifecycle_state: "active",
|
||||
device_count: "3",
|
||||
collection_count: "2",
|
||||
discovery_count: "1",
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function storedGrantRow() {
|
||||
return {
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
principal_kind: "user",
|
||||
principal_ref: "user:device-admin",
|
||||
project_role: "admin",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS,
|
||||
normalizeSensitiveReferenceManagementCommand,
|
||||
} from "../src/sensitive-reference-management.mjs";
|
||||
import {
|
||||
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
|
||||
normalizeDeviceManagementCommand,
|
||||
} from "../src/management-command.mjs";
|
||||
import {
|
||||
normalizeNdcCredentialReference as normalizeRuntimeCredentialReference,
|
||||
} from "../src/credential-reference.mjs";
|
||||
import {
|
||||
normalizeNdcCredentialReference as normalizePlatformCredentialReference,
|
||||
} from "../../../../platform/packages/external-provider-contract/src/credential-reference.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
||||
|
||||
test("credential binding commands share the strict management surface", () => {
|
||||
for (const kind of DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS) {
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
|
||||
}
|
||||
assert.equal(
|
||||
normalizeDeviceManagementCommand(
|
||||
"device_credential_binding.upsert",
|
||||
upsertInput(),
|
||||
).projectId,
|
||||
projectRef.slice("project:".length),
|
||||
);
|
||||
});
|
||||
|
||||
test("credential binding accepts only the platform canonical opaque ref", () => {
|
||||
const command = normalizeSensitiveReferenceManagementCommand(
|
||||
"device_credential_binding.upsert",
|
||||
upsertInput(),
|
||||
);
|
||||
|
||||
assert.deepEqual(command.credentialRef, {
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
});
|
||||
assert.equal(Object.isFrozen(command.credentialRef), true);
|
||||
assert.throws(
|
||||
() => normalizeSensitiveReferenceManagementCommand(
|
||||
"device_credential_binding.upsert",
|
||||
{
|
||||
...upsertInput(),
|
||||
credentialRef: {
|
||||
owner: "device_core",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
},
|
||||
},
|
||||
),
|
||||
/ndc_credential_reference_owner_invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeSensitiveReferenceManagementCommand(
|
||||
"device_credential_binding.upsert",
|
||||
{
|
||||
...upsertInput(),
|
||||
credentialRef: {
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "Bearer plaintext-is-forbidden",
|
||||
},
|
||||
},
|
||||
),
|
||||
/ndc_credential_reference_value_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("runtime credential reference adapter matches the platform contract", () => {
|
||||
const accepted = [
|
||||
{
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
},
|
||||
{
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:A1234567",
|
||||
},
|
||||
];
|
||||
for (const input of accepted) {
|
||||
assert.deepEqual(
|
||||
normalizeRuntimeCredentialReference(input),
|
||||
normalizePlatformCredentialReference(input),
|
||||
);
|
||||
}
|
||||
|
||||
const rejected = [
|
||||
null,
|
||||
[],
|
||||
{ owner: "device_core", reference: "ndc-credref:pilot-command-0001" },
|
||||
{ owner: "ndc_l2_credentials", reference: "secret:test" },
|
||||
{
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
token: "forbidden",
|
||||
},
|
||||
];
|
||||
for (const input of rejected) {
|
||||
let runtimeError;
|
||||
let platformError;
|
||||
try {
|
||||
normalizeRuntimeCredentialReference(input);
|
||||
} catch (error) {
|
||||
runtimeError = error;
|
||||
}
|
||||
try {
|
||||
normalizePlatformCredentialReference(input);
|
||||
} catch (error) {
|
||||
platformError = error;
|
||||
}
|
||||
assert.equal(runtimeError?.message, platformError?.message);
|
||||
}
|
||||
});
|
||||
|
||||
test("credential binding rejects raw secret-shaped fields", () => {
|
||||
for (const field of ["password", "token", "secretValue", "endpoint"]) {
|
||||
assert.throws(
|
||||
() => normalizeSensitiveReferenceManagementCommand(
|
||||
"device_credential_binding.upsert",
|
||||
{ ...upsertInput(), [field]: "forbidden" },
|
||||
),
|
||||
new RegExp(`device_management_command_field_unexpected:${field}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("credential revoke has no credential reference input", () => {
|
||||
const command = normalizeSensitiveReferenceManagementCommand(
|
||||
"device_credential_binding.revoke",
|
||||
{
|
||||
projectRef,
|
||||
deviceRef,
|
||||
purpose: "tracker.command",
|
||||
resolutionCode: "operator.rotation",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(command.resolutionCode, "operator.rotation");
|
||||
assert.equal("credentialRef" in command, false);
|
||||
});
|
||||
|
||||
function upsertInput() {
|
||||
return {
|
||||
projectRef,
|
||||
deviceRef,
|
||||
purpose: "tracker.command",
|
||||
credentialRef: {
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: "ndc-credref:pilot-command-0001",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const schemaUrl = new URL(
|
||||
"../migrations/008_device_sensitive_references.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const commandsUrl = new URL(
|
||||
"../migrations/009_device_sensitive_reference_commands.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("sensitive reference schema stores only digest, mask and canonical refs", async () => {
|
||||
const sql = await readFile(schemaUrl, "utf8");
|
||||
|
||||
assert.match(sql, /create table if not exists device_restricted_identifiers/);
|
||||
assert.match(sql, /identifier_digest text not null/);
|
||||
assert.match(sql, /identifier_masked text not null/);
|
||||
assert.match(sql, /device_restricted_identifiers_active_identity_idx/);
|
||||
assert.match(sql, /device_restricted_identifiers_primary_idx/);
|
||||
assert.match(sql, /device_identifier_ownership_mismatch/);
|
||||
assert.match(sql, /device_active_identifier_ownership_mismatch/);
|
||||
assert.match(sql, /deferrable initially deferred/);
|
||||
assert.match(sql, /create table if not exists device_credential_bindings/);
|
||||
assert.match(sql, /credential_owner = 'ndc_l2_credentials'/);
|
||||
assert.match(sql, /\^ndc-credref:/);
|
||||
assert.match(sql, /device_credential_binding_ownership_mismatch/);
|
||||
assert.match(sql, /device_transfer_active_credential_binding/);
|
||||
assert.match(sql, /owner_scope_id is null or credential_ref is null/);
|
||||
assert.doesNotMatch(sql, /imei\s+text|serial\s+text|password\s+text|token\s+text/i);
|
||||
assert.doesNotMatch(sql, /insert\s+into/i);
|
||||
});
|
||||
|
||||
test("credential commands extend durable receipts after their schema", async () => {
|
||||
const commands = await readFile(commandsUrl, "utf8");
|
||||
const repository = await readFile(repositoryUrl, "utf8");
|
||||
|
||||
assert.match(commands, /'device_credential_binding\.upsert'/);
|
||||
assert.match(commands, /'device_credential_binding\.revoke'/);
|
||||
const schemaIndex = repository.indexOf("008_device_sensitive_references.sql");
|
||||
const commandsIndex = repository.indexOf(
|
||||
"009_device_sensitive_reference_commands.sql",
|
||||
);
|
||||
assert.notEqual(schemaIndex, -1);
|
||||
assert.notEqual(commandsIndex, -1);
|
||||
assert.ok(schemaIndex < commandsIndex);
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
|
||||
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
|
||||
import { normalizeManagementActor } from "../src/project-management.mjs";
|
||||
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
const ownerId = "22222222-2222-4222-8222-222222222222";
|
||||
const deviceId = "33333333-3333-4333-8333-333333333333";
|
||||
const bindingId = "44444444-4444-4444-8444-444444444444";
|
||||
const canonicalRef = "ndc-credref:pilot-command-0001";
|
||||
|
||||
test("creates a canonical binding without returning or auditing its reference", async () => {
|
||||
const actor = managementActor();
|
||||
const command = upsertCommand();
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-credential-upsert"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("from device_credential_bindings", { rows: [] }),
|
||||
step("insert into device_credential_bindings", {
|
||||
rows: [bindingRow()],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_credential_binding.upsert",
|
||||
command,
|
||||
digestCharacter: "a",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.created, true);
|
||||
assert.equal(result.result.rotated, false);
|
||||
assert.equal(
|
||||
result.result.credentialBinding.credentialBindingRef,
|
||||
`credential-binding:${bindingId}`,
|
||||
);
|
||||
assert.equal(JSON.stringify(result.result).includes(canonicalRef), false);
|
||||
const auditCall = client.calls.find((call) =>
|
||||
String(call.sql).includes("insert into device_audit_events")
|
||||
);
|
||||
assert.ok(auditCall);
|
||||
assert.equal(JSON.stringify(auditCall.params).includes(canonicalRef), false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("revokes by device and purpose without accepting a credential ref", async () => {
|
||||
const actor = managementActor();
|
||||
const command = normalizeDeviceManagementCommand(
|
||||
"device_credential_binding.revoke",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
purpose: "tracker.command",
|
||||
resolutionCode: "operator.rotation",
|
||||
},
|
||||
);
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-credential-revoke"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("update device_credential_bindings", {
|
||||
rows: [bindingRow({ lifecycle_state: "revoked" })],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_credential_binding.revoke",
|
||||
command,
|
||||
digestCharacter: "b",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.revoked, true);
|
||||
assert.equal(result.result.credentialBinding.lifecycleState, "revoked");
|
||||
assert.equal("credentialRef" in command, false);
|
||||
assert.equal(JSON.stringify(result.result).includes(canonicalRef), false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("rotates an active binding atomically and keeps both refs out of audit", async () => {
|
||||
const actor = managementActor();
|
||||
const command = upsertCommand();
|
||||
const oldRef = "ndc-credref:pilot-command-old-0001";
|
||||
const client = scriptedClient([
|
||||
step("begin"),
|
||||
receiptStep("receipt-credential-rotate"),
|
||||
projectStep(),
|
||||
grantsStep(actor),
|
||||
step("from device_instances", { rows: [deviceRow()] }),
|
||||
step("from device_credential_bindings", {
|
||||
rows: [bindingRow({ credential_ref: oldRef })],
|
||||
}),
|
||||
step("update device_credential_bindings"),
|
||||
step("insert into device_credential_bindings", {
|
||||
rows: [bindingRow({
|
||||
id: "66666666-6666-4666-8666-666666666666",
|
||||
})],
|
||||
}),
|
||||
step("insert into device_audit_events"),
|
||||
step("update device_management_command_receipts"),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const result = await repository.executeManagementCommand(commandInput({
|
||||
actor,
|
||||
commandKind: "device_credential_binding.upsert",
|
||||
command,
|
||||
digestCharacter: "c",
|
||||
}));
|
||||
|
||||
assert.equal(result.result.created, true);
|
||||
assert.equal(result.result.rotated, true);
|
||||
const auditCall = client.calls.find((call) =>
|
||||
String(call.sql).includes("insert into device_audit_events")
|
||||
);
|
||||
assert.ok(auditCall);
|
||||
assert.equal(JSON.stringify(auditCall.params).includes(oldRef), false);
|
||||
assert.equal(JSON.stringify(auditCall.params).includes(canonicalRef), false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function upsertCommand() {
|
||||
return normalizeDeviceManagementCommand(
|
||||
"device_credential_binding.upsert",
|
||||
{
|
||||
projectRef: `project:${projectId}`,
|
||||
deviceRef: `device:${deviceId}`,
|
||||
purpose: "tracker.command",
|
||||
credentialRef: {
|
||||
owner: "ndc_l2_credentials",
|
||||
reference: canonicalRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function managementActor() {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:credential-operator",
|
||||
hubRole: "admin",
|
||||
groupRefs: [],
|
||||
ownerScopes: [],
|
||||
});
|
||||
}
|
||||
|
||||
function projectStep() {
|
||||
return step("from device_projects p", {
|
||||
rows: [{
|
||||
id: projectId,
|
||||
owner_scope_id: ownerId,
|
||||
lifecycle_state: "active",
|
||||
scope_kind: "company",
|
||||
owner_ref: "client:example-company",
|
||||
owner_display_name: "Example Company",
|
||||
owner_lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function grantsStep(actor) {
|
||||
return step("from device_project_grants", {
|
||||
rows: [{
|
||||
id: "55555555-5555-4555-8555-555555555555",
|
||||
principal_kind: "user",
|
||||
principal_ref: actor.userRef,
|
||||
project_role: "admin",
|
||||
capability_allow: [],
|
||||
capability_deny: [],
|
||||
lifecycle_state: "active",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function deviceRow() {
|
||||
return {
|
||||
id: deviceId,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
lifecycle_state: "claimed",
|
||||
};
|
||||
}
|
||||
|
||||
function bindingRow(overrides = {}) {
|
||||
return {
|
||||
id: bindingId,
|
||||
device_id: deviceId,
|
||||
owner_scope_id: ownerId,
|
||||
project_id: projectId,
|
||||
purpose: "tracker.command",
|
||||
credential_owner: "ndc_l2_credentials",
|
||||
lifecycle_state: "active",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function receiptStep(id) {
|
||||
return step("insert into device_management_command_receipts", {
|
||||
rows: [{ id }],
|
||||
});
|
||||
}
|
||||
|
||||
function commandInput({ actor, commandKind, command, digestCharacter }) {
|
||||
return {
|
||||
idempotencyKey: `phase24-${commandKind.replaceAll(".", "-")}-0001`,
|
||||
commandKind,
|
||||
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
|
||||
actor,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
function repositoryWithClient(client) {
|
||||
return new PostgresDeviceRepository({
|
||||
pool: {
|
||||
query: async () => ({ rows: [] }),
|
||||
connect: async () => client,
|
||||
end: async () => undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function step(includes, result = { rows: [] }) {
|
||||
return { includes, result };
|
||||
}
|
||||
|
||||
function scriptedClient(steps) {
|
||||
const queue = [...steps];
|
||||
return {
|
||||
calls: [],
|
||||
released: false,
|
||||
async query(sql, params = []) {
|
||||
this.calls.push({ sql, params });
|
||||
const next = queue.shift();
|
||||
assert.ok(next, `Unexpected query: ${sql}`);
|
||||
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
|
||||
return next.result;
|
||||
},
|
||||
release() {
|
||||
this.released = true;
|
||||
},
|
||||
remaining() {
|
||||
return queue.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createTypedCommandRuntime } from "../src/typed-command-runtime.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
||||
|
||||
test("keeps the B2 access code transient and emits only a typed offer", async () => {
|
||||
const planned = [];
|
||||
const dispatched = [];
|
||||
const runtime = createTypedCommandRuntime({
|
||||
now: () => new Date("2026-08-12T18:00:00.000Z"),
|
||||
repository: {
|
||||
async planTypedServicePing(value) {
|
||||
planned.push(value);
|
||||
return {
|
||||
replayed: false,
|
||||
commandId: "33333333-3333-4333-8333-333333333333",
|
||||
command: {
|
||||
lifecycleState: "queued",
|
||||
expiresAt: "2026-08-12T18:05:00.000Z",
|
||||
},
|
||||
};
|
||||
},
|
||||
async dispatchTypedCommand(value) {
|
||||
dispatched.push(value);
|
||||
return { lifecycleState: "dispatched" };
|
||||
},
|
||||
async recordTypedCommandStatus() {},
|
||||
},
|
||||
});
|
||||
await runtime.planServicePing({
|
||||
idempotencyKey: "idem-00000001",
|
||||
actor: { userRef: "user:test" },
|
||||
input: { projectRef, deviceRef, accessCode: "123456", expiresInSeconds: 300 },
|
||||
});
|
||||
assert.equal(JSON.stringify(planned).includes("123456"), false);
|
||||
const offer = await runtime.offerForDevice(deviceRef);
|
||||
assert.equal(offer.commandType, "service.ping");
|
||||
assert.equal(offer.accessCode, "123456");
|
||||
assert.match(offer.transportMessageRef, /^edge-command:/);
|
||||
assert.equal(dispatched.length, 1);
|
||||
});
|
||||
|
||||
test("expires a transient authorization through the durable ledger", async () => {
|
||||
let current = new Date("2026-08-12T18:00:00.000Z");
|
||||
const dispatches = [];
|
||||
const runtime = createTypedCommandRuntime({
|
||||
now: () => current,
|
||||
repository: {
|
||||
async planTypedServicePing() {
|
||||
return {
|
||||
replayed: false,
|
||||
commandId: "33333333-3333-4333-8333-333333333333",
|
||||
command: {
|
||||
lifecycleState: "queued",
|
||||
expiresAt: "2026-08-12T18:00:30.000Z",
|
||||
},
|
||||
};
|
||||
},
|
||||
async dispatchTypedCommand(value) {
|
||||
dispatches.push(value);
|
||||
return null;
|
||||
},
|
||||
async recordTypedCommandStatus() {},
|
||||
},
|
||||
});
|
||||
await runtime.planServicePing({
|
||||
idempotencyKey: "idem-00000002",
|
||||
actor: { userRef: "user:test" },
|
||||
input: { projectRef, deviceRef, accessCode: "123456", expiresInSeconds: 30 },
|
||||
});
|
||||
current = new Date("2026-08-12T18:00:31.000Z");
|
||||
assert.equal(await runtime.offerForDevice(deviceRef), null);
|
||||
assert.equal(dispatches.length, 1);
|
||||
assert.equal(runtime.status().transientAuthorizations, 0);
|
||||
});
|
||||
|
||||
test("does not recreate a transient authorization on an idempotent replay", async () => {
|
||||
const runtime = createTypedCommandRuntime({
|
||||
repository: {
|
||||
async planTypedServicePing() {
|
||||
return {
|
||||
replayed: true,
|
||||
commandId: "33333333-3333-4333-8333-333333333333",
|
||||
command: {
|
||||
lifecycleState: "queued",
|
||||
expiresAt: "2026-08-12T18:05:00.000Z",
|
||||
},
|
||||
};
|
||||
},
|
||||
async dispatchTypedCommand() {
|
||||
throw new Error("must_not_dispatch_replayed_secret");
|
||||
},
|
||||
async recordTypedCommandStatus() {},
|
||||
},
|
||||
});
|
||||
await runtime.planServicePing({
|
||||
idempotencyKey: "idem-00000003",
|
||||
actor: { userRef: "user:test" },
|
||||
input: { projectRef, deviceRef, accessCode: "654321", expiresInSeconds: 300 },
|
||||
});
|
||||
assert.equal(runtime.status().transientAuthorizations, 0);
|
||||
assert.equal(await runtime.offerForDevice(deviceRef), null);
|
||||
});
|
||||
Reference in New Issue
Block a user