Files
NODEDC_PLATFORM/device-plane/services/device-control-core/src/lifecycle-management.mjs
T

140 lines
3.8 KiB
JavaScript

export const DEVICE_LIFECYCLE_COMMAND_KINDS = Object.freeze([
"device.claim",
"device.transfer",
"discovery.reject",
"discovery.expire",
]);
const commandKindSet = new Set(DEVICE_LIFECYCLE_COMMAND_KINDS);
const keyPattern = /^[a-z][a-z0-9-]{1,62}$/;
const resolutionPattern = /^[a-z][a-z0-9._-]{1,63}$/;
export function isLifecycleManagementCommand(kind) {
return commandKindSet.has(kind);
}
export function normalizeLifecycleManagementCommand(kind, input) {
if (!commandKindSet.has(kind)) {
throw new TypeError("device_lifecycle_command_kind_invalid");
}
assertPlainObject(input);
if (kind === "device.claim") {
assertAllowedKeys(input, [
"projectRef",
"enrollmentIntentRef",
"discoveryRef",
"deviceKey",
"displayName",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
enrollmentIntentId: normalizeEntityRef(
input.enrollmentIntentRef,
"enrollment-intent",
),
discoveryId: normalizeEntityRef(input.discoveryRef, "discovery"),
deviceKey: normalizePattern(
input.deviceKey,
keyPattern,
"device_key_invalid",
),
displayName: normalizeDisplayText(input.displayName, 160),
});
}
if (kind === "device.transfer") {
assertAllowedKeys(input, [
"deviceRef",
"sourceProjectRef",
"targetProjectRef",
"targetDeviceKey",
]);
const sourceProjectId = normalizeEntityRef(
input.sourceProjectRef,
"project",
);
const targetProjectId = normalizeEntityRef(
input.targetProjectRef,
"project",
);
if (sourceProjectId === targetProjectId) {
throw new TypeError("device_transfer_target_same_as_source");
}
return Object.freeze({
deviceId: normalizeEntityRef(input.deviceRef, "device"),
sourceProjectId,
targetProjectId,
targetDeviceKey: normalizePattern(
input.targetDeviceKey,
keyPattern,
"device_transfer_target_key_invalid",
),
});
}
assertAllowedKeys(input, [
"projectRef",
"discoveryRef",
"resolutionCode",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
discoveryId: normalizeEntityRef(input.discoveryRef, "discovery"),
resolutionCode: normalizePattern(
input.resolutionCode,
resolutionPattern,
"device_discovery_resolution_code_invalid",
),
});
}
function normalizeEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function normalizePattern(value, pattern, code) {
if (typeof value !== "string" || !pattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function normalizeDisplayText(value, maxLength) {
if (typeof value !== "string") {
throw new TypeError("device_display_name_invalid");
}
const normalized = value.trim();
if (
normalized.length < 1
|| normalized.length > maxLength
|| /\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(normalized)
) {
throw new TypeError("device_display_name_invalid");
}
return normalized;
}
function assertPlainObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_lifecycle_command_invalid");
}
}
function assertAllowedKeys(input, allowed) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(input)) {
if (!allowedSet.has(key)) {
throw new TypeError(`device_management_command_field_unexpected:${key}`);
}
}
}