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}`); } } }