feat(device-plane): add typed B2 service ping transport
This commit is contained in:
@@ -255,6 +255,58 @@ test("management API exposes a repository idempotency conflict without retrying"
|
||||
}
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -424,12 +476,15 @@ test("gateway message endpoint returns acceptance only after repository commit",
|
||||
acceptAdapterMessage: async (value) => {
|
||||
stored = value;
|
||||
return {
|
||||
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",
|
||||
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",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,12 +25,15 @@ test("shared gateway ingest masks identifiers for HTTP and Edge callers", async
|
||||
async acceptAdapterMessage(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test",
|
||||
idempotencyKey: value.safeView.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
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",
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -40,7 +43,11 @@ test("shared gateway ingest masks identifiers for HTTP and Edge callers", async
|
||||
const acceptance = await ingest.acceptMessage(adapterMessage());
|
||||
|
||||
assert.equal(discovery.value.identifier.masked, "***********0001");
|
||||
assert.equal(acceptance.status, "accepted");
|
||||
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);
|
||||
|
||||
@@ -22,10 +22,11 @@ test("commits a gateway receipt before returning Core acceptance", async () => {
|
||||
|
||||
const result = await acceptGatewayMessage(messageInput(client));
|
||||
|
||||
assert.equal(result.status, "accepted");
|
||||
assert.equal(result.replayed, false);
|
||||
assert.equal(result.idempotencyKey, idempotencyKey);
|
||||
assert.equal(result.acceptedAt, acceptedAt.toISOString());
|
||||
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);
|
||||
});
|
||||
@@ -47,8 +48,8 @@ test("replays one durable receipt for the same normalized request", async () =>
|
||||
|
||||
const result = await acceptGatewayMessage(messageInput(client));
|
||||
|
||||
assert.equal(result.status, "accepted");
|
||||
assert.equal(result.replayed, true);
|
||||
assert.equal(result.acceptance.status, "accepted");
|
||||
assert.equal(result.acceptance.replayed, true);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
@@ -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