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()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user