From 54f34f867f21e3ea76cfcde01a4011270de7d0f9 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 31 Aug 2026 21:12:10 -0400 Subject: [PATCH 1/6] feat(web): include physical licensing identities in callbacks --- web/__test__/store/server.test.ts | 9 +++++++++ web/src/store/server.ts | 16 +++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index 9d1127c73c..4440f5cf73 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -188,6 +188,7 @@ const getStore = () => { description: store.description, deviceCount: store.deviceCount, expireTime: store.expireTime, + flashGuid: store.flashGuid || undefined, flashProduct: store.flashProduct, flashVendor: store.flashVendor, guid: store.guid, @@ -203,6 +204,7 @@ const getStore = () => { regTy: store.regTy, regUpdatesExpired: store.regUpdatesExpired, state: store.state, + tpmGuid: store.tpmGuid || undefined, wanFQDN: store.wanFQDN, }; @@ -218,6 +220,7 @@ const getStore = () => { deviceCount: store.deviceCount, description: store.description, expireTime: store.expireTime, + flashGuid: store.flashGuid || undefined, flashProduct: store.flashProduct, flashVendor: store.flashVendor, guid: store.guid, @@ -233,6 +236,7 @@ const getStore = () => { regTy: store.regTy, regUpdatesExpired: store.regUpdatesExpired, state: store.state, + tpmGuid: store.tpmGuid || undefined, wanFQDN: store.wanFQDN, }; @@ -651,6 +655,7 @@ describe('useServerStore', () => { deviceCount: 6, description: 'Test Server', expireTime: 123, + flashGuid: 'flash-guid-1', flashProduct: 'TestFlash', flashVendor: 'TestVendor', guid: '123456', @@ -675,6 +680,7 @@ describe('useServerStore', () => { expect(payload.description).toBe('Test Server'); expect(payload.deviceCount).toBe(6); expect(payload.expireTime).toBe(123); + expect(payload.flashGuid).toBe('flash-guid-1'); expect(payload.flashProduct).toBe('TestFlash'); expect(payload.flashVendor).toBe('TestVendor'); expect(payload.guid).toBe('123456'); @@ -767,6 +773,7 @@ describe('useServerStore', () => { regGuid: 'reg-guid-1', regTy: 'Plus', state: 'PLUS' as ServerState, + tpmGuid: '01-TPM-GUID-1', wanFQDN: 'test.myunraid.net', }); @@ -777,6 +784,7 @@ describe('useServerStore', () => { expect(payload.deviceCount).toBe(6); expect(payload.description).toBe('Test Server'); expect(payload.expireTime).toBe(123); + expect(payload.flashGuid).toBe('flash-guid-1'); expect(payload.flashProduct).toBe('TestFlash'); expect(payload.flashVendor).toBe('TestVendor'); expect(payload.guid).toBe('123456'); @@ -792,6 +800,7 @@ describe('useServerStore', () => { expect(payload.regTy).toBe('Plus'); expect(payload.regUpdatesExpired).toBe(true); expect(payload.state).toBe('PLUS'); + expect(payload.tpmGuid).toBe('01-TPM-GUID-1'); expect(payload.wanFQDN).toBe('test.myunraid.net'); }); diff --git a/web/src/store/server.ts b/web/src/store/server.ts index 5bde8e4b53..71270716c9 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -51,6 +51,8 @@ import { useThemeStore } from '~/store/theme'; import { useUnraidApiStore } from '~/store/unraidApi'; import { getRegistrationDeviceLimit, normalizeRegistrationType } from '~/utils/registration'; +type ServerCallbackPayload = ServerData & Pick; + export const useServerStore = defineStore('server', () => { const { t } = useI18n(); const accountStore = useAccountStore(); @@ -246,13 +248,16 @@ export const useServerStore = defineStore('server', () => { } }; - const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { - const payload: ServerData = { + const buildServerCallbackPayload = ( + overrides: Partial = {} + ): ServerCallbackPayload => { + const payload: ServerCallbackPayload = { connectPluginVersion: connectPluginVersion.value || undefined, connectState: getConnectState(), description: description.value, deviceCount: deviceCount.value, expireTime: expireTime.value, + flashGuid: flashGuid.value || undefined, flashProduct: flashProduct.value, flashVendor: flashVendor.value, guid: guid.value, @@ -268,6 +273,7 @@ export const useServerStore = defineStore('server', () => { regTy: regTy.value, regUpdatesExpired: regUpdatesExpired.value, state: state.value, + tpmGuid: tpmGuid.value || undefined, wanFQDN: wanFQDN.value, ...overrides, }; @@ -294,12 +300,12 @@ export const useServerStore = defineStore('server', () => { }; }; - const serverPurchasePayload = computed((): ServerData => buildServerCallbackPayload()); + const serverPurchasePayload = computed((): ServerCallbackPayload => buildServerCallbackPayload()); - const serverAccountPayload = computed((): ServerData => buildServerCallbackPayload()); + const serverAccountPayload = computed((): ServerCallbackPayload => buildServerCallbackPayload()); const serverReplacePayload = computed( - (): ServerData => ({ + (): ServerCallbackPayload => ({ ...buildServerCallbackPayload({ guid: replaceFlashGuid.value, }), From dbdcdc550a2224363742057ca192f9b7b06a2f33 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 31 Aug 2026 21:42:01 -0400 Subject: [PATCH 2/6] test(web): cover physical callback identities --- web/__test__/store/server.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index 4440f5cf73..31b2a47d6b 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -296,6 +296,14 @@ const getStore = () => { return store; }; +const getActualStore = () => + useServerStore( + createTestingPinia({ + createSpy: vi.fn, + stubActions: false, + }) + ); + // Mock dependent stores vi.mock('~/store/account', () => ({ useAccountStore: vi.fn(() => ({ @@ -804,6 +812,23 @@ describe('useServerStore', () => { expect(payload.wanFQDN).toBe('test.myunraid.net'); }); + it('should include physical identities in the actual callback payloads', () => { + const store = getActualStore(); + + store.setServer({ + flashGuid: '058F-6387-0000-0000F1F1E1C6', + guid: '058F-6387-0000-0000F1F1E1C6', + tpmGuid: '01-V35H8S0L1QHK1SBG1XHXJNH7', + } as Server); + + expect(store.serverPurchasePayload.flashGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(store.serverPurchasePayload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverAccountPayload.flashGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(store.serverAccountPayload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverReplacePayload.flashGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(store.serverReplacePayload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + }); + it('should create serverReplacePayload with TPM guid when available on flash boot', () => { const store = getStore(); From c62ab0ae88330c16012554cd907c4c679f84d094 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 31 Aug 2026 21:53:53 -0400 Subject: [PATCH 3/6] refactor(web): use shared callback server type --- web/src/store/server.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/web/src/store/server.ts b/web/src/store/server.ts index 71270716c9..ad7e88c32c 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -51,8 +51,6 @@ import { useThemeStore } from '~/store/theme'; import { useUnraidApiStore } from '~/store/unraidApi'; import { getRegistrationDeviceLimit, normalizeRegistrationType } from '~/utils/registration'; -type ServerCallbackPayload = ServerData & Pick; - export const useServerStore = defineStore('server', () => { const { t } = useI18n(); const accountStore = useAccountStore(); @@ -248,10 +246,8 @@ export const useServerStore = defineStore('server', () => { } }; - const buildServerCallbackPayload = ( - overrides: Partial = {} - ): ServerCallbackPayload => { - const payload: ServerCallbackPayload = { + const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { + const payload: ServerData = { connectPluginVersion: connectPluginVersion.value || undefined, connectState: getConnectState(), description: description.value, @@ -300,12 +296,12 @@ export const useServerStore = defineStore('server', () => { }; }; - const serverPurchasePayload = computed((): ServerCallbackPayload => buildServerCallbackPayload()); + const serverPurchasePayload = computed((): ServerData => buildServerCallbackPayload()); - const serverAccountPayload = computed((): ServerCallbackPayload => buildServerCallbackPayload()); + const serverAccountPayload = computed((): ServerData => buildServerCallbackPayload()); const serverReplacePayload = computed( - (): ServerCallbackPayload => ({ + (): ServerData => ({ ...buildServerCallbackPayload({ guid: replaceFlashGuid.value, }), From d4748c6fe851e38599c4091ed69a1c231755f752 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 31 Aug 2026 21:58:25 -0400 Subject: [PATCH 4/6] refactor(web): infer callback payload fields --- web/src/store/server.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/web/src/store/server.ts b/web/src/store/server.ts index ad7e88c32c..3101d56ab1 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -246,8 +246,8 @@ export const useServerStore = defineStore('server', () => { } }; - const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { - const payload: ServerData = { + const buildServerCallbackPayload = (overrides: Partial = {}) => { + const payload = { connectPluginVersion: connectPluginVersion.value || undefined, connectState: getConnectState(), description: description.value, @@ -296,17 +296,15 @@ export const useServerStore = defineStore('server', () => { }; }; - const serverPurchasePayload = computed((): ServerData => buildServerCallbackPayload()); + const serverPurchasePayload = computed(() => buildServerCallbackPayload()); - const serverAccountPayload = computed((): ServerData => buildServerCallbackPayload()); + const serverAccountPayload = computed(() => buildServerCallbackPayload()); - const serverReplacePayload = computed( - (): ServerData => ({ - ...buildServerCallbackPayload({ - guid: replaceFlashGuid.value, - }), - }) - ); + const serverReplacePayload = computed(() => ({ + ...buildServerCallbackPayload({ + guid: replaceFlashGuid.value, + }), + })); const serverDebugPayload = computed((): Server => { const payload = { From 7668005cefa698afc717a1688ffa1f29629ee9d0 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 31 Aug 2026 22:43:21 -0400 Subject: [PATCH 5/6] chore(web): use published callback contract --- pnpm-lock.yaml | 10 +++++----- web/package.json | 2 +- web/src/store/server.ts | 20 +++++++++++--------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea27da0b87..03b5eb3b35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1139,8 +1139,8 @@ importers: specifier: 8.21.3 version: 8.21.3(vue@3.5.20(typescript@5.9.2)) '@unraid/shared-callbacks': - specifier: 3.1.0 - version: 3.1.0 + specifier: 3.2.0 + version: 3.2.0 '@unraid/ui': specifier: link:../unraid-ui version: link:../unraid-ui @@ -4943,8 +4943,8 @@ packages: cpu: [x64, arm64] os: [linux, darwin] - '@unraid/shared-callbacks@3.1.0': - resolution: {integrity: sha512-Zvz9nlvLSjbTstplvarbH2rwZrsl4ns7djpDmqPi4xH35PvYeK9ut1A/Hkd2Rbl/+nvAMF77d5ZzZkSTb4IoSg==} + '@unraid/shared-callbacks@3.2.0': + resolution: {integrity: sha512-MNkKkbjJLt9lkT5xk7ndBk1zQGHYwR+MxbWTcauc5jQpsEy4t2h8HLhOIFdU2ORgKT9q1f0KmPjCMSEnwT2hIA==} '@unraid/tailwind-rem-to-rem@2.0.0': resolution: {integrity: sha512-zccpQx5fvEBkAB0JkRwwtyRrT9l26LsjkozLy44LGv0NdZGaxgscniIqJRM+OQj5pSpsWDzExebAtUKdE98Flg==} @@ -16218,7 +16218,7 @@ snapshots: - encoding - supports-color - '@unraid/shared-callbacks@3.1.0': + '@unraid/shared-callbacks@3.2.0': dependencies: crypto-js: 4.2.0 diff --git a/web/package.json b/web/package.json index cd1bdf037e..a8a1559297 100644 --- a/web/package.json +++ b/web/package.json @@ -116,7 +116,7 @@ "@jsonforms/vue-vuetify": "3.6.0", "@nuxt/ui": "4.8.2", "@tanstack/vue-table": "8.21.3", - "@unraid/shared-callbacks": "3.1.0", + "@unraid/shared-callbacks": "3.2.0", "@unraid/ui": "link:../unraid-ui", "@vue/apollo-composable": "4.2.2", "@vueuse/components": "13.8.0", diff --git a/web/src/store/server.ts b/web/src/store/server.ts index 3101d56ab1..ad7e88c32c 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -246,8 +246,8 @@ export const useServerStore = defineStore('server', () => { } }; - const buildServerCallbackPayload = (overrides: Partial = {}) => { - const payload = { + const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { + const payload: ServerData = { connectPluginVersion: connectPluginVersion.value || undefined, connectState: getConnectState(), description: description.value, @@ -296,15 +296,17 @@ export const useServerStore = defineStore('server', () => { }; }; - const serverPurchasePayload = computed(() => buildServerCallbackPayload()); + const serverPurchasePayload = computed((): ServerData => buildServerCallbackPayload()); - const serverAccountPayload = computed(() => buildServerCallbackPayload()); + const serverAccountPayload = computed((): ServerData => buildServerCallbackPayload()); - const serverReplacePayload = computed(() => ({ - ...buildServerCallbackPayload({ - guid: replaceFlashGuid.value, - }), - })); + const serverReplacePayload = computed( + (): ServerData => ({ + ...buildServerCallbackPayload({ + guid: replaceFlashGuid.value, + }), + }) + ); const serverDebugPayload = computed((): Server => { const payload = { From 4abc2ddcb94c6ede20aa8e92593683c921f43541 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 31 Aug 2026 23:26:39 -0400 Subject: [PATCH 6/6] feat(web): show flash and TPM licensing GUIDs --- web/__test__/components/Registration.test.ts | 18 +++++++++++ .../components/Registration.standalone.vue | 32 ++++++++++++++----- web/src/locales/en.json | 1 + 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts index 788542a161..36661f6ea1 100644 --- a/web/__test__/components/Registration.test.ts +++ b/web/__test__/components/Registration.test.ts @@ -351,9 +351,27 @@ describe('Registration.standalone.vue', () => { expect(moveButton.exists()).toBe(true); expect(moveButton.attributes('disabled')).toBeUndefined(); + expect(findItemByLabel(t('registration.flashGuid'))?.props('text')).toBe( + '058F-6387-0000-0000F1F1E1C6' + ); expect(findItemByLabel(t('TPM GUID'))?.props('text')).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); }); + it('shows both licensing GUIDs when no key is installed', async () => { + serverStore.state = 'ENOKEYFILE'; + serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.tpmGuid = '01-V35H8S0L1QHK1SBG1XHXJNH7'; + + await wrapper.vm.$nextTick(); + + expect(findItemByLabel(t('registration.flashGuid'))?.props('text')).toBe( + '058F-6387-0000-0000F1F1E1C6' + ); + expect(findItemByLabel(t('TPM GUID'))?.props('text')).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(wrapper.find('[data-testid="move-license-to-tpm"]').exists()).toBe(false); + }); + it('shows Move License to TPM when flashGuid is missing but the active GUID is still a flash GUID', async () => { serverStore.state = 'PRO'; serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue index d68331d4d2..0554e6ac09 100644 --- a/web/src/components/Registration.standalone.vue +++ b/web/src/components/Registration.standalone.vue @@ -46,6 +46,7 @@ const { hasDistinctTpmGuid, dateTimeFormat, deviceCount, + flashGuid, flashProduct, flashVendor, guid, @@ -133,18 +134,24 @@ const showTpmTransferButton = computed((): boolean => ); const disableTpmTransferButton = computed((): boolean => showTrialExpiration.value); -// Organize items into three sections -const bootDeviceItems = computed((): RegistrationItemProps[] => { - return [ - ...(guid.value +const licensingGuidItems = computed((): RegistrationItemProps[] => { + const items: RegistrationItemProps[] = [ + ...(flashGuid.value ? [ { - label: t('registration.deviceGuid'), - text: guid.value, + label: t('registration.flashGuid'), + text: flashGuid.value, }, ] - : []), - ...(showTpmTransferButton.value && tpmGuid.value + : guid.value + ? [ + { + label: t('registration.deviceGuid'), + text: guid.value, + }, + ] + : []), + ...(tpmGuid.value && (flashGuid.value || tpmGuid.value !== guid.value) ? [ { label: t('registration.tpmGuid'), @@ -152,6 +159,15 @@ const bootDeviceItems = computed((): RegistrationItemProps[] => { }, ] : []), + ]; + + return items; +}); + +// Organize items into three sections +const bootDeviceItems = computed((): RegistrationItemProps[] => { + return [ + ...licensingGuidItems.value, ...(bootDeviceType.value ? [ { diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 0214592618..255f45f758 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -692,6 +692,7 @@ "registration.bootDeviceType.internalBootMulti": "Internal Boot (Multi-device)", "registration.bootDeviceType.tpm": "TPM", "registration.deviceGuid": "Device GUID", + "registration.flashGuid": "USB Flash GUID", "registration.flashProduct": "Flash Product", "registration.flashVendor": "Flash Vendor", "registration.general.goToToolsRegistrationToLearn": "Go to Tools > Registration to Learn More",